@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.
- package/dist/cjs/package.json +2 -2
- package/dist/cjs/src/MessageBoxClient.js +82 -31
- package/dist/cjs/src/MessageBoxClient.js.map +1 -1
- package/dist/cjs/src/PeerPayClient.js +58 -57
- package/dist/cjs/src/PeerPayClient.js.map +1 -1
- package/dist/cjs/src/__tests/MessageBoxClient.test.js +90 -0
- package/dist/cjs/src/__tests/MessageBoxClient.test.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/MessageBoxClient.js +81 -32
- package/dist/esm/src/MessageBoxClient.js.map +1 -1
- package/dist/esm/src/PeerPayClient.js +59 -57
- package/dist/esm/src/PeerPayClient.js.map +1 -1
- package/dist/esm/src/__tests/MessageBoxClient.test.js +91 -1
- package/dist/esm/src/__tests/MessageBoxClient.test.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/MessageBoxClient.d.ts.map +1 -1
- package/dist/types/src/PeerPayClient.d.ts +1 -0
- package/dist/types/src/PeerPayClient.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +1 -1
- package/package.json +2 -2
- package/src/MessageBoxClient.ts +141 -86
- package/src/PeerPayClient.ts +69 -63
- package/src/__tests/MessageBoxClient.test.ts +129 -16
package/src/PeerPayClient.ts
CHANGED
|
@@ -12,10 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
import { MessageBoxClient } from './MessageBoxClient.js'
|
|
14
14
|
import { PeerMessage } from './types.js'
|
|
15
|
-
import { WalletInterface,
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
149
|
+
)
|
|
160
150
|
|
|
161
|
-
if (
|
|
162
|
-
|
|
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:',
|
|
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:
|
|
173
|
-
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
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
172
|
-
|
|
171
|
+
// Manually set identity key
|
|
172
|
+
; (messageBoxClient as any).myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
|
|
173
173
|
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
734
|
-
|
|
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
|
})
|