UNPKG

@bsv/p2p

Version:

A client for P2P messaging and payments

342 lines (297 loc) • 12.6 kB
import { MessageBoxClient, PeerMessage } from './MessageBoxClient.js' import { WalletClient, P2PKH, PublicKey, createNonce, AtomicBEEF, AuthFetch, Base64String } from '@bsv/sdk' import { Logger } from './Utils/logger.js' export const STANDARD_PAYMENT_MESSAGEBOX = 'payment_inbox' const STANDARD_PAYMENT_OUTPUT_INDEX = 0 /** * Configuration options for initializing PeerPayClient. */ export interface PeerPayClientConfig { messageBoxHost?: string walletClient: WalletClient enableLogging?: boolean // 🔹 Added optional logging flag } /** * Represents the parameters required to initiate a payment. */ export interface PaymentParams { recipient: string amount: number } /** * Represents a structured payment token. */ export interface PaymentToken { customInstructions: { derivationPrefix: Base64String, derivationSuffix: Base64String, } transaction: AtomicBEEF, amount: number } /** * Represents an incoming payment received via MessageBox. */ export interface IncomingPayment { messageId: string sender: string token: PaymentToken } /** * PeerPayClient enables peer-to-peer Bitcoin payments using MessageBox. */ export class PeerPayClient extends MessageBoxClient { private readonly peerPayWalletClient: WalletClient private _authFetchInstance?: AuthFetch constructor(config: PeerPayClientConfig) { const { messageBoxHost = 'https://messagebox.babbage.systems', walletClient, enableLogging = false } = config // 🔹 Pass enableLogging to MessageBoxClient super({ host: messageBoxHost, walletClient, enableLogging }) this.peerPayWalletClient = walletClient } private get authFetchInstance(): AuthFetch { if (!this._authFetchInstance) { this._authFetchInstance = new AuthFetch(this.peerPayWalletClient) } return this._authFetchInstance } /** * Generates a valid payment token for a recipient. * * This function derives a unique public key for the recipient, constructs a P2PKH locking script, * and creates a payment action with the specified amount. * * @param {PaymentParams} payment - The payment details. * @param {string} payment.recipient - The recipient's identity key. * @param {number} payment.amount - The amount in satoshis to send. * @returns {Promise<PaymentToken>} A valid payment token containing transaction details. * @throws {Error} If the recipient's public key cannot be derived. */ async createPaymentToken(payment: PaymentParams): Promise<PaymentToken> { if (payment.amount <= 0) { throw new Error('Invalid payment details: recipient and valid amount are required') }; // Generate derivation paths using correct nonce function const derivationPrefix = await createNonce(this.peerPayWalletClient) const derivationSuffix = await createNonce(this.peerPayWalletClient) Logger.log(`[PP CLIENT] Derivation Prefix: ${derivationPrefix}`) Logger.log(`[PP CLIENT] Derivation Suffix: ${derivationSuffix}`) // Get recipient's derived public key const { publicKey: derivedKeyResult } = await this.peerPayWalletClient.getPublicKey({ protocolID: [2, '3241645161d8'], keyID: `${derivationPrefix} ${derivationSuffix}`, counterparty: payment.recipient }) Logger.log(`[PP CLIENT] Derived Public Key: ${derivedKeyResult}`) if (derivedKeyResult == null || derivedKeyResult.trim() === '') { throw new Error('Failed to derive recipient’s public key') } // Create locking script using recipient's public key const lockingScript = new P2PKH().lock(PublicKey.fromString(derivedKeyResult).toAddress()).toHex() Logger.log(`[PP CLIENT] Locking Script: ${lockingScript}`) // Create the payment action const paymentAction = await this.peerPayWalletClient.createAction({ description: 'PeerPay payment', outputs: [{ satoshis: payment.amount, lockingScript, customInstructions: JSON.stringify({ derivationPrefix, derivationSuffix, payee: payment.recipient }), outputDescription: 'Payment for PeerPay transaction' }], options: { randomizeOutputs: false } }) if (paymentAction.tx === undefined) { throw new Error('Transaction creation failed!') } Logger.log(`[PP CLIENT] Payment Action:`, paymentAction) return { customInstructions: { derivationPrefix, derivationSuffix }, transaction: paymentAction.tx, amount: payment.amount } } /** * Sends Bitcoin to a PeerPay recipient. * * This function validates the payment details and delegates the transaction * to `sendLivePayment` for processing. * * @param {PaymentParams} payment - The payment details. * @param {string} payment.recipient - The recipient's identity key. * @param {number} payment.amount - The amount in satoshis to send. * @returns {Promise<any>} Resolves with the payment result. * @throws {Error} If the recipient is missing or the amount is invalid. */ async sendPayment(payment: PaymentParams): Promise<any> { if (payment.recipient == null || payment.recipient.trim() === '' || payment.amount <= 0) { throw new Error('Invalid payment details: recipient and valid amount are required') } const paymentToken = await this.createPaymentToken(payment) // Ensure the recipient is included before sending await this.sendMessage({ recipient: payment.recipient, messageBox: STANDARD_PAYMENT_MESSAGEBOX, body: paymentToken }) } /** * Sends Bitcoin to a PeerPay recipient over WebSockets. * * This function generates a payment token and transmits it over WebSockets * using `sendLiveMessage`. The recipient’s identity key is explicitly included * to ensure proper message routing. * * @param {PaymentParams} payment - The payment details. * @param {string} payment.recipient - The recipient's identity key. * @param {number} payment.amount - The amount in satoshis to send. * @returns {Promise<void>} Resolves when the payment has been sent. * @throws {Error} If payment token generation fails. */ async sendLivePayment(payment: PaymentParams): Promise<void> { const paymentToken = await this.createPaymentToken(payment) // Ensure the recipient is included before sending await this.sendLiveMessage({ recipient: payment.recipient, messageBox: STANDARD_PAYMENT_MESSAGEBOX, body: paymentToken }) } /** * Listens for incoming Bitcoin payments over WebSockets. * * This function listens for messages in the standard payment message box and * converts incoming `PeerMessage` objects into `IncomingPayment` objects * before invoking the `onPayment` callback. * * @param {Object} obj - The configuration object. * @param {Function} obj.onPayment - Callback function triggered when a payment is received. * @returns {Promise<void>} Resolves when the listener is successfully set up. */ async listenForLivePayments({ onPayment }: { onPayment: (payment: IncomingPayment) => void }): Promise<void> { await this.listenForLiveMessages({ messageBox: STANDARD_PAYMENT_MESSAGEBOX, // Convert PeerMessage → IncomingPayment before calling onPayment onMessage: (message: PeerMessage) => { Logger.log('[MB CLIENT] Received Live Payment:', message); const incomingPayment: IncomingPayment = { messageId: message.messageId, sender: message.sender, token: JSON.parse(message.body) } Logger.log('[PP CLIENT] Converted PeerMessage to IncomingPayment:', incomingPayment) onPayment(incomingPayment) } }) } /** * Accepts an incoming Bitcoin payment and moves it into the default wallet basket. * * This function processes a received payment by submitting it for internalization * using the wallet client's `internalizeAction` method. The payment details * are extracted from the `IncomingPayment` object. * * @param {IncomingPayment} payment - The payment object containing transaction details. * @returns {Promise<any>} Resolves with the payment result if successful. * @throws {Error} If payment processing fails. */ async acceptPayment(payment: IncomingPayment): Promise<any> { try { Logger.log(`[PP CLIENT] Processing payment: ${JSON.stringify(payment, null, 2)}`) const paymentResult = await this.peerPayWalletClient.internalizeAction({ tx: payment.token.transaction, outputs: [{ paymentRemittance: { derivationPrefix: payment.token.customInstructions.derivationPrefix, derivationSuffix: payment.token.customInstructions.derivationSuffix, senderIdentityKey: payment.sender }, outputIndex: STANDARD_PAYMENT_OUTPUT_INDEX, protocol: 'wallet payment' }], description: 'PeerPay Payment' }) Logger.log(`[PP CLIENT] Payment internalized successfully: ${JSON.stringify(paymentResult, null, 2)}`) Logger.log(`[PP CLIENT] Acknowledging payment with messageId: ${payment.messageId}`) await this.acknowledgeMessage({ messageIds: [payment.messageId] }) return { payment, paymentResult } } catch (error) { Logger.error(`[PP CLIENT] Error accepting payment: ${String(error)}`) return 'Unable to receive payment!' } } /** * Rejects an incoming Bitcoin payment by refunding it to the sender, minus a fee. * * If the payment amount is too small (less than 1000 satoshis after deducting the fee), * the payment is simply acknowledged and ignored. Otherwise, the function first accepts * the payment, then sends a new transaction refunding the sender. * * @param {IncomingPayment} payment - The payment object containing transaction details. * @returns {Promise<void>} Resolves when the payment is either acknowledged or refunded. */ async rejectPayment(payment: IncomingPayment): Promise<void> { Logger.log(`[PP CLIENT] Rejecting payment: ${JSON.stringify(payment, null, 2)}`); if (payment.token.amount - 1000 < 1000) { Logger.log(`[PP CLIENT] Payment amount too small after fee, just acknowledging.`); try { Logger.log(`[PP CLIENT] Attempting to acknowledge message ${payment.messageId}...`); if (!this.authFetch) { Logger.warn('[PP CLIENT] Warning: authFetch is undefined! Ensure PeerPayClient is initialized correctly.'); } Logger.log(`[PP CLIENT] authFetch instance:`, this.authFetch); const response = await this.acknowledgeMessage({ messageIds: [payment.messageId] }); Logger.log(`[PP CLIENT] Acknowledgment response: ${response}`); } catch (error: any) { if (error.message.includes('401')) { Logger.warn(`[PP CLIENT] Authentication issue while acknowledging: ${error.message}`); } else { Logger.error(`[PP CLIENT] Error acknowledging message: ${error.message}`); throw error; // Only throw if it's another type of error } } return; } Logger.log('[PP CLIENT] Accepting payment before refunding...'); await this.acceptPayment(payment); Logger.log(`[PP CLIENT] Sending refund of ${payment.token.amount - 1000} to ${payment.sender}...`); await this.sendPayment({ recipient: payment.sender, amount: payment.token.amount - 1000 // Deduct fee }); Logger.log('[PP CLIENT] Payment successfully rejected and refunded.'); try { Logger.log(`[PP CLIENT] Acknowledging message ${payment.messageId} after refunding...`); await this.acknowledgeMessage({ messageIds: [payment.messageId] }); Logger.log(`[PP CLIENT] Acknowledgment after refund successful.`); } catch (error: any) { Logger.error(`[PP CLIENT] Error acknowledging message after refund: ${error.message}`); } } /** * Retrieves a list of incoming Bitcoin payments from the message box. * * This function queries the message box for new messages and transforms * them into `IncomingPayment` objects by extracting relevant fields. * * @returns {Promise<IncomingPayment[]>} Resolves with an array of pending payments. */ async listIncomingPayments(): Promise<IncomingPayment[]> { const messages = await this.listMessages({ messageBox: STANDARD_PAYMENT_MESSAGEBOX }) return messages.map((msg: any) => ({ messageId: msg.messageId, sender: msg.sender, token: JSON.parse(msg.body) })) } }