UNPKG

@bsv/message-box-client

Version:

A client for P2P messaging and payments

348 lines • 16.5 kB
"use strict"; /** * PeerPayClient * * Extends `MessageBoxClient` to enable Bitcoin payments using the MetaNet identity system. * * This client handles payment token creation, message transmission over HTTP/WebSocket, * payment reception (including acceptance and rejection logic), and listing of pending payments. * * It uses authenticated and encrypted message transmission to ensure secure payment flows * between identified peers on the BSV network. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.PeerPayClient = exports.STANDARD_PAYMENT_MESSAGEBOX = void 0; const MessageBoxClient_js_1 = require("./MessageBoxClient.js"); const sdk_1 = require("@bsv/sdk"); const Logger = __importStar(require("./Utils/logger.js")); function safeParse(input) { try { return typeof input === 'string' ? JSON.parse(input) : input; } catch (e) { Logger.error('[PP CLIENT] Failed to parse input in safeParse:', input); // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const fallback = {}; return fallback; } } exports.STANDARD_PAYMENT_MESSAGEBOX = 'payment_inbox'; const STANDARD_PAYMENT_OUTPUT_INDEX = 0; /** * PeerPayClient enables peer-to-peer Bitcoin payments using MessageBox. */ class PeerPayClient extends MessageBoxClient_js_1.MessageBoxClient { constructor(config) { const { messageBoxHost = 'https://messagebox.babbage.systems', walletClient, enableLogging = false, originator } = config; // 🔹 Pass enableLogging to MessageBoxClient super({ host: messageBoxHost, walletClient, enableLogging, originator }); this.peerPayWalletClient = walletClient; this.originator = originator; } get authFetchInstance() { if (this._authFetchInstance === null || this._authFetchInstance === undefined) { this._authFetchInstance = new sdk_1.AuthFetch(this.peerPayWalletClient, undefined, undefined, this.originator); } 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) { 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 (0, sdk_1.createNonce)(this.peerPayWalletClient); const derivationSuffix = await (0, sdk_1.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 }, this.originator); 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 sdk_1.P2PKH().lock(sdk_1.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 } }, this.originator); 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. * @param {string} [hostOverride] - Optional host override for the message box server. * @returns {Promise<any>} Resolves with the payment result. * @throws {Error} If the recipient is missing or the amount is invalid. */ async sendPayment(payment, hostOverride) { 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 sendings await this.sendMessage({ recipient: payment.recipient, messageBox: exports.STANDARD_PAYMENT_MESSAGEBOX, body: JSON.stringify(paymentToken) }, hostOverride); } /** * 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. * @param {string} [overrideHost] - Optional host override for WebSocket connection. * @returns {Promise<void>} Resolves when the payment has been sent. * @throws {Error} If payment token generation fails. */ async sendLivePayment(payment, overrideHost) { const paymentToken = await this.createPaymentToken(payment); try { // Attempt WebSocket first await this.sendLiveMessage({ recipient: payment.recipient, messageBox: exports.STANDARD_PAYMENT_MESSAGEBOX, body: JSON.stringify(paymentToken), }, overrideHost); } catch (err) { Logger.warn('[PP CLIENT] sendLiveMessage failed, falling back to HTTP:', err); // Fallback to HTTP if WebSocket fails await this.sendMessage({ recipient: payment.recipient, messageBox: exports.STANDARD_PAYMENT_MESSAGEBOX, body: JSON.stringify(paymentToken), }, overrideHost); } } /** * 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. * @param {string} [obj.overrideHost] - Optional host override for WebSocket connection. * @returns {Promise<void>} Resolves when the listener is successfully set up. */ async listenForLivePayments({ onPayment, overrideHost, }) { await this.listenForLiveMessages({ messageBox: exports.STANDARD_PAYMENT_MESSAGEBOX, overrideHost, // Convert PeerMessage → IncomingPayment before calling onPayment onMessage: (message) => { Logger.log('[MB CLIENT] Received Live Payment:', message); const incomingPayment = { messageId: message.messageId, sender: message.sender, token: safeParse(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) { 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' }, this.originator); 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) { 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 === null || this.authFetch === undefined) { 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) { if (error != null && typeof error === 'object' && 'message' in error && typeof error.message === 'string' && 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) { 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. * * @param {string} [overrideHost] - Optional host override to list payments from * @returns {Promise<IncomingPayment[]>} Resolves with an array of pending payments. */ async listIncomingPayments(overrideHost) { const messages = await this.listMessages({ messageBox: exports.STANDARD_PAYMENT_MESSAGEBOX, host: overrideHost }); return messages.map((msg) => { const parsedToken = safeParse(msg.body); return { messageId: msg.messageId, sender: msg.sender, token: parsedToken }; }); } } exports.PeerPayClient = PeerPayClient; //# sourceMappingURL=PeerPayClient.js.map