From e8f29566b3a6930a8f219e3fde85f1e1a2132c5a Mon Sep 17 00:00:00 2001 From: Deggen Date: Wed, 27 Aug 2025 11:09:47 -0500 Subject: [PATCH 1/2] robust broadcasting config with multiple failovers --- back/package-lock.json | 12 ++-- back/package.json | 4 +- back/src/BitailsBroadcaster.ts | 82 +++++++++++++++++++++++ back/src/arc.ts | 91 ++++++++++++++++++-------- back/src/functions/allFunds.ts | 6 +- back/src/functions/utxoStatusUptate.ts | 4 +- back/tsconfig.json | 2 + 7 files changed, 159 insertions(+), 42 deletions(-) create mode 100644 back/src/BitailsBroadcaster.ts diff --git a/back/package-lock.json b/back/package-lock.json index 891250e..8d81896 100644 --- a/back/package-lock.json +++ b/back/package-lock.json @@ -1,15 +1,15 @@ { "name": "truth-machine", - "version": "1.1.1", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "truth-machine", - "version": "1.1.1", + "version": "1.2.0", "license": "ISC", "dependencies": { - "@bsv/sdk": "^1.6.18", + "@bsv/sdk": "^1.6.24", "@bsv/templates": "^1.1.0", "@types/express": "^5.0.0", "cors": "^2.8.5", @@ -25,9 +25,9 @@ } }, "node_modules/@bsv/sdk": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/@bsv/sdk/-/sdk-1.6.18.tgz", - "integrity": "sha512-Cg6PZzvdBO1wtBL2XVEmy0rvB4H8pb41LqCoTmRFyjqyE3KBxOwN2gYeqFYWVa8UIqEyirjij0BUwNs3yOKqKA==", + "version": "1.6.24", + "resolved": "https://registry.npmjs.org/@bsv/sdk/-/sdk-1.6.24.tgz", + "integrity": "sha512-st8LfNZGyj5EHPedruTfttmMQjw9NeX/TGlX+hq+TG3rAzpw17AxY8SlseDzIa5ZrEcgIxuhtEaeKD+kbphqDA==", "license": "SEE LICENSE IN LICENSE.txt" }, "node_modules/@bsv/templates": { diff --git a/back/package.json b/back/package.json index 7b0a243..dba6809 100644 --- a/back/package.json +++ b/back/package.json @@ -1,6 +1,6 @@ { "name": "truth-machine", - "version": "1.1.1", + "version": "1.2.0", "main": "index.js", "scripts": { "dev": "tsx watch ./src/index.ts", @@ -12,7 +12,7 @@ "license": "ISC", "description": "", "dependencies": { - "@bsv/sdk": "^1.6.18", + "@bsv/sdk": "^1.6.24", "@bsv/templates": "^1.1.0", "@types/express": "^5.0.0", "cors": "^2.8.5", diff --git a/back/src/BitailsBroadcaster.ts b/back/src/BitailsBroadcaster.ts new file mode 100644 index 0000000..af36d75 --- /dev/null +++ b/back/src/BitailsBroadcaster.ts @@ -0,0 +1,82 @@ +import { + BroadcastResponse, + BroadcastFailure, + Broadcaster, + HttpClient, + defaultHttpClient, + Transaction, + } from '@bsv/sdk' + + /** + * Represents an WhatsOnChain transaction broadcaster. + */ + export default class BitailsBroadcaster implements Broadcaster { + readonly network: string + private readonly URL: string + private readonly httpClient: HttpClient + + /** + * Constructs an instance of the WhatsOnChain broadcaster. + * + * @param {'main' | 'test' | 'stn'} network - The BSV network to use when calling the WhatsOnChain API. + * @param {HttpClient} httpClient - The HTTP client used to make requests to the API. + */ + constructor( + httpClient: HttpClient = defaultHttpClient() + ) { + this.URL = `https://api.bitails.io/tx/broadcast` + this.httpClient = httpClient + } + + /** + * Broadcasts a transaction via WhatsOnChain. + * + * @param {Transaction} tx - The transaction to be broadcasted. + * @returns {Promise} A promise that resolves to either a success or failure response. + */ + async broadcast( + tx: Transaction + ): Promise { + const rawTx = tx.toHex() + + const requestOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json' + }, + data: { raw: rawTx } + } + + try { + const response = await this.httpClient.request<{ txid: string }>( + this.URL, + requestOptions + ) + if (response.ok) { + const txid = response.data.txid + return { + status: 'success', + txid, + message: 'broadcast successful' + } + } else { + return { + status: 'error', + code: response.status.toString() ?? 'ERR_UNKNOWN', + description: response.data ?? 'Unknown error' + } + } + } catch (error) { + return { + status: 'error', + code: '500', + description: + typeof error.message === 'string' + ? error.message + : 'Internal Server Error' + } + } + } + } + \ No newline at end of file diff --git a/back/src/arc.ts b/back/src/arc.ts index 20fded4..831415c 100644 --- a/back/src/arc.ts +++ b/back/src/arc.ts @@ -1,39 +1,72 @@ -/** - * Configuration for TAAL's ARC (Authenticated Resource Calls) service. - * This module sets up and exports an ARC client instance for blockchain data management. - * - * The ARC service provides reliable access to the Bitcoin SV blockchain with features like: - * - Transaction broadcasting - * - Merkle proof verification - * - Callback notifications for transaction confirmations - */ - -import { ARC } from "@bsv/sdk" +import { ARC, WhatsOnChainBroadcaster, Broadcaster, Transaction, BroadcastResponse, BroadcastFailure } from "@bsv/sdk" +import BitailsBroadcaster from "./BitailsBroadcaster" import dotenv from 'dotenv' dotenv.config() // Environment variables for ARC configuration export const { NETWORK, DOMAIN, CALLBACK_TOKEN, ARC_API_KEY, TEST_ARC_API_KEY } = process.env -/** - * ARC client configuration options - * @property {string} callbackUrl - Webhook URL where ARC will send transaction notifications - * @property {string} callbackToken - Authentication token for securing webhook endpoints - */ -const options = { - callbackUrl: 'https://' + DOMAIN + '/callback', - callbackToken: CALLBACK_TOKEN, - apiKey: NETWORK === 'main' ? ARC_API_KEY : TEST_ARC_API_KEY, +export const ARC_URL = NETWORK !== 'main' ? 'https://arc-test.taal.com' : 'https://arc.taal.com' + +class SuperArc implements Broadcaster { + private broadcasters: Broadcaster[] + + constructor() { + const taal = new ARC('https://arc.taal.com', { + callbackUrl: 'https://' + DOMAIN + '/callback', + callbackToken: CALLBACK_TOKEN, + apiKey: ARC_API_KEY, + }) + const gorillaPool = new ARC('https://arc.gorillapool.io', { + callbackUrl: 'https://' + DOMAIN + '/callback', + callbackToken: CALLBACK_TOKEN + }) + const bsva = new ARC('https://arc-mainnet-staging-eu-1.bsvb.tech', { + callbackUrl: 'https://' + DOMAIN + '/callback', + callbackToken: CALLBACK_TOKEN + }) + const WoC = new WhatsOnChainBroadcaster('main') + const bitails = new BitailsBroadcaster() + this.broadcasters = [taal, gorillaPool, bsva, WoC, bitails] + } + + // this function tries each of the available broadcaster options in order, returning on first success. + // This is done sequentially such that if ARC TAAL works, no other options are used, but if ARC TAAL fails, then we try other options. + async broadcast(tx: Transaction): Promise { + for (const broadcaster of this.broadcasters) { + const response = await broadcaster.broadcast(tx) + if (response.status === 'success') { + return response + } + } + return { + status: 'error', + code: 'ERR_UNKNOWN', + description: 'Failed to broadcast transaction to any of the configured broadcasters' + } + } } -export const ARC_URL = (NETWORK === 'main') - ? 'https://arc.taal.com' - : 'https://arc-test.taal.com' +function createBroadcaster() { + // if we're on testnet just use TAAL + if (NETWORK !== 'main') { + return new ARC('https://arc-test.taal.com', { + callbackUrl: 'https://' + DOMAIN + '/callback', + callbackToken: CALLBACK_TOKEN, + apiKey: TEST_ARC_API_KEY, + }) + } + // otherwise set up a failover which tries a bunch of ways to broadcast the tx + return new SuperArc() +} + + +const broadcaster = createBroadcaster() -/** - * Initialize ARC client based on network environment - * Uses production endpoint for 'main' network, test endpoint otherwise - */ -const Arc = new ARC(ARC_URL, options) +export const ArcTaal = new ARC(ARC_URL, { + callbackUrl: 'https://' + DOMAIN + '/callback', + callbackToken: CALLBACK_TOKEN, + apiKey: ARC_API_KEY, +}) -export default Arc \ No newline at end of file +export default broadcaster \ No newline at end of file diff --git a/back/src/functions/allFunds.ts b/back/src/functions/allFunds.ts index ddcc08f..0875661 100644 --- a/back/src/functions/allFunds.ts +++ b/back/src/functions/allFunds.ts @@ -2,7 +2,7 @@ import { Request, Response } from 'express' import { fromUtxo, MerklePath, P2PKH, SatoshisPerKilobyte, Transaction } from '@bsv/sdk' import HashPuzzle from '../HashPuzzle' import db from '../db' -import Arc from '../arc' +import { ArcTaal } from '../arc' import { address, key } from '../functions/address' import woc from '../woc' @@ -59,7 +59,7 @@ export default async function (req: Request, res: Response) { const fundsTxId = fundsTx.id('hex') // Let's ensure this gets out quickly - const fundsTxResponse = await fundsTx.broadcast(Arc) + const fundsTxResponse = await fundsTx.broadcast(ArcTaal) if (fundsTxResponse.status !== 'success') { res.send({ error: 'fundsTxResponse', fundsTxResponse }) @@ -99,7 +99,7 @@ export default async function (req: Request, res: Response) { } // Broadcast transactions - const responses = await Arc.broadcastMany(tokenCreationTxs) + const responses = await ArcTaal.broadcastMany(tokenCreationTxs) const tokenTxs = responses.map((txResponse: any, i) => { const tokenTx = tokenCreationTxs[i] diff --git a/back/src/functions/utxoStatusUptate.ts b/back/src/functions/utxoStatusUptate.ts index 22b0237..338bb09 100644 --- a/back/src/functions/utxoStatusUptate.ts +++ b/back/src/functions/utxoStatusUptate.ts @@ -2,6 +2,7 @@ import { Request, Response } from 'express' import db from '../db' import { MerklePath, Beef } from '@bsv/sdk' import { ARC_URL, NETWORK } from '../arc' +import woc from '../woc' async function updateRecords(txid: string, merklePath: string, arc: any = { status: 'WoC retrieved' }): Promise { const document = await db.collection('txs').findOne({ txid }) @@ -18,8 +19,7 @@ async function updateRecords(txid: string, merklePath: string, arc: any = { stat async function getBeefFromWoc(txid: string): Promise { try { - const woc = await (await fetch(`https://api.whatsonchain.com/v1/bsv/${NETWORK}/tx/${txid}/beef`)).text() - return woc + return await woc.getBeef(txid) } catch (error) { console.error('Failed to get Beef from WhatOnChain', error) return null diff --git a/back/tsconfig.json b/back/tsconfig.json index 2d8d3ef..05e8950 100644 --- a/back/tsconfig.json +++ b/back/tsconfig.json @@ -1,5 +1,7 @@ { "compilerOptions": { + // Target ES2020 to support BigInt literals + "target": "ES2020", // Treat files as modules even if it doesn't use import/export "moduleDetection": "force", From 53d76de141d72a26fb114c6d6c4a9fd75dbf5037 Mon Sep 17 00:00:00 2001 From: deggen Date: Mon, 1 Sep 2025 07:54:11 -0500 Subject: [PATCH 2/2] Update back/src/BitailsBroadcaster.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- back/src/BitailsBroadcaster.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/back/src/BitailsBroadcaster.ts b/back/src/BitailsBroadcaster.ts index af36d75..abde287 100644 --- a/back/src/BitailsBroadcaster.ts +++ b/back/src/BitailsBroadcaster.ts @@ -11,14 +11,12 @@ import { * Represents an WhatsOnChain transaction broadcaster. */ export default class BitailsBroadcaster implements Broadcaster { - readonly network: string private readonly URL: string private readonly httpClient: HttpClient /** * Constructs an instance of the WhatsOnChain broadcaster. * - * @param {'main' | 'test' | 'stn'} network - The BSV network to use when calling the WhatsOnChain API. * @param {HttpClient} httpClient - The HTTP client used to make requests to the API. */ constructor(