Copy import { Transaction, ARC } from '@bsv/sdk'
/**
* Advanced ARC Broadcaster
*
* Implements retry logic, failover, and batch broadcasting
*/
class AdvancedARCBroadcaster {
private arcEndpoints: string[]
private currentEndpointIndex: number = 0
private maxRetries: number = 3
private retryDelay: number = 1000 // Start with 1 second
constructor(arcEndpoints: string[] = [
'https://arc.taal.com',
'https://arc.gorillapool.io'
]) {
this.arcEndpoints = arcEndpoints
}
/**
* Broadcast with automatic retry and failover
*/
async broadcastWithRetry(
tx: Transaction,
options: BroadcastOptions = {}
): Promise<ARCBroadcastResult> {
const {
maxRetries = this.maxRetries,
retryDelay = this.retryDelay,
validateBefore = true
} = options
// Validate transaction before broadcasting
if (validateBefore) {
const validation = this.validateTransaction(tx)
if (!validation.valid) {
throw new Error(`Transaction validation failed: ${validation.errors.join(', ')}`)
}
console.log('Transaction validation passed')
}
let lastError: Error | null = null
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const arcUrl = this.arcEndpoints[this.currentEndpointIndex]
console.log(`Broadcast attempt ${attempt}/${maxRetries}`)
console.log(`Using ARC endpoint: ${arcUrl}`)
const arc = new ARC(arcUrl)
const response = await arc.broadcastTransaction(tx)
console.log('Broadcast successful!')
return {
txid: response.txid,
status: response.txStatus,
timestamp: Date.now(),
arcUrl,
attempts: attempt
}
} catch (error) {
lastError = error
console.error(`Attempt ${attempt} failed:`, error.message)
// Try next endpoint
this.currentEndpointIndex = (this.currentEndpointIndex + 1) % this.arcEndpoints.length
if (attempt < maxRetries) {
// Exponential backoff
const waitTime = Math.min(retryDelay * Math.pow(2, attempt - 1), 10000)
console.log(`Waiting ${waitTime}ms before retry...`)
await new Promise(resolve => setTimeout(resolve, waitTime))
}
}
}
throw new Error(`Broadcast failed after ${maxRetries} attempts: ${lastError?.message}`)
}
/**
* Broadcast to multiple ARC endpoints simultaneously
*/
async broadcastToMultipleArcs(tx: Transaction): Promise<MultiARCResult> {
console.log(`Broadcasting to ${this.arcEndpoints.length} ARC endpoints`)
const startTime = Date.now()
const promises = this.arcEndpoints.map(async (arcUrl) => {
try {
const arc = new ARC(arcUrl)
const response = await arc.broadcastTransaction(tx)
return {
arcUrl,
success: true,
txid: response.txid,
status: response.txStatus,
responseTime: Date.now() - startTime
}
} catch (error) {
return {
arcUrl,
success: false,
error: error.message,
responseTime: Date.now() - startTime
}
}
})
const results = await Promise.all(promises)
const successful = results.filter(r => r.success).length
const failed = results.filter(r => !r.success).length
console.log(`Broadcast results: ${successful} successful, ${failed} failed`)
return {
results,
successful,
failed,
totalTime: Date.now() - startTime
}
}
/**
* Batch broadcast multiple transactions
*/
async broadcastBatch(
transactions: Transaction[],
options: BatchOptions = {}
): Promise<BatchResult[]> {
const {
parallel = false,
delayBetween = 100,
continueOnError = true
} = options
console.log(`Broadcasting batch of ${transactions.length} transactions`)
console.log(`Mode: ${parallel ? 'parallel' : 'sequential'}`)
const results: BatchResult[] = []
if (parallel) {
// Broadcast all transactions in parallel
const promises = transactions.map(async (tx, index) => {
try {
const result = await this.broadcastWithRetry(tx)
return {
index,
success: true,
txid: result.txid,
status: result.status
}
} catch (error) {
return {
index,
success: false,
txid: tx.id('hex'),
error: error.message
}
}
})
return await Promise.all(promises)
} else {
// Broadcast sequentially
for (let i = 0; i < transactions.length; i++) {
const tx = transactions[i]
try {
console.log(`Broadcasting transaction ${i + 1}/${transactions.length}`)
const result = await this.broadcastWithRetry(tx)
results.push({
index: i,
success: true,
txid: result.txid,
status: result.status
})
// Delay between broadcasts
if (i < transactions.length - 1 && delayBetween > 0) {
await new Promise(resolve => setTimeout(resolve, delayBetween))
}
} catch (error) {
console.error(`Transaction ${i + 1} failed:`, error.message)
results.push({
index: i,
success: false,
txid: tx.id('hex'),
error: error.message
})
if (!continueOnError) {
break
}
}
}
const successful = results.filter(r => r.success).length
console.log(`Batch complete: ${successful}/${transactions.length} successful`)
return results
}
}
/**
* Validate transaction before broadcasting
*/
private validateTransaction(tx: Transaction): ValidationResult {
const errors: string[] = []
// Check inputs
if (tx.inputs.length === 0) {
errors.push('Transaction has no inputs')
}
// Check outputs
if (tx.outputs.length === 0) {
errors.push('Transaction has no outputs')
}
// Check output amounts
for (let i = 0; i < tx.outputs.length; i++) {
const output = tx.outputs[i]
if (output.satoshis < 0) {
errors.push(`Output ${i} has negative amount`)
}
if (output.satoshis > 0 && output.satoshis < 546) {
errors.push(`Output ${i} below dust threshold (546 sats)`)
}
}
// Check for unlocking scripts
for (let i = 0; i < tx.inputs.length; i++) {
const input = tx.inputs[i]
if (!input.unlockingScript || input.unlockingScript.chunks.length === 0) {
errors.push(`Input ${i} has no unlocking script`)
}
}
// Check transaction size (max 100MB but warn at 10MB)
const txSize = tx.toHex().length / 2
if (txSize > 10000000) {
errors.push(`Transaction is very large (${txSize} bytes)`)
}
return {
valid: errors.length === 0,
errors
}
}
/**
* Get best ARC endpoint based on response time
*/
async getBestEndpoint(): Promise<string> {
console.log('Testing ARC endpoints...')
const tests = this.arcEndpoints.map(async (arcUrl) => {
const startTime = Date.now()
try {
await fetch(`${arcUrl}/v1/policy`)
const responseTime = Date.now() - startTime
return {
arcUrl,
responseTime,
available: true
}
} catch (error) {
return {
arcUrl,
responseTime: Infinity,
available: false
}
}
})
const results = await Promise.all(tests)
// Sort by response time
results.sort((a, b) => a.responseTime - b.responseTime)
const best = results[0]
if (best.available) {
console.log(`Best endpoint: ${best.arcUrl} (${best.responseTime}ms)`)
return best.arcUrl
}
throw new Error('No ARC endpoints available')
}
}
interface BroadcastOptions {
maxRetries?: number
retryDelay?: number
validateBefore?: boolean
}
interface BatchOptions {
parallel?: boolean
delayBetween?: number
continueOnError?: boolean
}
interface ARCBroadcastResult {
txid: string
status: string
timestamp: number
arcUrl?: string
attempts?: number
}
interface MultiARCResult {
results: Array<{
arcUrl: string
success: boolean
txid?: string
status?: string
error?: string
responseTime: number
}>
successful: number
failed: number
totalTime: number
}
interface BatchResult {
index: number
success: boolean
txid: string
status?: string
error?: string
}
interface ValidationResult {
valid: boolean
errors: string[]
}
/**
* Usage Example
*/
async function advancedARCExample() {
const broadcaster = new AdvancedARCBroadcaster([
'https://arc.taal.com',
'https://arc.gorillapool.io'
])
// Get best endpoint
const bestEndpoint = await broadcaster.getBestEndpoint()
console.log('Best endpoint:', bestEndpoint)
// Create transaction
const privateKey = PrivateKey.fromRandom()
const tx = new Transaction()
// ... add inputs and outputs ...
await tx.sign()
// Broadcast with retry
try {
const result = await broadcaster.broadcastWithRetry(tx, {
maxRetries: 5,
retryDelay: 1000,
validateBefore: true
})
console.log('Broadcast successful:', result)
} catch (error) {
console.error('All retries failed:', error.message)
}
// Broadcast to multiple ARCs
const multiResult = await broadcaster.broadcastToMultipleArcs(tx)
console.log('Multi-ARC broadcast:', multiResult)
// Batch broadcast
const transactions: Transaction[] = [] // Array of transactions
const batchResults = await broadcaster.broadcastBatch(transactions, {
parallel: false,
delayBetween: 100,
continueOnError: true
})
console.log('Batch results:', batchResults)
}