import { ARC, Transaction, PrivateKey, P2PKH } from '@bsv/sdk';
import { SatoshisPerKilobyte } from '@bsv/sdk/transaction/fee-models';
class PaymentProcessor {
private arc: ARC;
private privKey: PrivateKey;
private pendingPayments: Map<string, PaymentRecord>;
constructor(
arcUrl: string,
apiKey: string,
walletPrivKey: PrivateKey,
callbackUrl: string
) {
this.arc = new ARC(arcUrl, {
apiKey,
deploymentId: 'payment-processor-v1.0.0',
callbackUrl,
callbackToken: process.env.CALLBACK_SECRET!
});
this.privKey = walletPrivKey;
this.pendingPayments = new Map();
}
/**
* Process payment transaction
*/
async processPayment(
recipientAddress: string,
amount: number,
utxos: Array<{ tx: Transaction; outputIndex: number }>,
metadata?: any
): Promise<{ txid: string; status: string }> {
try {
// Get current fee policy
const feePolicy = await this.arc.getFeePolicy();
const feeModel = new SatoshisPerKilobyte(feePolicy.standard);
// Create transaction
const tx = new Transaction();
// Add inputs
for (const utxo of utxos) {
tx.addInput({
sourceTransaction: utxo.tx,
sourceOutputIndex: utxo.outputIndex,
unlockingScriptTemplate: new P2PKH().unlock(this.privKey)
});
}
// Add payment output
tx.addOutput({
lockingScript: new P2PKH().lock(recipientAddress),
satoshis: amount
});
// Add change output
tx.addOutput({
lockingScript: new P2PKH().lock(this.privKey.toPublicKey().toAddress()),
change: true
});
// Calculate fee and sign
await tx.fee(feeModel);
await tx.sign();
// Store payment record
const txid = tx.id('hex');
this.pendingPayments.set(txid, {
txid,
recipientAddress,
amount,
timestamp: Date.now(),
status: 'pending',
metadata
});
// Broadcast
const response = await tx.broadcast(this.arc);
// Update status
this.pendingPayments.get(txid)!.status = response.status;
console.log('Payment broadcast successful');
console.log('TXID:', response.txid);
console.log('Amount:', amount, 'satoshis');
console.log('Recipient:', recipientAddress);
return {
txid: response.txid,
status: response.status
};
} catch (error) {
console.error('Payment processing failed:', error.message);
// Handle specific errors
if (error.code === 'INSUFFICIENT_FEE') {
throw new Error(`Fee too low. Minimum: ${error.minimumFee} satoshis`);
} else if (error.code === 'DOUBLE_SPEND') {
throw new Error(`Double-spend detected: ${error.competingTxid}`);
} else if (error.code === 'INVALID_TRANSACTION') {
throw new Error(`Invalid transaction: ${error.details}`);
}
throw error;
}
}
/**
* Handle webhook callback
*/
handleCallback(event: any) {
const txid = event.txid;
const payment = this.pendingPayments.get(txid);
if (!payment) {
console.warn('Received callback for unknown transaction:', txid);
return;
}
switch (event.type) {
case 'TRANSACTION_MINED':
console.log('Payment mined:', txid);
payment.status = 'mined';
payment.blockHeight = event.blockHeight;
payment.blockHash = event.blockHash;
break;
case 'TRANSACTION_CONFIRMED':
console.log('Payment confirmed:', txid);
payment.status = 'confirmed';
payment.confirmations = event.confirmations;
// Payment is now final - update business logic
this.finalizePayment(payment);
break;
case 'TRANSACTION_REJECTED':
console.error('Payment rejected:', txid);
payment.status = 'rejected';
payment.rejectReason = event.rejectReason;
// Handle failed payment
this.handleFailedPayment(payment);
break;
case 'DOUBLE_SPEND_ATTEMPT':
console.error('Double-spend attempt:', txid);
payment.status = 'double_spend';
payment.competingTxid = event.competingTxid;
// Alert and freeze
this.handleDoubleSpend(payment);
break;
}
}
/**
* Query payment status
*/
async getPaymentStatus(txid: string): Promise<PaymentRecord> {
const payment = this.pendingPayments.get(txid);
if (!payment) {
throw new Error('Payment not found');
}
// Refresh status from ARC
try {
const status = await this.arc.getTransactionStatus(txid);
payment.status = status.status;
payment.confirmations = status.confirmations;
payment.blockHeight = status.blockHeight;
payment.blockHash = status.blockHash;
} catch (error) {
console.error('Failed to refresh payment status:', error.message);
}
return payment;
}
private finalizePayment(payment: PaymentRecord) {
console.log('Finalizing payment:', payment.txid);
// Update database, send confirmation email, etc.
}
private handleFailedPayment(payment: PaymentRecord) {
console.error('Handling failed payment:', payment.txid);
// Refund, retry, notify user, etc.
}
private handleDoubleSpend(payment: PaymentRecord) {
console.error('CRITICAL: Double-spend detected for payment:', payment.txid);
// Freeze account, alert security, investigate, etc.
}
}
interface PaymentRecord {
txid: string;
recipientAddress: string;
amount: number;
timestamp: number;
status: string;
metadata?: any;
blockHeight?: number;
blockHash?: string;
confirmations?: number;
rejectReason?: string;
competingTxid?: string;
}
// Usage
const processor = new PaymentProcessor(
'https://api.taal.com/arc',
'mainnet_xxx',
PrivateKey.fromWif('L5...'),
'https://myapp.com/callbacks'
);
// Process payment
const result = await processor.processPayment(
'1RecipientAddress...',
10000, // 10000 satoshis
[{ tx: utxoTx, outputIndex: 0 }],
{ orderId: '12345', customerId: 'user_abc' }
);
console.log('Payment TXID:', result.txid);