@bsv/message-box-client 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,386 @@
1
+ /**
2
+ * PeerPayClient
3
+ *
4
+ * Extends `MessageBoxClient` to enable Bitcoin payments using the MetaNet identity system.
5
+ *
6
+ * This client handles payment token creation, message transmission over HTTP/WebSocket,
7
+ * payment reception (including acceptance and rejection logic), and listing of pending payments.
8
+ *
9
+ * It uses authenticated and encrypted message transmission to ensure secure payment flows
10
+ * between identified peers on the BSV network.
11
+ */
12
+
13
+ import { MessageBoxClient } from './MessageBoxClient.js'
14
+ import { PeerMessage } from './types.js'
15
+ import { WalletClient, P2PKH, PublicKey, createNonce, AtomicBEEF, AuthFetch, Base64String } from '@bsv/sdk'
16
+ import { Logger } from './Utils/logger.js'
17
+
18
+ function safeParse<T> (input: any): T {
19
+ try {
20
+ return typeof input === 'string' ? JSON.parse(input) : input
21
+ } catch (e) {
22
+ Logger.error('[PP CLIENT] Failed to parse input in safeParse:', input)
23
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
24
+ const fallback = {} as T
25
+ return fallback
26
+ }
27
+ }
28
+
29
+ export const STANDARD_PAYMENT_MESSAGEBOX = 'payment_inbox'
30
+ const STANDARD_PAYMENT_OUTPUT_INDEX = 0
31
+
32
+ /**
33
+ * Configuration options for initializing PeerPayClient.
34
+ */
35
+ export interface PeerPayClientConfig {
36
+ messageBoxHost?: string
37
+ walletClient: WalletClient
38
+ enableLogging?: boolean // Added optional logging flag
39
+ }
40
+
41
+ /**
42
+ * Represents the parameters required to initiate a payment.
43
+ */
44
+ export interface PaymentParams {
45
+ recipient: string
46
+ amount: number
47
+ }
48
+
49
+ /**
50
+ * Represents a structured payment token.
51
+ */
52
+ export interface PaymentToken {
53
+ customInstructions: {
54
+ derivationPrefix: Base64String
55
+ derivationSuffix: Base64String
56
+ }
57
+ transaction: AtomicBEEF
58
+ amount: number
59
+ }
60
+
61
+ /**
62
+ * Represents an incoming payment received via MessageBox.
63
+ */
64
+ export interface IncomingPayment {
65
+ messageId: string
66
+ sender: string
67
+ token: PaymentToken
68
+ }
69
+
70
+ /**
71
+ * PeerPayClient enables peer-to-peer Bitcoin payments using MessageBox.
72
+ */
73
+ export class PeerPayClient extends MessageBoxClient {
74
+ private readonly peerPayWalletClient: WalletClient
75
+ private _authFetchInstance?: AuthFetch
76
+
77
+ constructor (config: PeerPayClientConfig) {
78
+ const { messageBoxHost = 'https://messagebox.babbage.systems', walletClient, enableLogging = false } = config
79
+
80
+ // 🔹 Pass enableLogging to MessageBoxClient
81
+ super({ host: messageBoxHost, walletClient, enableLogging })
82
+
83
+ this.peerPayWalletClient = walletClient
84
+ }
85
+
86
+ private get authFetchInstance (): AuthFetch {
87
+ if (this._authFetchInstance === null || this._authFetchInstance === undefined) {
88
+ this._authFetchInstance = new AuthFetch(this.peerPayWalletClient)
89
+ }
90
+ return this._authFetchInstance
91
+ }
92
+
93
+ /**
94
+ * Generates a valid payment token for a recipient.
95
+ *
96
+ * This function derives a unique public key for the recipient, constructs a P2PKH locking script,
97
+ * and creates a payment action with the specified amount.
98
+ *
99
+ * @param {PaymentParams} payment - The payment details.
100
+ * @param {string} payment.recipient - The recipient's identity key.
101
+ * @param {number} payment.amount - The amount in satoshis to send.
102
+ * @returns {Promise<PaymentToken>} A valid payment token containing transaction details.
103
+ * @throws {Error} If the recipient's public key cannot be derived.
104
+ */
105
+ async createPaymentToken (payment: PaymentParams): Promise<PaymentToken> {
106
+ if (payment.amount <= 0) {
107
+ throw new Error('Invalid payment details: recipient and valid amount are required')
108
+ };
109
+
110
+ // Generate derivation paths using correct nonce function
111
+ const derivationPrefix = await createNonce(this.peerPayWalletClient)
112
+ const derivationSuffix = await createNonce(this.peerPayWalletClient)
113
+
114
+ Logger.log(`[PP CLIENT] Derivation Prefix: ${derivationPrefix}`)
115
+ Logger.log(`[PP CLIENT] Derivation Suffix: ${derivationSuffix}`)
116
+
117
+ // Get recipient's derived public key
118
+ const { publicKey: derivedKeyResult } = await this.peerPayWalletClient.getPublicKey({
119
+ protocolID: [2, '3241645161d8'],
120
+ keyID: `${derivationPrefix} ${derivationSuffix}`,
121
+ counterparty: payment.recipient
122
+ })
123
+
124
+ Logger.log(`[PP CLIENT] Derived Public Key: ${derivedKeyResult}`)
125
+
126
+ if (derivedKeyResult == null || derivedKeyResult.trim() === '') {
127
+ throw new Error('Failed to derive recipient’s public key')
128
+ }
129
+
130
+ // Create locking script using recipient's public key
131
+ const lockingScript = new P2PKH().lock(PublicKey.fromString(derivedKeyResult).toAddress()).toHex()
132
+
133
+ Logger.log(`[PP CLIENT] Locking Script: ${lockingScript}`)
134
+
135
+ // Create the payment action
136
+ const paymentAction = await this.peerPayWalletClient.createAction({
137
+ description: 'PeerPay payment',
138
+ outputs: [{
139
+ satoshis: payment.amount,
140
+ lockingScript,
141
+ customInstructions: JSON.stringify({
142
+ derivationPrefix,
143
+ derivationSuffix,
144
+ payee: payment.recipient
145
+ }),
146
+ outputDescription: 'Payment for PeerPay transaction'
147
+ }],
148
+ options: {
149
+ randomizeOutputs: false
150
+ }
151
+ })
152
+
153
+ if (paymentAction.tx === undefined) {
154
+ throw new Error('Transaction creation failed!')
155
+ }
156
+
157
+ Logger.log('[PP CLIENT] Payment Action:', paymentAction)
158
+
159
+ return {
160
+ customInstructions: {
161
+ derivationPrefix,
162
+ derivationSuffix
163
+ },
164
+ transaction: paymentAction.tx,
165
+ amount: payment.amount
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Sends Bitcoin to a PeerPay recipient.
171
+ *
172
+ * This function validates the payment details and delegates the transaction
173
+ * to `sendLivePayment` for processing.
174
+ *
175
+ * @param {PaymentParams} payment - The payment details.
176
+ * @param {string} payment.recipient - The recipient's identity key.
177
+ * @param {number} payment.amount - The amount in satoshis to send.
178
+ * @returns {Promise<any>} Resolves with the payment result.
179
+ * @throws {Error} If the recipient is missing or the amount is invalid.
180
+ */
181
+ async sendPayment (payment: PaymentParams): Promise<any> {
182
+ if (payment.recipient == null || payment.recipient.trim() === '' || payment.amount <= 0) {
183
+ throw new Error('Invalid payment details: recipient and valid amount are required')
184
+ }
185
+
186
+ const paymentToken = await this.createPaymentToken(payment)
187
+
188
+ // Ensure the recipient is included before sending
189
+ await this.sendMessage({
190
+ recipient: payment.recipient,
191
+ messageBox: STANDARD_PAYMENT_MESSAGEBOX,
192
+ body: JSON.stringify(paymentToken)
193
+ })
194
+ }
195
+
196
+ /**
197
+ * Sends Bitcoin to a PeerPay recipient over WebSockets.
198
+ *
199
+ * This function generates a payment token and transmits it over WebSockets
200
+ * using `sendLiveMessage`. The recipient’s identity key is explicitly included
201
+ * to ensure proper message routing.
202
+ *
203
+ * @param {PaymentParams} payment - The payment details.
204
+ * @param {string} payment.recipient - The recipient's identity key.
205
+ * @param {number} payment.amount - The amount in satoshis to send.
206
+ * @returns {Promise<void>} Resolves when the payment has been sent.
207
+ * @throws {Error} If payment token generation fails.
208
+ */
209
+ async sendLivePayment (payment: PaymentParams): Promise<void> {
210
+ const paymentToken = await this.createPaymentToken(payment)
211
+
212
+ try {
213
+ // Attempt WebSocket first
214
+ await this.sendLiveMessage({
215
+ recipient: payment.recipient,
216
+ messageBox: STANDARD_PAYMENT_MESSAGEBOX,
217
+ body: JSON.stringify(paymentToken)
218
+ })
219
+ } catch (err) {
220
+ Logger.warn('[PP CLIENT] sendLiveMessage failed, falling back to HTTP:', err)
221
+
222
+ // Fallback to HTTP if WebSocket fails
223
+ await this.sendMessage({
224
+ recipient: payment.recipient,
225
+ messageBox: STANDARD_PAYMENT_MESSAGEBOX,
226
+ body: JSON.stringify(paymentToken)
227
+ })
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Listens for incoming Bitcoin payments over WebSockets.
233
+ *
234
+ * This function listens for messages in the standard payment message box and
235
+ * converts incoming `PeerMessage` objects into `IncomingPayment` objects
236
+ * before invoking the `onPayment` callback.
237
+ *
238
+ * @param {Object} obj - The configuration object.
239
+ * @param {Function} obj.onPayment - Callback function triggered when a payment is received.
240
+ * @returns {Promise<void>} Resolves when the listener is successfully set up.
241
+ */
242
+ async listenForLivePayments ({
243
+ onPayment
244
+ }: { onPayment: (payment: IncomingPayment) => void }): Promise<void> {
245
+ await this.listenForLiveMessages({
246
+ messageBox: STANDARD_PAYMENT_MESSAGEBOX,
247
+
248
+ // Convert PeerMessage → IncomingPayment before calling onPayment
249
+ onMessage: (message: PeerMessage) => {
250
+ Logger.log('[MB CLIENT] Received Live Payment:', message)
251
+ const incomingPayment: IncomingPayment = {
252
+ messageId: message.messageId,
253
+ sender: message.sender,
254
+ token: safeParse<PaymentToken>(message.body)
255
+ }
256
+ Logger.log('[PP CLIENT] Converted PeerMessage to IncomingPayment:', incomingPayment)
257
+ onPayment(incomingPayment)
258
+ }
259
+ })
260
+ }
261
+
262
+ /**
263
+ * Accepts an incoming Bitcoin payment and moves it into the default wallet basket.
264
+ *
265
+ * This function processes a received payment by submitting it for internalization
266
+ * using the wallet client's `internalizeAction` method. The payment details
267
+ * are extracted from the `IncomingPayment` object.
268
+ *
269
+ * @param {IncomingPayment} payment - The payment object containing transaction details.
270
+ * @returns {Promise<any>} Resolves with the payment result if successful.
271
+ * @throws {Error} If payment processing fails.
272
+ */
273
+ async acceptPayment (payment: IncomingPayment): Promise<any> {
274
+ try {
275
+ Logger.log(`[PP CLIENT] Processing payment: ${JSON.stringify(payment, null, 2)}`)
276
+
277
+ const paymentResult = await this.peerPayWalletClient.internalizeAction({
278
+ tx: payment.token.transaction,
279
+ outputs: [{
280
+ paymentRemittance: {
281
+ derivationPrefix: payment.token.customInstructions.derivationPrefix,
282
+ derivationSuffix: payment.token.customInstructions.derivationSuffix,
283
+ senderIdentityKey: payment.sender
284
+ },
285
+ outputIndex: STANDARD_PAYMENT_OUTPUT_INDEX,
286
+ protocol: 'wallet payment'
287
+ }],
288
+ description: 'PeerPay Payment'
289
+ })
290
+
291
+ Logger.log(`[PP CLIENT] Payment internalized successfully: ${JSON.stringify(paymentResult, null, 2)}`)
292
+ Logger.log(`[PP CLIENT] Acknowledging payment with messageId: ${payment.messageId}`)
293
+
294
+ await this.acknowledgeMessage({ messageIds: [payment.messageId] })
295
+
296
+ return { payment, paymentResult }
297
+ } catch (error) {
298
+ Logger.error(`[PP CLIENT] Error accepting payment: ${String(error)}`)
299
+ return 'Unable to receive payment!'
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Rejects an incoming Bitcoin payment by refunding it to the sender, minus a fee.
305
+ *
306
+ * If the payment amount is too small (less than 1000 satoshis after deducting the fee),
307
+ * the payment is simply acknowledged and ignored. Otherwise, the function first accepts
308
+ * the payment, then sends a new transaction refunding the sender.
309
+ *
310
+ * @param {IncomingPayment} payment - The payment object containing transaction details.
311
+ * @returns {Promise<void>} Resolves when the payment is either acknowledged or refunded.
312
+ */
313
+ async rejectPayment (payment: IncomingPayment): Promise<void> {
314
+ Logger.log(`[PP CLIENT] Rejecting payment: ${JSON.stringify(payment, null, 2)}`)
315
+
316
+ if (payment.token.amount - 1000 < 1000) {
317
+ Logger.log('[PP CLIENT] Payment amount too small after fee, just acknowledging.')
318
+
319
+ try {
320
+ Logger.log(`[PP CLIENT] Attempting to acknowledge message ${payment.messageId}...`)
321
+ if (this.authFetch === null || this.authFetch === undefined) {
322
+ Logger.warn('[PP CLIENT] Warning: authFetch is undefined! Ensure PeerPayClient is initialized correctly.')
323
+ }
324
+ Logger.log('[PP CLIENT] authFetch instance:', this.authFetch)
325
+ const response = await this.acknowledgeMessage({ messageIds: [payment.messageId] })
326
+ Logger.log(`[PP CLIENT] Acknowledgment response: ${response}`)
327
+ } catch (error: any) {
328
+ if (
329
+ error != null &&
330
+ typeof error === 'object' &&
331
+ 'message' in error &&
332
+ typeof (error as { message: unknown }).message === 'string' &&
333
+ (error as { message: string }).message.includes('401')
334
+ ) {
335
+ Logger.warn(`[PP CLIENT] Authentication issue while acknowledging: ${(error as { message: string }).message}`)
336
+ } else {
337
+ Logger.error(`[PP CLIENT] Error acknowledging message: ${(error as { message: string }).message}`)
338
+ throw error // Only throw if it's another type of error
339
+ }
340
+ }
341
+
342
+ return
343
+ }
344
+
345
+ Logger.log('[PP CLIENT] Accepting payment before refunding...')
346
+ await this.acceptPayment(payment)
347
+
348
+ Logger.log(`[PP CLIENT] Sending refund of ${payment.token.amount - 1000} to ${payment.sender}...`)
349
+ await this.sendPayment({
350
+ recipient: payment.sender,
351
+ amount: payment.token.amount - 1000 // Deduct fee
352
+ })
353
+
354
+ Logger.log('[PP CLIENT] Payment successfully rejected and refunded.')
355
+
356
+ try {
357
+ Logger.log(`[PP CLIENT] Acknowledging message ${payment.messageId} after refunding...`)
358
+ await this.acknowledgeMessage({ messageIds: [payment.messageId] })
359
+ Logger.log('[PP CLIENT] Acknowledgment after refund successful.')
360
+ } catch (error: any) {
361
+ Logger.error(`[PP CLIENT] Error acknowledging message after refund: ${(error as { message: string }).message}`)
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Retrieves a list of incoming Bitcoin payments from the message box.
367
+ *
368
+ * This function queries the message box for new messages and transforms
369
+ * them into `IncomingPayment` objects by extracting relevant fields.
370
+ *
371
+ * @returns {Promise<IncomingPayment[]>} Resolves with an array of pending payments.
372
+ */
373
+ async listIncomingPayments (): Promise<IncomingPayment[]> {
374
+ const messages = await this.listMessages({ messageBox: STANDARD_PAYMENT_MESSAGEBOX })
375
+
376
+ return messages.map((msg: any) => {
377
+ const parsedToken = safeParse<PaymentToken>(msg.body)
378
+
379
+ return {
380
+ messageId: msg.messageId,
381
+ sender: msg.sender,
382
+ token: parsedToken
383
+ }
384
+ })
385
+ }
386
+ }
@@ -0,0 +1,27 @@
1
+ export class Logger {
2
+ private static isEnabled = false
3
+
4
+ static enable (): void {
5
+ this.isEnabled = true
6
+ }
7
+
8
+ static disable (): void {
9
+ this.isEnabled = false
10
+ }
11
+
12
+ static log (...args: unknown[]): void {
13
+ if (this.isEnabled) {
14
+ console.log(...args)
15
+ }
16
+ }
17
+
18
+ static warn (...args: unknown[]): void {
19
+ if (this.isEnabled) {
20
+ console.warn(...args)
21
+ }
22
+ }
23
+
24
+ static error (...args: unknown[]): void {
25
+ console.error(...args)
26
+ }
27
+ }