Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions back/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions back/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand Down
82 changes: 82 additions & 0 deletions back/src/BitailsBroadcaster.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
BroadcastResponse,
BroadcastFailure,
Broadcaster,
HttpClient,
defaultHttpClient,
Transaction,
} from '@bsv/sdk'

/**
* Represents an WhatsOnChain transaction broadcaster.
*/
Comment thread
sirdeggen marked this conversation as resolved.
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.
Comment thread
sirdeggen marked this conversation as resolved.
Outdated
* @param {HttpClient} httpClient - The HTTP client used to make requests to the API.
*/
Comment thread
sirdeggen marked this conversation as resolved.
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<BroadcastResponse | BroadcastFailure>} A promise that resolves to either a success or failure response.
*/
Comment thread
sirdeggen marked this conversation as resolved.
async broadcast(
tx: Transaction
): Promise<BroadcastResponse | BroadcastFailure> {
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'
}
}
}
}

91 changes: 62 additions & 29 deletions back/src/arc.ts
Original file line number Diff line number Diff line change
@@ -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', {
Comment thread
sirdeggen marked this conversation as resolved.
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<BroadcastResponse | BroadcastFailure> {
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
export default broadcaster
6 changes: 3 additions & 3 deletions back/src/functions/allFunds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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 })
Expand Down Expand Up @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions back/src/functions/utxoStatusUptate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
const document = await db.collection('txs').findOne({ txid })
Expand All @@ -18,8 +19,7 @@ async function updateRecords(txid: string, merklePath: string, arc: any = { stat

async function getBeefFromWoc(txid: string): Promise<string | null> {
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
Expand Down
2 changes: 2 additions & 0 deletions back/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down