@bsv/message-box-client 2.0.4 → 2.0.6

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.
@@ -12,10 +12,14 @@
12
12
 
13
13
  import { MessageBoxClient } from './MessageBoxClient.js'
14
14
  import { PeerMessage } from './types.js'
15
- import { WalletInterface, P2PKH, PublicKey, createNonce, AtomicBEEF, AuthFetch, Base64String, OriginatorDomainNameStringUnder250Bytes } from '@bsv/sdk'
15
+ import { WalletInterface, AtomicBEEF, AuthFetch, Base64String, OriginatorDomainNameStringUnder250Bytes, Brc29RemittanceModule } from '@bsv/sdk'
16
16
 
17
17
  import * as Logger from './Utils/logger.js'
18
18
 
19
+ function toNumberArray (tx: AtomicBEEF): number[] {
20
+ return Array.isArray(tx) ? tx : Array.from(tx)
21
+ }
22
+
19
23
  function safeParse<T> (input: any): T {
20
24
  try {
21
25
  return typeof input === 'string' ? JSON.parse(input) : input
@@ -79,6 +83,7 @@ export class PeerPayClient extends MessageBoxClient {
79
83
  private readonly peerPayWalletClient: WalletInterface
80
84
  private _authFetchInstance?: AuthFetch
81
85
  private readonly messageBox: string
86
+ private readonly settlementModule: Brc29RemittanceModule
82
87
 
83
88
  constructor (config: PeerPayClientConfig) {
84
89
  const { messageBoxHost = 'https://messagebox.babbage.systems', walletClient, enableLogging = false, originator } = config
@@ -89,6 +94,16 @@ export class PeerPayClient extends MessageBoxClient {
89
94
  this.messageBox = config.messageBox ?? STANDARD_PAYMENT_MESSAGEBOX
90
95
  this.peerPayWalletClient = walletClient
91
96
  this.originator = originator
97
+
98
+ this.settlementModule = new Brc29RemittanceModule({
99
+ protocolID: [2, '3241645161d8'],
100
+ labels: ['peerpay'],
101
+ description: 'PeerPay payment',
102
+ outputDescription: 'Payment for PeerPay transaction',
103
+ internalizeProtocol: 'wallet payment',
104
+ refundFeeSatoshis: 1000,
105
+ minRefundSatoshis: 1000
106
+ })
92
107
  }
93
108
 
94
109
  private get authFetchInstance (): AuthFetch {
@@ -115,62 +130,40 @@ export class PeerPayClient extends MessageBoxClient {
115
130
  throw new Error('Invalid payment details: recipient and valid amount are required')
116
131
  };
117
132
 
118
- // Generate derivation paths using correct nonce function
119
- const derivationPrefix = await createNonce(this.peerPayWalletClient, 'self', this.originator)
120
- const derivationSuffix = await createNonce(this.peerPayWalletClient, 'self', this.originator)
121
-
122
- Logger.log(`[PP CLIENT] Derivation Prefix: ${derivationPrefix}`)
123
- Logger.log(`[PP CLIENT] Derivation Suffix: ${derivationSuffix}`)
124
- // Get recipient's derived public key
125
- const { publicKey: derivedKeyResult } = await this.peerPayWalletClient.getPublicKey({
126
- protocolID: [2, '3241645161d8'],
127
- keyID: `${derivationPrefix} ${derivationSuffix}`,
128
- counterparty: payment.recipient
129
- }, this.originator)
130
-
131
- Logger.log(`[PP CLIENT] Derived Public Key: ${derivedKeyResult}`)
132
-
133
- if (derivedKeyResult == null || derivedKeyResult.trim() === '') {
134
- throw new Error('Failed to derive recipient’s public key')
135
- }
136
-
137
- // Create locking script using recipient's public key
138
- const lockingScript = new P2PKH().lock(PublicKey.fromString(derivedKeyResult).toAddress()).toHex()
139
-
140
- Logger.log(`[PP CLIENT] Locking Script: ${lockingScript}`)
141
-
142
- // Create the payment action
143
- const paymentAction = await this.peerPayWalletClient.createAction({
144
- description: 'PeerPay payment',
145
- labels: ['peerpay'],
146
- outputs: [{
147
- satoshis: payment.amount,
148
- lockingScript,
149
- customInstructions: JSON.stringify({
150
- derivationPrefix,
151
- derivationSuffix,
152
- payee: payment.recipient
153
- }),
154
- outputDescription: 'Payment for PeerPay transaction'
155
- }],
156
- options: {
157
- randomizeOutputs: false
133
+ const result = await this.settlementModule.buildSettlement(
134
+ {
135
+ threadId: 'peerpay',
136
+ option: {
137
+ amountSatoshis: payment.amount,
138
+ payee: payment.recipient,
139
+ labels: ['peerpay'],
140
+ description: 'PeerPay payment'
141
+ }
142
+ },
143
+ {
144
+ wallet: this.peerPayWalletClient,
145
+ originator: this.originator,
146
+ now: () => Date.now(),
147
+ logger: Logger
158
148
  }
159
- }, this.originator)
149
+ )
160
150
 
161
- if (paymentAction.tx === undefined) {
162
- throw new Error('Transaction creation failed!')
151
+ if (result.action === 'terminate') {
152
+ if (result.termination.code === 'brc29.public_key_missing') {
153
+ throw new Error('Failed to derive recipient’s public key')
154
+ }
155
+ throw new Error(result.termination.message)
163
156
  }
164
157
 
165
- Logger.log('[PP CLIENT] Payment Action:', paymentAction)
158
+ Logger.log('[PP CLIENT] Payment Action Settlement Artifact:', result.artifact)
166
159
 
167
160
  return {
168
161
  customInstructions: {
169
- derivationPrefix,
170
- derivationSuffix
162
+ derivationPrefix: result.artifact.customInstructions.derivationPrefix as Base64String,
163
+ derivationSuffix: result.artifact.customInstructions.derivationSuffix as Base64String
171
164
  },
172
- transaction: paymentAction.tx,
173
- amount: payment.amount
165
+ transaction: result.artifact.transaction as AtomicBEEF,
166
+ amount: result.artifact.amountSatoshis
174
167
  }
175
168
  }
176
169
 
@@ -290,20 +283,33 @@ export class PeerPayClient extends MessageBoxClient {
290
283
  try {
291
284
  Logger.log(`[PP CLIENT] Processing payment: ${JSON.stringify(payment, null, 2)}`)
292
285
 
293
- const paymentResult = await this.peerPayWalletClient.internalizeAction({
294
- tx: payment.token.transaction,
295
- outputs: [{
296
- paymentRemittance: {
297
- derivationPrefix: payment.token.customInstructions.derivationPrefix,
298
- derivationSuffix: payment.token.customInstructions.derivationSuffix,
299
- senderIdentityKey: payment.sender
300
- },
301
- outputIndex: payment.token.outputIndex ?? STANDARD_PAYMENT_OUTPUT_INDEX,
302
- protocol: 'wallet payment'
303
- }],
304
- labels: ['peerpay'],
305
- description: 'PeerPay Payment'
306
- }, this.originator)
286
+ const acceptResult = await this.settlementModule.acceptSettlement(
287
+ {
288
+ threadId: 'peerpay',
289
+ sender: payment.sender,
290
+ settlement: {
291
+ customInstructions: {
292
+ derivationPrefix: payment.token.customInstructions.derivationPrefix,
293
+ derivationSuffix: payment.token.customInstructions.derivationSuffix
294
+ },
295
+ transaction: toNumberArray(payment.token.transaction),
296
+ amountSatoshis: payment.token.amount,
297
+ outputIndex: payment.token.outputIndex ?? STANDARD_PAYMENT_OUTPUT_INDEX
298
+ }
299
+ },
300
+ {
301
+ wallet: this.peerPayWalletClient,
302
+ originator: this.originator,
303
+ now: () => Date.now(),
304
+ logger: Logger
305
+ }
306
+ )
307
+
308
+ if (acceptResult.action === 'terminate') {
309
+ throw new Error(acceptResult.termination.message)
310
+ }
311
+
312
+ const paymentResult = acceptResult.receiptData?.internalizeResult
307
313
 
308
314
  Logger.log(`[PP CLIENT] Payment internalized successfully: ${JSON.stringify(paymentResult, null, 2)}`)
309
315
  Logger.log(`[PP CLIENT] Acknowledging payment with messageId: ${payment.messageId}`)
@@ -1,6 +1,6 @@
1
1
  /* eslint-env jest */
2
2
  import { MessageBoxClient } from '../MessageBoxClient.js'
3
- import { WalletClient, AuthFetch, Transaction, LockingScript } from '@bsv/sdk'
3
+ import { WalletClient, AuthFetch, Transaction, LockingScript, PushDrop, TopicBroadcaster, Beef } from '@bsv/sdk'
4
4
 
5
5
  // MOCK: WalletClient methods globally
6
6
  jest.spyOn(WalletClient.prototype, 'createHmac').mockResolvedValue({
@@ -168,11 +168,11 @@ describe('MessageBoxClient', () => {
168
168
  // Bypass the real connection logic
169
169
  jest.spyOn(messageBoxClient, 'initializeConnection').mockImplementation(async () => { })
170
170
 
171
- // Manually set identity key
172
- ; (messageBoxClient as any).myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
171
+ // Manually set identity key
172
+ ; (messageBoxClient as any).myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
173
173
 
174
- // Simulate WebSocket not initialized
175
- ; (messageBoxClient as any).socket = null
174
+ // Simulate WebSocket not initialized
175
+ ; (messageBoxClient as any).socket = null
176
176
 
177
177
  // Expect it to fall back to HTTP and succeed
178
178
  const result = await messageBoxClient.sendLiveMessage({
@@ -282,7 +282,7 @@ describe('MessageBoxClient', () => {
282
282
  enableLogging: true
283
283
  })
284
284
  await messageBoxClient.init()
285
- ; (messageBoxClient as any).myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
285
+ ; (messageBoxClient as any).myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
286
286
  jest.spyOn(messageBoxClient.authFetch, 'fetch').mockResolvedValue({
287
287
  json: async () => ({
288
288
  status: 'success',
@@ -309,7 +309,7 @@ describe('MessageBoxClient', () => {
309
309
  enableLogging: true
310
310
  })
311
311
  await messageBoxClient.init()
312
- ; (messageBoxClient as any).myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
312
+ ; (messageBoxClient as any).myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
313
313
 
314
314
  jest.spyOn(messageBoxClient.authFetch, 'fetch').mockResolvedValue({
315
315
  json: async () => JSON.parse(VALID_LIST_AND_READ_RESULT.body),
@@ -330,7 +330,7 @@ describe('MessageBoxClient', () => {
330
330
  enableLogging: true
331
331
  })
332
332
  await messageBoxClient.init()
333
- ; (messageBoxClient as any).myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
333
+ ; (messageBoxClient as any).myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
334
334
 
335
335
  jest.spyOn(messageBoxClient.authFetch, 'fetch').mockResolvedValue({
336
336
  json: async () => JSON.parse(VALID_ACK_RESULT.body),
@@ -352,7 +352,7 @@ describe('MessageBoxClient', () => {
352
352
  })
353
353
  await messageBoxClient.init()
354
354
 
355
- ; (messageBoxClient as any).myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
355
+ ; (messageBoxClient as any).myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
356
356
 
357
357
  jest.spyOn(messageBoxClient.authFetch, 'fetch')
358
358
  .mockResolvedValue({
@@ -425,7 +425,7 @@ describe('MessageBoxClient', () => {
425
425
  enableLogging: true
426
426
  })
427
427
  await messageBoxClient.init()
428
- ; (messageBoxClient as any).myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
428
+ ; (messageBoxClient as any).myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
429
429
 
430
430
  jest.spyOn(messageBoxClient.authFetch, 'fetch')
431
431
  .mockResolvedValue({
@@ -715,10 +715,10 @@ describe('MessageBoxClient', () => {
715
715
  })
716
716
  await client.init()
717
717
 
718
- // For this ONE call return two adverts – the first is selected
719
- ; (MessageBoxClient.prototype as any).queryAdvertisements
720
- .mockResolvedValueOnce([
721
- { host: 'https://peer.box' }, { host: 'https://second.box' }])
718
+ // For this ONE call return two adverts – the first is selected
719
+ ; (MessageBoxClient.prototype as any).queryAdvertisements
720
+ .mockResolvedValueOnce([
721
+ { host: 'https://peer.box' }, { host: 'https://second.box' }])
722
722
 
723
723
  const result = await client.resolveHostForRecipient('02aa…deadbeef')
724
724
  expect(result).toBe('https://peer.box')
@@ -730,11 +730,124 @@ describe('MessageBoxClient', () => {
730
730
  host: 'https://default.box'
731
731
  })
732
732
  await client.init()
733
- ; (MessageBoxClient.prototype as any).queryAdvertisements
734
- .mockResolvedValueOnce([])
733
+ ; (MessageBoxClient.prototype as any).queryAdvertisements
734
+ .mockResolvedValueOnce([])
735
735
 
736
736
  const result = await client.resolveHostForRecipient('03bb…cafef00d')
737
737
 
738
738
  expect(result).toBe('https://default.box')
739
739
  })
740
+
741
+ it('anointHost queries advertisements for existing host records', async () => {
742
+ const anointMock = MessageBoxClient.prototype.anointHost as unknown as jest.Mock
743
+ anointMock.mockRestore()
744
+
745
+ const client = new MessageBoxClient({
746
+ walletClient: mockWalletClient,
747
+ host: 'https://default.box'
748
+ })
749
+
750
+ const querySpy = jest.spyOn(client as any, 'queryAdvertisements').mockResolvedValue([])
751
+ jest.spyOn(mockWalletClient, 'listOutputs').mockResolvedValue({ outputs: [] } as any)
752
+ jest.spyOn(PushDrop.prototype, 'lock').mockResolvedValue({
753
+ toHex: () => '00',
754
+ toASM: () => 'OP_FALSE'
755
+ } as any)
756
+ jest.spyOn(Transaction, 'fromAtomicBEEF').mockReturnValue({} as any)
757
+ jest.spyOn(TopicBroadcaster.prototype as any, 'broadcast').mockResolvedValue({ txid: 'broadcasted-txid' })
758
+
759
+ await client.anointHost('https://fresh.host')
760
+
761
+ expect(querySpy).toHaveBeenCalledWith(
762
+ '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
763
+ )
764
+ })
765
+
766
+ it('anointHost signs all revoke inputs when multiple spendable advertisements exist', async () => {
767
+ const client = new MessageBoxClient({
768
+ walletClient: mockWalletClient,
769
+ host: 'https://default.box'
770
+ })
771
+
772
+ const tokenA = {
773
+ host: 'http://localhost:8080',
774
+ txid: 'tx-a',
775
+ outputIndex: 0,
776
+ lockingScript: {} as any,
777
+ beef: [1, 2, 3]
778
+ }
779
+ const tokenB = {
780
+ host: 'https://valid.host',
781
+ txid: 'tx-b',
782
+ outputIndex: 1,
783
+ lockingScript: {} as any,
784
+ beef: [4, 5, 6]
785
+ }
786
+
787
+ jest.spyOn(client as any, 'queryAdvertisements').mockResolvedValue([tokenA, tokenB])
788
+ jest.spyOn(mockWalletClient, 'listOutputs').mockResolvedValue({
789
+ outputs: [
790
+ { spendable: true, outpoint: 'tx-a.0' },
791
+ { spendable: true, outpoint: 'tx-b.1' }
792
+ ]
793
+ } as any)
794
+
795
+ jest.spyOn(PushDrop.prototype, 'lock').mockResolvedValue({
796
+ toHex: () => '00',
797
+ toASM: () => 'OP_FALSE'
798
+ } as any)
799
+
800
+ const unlockSignMock = jest
801
+ .fn()
802
+ .mockImplementation(async (_tx: unknown, inputIndex: number) => ({ toHex: () => `unlock-${inputIndex}` }))
803
+ jest.spyOn(PushDrop.prototype, 'unlock').mockReturnValue({ sign: unlockSignMock } as any)
804
+
805
+ const mergedBeef = {
806
+ mergeBeef: jest.fn(),
807
+ toBinary: jest.fn().mockReturnValue([9, 9, 9])
808
+ }
809
+ jest.spyOn(Beef, 'fromBinary').mockImplementation(() => mergedBeef as any)
810
+
811
+ const fromAtomicSpy = jest.spyOn(Transaction, 'fromAtomicBEEF')
812
+ fromAtomicSpy
813
+ .mockReturnValueOnce({} as any)
814
+ .mockReturnValueOnce({} as any)
815
+ jest.spyOn(Transaction, 'fromBEEF').mockReturnValue({ outputs: [{ satoshis: 1 }, { satoshis: 1 }] } as any)
816
+
817
+ const createActionSpy = jest.spyOn(mockWalletClient, 'createAction').mockResolvedValue({
818
+ signableTransaction: {
819
+ tx: [7, 7, 7],
820
+ reference: 'sign-ref'
821
+ }
822
+ } as any)
823
+ const signActionSpy = jest.spyOn(mockWalletClient, 'signAction').mockResolvedValue({
824
+ tx: [8, 8, 8],
825
+ txid: 'signed-txid'
826
+ } as any)
827
+ jest.spyOn(TopicBroadcaster.prototype as any, 'broadcast').mockResolvedValue({ txid: 'broadcasted-txid' })
828
+
829
+ const result = await client.anointHost('https://new.host')
830
+
831
+ expect(createActionSpy).toHaveBeenCalledWith(
832
+ expect.objectContaining({
833
+ inputBEEF: [9, 9, 9],
834
+ inputs: [
835
+ expect.objectContaining({ outpoint: 'tx-a.0' }),
836
+ expect.objectContaining({ outpoint: 'tx-b.1' })
837
+ ]
838
+ }),
839
+ undefined
840
+ )
841
+ expect(signActionSpy).toHaveBeenCalledWith(
842
+ expect.objectContaining({
843
+ reference: 'sign-ref',
844
+ spends: {
845
+ 0: { unlockingScript: 'unlock-0' },
846
+ 1: { unlockingScript: 'unlock-1' }
847
+ }
848
+ }),
849
+ undefined
850
+ )
851
+ expect(result).toEqual({ txid: 'broadcasted-txid' })
852
+ })
740
853
  })