@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.
- package/LICENSE.txt +28 -0
- package/README.md +1013 -0
- package/mod.ts +3 -0
- package/package.json +80 -0
- package/src/MessageBoxClient.ts +1341 -0
- package/src/PeerPayClient.ts +386 -0
- package/src/Utils/logger.ts +27 -0
- package/src/__tests/MessageBoxClient.test.ts +763 -0
- package/src/__tests/PeerPayClientUnit.test.ts +245 -0
- package/src/__tests/integration/integrationEncrypted.test.ts +103 -0
- package/src/__tests/integration/integrationHTTP.test.ts +158 -0
- package/src/__tests/integration/integrationOverlay.test.ts +163 -0
- package/src/__tests/integration/integrationWS.test.ts +147 -0
- package/src/__tests/integration/testServer.ts +68 -0
- package/src/types.ts +108 -0
|
@@ -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
|
+
}
|