@bsv/sdk 2.0.12 → 2.0.13
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 +1 -1
- package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js +827 -0
- package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
- package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +654 -0
- package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
- package/dist/cjs/src/transaction/MerklePath.js +132 -0
- package/dist/cjs/src/transaction/MerklePath.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js +825 -0
- package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
- package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +619 -0
- package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
- package/dist/esm/src/transaction/MerklePath.js +132 -0
- package/dist/esm/src/transaction/MerklePath.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts +21 -0
- package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts.map +1 -0
- package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts +2 -0
- package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts.map +1 -0
- package/dist/types/src/transaction/MerklePath.d.ts +27 -0
- package/dist/types/src/transaction/MerklePath.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +1 -1
- package/dist/umd/bundle.js.map +1 -1
- package/docs/reference/storage.md +1 -1
- package/docs/reference/transaction.md +40 -0
- package/package.json +1 -1
- package/src/auth/clients/__tests__/AuthFetch.additional.test.ts +1131 -0
- package/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.ts +770 -0
- package/src/compat/__tests/Mnemonic.additional.test.ts +64 -0
- package/src/identity/__tests/IdentityClient.additional.test.ts +767 -0
- package/src/kvstore/__tests/LocalKVStore.additional.test.ts +611 -0
- package/src/kvstore/__tests/kvStoreInterpreter.test.ts +327 -0
- package/src/overlay-tools/__tests/HostReputationTracker.additional.test.ts +561 -0
- package/src/overlay-tools/__tests/LookupResolver.additional.test.ts +612 -0
- package/src/overlay-tools/__tests/withDoubleSpendRetry.test.ts +278 -0
- package/src/primitives/__tests/BigNumber.additional.test.ts +79 -0
- package/src/primitives/__tests/Curve.additional.test.ts +208 -0
- package/src/primitives/__tests/ECDSA.additional.test.ts +122 -0
- package/src/primitives/__tests/Hash.additional.test.ts +59 -0
- package/src/primitives/__tests/JacobianPoint.test.ts +308 -0
- package/src/primitives/__tests/Point.additional.test.ts +503 -0
- package/src/primitives/__tests/PublicKey.additional.test.ts +383 -0
- package/src/primitives/__tests/Random.additional.test.ts +262 -0
- package/src/primitives/__tests/Signature.test.ts +333 -0
- package/src/primitives/__tests/TransactionSignature.additional.test.ts +241 -0
- package/src/registry/__tests/RegistryClient.additional.test.ts +750 -0
- package/src/remittance/__tests/BasicBRC29.additional.test.ts +657 -0
- package/src/remittance/__tests/RemittanceManager.additional.test.ts +1272 -0
- package/src/script/__tests/LockingUnlockingScript.test.ts +79 -0
- package/src/script/__tests/Script.additional.test.ts +100 -0
- package/src/script/__tests/ScriptEvaluationError.test.ts +98 -0
- package/src/script/__tests/Spend.additional.test.ts +837 -0
- package/src/script/templates/__tests/RPuzzle.test.ts +134 -0
- package/src/transaction/MerklePath.ts +155 -0
- package/src/transaction/__tests/BeefParty.additional.test.ts +22 -0
- package/src/transaction/__tests/Broadcaster.test.ts +159 -0
- package/src/transaction/__tests/MerklePath.bench.test.ts +105 -0
- package/src/transaction/__tests/MerklePath.test.ts +80 -0
- package/src/transaction/__tests/Transaction.additional.test.ts +225 -0
- package/src/transaction/broadcasters/__tests/ARC.additional.test.ts +585 -0
- package/src/transaction/broadcasters/__tests/Teranode.test.ts +349 -0
- package/src/transaction/chaintrackers/__tests/BlockHeadersService.test.ts +253 -0
- package/src/transaction/chaintrackers/__tests/DefaultChainTracker.test.ts +44 -0
- package/src/transaction/chaintrackers/__tests/WhatsOnChain.additional.test.ts +193 -0
- package/src/transaction/fee-models/__tests/SatoshisPerKilobyte.test.ts +262 -0
- package/src/transaction/http/__tests/BinaryFetchClient.test.ts +212 -0
- package/src/transaction/http/__tests/DefaultHttpClient.additional.test.ts +192 -0
- package/src/transaction/http/__tests/DefaultHttpClient.test.ts +71 -0
- package/src/wallet/__tests/ProtoWallet.additional.test.ts +134 -0
- package/src/wallet/__tests/WERR.test.ts +212 -0
- package/src/wallet/__tests/WalletClient.additional.test.ts +699 -0
- package/src/wallet/__tests/WalletClient.substrate.test.ts +759 -0
- package/src/wallet/__tests/WalletError.test.ts +290 -0
- package/src/wallet/__tests/validationHelpers.test.ts +1218 -0
- package/src/wallet/substrates/__tests/HTTPWalletJSON.test.ts +496 -0
- package/src/wallet/substrates/__tests/HTTPWalletWire.test.ts +273 -0
|
@@ -0,0 +1,1272 @@
|
|
|
1
|
+
import type { CommsLayer } from '../CommsLayer.js'
|
|
2
|
+
import type { IdentityLayer } from '../IdentityLayer.js'
|
|
3
|
+
import type { RemittanceModule } from '../RemittanceModule.js'
|
|
4
|
+
import type { ComposeInvoiceInput } from '../RemittanceManager.js'
|
|
5
|
+
import type { PeerMessage, RemittanceEnvelope, Termination, ThreadId } from '../types.js'
|
|
6
|
+
import type { WalletInterface, PubKeyHex } from '../../wallet/Wallet.interfaces.js'
|
|
7
|
+
import { RemittanceManager, DEFAULT_REMITTANCE_MESSAGEBOX } from '../RemittanceManager.js'
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Shared test infrastructure (mirrors the existing test file's helpers)
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
class MessageBus {
|
|
14
|
+
private messages: PeerMessage[] = []
|
|
15
|
+
private nextId = 1
|
|
16
|
+
|
|
17
|
+
send (sender: PubKeyHex, recipient: PubKeyHex, messageBox: string, body: string): string {
|
|
18
|
+
const messageId = `msg-${this.nextId++}`
|
|
19
|
+
this.messages.push({ messageId, sender, recipient, messageBox, body })
|
|
20
|
+
return messageId
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
list (recipient: PubKeyHex, messageBox: string): PeerMessage[] {
|
|
24
|
+
return this.messages.filter((m) => m.recipient === recipient && m.messageBox === messageBox)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
ack (recipient: PubKeyHex, messageIds: string[]): void {
|
|
28
|
+
this.messages = this.messages.filter(
|
|
29
|
+
(m) => m.recipient !== recipient || !messageIds.includes(m.messageId)
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
all (): PeerMessage[] { return [...this.messages] }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class TestComms implements CommsLayer {
|
|
37
|
+
constructor (private readonly owner: PubKeyHex, private readonly bus: MessageBus) {}
|
|
38
|
+
|
|
39
|
+
async sendMessage (args: { recipient: PubKeyHex; messageBox: string; body: string }): Promise<string> {
|
|
40
|
+
return this.bus.send(this.owner, args.recipient, args.messageBox, args.body)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async listMessages (args: { messageBox: string; host?: string }): Promise<PeerMessage[]> {
|
|
44
|
+
return this.bus.list(this.owner, args.messageBox)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async acknowledgeMessage (args: { messageIds: string[] }): Promise<void> {
|
|
48
|
+
this.bus.ack(this.owner, args.messageIds)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const makeWallet = (identityKey: PubKeyHex): WalletInterface =>
|
|
53
|
+
({ getPublicKey: async () => ({ publicKey: identityKey }) } as unknown as WalletInterface)
|
|
54
|
+
|
|
55
|
+
const makeInvoiceInput = (overrides: Partial<ComposeInvoiceInput> = {}): ComposeInvoiceInput => ({
|
|
56
|
+
lineItems: [],
|
|
57
|
+
total: { value: '1000', unit: { namespace: 'bsv', code: 'sat', decimals: 0 } },
|
|
58
|
+
note: 'Test invoice',
|
|
59
|
+
invoiceNumber: 'INV-1',
|
|
60
|
+
...overrides
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const parseEnvelope = (msg: PeerMessage): RemittanceEnvelope => JSON.parse(msg.body) as RemittanceEnvelope
|
|
64
|
+
|
|
65
|
+
const makeThreadIdFactory = (): (() => ThreadId) => {
|
|
66
|
+
let i = 0
|
|
67
|
+
return () => `thread-${++i}` as ThreadId
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const tick = async (): Promise<void> => await new Promise((resolve) => setTimeout(resolve, 0))
|
|
71
|
+
|
|
72
|
+
const makeModule = (overrides: Partial<RemittanceModule<any, any, any>> = {}): RemittanceModule<any, any, any> => ({
|
|
73
|
+
id: 'test-module',
|
|
74
|
+
name: 'Test Module',
|
|
75
|
+
allowUnsolicitedSettlements: false,
|
|
76
|
+
createOption: async () => ({}),
|
|
77
|
+
buildSettlement: async () => ({ action: 'settle', artifact: {} }),
|
|
78
|
+
acceptSettlement: async () => ({ action: 'accept', receiptData: {} }),
|
|
79
|
+
...overrides
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const makeIdentityLayer = (): IdentityLayer => ({
|
|
83
|
+
determineCertificatesToRequest: async ({ threadId }) => ({
|
|
84
|
+
kind: 'identityVerificationRequest',
|
|
85
|
+
threadId,
|
|
86
|
+
request: { types: { basic: ['name'] }, certifiers: ['certifier-key'] }
|
|
87
|
+
}),
|
|
88
|
+
respondToRequest: async ({ threadId }) => ({
|
|
89
|
+
action: 'respond',
|
|
90
|
+
response: {
|
|
91
|
+
kind: 'identityVerificationResponse',
|
|
92
|
+
threadId,
|
|
93
|
+
certificates: [{
|
|
94
|
+
type: 'YmFzaWM=',
|
|
95
|
+
certifier: 'certifier-key',
|
|
96
|
+
subject: 'subject-key',
|
|
97
|
+
fields: { name: 'QWxpY2U=' },
|
|
98
|
+
signature: 'deadbeef',
|
|
99
|
+
serialNumber: 'c2VyaWFs',
|
|
100
|
+
revocationOutpoint: 'outpoint',
|
|
101
|
+
keyringForVerifier: { name: 'a2V5' }
|
|
102
|
+
}]
|
|
103
|
+
}
|
|
104
|
+
}),
|
|
105
|
+
assessReceivedCertificateSufficiency: async (_cp, _received, threadId) => ({
|
|
106
|
+
kind: 'identityVerificationAcknowledgment',
|
|
107
|
+
threadId
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Tests
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
describe('RemittanceManager additional coverage', () => {
|
|
116
|
+
describe('init() and state persistence', () => {
|
|
117
|
+
it('init does nothing when stateLoader is not configured', async () => {
|
|
118
|
+
const bus = new MessageBus()
|
|
119
|
+
const manager = new RemittanceManager(
|
|
120
|
+
{ remittanceModules: [makeModule()], threadIdFactory: makeThreadIdFactory() },
|
|
121
|
+
makeWallet('k1'),
|
|
122
|
+
new TestComms('k1', bus)
|
|
123
|
+
)
|
|
124
|
+
await expect(manager.init()).resolves.toBeUndefined()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('init loads state from stateLoader and sets defaultPaymentOptionId', async () => {
|
|
128
|
+
const bus = new MessageBus()
|
|
129
|
+
const state = {
|
|
130
|
+
v: 1 as const,
|
|
131
|
+
threads: [],
|
|
132
|
+
defaultPaymentOptionId: 'test-module'
|
|
133
|
+
}
|
|
134
|
+
const manager = new RemittanceManager(
|
|
135
|
+
{
|
|
136
|
+
remittanceModules: [makeModule()],
|
|
137
|
+
threadIdFactory: makeThreadIdFactory(),
|
|
138
|
+
stateLoader: async () => state
|
|
139
|
+
},
|
|
140
|
+
makeWallet('k1'),
|
|
141
|
+
new TestComms('k1', bus)
|
|
142
|
+
)
|
|
143
|
+
await manager.init()
|
|
144
|
+
expect((manager as any).defaultPaymentOptionId).toBe('test-module')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('init skips state loading when stateLoader returns undefined', async () => {
|
|
148
|
+
const bus = new MessageBus()
|
|
149
|
+
const manager = new RemittanceManager(
|
|
150
|
+
{
|
|
151
|
+
remittanceModules: [makeModule()],
|
|
152
|
+
threadIdFactory: makeThreadIdFactory(),
|
|
153
|
+
stateLoader: async () => undefined
|
|
154
|
+
},
|
|
155
|
+
makeWallet('k1'),
|
|
156
|
+
new TestComms('k1', bus)
|
|
157
|
+
)
|
|
158
|
+
await expect(manager.init()).resolves.toBeUndefined()
|
|
159
|
+
expect(manager.threads).toHaveLength(0)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('saveState returns a serializable snapshot including threads', async () => {
|
|
163
|
+
const bus = new MessageBus()
|
|
164
|
+
const manager = new RemittanceManager(
|
|
165
|
+
{ remittanceModules: [makeModule()], threadIdFactory: makeThreadIdFactory() },
|
|
166
|
+
makeWallet('k1'),
|
|
167
|
+
new TestComms('k1', bus)
|
|
168
|
+
)
|
|
169
|
+
await manager.sendInvoice('k2', makeInvoiceInput())
|
|
170
|
+
const state = manager.saveState()
|
|
171
|
+
expect(state.v).toBe(1)
|
|
172
|
+
expect(state.threads).toHaveLength(1)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('loadState throws on unsupported version', () => {
|
|
176
|
+
const bus = new MessageBus()
|
|
177
|
+
const manager = new RemittanceManager(
|
|
178
|
+
{ remittanceModules: [makeModule()], threadIdFactory: makeThreadIdFactory() },
|
|
179
|
+
makeWallet('k1'),
|
|
180
|
+
new TestComms('k1', bus)
|
|
181
|
+
)
|
|
182
|
+
expect(() => manager.loadState({ v: 2 as any, threads: [] })).toThrow('Unsupported RemittanceManagerState version')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('loadState restores threads', async () => {
|
|
186
|
+
const bus = new MessageBus()
|
|
187
|
+
const manager = new RemittanceManager(
|
|
188
|
+
{ remittanceModules: [makeModule()], threadIdFactory: makeThreadIdFactory() },
|
|
189
|
+
makeWallet('k1'),
|
|
190
|
+
new TestComms('k1', bus)
|
|
191
|
+
)
|
|
192
|
+
await manager.sendInvoice('k2', makeInvoiceInput())
|
|
193
|
+
const snapshot = manager.saveState()
|
|
194
|
+
|
|
195
|
+
const manager2 = new RemittanceManager(
|
|
196
|
+
{ remittanceModules: [makeModule()], threadIdFactory: makeThreadIdFactory() },
|
|
197
|
+
makeWallet('k1'),
|
|
198
|
+
new TestComms('k1', bus)
|
|
199
|
+
)
|
|
200
|
+
manager2.loadState(snapshot)
|
|
201
|
+
expect(manager2.threads).toHaveLength(1)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('persistState calls stateSaver with current state', async () => {
|
|
205
|
+
const bus = new MessageBus()
|
|
206
|
+
const stateSaver = jest.fn()
|
|
207
|
+
const manager = new RemittanceManager(
|
|
208
|
+
{ remittanceModules: [makeModule()], threadIdFactory: makeThreadIdFactory(), stateSaver },
|
|
209
|
+
makeWallet('k1'),
|
|
210
|
+
new TestComms('k1', bus)
|
|
211
|
+
)
|
|
212
|
+
await manager.persistState()
|
|
213
|
+
expect(stateSaver).toHaveBeenCalledWith(expect.objectContaining({ v: 1 }))
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('persistState does nothing without stateSaver', async () => {
|
|
217
|
+
const bus = new MessageBus()
|
|
218
|
+
const manager = new RemittanceManager(
|
|
219
|
+
{ remittanceModules: [makeModule()], threadIdFactory: makeThreadIdFactory() },
|
|
220
|
+
makeWallet('k1'),
|
|
221
|
+
new TestComms('k1', bus)
|
|
222
|
+
)
|
|
223
|
+
await expect(manager.persistState()).resolves.toBeUndefined()
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
describe('thread accessors', () => {
|
|
228
|
+
it('getThread returns undefined for unknown threadId', async () => {
|
|
229
|
+
const bus = new MessageBus()
|
|
230
|
+
const manager = new RemittanceManager(
|
|
231
|
+
{ remittanceModules: [makeModule()], threadIdFactory: makeThreadIdFactory() },
|
|
232
|
+
makeWallet('k1'),
|
|
233
|
+
new TestComms('k1', bus)
|
|
234
|
+
)
|
|
235
|
+
expect(manager.getThread('does-not-exist' as ThreadId)).toBeUndefined()
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('getThreadOrThrow throws for unknown threadId', async () => {
|
|
239
|
+
const bus = new MessageBus()
|
|
240
|
+
const manager = new RemittanceManager(
|
|
241
|
+
{ remittanceModules: [makeModule()], threadIdFactory: makeThreadIdFactory() },
|
|
242
|
+
makeWallet('k1'),
|
|
243
|
+
new TestComms('k1', bus)
|
|
244
|
+
)
|
|
245
|
+
expect(() => manager.getThreadOrThrow('nope' as ThreadId)).toThrow('Unknown thread: nope')
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('getThreadHandle returns a handle with the correct threadId', async () => {
|
|
249
|
+
const bus = new MessageBus()
|
|
250
|
+
const manager = new RemittanceManager(
|
|
251
|
+
{ remittanceModules: [makeModule()], threadIdFactory: makeThreadIdFactory() },
|
|
252
|
+
makeWallet('k1'),
|
|
253
|
+
new TestComms('k1', bus)
|
|
254
|
+
)
|
|
255
|
+
const handle = await manager.sendInvoice('k2', makeInvoiceInput())
|
|
256
|
+
const h2 = manager.getThreadHandle(handle.threadId)
|
|
257
|
+
expect(h2.threadId).toBe(handle.threadId)
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
describe('preselectPaymentOption', () => {
|
|
262
|
+
it('uses preselectPaymentOption when no optionId is given to pay()', async () => {
|
|
263
|
+
const bus = new MessageBus()
|
|
264
|
+
const buildSettlement = jest.fn(async () => ({ action: 'settle' as const, artifact: {} }))
|
|
265
|
+
const mod = makeModule({ id: 'selected-mod', buildSettlement })
|
|
266
|
+
|
|
267
|
+
const maker = new RemittanceManager(
|
|
268
|
+
{ remittanceModules: [mod], options: { receiptProvided: false }, threadIdFactory: makeThreadIdFactory() },
|
|
269
|
+
makeWallet('maker-key'),
|
|
270
|
+
new TestComms('maker-key', bus)
|
|
271
|
+
)
|
|
272
|
+
const taker = new RemittanceManager(
|
|
273
|
+
{ remittanceModules: [mod], options: { receiptProvided: false }, threadIdFactory: makeThreadIdFactory() },
|
|
274
|
+
makeWallet('taker-key'),
|
|
275
|
+
new TestComms('taker-key', bus)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
const handle = await maker.sendInvoice('taker-key', makeInvoiceInput())
|
|
279
|
+
await taker.syncThreads()
|
|
280
|
+
|
|
281
|
+
taker.preselectPaymentOption('selected-mod')
|
|
282
|
+
await taker.pay(handle.threadId)
|
|
283
|
+
|
|
284
|
+
expect(buildSettlement).toHaveBeenCalled()
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
describe('findInvoicesPayable and findReceivableInvoices', () => {
|
|
289
|
+
it('findInvoicesPayable returns threads where we are taker and invoice not yet paid', async () => {
|
|
290
|
+
const bus = new MessageBus()
|
|
291
|
+
const mod = makeModule()
|
|
292
|
+
|
|
293
|
+
const maker = new RemittanceManager(
|
|
294
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
295
|
+
makeWallet('maker-key'),
|
|
296
|
+
new TestComms('maker-key', bus)
|
|
297
|
+
)
|
|
298
|
+
const taker = new RemittanceManager(
|
|
299
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
300
|
+
makeWallet('taker-key'),
|
|
301
|
+
new TestComms('taker-key', bus)
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
await maker.sendInvoice('taker-key', makeInvoiceInput())
|
|
305
|
+
await taker.syncThreads()
|
|
306
|
+
|
|
307
|
+
const payable = taker.findInvoicesPayable()
|
|
308
|
+
expect(payable).toHaveLength(1)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('findInvoicesPayable filters by counterparty when provided', async () => {
|
|
312
|
+
const bus = new MessageBus()
|
|
313
|
+
const mod = makeModule()
|
|
314
|
+
|
|
315
|
+
const maker = new RemittanceManager(
|
|
316
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
317
|
+
makeWallet('maker-key'),
|
|
318
|
+
new TestComms('maker-key', bus)
|
|
319
|
+
)
|
|
320
|
+
const taker = new RemittanceManager(
|
|
321
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
322
|
+
makeWallet('taker-key'),
|
|
323
|
+
new TestComms('taker-key', bus)
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
await maker.sendInvoice('taker-key', makeInvoiceInput())
|
|
327
|
+
await taker.syncThreads()
|
|
328
|
+
|
|
329
|
+
expect(taker.findInvoicesPayable('maker-key')).toHaveLength(1)
|
|
330
|
+
expect(taker.findInvoicesPayable('other-key')).toHaveLength(0)
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it('findReceivableInvoices returns threads where we are maker and waiting on payment', async () => {
|
|
334
|
+
const bus = new MessageBus()
|
|
335
|
+
const mod = makeModule()
|
|
336
|
+
|
|
337
|
+
const maker = new RemittanceManager(
|
|
338
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
339
|
+
makeWallet('maker-key'),
|
|
340
|
+
new TestComms('maker-key', bus)
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
await maker.sendInvoice('taker-key', makeInvoiceInput())
|
|
344
|
+
const receivable = maker.findReceivableInvoices()
|
|
345
|
+
expect(receivable).toHaveLength(1)
|
|
346
|
+
expect(maker.findReceivableInvoices('taker-key')).toHaveLength(1)
|
|
347
|
+
expect(maker.findReceivableInvoices('other')).toHaveLength(0)
|
|
348
|
+
})
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
describe('sendInvoiceForThread', () => {
|
|
352
|
+
it('throws when called on a non-maker thread', async () => {
|
|
353
|
+
const bus = new MessageBus()
|
|
354
|
+
const mod = makeModule()
|
|
355
|
+
|
|
356
|
+
const maker = new RemittanceManager(
|
|
357
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
358
|
+
makeWallet('maker-key'),
|
|
359
|
+
new TestComms('maker-key', bus)
|
|
360
|
+
)
|
|
361
|
+
const taker = new RemittanceManager(
|
|
362
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
363
|
+
makeWallet('taker-key'),
|
|
364
|
+
new TestComms('taker-key', bus)
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
await maker.sendInvoice('taker-key', makeInvoiceInput())
|
|
368
|
+
await taker.syncThreads()
|
|
369
|
+
|
|
370
|
+
const takerThread = taker.threads[0]
|
|
371
|
+
await expect(
|
|
372
|
+
taker.sendInvoiceForThread(takerThread.threadId, makeInvoiceInput())
|
|
373
|
+
).rejects.toThrow('Only makers can send invoices')
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('throws when the thread already has an invoice', async () => {
|
|
377
|
+
const bus = new MessageBus()
|
|
378
|
+
const mod = makeModule()
|
|
379
|
+
|
|
380
|
+
const maker = new RemittanceManager(
|
|
381
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
382
|
+
makeWallet('maker-key'),
|
|
383
|
+
new TestComms('maker-key', bus)
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
const handle = await maker.sendInvoice('taker-key', makeInvoiceInput())
|
|
387
|
+
await expect(
|
|
388
|
+
maker.sendInvoiceForThread(handle.threadId, makeInvoiceInput())
|
|
389
|
+
).rejects.toThrow('Thread already has an invoice')
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('throws when thread is in error state', async () => {
|
|
393
|
+
const bus = new MessageBus()
|
|
394
|
+
const mod = makeModule()
|
|
395
|
+
|
|
396
|
+
const maker = new RemittanceManager(
|
|
397
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
398
|
+
makeWallet('maker-key'),
|
|
399
|
+
new TestComms('maker-key', bus)
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
const handle = await maker.sendInvoice('taker-key', makeInvoiceInput())
|
|
403
|
+
const thread = maker.getThreadOrThrow(handle.threadId)
|
|
404
|
+
// Force error state
|
|
405
|
+
thread.flags.error = true
|
|
406
|
+
|
|
407
|
+
await expect(
|
|
408
|
+
maker.sendInvoiceForThread(handle.threadId, makeInvoiceInput())
|
|
409
|
+
).rejects.toThrow('Thread is in error state')
|
|
410
|
+
})
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
describe('pay() edge cases', () => {
|
|
414
|
+
it('throws when trying to pay a thread with no invoice', async () => {
|
|
415
|
+
const bus = new MessageBus()
|
|
416
|
+
const mod = makeModule({ allowUnsolicitedSettlements: true })
|
|
417
|
+
|
|
418
|
+
const taker = new RemittanceManager(
|
|
419
|
+
{ remittanceModules: [mod], options: { receiptProvided: false }, threadIdFactory: makeThreadIdFactory() },
|
|
420
|
+
makeWallet('taker-key'),
|
|
421
|
+
new TestComms('taker-key', bus)
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
const handle = await taker.sendUnsolicitedSettlement('maker-key', { moduleId: mod.id, option: {} })
|
|
425
|
+
await expect(
|
|
426
|
+
taker.pay(handle.threadId)
|
|
427
|
+
).rejects.toThrow('Thread has no invoice to pay')
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
it('throws when invoice is expired', async () => {
|
|
431
|
+
const bus = new MessageBus()
|
|
432
|
+
const mod = makeModule()
|
|
433
|
+
|
|
434
|
+
const maker = new RemittanceManager(
|
|
435
|
+
{
|
|
436
|
+
remittanceModules: [mod],
|
|
437
|
+
options: { invoiceExpirySeconds: 1 },
|
|
438
|
+
threadIdFactory: makeThreadIdFactory(),
|
|
439
|
+
// Invoice created at t=1_000_000ms; expires at t=1_001_000ms
|
|
440
|
+
now: () => 1_000_000
|
|
441
|
+
},
|
|
442
|
+
makeWallet('maker-key'),
|
|
443
|
+
new TestComms('maker-key', bus)
|
|
444
|
+
)
|
|
445
|
+
const taker = new RemittanceManager(
|
|
446
|
+
{
|
|
447
|
+
remittanceModules: [mod],
|
|
448
|
+
options: { receiptProvided: false },
|
|
449
|
+
threadIdFactory: makeThreadIdFactory(),
|
|
450
|
+
// Taker's clock is at t=2_000_000ms, well past the expiry
|
|
451
|
+
now: () => 2_000_000
|
|
452
|
+
},
|
|
453
|
+
makeWallet('taker-key'),
|
|
454
|
+
new TestComms('taker-key', bus)
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
const handle = await maker.sendInvoice('taker-key', makeInvoiceInput())
|
|
458
|
+
await taker.syncThreads()
|
|
459
|
+
|
|
460
|
+
const takerThreadId = taker.threads[0].threadId
|
|
461
|
+
await expect(taker.pay(takerThreadId)).rejects.toThrow('Invoice is expired')
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
it('throws when no remittance options are available', async () => {
|
|
465
|
+
const bus = new MessageBus()
|
|
466
|
+
|
|
467
|
+
// Module that creates no option (no createOption fn)
|
|
468
|
+
const mod: RemittanceModule<any, any, any> = {
|
|
469
|
+
id: 'no-option-module',
|
|
470
|
+
name: 'No Option',
|
|
471
|
+
allowUnsolicitedSettlements: false,
|
|
472
|
+
buildSettlement: async () => ({ action: 'settle', artifact: {} }),
|
|
473
|
+
acceptSettlement: async () => ({ action: 'accept', receiptData: {} })
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const maker = new RemittanceManager(
|
|
477
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
478
|
+
makeWallet('maker-key'),
|
|
479
|
+
new TestComms('maker-key', bus)
|
|
480
|
+
)
|
|
481
|
+
const taker = new RemittanceManager(
|
|
482
|
+
{ remittanceModules: [mod], options: { receiptProvided: false }, threadIdFactory: makeThreadIdFactory() },
|
|
483
|
+
makeWallet('taker-key'),
|
|
484
|
+
new TestComms('taker-key', bus)
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
const handle = await maker.sendInvoice('taker-key', makeInvoiceInput())
|
|
488
|
+
await taker.syncThreads()
|
|
489
|
+
const takerThread = taker.threads[0]
|
|
490
|
+
|
|
491
|
+
await expect(taker.pay(takerThread.threadId)).rejects.toThrow('No remittance options available on invoice')
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
it('throws when trying to pay a thread already in error state', async () => {
|
|
495
|
+
const bus = new MessageBus()
|
|
496
|
+
const mod = makeModule()
|
|
497
|
+
|
|
498
|
+
const maker = new RemittanceManager(
|
|
499
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
500
|
+
makeWallet('maker-key'),
|
|
501
|
+
new TestComms('maker-key', bus)
|
|
502
|
+
)
|
|
503
|
+
const taker = new RemittanceManager(
|
|
504
|
+
{ remittanceModules: [mod], options: { receiptProvided: false }, threadIdFactory: makeThreadIdFactory() },
|
|
505
|
+
makeWallet('taker-key'),
|
|
506
|
+
new TestComms('taker-key', bus)
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
const handle = await maker.sendInvoice('taker-key', makeInvoiceInput())
|
|
510
|
+
await taker.syncThreads()
|
|
511
|
+
|
|
512
|
+
const takerThread = taker.threads[0]
|
|
513
|
+
takerThread.flags.error = true
|
|
514
|
+
|
|
515
|
+
await expect(taker.pay(takerThread.threadId)).rejects.toThrow('Thread is in error state')
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
it('throws when trying to pay an invoice that is already settled', async () => {
|
|
519
|
+
const bus = new MessageBus()
|
|
520
|
+
const mod = makeModule({ allowUnsolicitedSettlements: false })
|
|
521
|
+
|
|
522
|
+
const maker = new RemittanceManager(
|
|
523
|
+
{ remittanceModules: [mod], options: { receiptProvided: false, autoIssueReceipt: false }, threadIdFactory: makeThreadIdFactory() },
|
|
524
|
+
makeWallet('maker-key'),
|
|
525
|
+
new TestComms('maker-key', bus)
|
|
526
|
+
)
|
|
527
|
+
const taker = new RemittanceManager(
|
|
528
|
+
{ remittanceModules: [mod], options: { receiptProvided: false }, threadIdFactory: makeThreadIdFactory() },
|
|
529
|
+
makeWallet('taker-key'),
|
|
530
|
+
new TestComms('taker-key', bus)
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
const handle = await maker.sendInvoice('taker-key', makeInvoiceInput())
|
|
534
|
+
await taker.syncThreads()
|
|
535
|
+
|
|
536
|
+
const takerThread = taker.threads[0]
|
|
537
|
+
await taker.pay(takerThread.threadId)
|
|
538
|
+
|
|
539
|
+
await expect(taker.pay(takerThread.threadId)).rejects.toThrow('Invoice already paid')
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
it('throws when module is not found for the chosen option', async () => {
|
|
543
|
+
const bus = new MessageBus()
|
|
544
|
+
const mod = makeModule()
|
|
545
|
+
|
|
546
|
+
const maker = new RemittanceManager(
|
|
547
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
548
|
+
makeWallet('maker-key'),
|
|
549
|
+
new TestComms('maker-key', bus)
|
|
550
|
+
)
|
|
551
|
+
const taker = new RemittanceManager(
|
|
552
|
+
{ remittanceModules: [mod], options: { receiptProvided: false }, threadIdFactory: makeThreadIdFactory() },
|
|
553
|
+
makeWallet('taker-key'),
|
|
554
|
+
new TestComms('taker-key', bus)
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
const handle = await maker.sendInvoice('taker-key', makeInvoiceInput())
|
|
558
|
+
await taker.syncThreads()
|
|
559
|
+
|
|
560
|
+
const takerThread = taker.threads[0]
|
|
561
|
+
await expect(taker.pay(takerThread.threadId, 'unknown-module-id')).rejects.toThrow(
|
|
562
|
+
'No configured remittance module for option: unknown-module-id'
|
|
563
|
+
)
|
|
564
|
+
})
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
describe('sendUnsolicitedSettlement edge cases', () => {
|
|
568
|
+
it('throws when the module does not allow unsolicited settlements', async () => {
|
|
569
|
+
const bus = new MessageBus()
|
|
570
|
+
const mod = makeModule({ id: 'no-unsolicited', allowUnsolicitedSettlements: false })
|
|
571
|
+
|
|
572
|
+
const taker = new RemittanceManager(
|
|
573
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
574
|
+
makeWallet('taker-key'),
|
|
575
|
+
new TestComms('taker-key', bus)
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
await expect(
|
|
579
|
+
taker.sendUnsolicitedSettlement('maker-key', { moduleId: 'no-unsolicited', option: {} })
|
|
580
|
+
).rejects.toThrow('does not allow unsolicited settlements')
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
it('throws when module id is unknown', async () => {
|
|
584
|
+
const bus = new MessageBus()
|
|
585
|
+
const mod = makeModule({ allowUnsolicitedSettlements: true })
|
|
586
|
+
|
|
587
|
+
const taker = new RemittanceManager(
|
|
588
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
589
|
+
makeWallet('taker-key'),
|
|
590
|
+
new TestComms('taker-key', bus)
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
await expect(
|
|
594
|
+
taker.sendUnsolicitedSettlement('maker-key', { moduleId: 'no-such-module', option: {} })
|
|
595
|
+
).rejects.toThrow('No configured remittance module for option: no-such-module')
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
it('sends termination when buildSettlement returns terminate for unsolicited', async () => {
|
|
599
|
+
const bus = new MessageBus()
|
|
600
|
+
const termination: Termination = { code: 'build.fail', message: 'build failed' }
|
|
601
|
+
const mod = makeModule({
|
|
602
|
+
id: 'term-mod',
|
|
603
|
+
allowUnsolicitedSettlements: true,
|
|
604
|
+
buildSettlement: async () => ({ action: 'terminate', termination })
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
const taker = new RemittanceManager(
|
|
608
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
609
|
+
makeWallet('taker-key'),
|
|
610
|
+
new TestComms('taker-key', bus)
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
const handle = await taker.sendUnsolicitedSettlement('maker-key', { moduleId: 'term-mod', option: {} })
|
|
614
|
+
const thread = taker.getThreadOrThrow(handle.threadId)
|
|
615
|
+
expect(thread.state).toBe('terminated')
|
|
616
|
+
})
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
describe('inbound message edge cases', () => {
|
|
620
|
+
it('ignores messages that do not parse as valid envelopes', async () => {
|
|
621
|
+
const bus = new MessageBus()
|
|
622
|
+
const mod = makeModule()
|
|
623
|
+
const manager = new RemittanceManager(
|
|
624
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
625
|
+
makeWallet('k1'),
|
|
626
|
+
new TestComms('k1', bus)
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
// Send an unparseable message
|
|
630
|
+
bus.send('k2', 'k1', DEFAULT_REMITTANCE_MESSAGEBOX, 'not json at all')
|
|
631
|
+
bus.send('k2', 'k1', DEFAULT_REMITTANCE_MESSAGEBOX, JSON.stringify({ v: 1, kind: 'invoice' })) // missing threadId
|
|
632
|
+
bus.send('k2', 'k1', DEFAULT_REMITTANCE_MESSAGEBOX, JSON.stringify({ v: 2, kind: 'invoice', threadId: 't', id: 'i' })) // wrong version
|
|
633
|
+
|
|
634
|
+
await manager.syncThreads()
|
|
635
|
+
expect(manager.threads).toHaveLength(0)
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
it('deduplicates already-processed message IDs', async () => {
|
|
639
|
+
const bus = new MessageBus()
|
|
640
|
+
const mod = makeModule()
|
|
641
|
+
|
|
642
|
+
const maker = new RemittanceManager(
|
|
643
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
644
|
+
makeWallet('maker-key'),
|
|
645
|
+
new TestComms('maker-key', bus)
|
|
646
|
+
)
|
|
647
|
+
const taker = new RemittanceManager(
|
|
648
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
649
|
+
makeWallet('taker-key'),
|
|
650
|
+
new TestComms('taker-key', bus)
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
await maker.sendInvoice('taker-key', makeInvoiceInput())
|
|
654
|
+
// Sync twice — second sync should see the same message re-listed (bus doesn't ack in this test manually)
|
|
655
|
+
// To simulate re-delivery, don't ack by using a non-acking comms
|
|
656
|
+
const msgs = bus.list('taker-key', DEFAULT_REMITTANCE_MESSAGEBOX)
|
|
657
|
+
expect(msgs).toHaveLength(1)
|
|
658
|
+
|
|
659
|
+
await taker.syncThreads()
|
|
660
|
+
expect(taker.threads).toHaveLength(1)
|
|
661
|
+
const threadId = taker.threads[0].threadId
|
|
662
|
+
|
|
663
|
+
// simulate re-delivery
|
|
664
|
+
await taker.syncThreads()
|
|
665
|
+
// Still only one thread, not duplicated
|
|
666
|
+
expect(taker.threads).toHaveLength(1)
|
|
667
|
+
expect(taker.getThreadOrThrow(threadId).processedMessageIds).toHaveLength(1)
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
it('receives identity verification request but sends termination when no identity layer configured', async () => {
|
|
671
|
+
const bus = new MessageBus()
|
|
672
|
+
const mod = makeModule()
|
|
673
|
+
|
|
674
|
+
const identityLayer = makeIdentityLayer()
|
|
675
|
+
const sender = new RemittanceManager(
|
|
676
|
+
{
|
|
677
|
+
remittanceModules: [mod],
|
|
678
|
+
identityLayer,
|
|
679
|
+
options: { identityOptions: { makerRequestIdentity: 'beforeInvoicing' }, identityTimeoutMs: 100, identityPollIntervalMs: 5 },
|
|
680
|
+
threadIdFactory: makeThreadIdFactory()
|
|
681
|
+
},
|
|
682
|
+
makeWallet('sender-key'),
|
|
683
|
+
new TestComms('sender-key', bus)
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
// receiver has NO identity layer
|
|
687
|
+
const receiver = new RemittanceManager(
|
|
688
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
689
|
+
makeWallet('receiver-key'),
|
|
690
|
+
new TestComms('receiver-key', bus)
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
// Sender sends an identity verification request
|
|
694
|
+
const identityRequestEnv: RemittanceEnvelope = {
|
|
695
|
+
v: 1,
|
|
696
|
+
id: 'env-id-req',
|
|
697
|
+
kind: 'identityVerificationRequest',
|
|
698
|
+
threadId: 'thread-noid' as ThreadId,
|
|
699
|
+
createdAt: 1,
|
|
700
|
+
payload: {
|
|
701
|
+
kind: 'identityVerificationRequest',
|
|
702
|
+
threadId: 'thread-noid',
|
|
703
|
+
request: { types: { basic: ['name'] }, certifiers: ['c'] }
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
bus.send('sender-key', 'receiver-key', DEFAULT_REMITTANCE_MESSAGEBOX, JSON.stringify(identityRequestEnv))
|
|
707
|
+
|
|
708
|
+
await receiver.syncThreads()
|
|
709
|
+
// Should have sent a termination back
|
|
710
|
+
const termMsgs = bus.list('sender-key', DEFAULT_REMITTANCE_MESSAGEBOX)
|
|
711
|
+
const termEnv = JSON.parse(termMsgs[0].body) as RemittanceEnvelope
|
|
712
|
+
expect(termEnv.kind).toBe('termination')
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
it('receives identity verification response but sends termination when no identity layer configured', async () => {
|
|
716
|
+
const bus = new MessageBus()
|
|
717
|
+
const mod = makeModule()
|
|
718
|
+
|
|
719
|
+
const receiver = new RemittanceManager(
|
|
720
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
721
|
+
makeWallet('receiver-key'),
|
|
722
|
+
new TestComms('receiver-key', bus)
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
const identityResponseEnv: RemittanceEnvelope = {
|
|
726
|
+
v: 1,
|
|
727
|
+
id: 'env-id-resp',
|
|
728
|
+
kind: 'identityVerificationResponse',
|
|
729
|
+
threadId: 'thread-noid-resp' as ThreadId,
|
|
730
|
+
createdAt: 1,
|
|
731
|
+
payload: {
|
|
732
|
+
kind: 'identityVerificationResponse',
|
|
733
|
+
threadId: 'thread-noid-resp',
|
|
734
|
+
certificates: []
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
bus.send('sender-key', 'receiver-key', DEFAULT_REMITTANCE_MESSAGEBOX, JSON.stringify(identityResponseEnv))
|
|
738
|
+
|
|
739
|
+
await receiver.syncThreads()
|
|
740
|
+
const termMsgs = bus.list('sender-key', DEFAULT_REMITTANCE_MESSAGEBOX)
|
|
741
|
+
const termEnv = JSON.parse(termMsgs[0].body) as RemittanceEnvelope
|
|
742
|
+
expect(termEnv.kind).toBe('termination')
|
|
743
|
+
})
|
|
744
|
+
|
|
745
|
+
it('settlement received when module not found sends termination', async () => {
|
|
746
|
+
const bus = new MessageBus()
|
|
747
|
+
const mod = makeModule({ id: 'known-mod' })
|
|
748
|
+
|
|
749
|
+
const maker = new RemittanceManager(
|
|
750
|
+
{ remittanceModules: [mod], options: { receiptProvided: false }, threadIdFactory: makeThreadIdFactory() },
|
|
751
|
+
makeWallet('maker-key'),
|
|
752
|
+
new TestComms('maker-key', bus)
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
const settlementEnv: RemittanceEnvelope = {
|
|
756
|
+
v: 1,
|
|
757
|
+
id: 'env-settle',
|
|
758
|
+
kind: 'settlement',
|
|
759
|
+
threadId: 'thread-settle-unknown' as ThreadId,
|
|
760
|
+
createdAt: 1,
|
|
761
|
+
payload: {
|
|
762
|
+
kind: 'settlement',
|
|
763
|
+
threadId: 'thread-settle-unknown',
|
|
764
|
+
moduleId: 'unknown-mod', // not registered
|
|
765
|
+
optionId: 'unknown-mod',
|
|
766
|
+
sender: 'taker-key',
|
|
767
|
+
createdAt: 1,
|
|
768
|
+
artifact: {}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
bus.send('taker-key', 'maker-key', DEFAULT_REMITTANCE_MESSAGEBOX, JSON.stringify(settlementEnv))
|
|
773
|
+
await maker.syncThreads()
|
|
774
|
+
|
|
775
|
+
const termMsgs = bus.list('taker-key', DEFAULT_REMITTANCE_MESSAGEBOX)
|
|
776
|
+
expect(termMsgs.length).toBeGreaterThan(0)
|
|
777
|
+
const termEnv = JSON.parse(termMsgs[0].body) as RemittanceEnvelope
|
|
778
|
+
expect(termEnv.kind).toBe('termination')
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
it('settlement received for a module that does not allow unsolicited settlements sends termination', async () => {
|
|
782
|
+
const bus = new MessageBus()
|
|
783
|
+
const mod = makeModule({ id: 'no-unsolicited', allowUnsolicitedSettlements: false })
|
|
784
|
+
|
|
785
|
+
const maker = new RemittanceManager(
|
|
786
|
+
{ remittanceModules: [mod], options: { receiptProvided: false }, threadIdFactory: makeThreadIdFactory() },
|
|
787
|
+
makeWallet('maker-key'),
|
|
788
|
+
new TestComms('maker-key', bus)
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
const settlementEnv: RemittanceEnvelope = {
|
|
792
|
+
v: 1,
|
|
793
|
+
id: 'env-settle2',
|
|
794
|
+
kind: 'settlement',
|
|
795
|
+
threadId: 'thread-settle-nosolicited' as ThreadId,
|
|
796
|
+
createdAt: 1,
|
|
797
|
+
payload: {
|
|
798
|
+
kind: 'settlement',
|
|
799
|
+
threadId: 'thread-settle-nosolicited',
|
|
800
|
+
moduleId: 'no-unsolicited',
|
|
801
|
+
optionId: 'no-unsolicited',
|
|
802
|
+
sender: 'taker-key',
|
|
803
|
+
createdAt: 1,
|
|
804
|
+
artifact: {}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
bus.send('taker-key', 'maker-key', DEFAULT_REMITTANCE_MESSAGEBOX, JSON.stringify(settlementEnv))
|
|
809
|
+
await maker.syncThreads()
|
|
810
|
+
|
|
811
|
+
const termMsgs = bus.list('taker-key', DEFAULT_REMITTANCE_MESSAGEBOX)
|
|
812
|
+
expect(termMsgs.length).toBeGreaterThan(0)
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
it('termination received triggers processTermination on module when settlement exists', async () => {
|
|
816
|
+
const bus = new MessageBus()
|
|
817
|
+
const processTermination = jest.fn()
|
|
818
|
+
const mod = makeModule({
|
|
819
|
+
id: 'term-receiver',
|
|
820
|
+
allowUnsolicitedSettlements: false,
|
|
821
|
+
processTermination
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
const maker = new RemittanceManager(
|
|
825
|
+
{ remittanceModules: [mod], options: { receiptProvided: true, autoIssueReceipt: false }, threadIdFactory: makeThreadIdFactory() },
|
|
826
|
+
makeWallet('maker-key'),
|
|
827
|
+
new TestComms('maker-key', bus)
|
|
828
|
+
)
|
|
829
|
+
const taker = new RemittanceManager(
|
|
830
|
+
{ remittanceModules: [mod], options: { receiptProvided: false }, threadIdFactory: makeThreadIdFactory() },
|
|
831
|
+
makeWallet('taker-key'),
|
|
832
|
+
new TestComms('taker-key', bus)
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
const handle = await maker.sendInvoice('taker-key', makeInvoiceInput())
|
|
836
|
+
await taker.syncThreads()
|
|
837
|
+
const takerThread = taker.threads[0]
|
|
838
|
+
|
|
839
|
+
// Taker pays invoice
|
|
840
|
+
await taker.pay(takerThread.threadId)
|
|
841
|
+
|
|
842
|
+
// Maker processes settlement
|
|
843
|
+
await maker.syncThreads()
|
|
844
|
+
|
|
845
|
+
// Simulate taker receiving a termination
|
|
846
|
+
const terminationEnv: RemittanceEnvelope = {
|
|
847
|
+
v: 1,
|
|
848
|
+
id: 'env-term',
|
|
849
|
+
kind: 'termination',
|
|
850
|
+
threadId: takerThread.threadId,
|
|
851
|
+
createdAt: 1,
|
|
852
|
+
payload: { code: 'rejected', message: 'Payment rejected' }
|
|
853
|
+
}
|
|
854
|
+
bus.send('maker-key', 'taker-key', DEFAULT_REMITTANCE_MESSAGEBOX, JSON.stringify(terminationEnv))
|
|
855
|
+
await taker.syncThreads()
|
|
856
|
+
|
|
857
|
+
expect(processTermination).toHaveBeenCalled()
|
|
858
|
+
})
|
|
859
|
+
|
|
860
|
+
it('unknown envelope kind causes thread to enter errored state', async () => {
|
|
861
|
+
const bus = new MessageBus()
|
|
862
|
+
const mod = makeModule()
|
|
863
|
+
|
|
864
|
+
const manager = new RemittanceManager(
|
|
865
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
866
|
+
makeWallet('k1'),
|
|
867
|
+
new TestComms('k1', bus)
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
const unknownEnv = {
|
|
871
|
+
v: 1,
|
|
872
|
+
id: 'env-unknown',
|
|
873
|
+
kind: 'unknown-kind',
|
|
874
|
+
threadId: 'thread-unknown',
|
|
875
|
+
createdAt: 1,
|
|
876
|
+
payload: {}
|
|
877
|
+
}
|
|
878
|
+
bus.send('k2', 'k1', DEFAULT_REMITTANCE_MESSAGEBOX, JSON.stringify(unknownEnv))
|
|
879
|
+
await manager.syncThreads()
|
|
880
|
+
|
|
881
|
+
const thread = manager.getThreadOrThrow('thread-unknown' as ThreadId)
|
|
882
|
+
expect(thread.state).toBe('errored')
|
|
883
|
+
})
|
|
884
|
+
})
|
|
885
|
+
|
|
886
|
+
describe('event listeners', () => {
|
|
887
|
+
it('onEvent registers and fires for each remittance lifecycle event', async () => {
|
|
888
|
+
const bus = new MessageBus()
|
|
889
|
+
const mod = makeModule({ id: 'ev-mod', allowUnsolicitedSettlements: true })
|
|
890
|
+
|
|
891
|
+
const events: string[] = []
|
|
892
|
+
const manager = new RemittanceManager(
|
|
893
|
+
{
|
|
894
|
+
remittanceModules: [mod],
|
|
895
|
+
onEvent: (e) => events.push(e.type),
|
|
896
|
+
threadIdFactory: makeThreadIdFactory()
|
|
897
|
+
},
|
|
898
|
+
makeWallet('k1'),
|
|
899
|
+
new TestComms('k1', bus)
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
await manager.sendUnsolicitedSettlement('k2', { moduleId: 'ev-mod', option: {} })
|
|
903
|
+
expect(events).toContain('threadCreated')
|
|
904
|
+
expect(events).toContain('settlementSent')
|
|
905
|
+
})
|
|
906
|
+
|
|
907
|
+
it('onEvent listener can be removed via returned disposer', async () => {
|
|
908
|
+
const bus = new MessageBus()
|
|
909
|
+
const mod = makeModule()
|
|
910
|
+
|
|
911
|
+
const events: string[] = []
|
|
912
|
+
const manager = new RemittanceManager(
|
|
913
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
914
|
+
makeWallet('k1'),
|
|
915
|
+
new TestComms('k1', bus)
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
const dispose = manager.onEvent((e) => events.push(e.type))
|
|
919
|
+
dispose()
|
|
920
|
+
|
|
921
|
+
await manager.sendInvoice('k2', makeInvoiceInput())
|
|
922
|
+
expect(events).toHaveLength(0)
|
|
923
|
+
})
|
|
924
|
+
|
|
925
|
+
it('fires all event handler callbacks configured via events object', async () => {
|
|
926
|
+
const bus = new MessageBus()
|
|
927
|
+
const mod = makeModule({
|
|
928
|
+
id: 'events-all',
|
|
929
|
+
allowUnsolicitedSettlements: false,
|
|
930
|
+
createOption: async () => ({}),
|
|
931
|
+
buildSettlement: async () => ({ action: 'settle', artifact: {} }),
|
|
932
|
+
acceptSettlement: async () => ({ action: 'accept', receiptData: {} })
|
|
933
|
+
})
|
|
934
|
+
|
|
935
|
+
const onThreadCreated = jest.fn()
|
|
936
|
+
const onStateChanged = jest.fn()
|
|
937
|
+
const onInvoiceSent = jest.fn()
|
|
938
|
+
const onInvoiceReceived = jest.fn()
|
|
939
|
+
const onSettlementSent = jest.fn()
|
|
940
|
+
const onSettlementReceived = jest.fn()
|
|
941
|
+
const onReceiptSent = jest.fn()
|
|
942
|
+
const onReceiptReceived = jest.fn()
|
|
943
|
+
|
|
944
|
+
const maker = new RemittanceManager(
|
|
945
|
+
{
|
|
946
|
+
remittanceModules: [mod],
|
|
947
|
+
options: { receiptProvided: true, autoIssueReceipt: true },
|
|
948
|
+
events: {
|
|
949
|
+
onThreadCreated,
|
|
950
|
+
onStateChanged,
|
|
951
|
+
onInvoiceSent,
|
|
952
|
+
onSettlementReceived,
|
|
953
|
+
onReceiptSent
|
|
954
|
+
},
|
|
955
|
+
threadIdFactory: makeThreadIdFactory()
|
|
956
|
+
},
|
|
957
|
+
makeWallet('maker-key'),
|
|
958
|
+
new TestComms('maker-key', bus)
|
|
959
|
+
)
|
|
960
|
+
const taker = new RemittanceManager(
|
|
961
|
+
{
|
|
962
|
+
remittanceModules: [mod],
|
|
963
|
+
options: { receiptProvided: true },
|
|
964
|
+
events: {
|
|
965
|
+
onInvoiceReceived,
|
|
966
|
+
onSettlementSent,
|
|
967
|
+
onReceiptReceived
|
|
968
|
+
},
|
|
969
|
+
threadIdFactory: makeThreadIdFactory()
|
|
970
|
+
},
|
|
971
|
+
makeWallet('taker-key'),
|
|
972
|
+
new TestComms('taker-key', bus)
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
const handle = await maker.sendInvoice('taker-key', makeInvoiceInput())
|
|
976
|
+
await taker.syncThreads()
|
|
977
|
+
const payPromise = taker.pay(handle.threadId, 'events-all')
|
|
978
|
+
await tick()
|
|
979
|
+
await maker.syncThreads()
|
|
980
|
+
await payPromise
|
|
981
|
+
|
|
982
|
+
expect(onThreadCreated).toHaveBeenCalled()
|
|
983
|
+
expect(onInvoiceSent).toHaveBeenCalled()
|
|
984
|
+
expect(onStateChanged).toHaveBeenCalled()
|
|
985
|
+
expect(onInvoiceReceived).toHaveBeenCalled()
|
|
986
|
+
expect(onSettlementSent).toHaveBeenCalled()
|
|
987
|
+
expect(onSettlementReceived).toHaveBeenCalled()
|
|
988
|
+
expect(onReceiptSent).toHaveBeenCalled()
|
|
989
|
+
expect(onReceiptReceived).toHaveBeenCalled()
|
|
990
|
+
})
|
|
991
|
+
})
|
|
992
|
+
|
|
993
|
+
describe('startListening', () => {
|
|
994
|
+
it('throws when CommsLayer does not support live messages', async () => {
|
|
995
|
+
const bus = new MessageBus()
|
|
996
|
+
const mod = makeModule()
|
|
997
|
+
const manager = new RemittanceManager(
|
|
998
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
999
|
+
makeWallet('k1'),
|
|
1000
|
+
new TestComms('k1', bus) // TestComms does not implement listenForLiveMessages
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
await expect(manager.startListening()).rejects.toThrow('CommsLayer does not support live message listening')
|
|
1004
|
+
})
|
|
1005
|
+
})
|
|
1006
|
+
|
|
1007
|
+
describe('sendEnvelope with live message fallback', () => {
|
|
1008
|
+
it('falls back to sendMessage when sendLiveMessage fails', async () => {
|
|
1009
|
+
const bus = new MessageBus()
|
|
1010
|
+
const mod = makeModule()
|
|
1011
|
+
|
|
1012
|
+
const sendLiveMessage = jest.fn(async () => { throw new Error('live failed') })
|
|
1013
|
+
const sendMessage = jest.fn(async () => 'msg-123')
|
|
1014
|
+
const listMessages = jest.fn(async () => [])
|
|
1015
|
+
const acknowledgeMessage = jest.fn(async () => undefined)
|
|
1016
|
+
|
|
1017
|
+
const comms: CommsLayer = { sendMessage, listMessages, acknowledgeMessage, sendLiveMessage }
|
|
1018
|
+
const logger = { log: jest.fn(), warn: jest.fn(), error: jest.fn() }
|
|
1019
|
+
|
|
1020
|
+
const manager = new RemittanceManager(
|
|
1021
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory(), logger },
|
|
1022
|
+
makeWallet('k1'),
|
|
1023
|
+
comms
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
await manager.sendInvoice('k2', makeInvoiceInput())
|
|
1027
|
+
expect(sendLiveMessage).toHaveBeenCalled()
|
|
1028
|
+
expect(sendMessage).toHaveBeenCalled()
|
|
1029
|
+
})
|
|
1030
|
+
})
|
|
1031
|
+
|
|
1032
|
+
describe('waitForReceipt timeout', () => {
|
|
1033
|
+
it('throws when receipt does not arrive within the timeout', async () => {
|
|
1034
|
+
const bus = new MessageBus()
|
|
1035
|
+
const mod = makeModule({ allowUnsolicitedSettlements: false })
|
|
1036
|
+
|
|
1037
|
+
const maker = new RemittanceManager(
|
|
1038
|
+
{ remittanceModules: [mod], options: { receiptProvided: false }, threadIdFactory: makeThreadIdFactory() },
|
|
1039
|
+
makeWallet('maker-key'),
|
|
1040
|
+
new TestComms('maker-key', bus)
|
|
1041
|
+
)
|
|
1042
|
+
|
|
1043
|
+
const handle = await maker.sendInvoice('k2', makeInvoiceInput())
|
|
1044
|
+
const thread = maker.getThreadOrThrow(handle.threadId)
|
|
1045
|
+
// Manually set the thread to 'settled' without an actual receipt
|
|
1046
|
+
thread.flags.hasPaid = true
|
|
1047
|
+
thread.state = 'settled'
|
|
1048
|
+
|
|
1049
|
+
await expect(
|
|
1050
|
+
maker.waitForReceipt(handle.threadId, { timeoutMs: 10, pollIntervalMs: 5 })
|
|
1051
|
+
).rejects.toThrow('Timed out waiting for receipt')
|
|
1052
|
+
})
|
|
1053
|
+
})
|
|
1054
|
+
|
|
1055
|
+
describe('waitForSettlement timeout', () => {
|
|
1056
|
+
it('throws when settlement does not arrive within the timeout', async () => {
|
|
1057
|
+
const bus = new MessageBus()
|
|
1058
|
+
const mod = makeModule()
|
|
1059
|
+
|
|
1060
|
+
const maker = new RemittanceManager(
|
|
1061
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
1062
|
+
makeWallet('maker-key'),
|
|
1063
|
+
new TestComms('maker-key', bus)
|
|
1064
|
+
)
|
|
1065
|
+
|
|
1066
|
+
const handle = await maker.sendInvoice('k2', makeInvoiceInput())
|
|
1067
|
+
|
|
1068
|
+
await expect(
|
|
1069
|
+
maker.waitForSettlement(handle.threadId, { timeoutMs: 10, pollIntervalMs: 5 })
|
|
1070
|
+
).rejects.toThrow('Timed out waiting for settlement')
|
|
1071
|
+
})
|
|
1072
|
+
|
|
1073
|
+
it('returns settlement immediately if already set', async () => {
|
|
1074
|
+
const bus = new MessageBus()
|
|
1075
|
+
const mod = makeModule({ allowUnsolicitedSettlements: false })
|
|
1076
|
+
|
|
1077
|
+
const maker = new RemittanceManager(
|
|
1078
|
+
{ remittanceModules: [mod], options: { receiptProvided: true, autoIssueReceipt: true }, threadIdFactory: makeThreadIdFactory() },
|
|
1079
|
+
makeWallet('maker-key'),
|
|
1080
|
+
new TestComms('maker-key', bus)
|
|
1081
|
+
)
|
|
1082
|
+
const taker = new RemittanceManager(
|
|
1083
|
+
{ remittanceModules: [mod], options: { receiptProvided: false }, threadIdFactory: makeThreadIdFactory() },
|
|
1084
|
+
makeWallet('taker-key'),
|
|
1085
|
+
new TestComms('taker-key', bus)
|
|
1086
|
+
)
|
|
1087
|
+
|
|
1088
|
+
const handle = await maker.sendInvoice('taker-key', makeInvoiceInput())
|
|
1089
|
+
await taker.syncThreads()
|
|
1090
|
+
await taker.pay(handle.threadId, 'test-module')
|
|
1091
|
+
await maker.syncThreads()
|
|
1092
|
+
|
|
1093
|
+
const settlement = await maker.waitForSettlement(handle.threadId, { timeoutMs: 1000 })
|
|
1094
|
+
expect(settlement).toBeDefined()
|
|
1095
|
+
expect((settlement as any).kind).toBe('settlement')
|
|
1096
|
+
})
|
|
1097
|
+
})
|
|
1098
|
+
|
|
1099
|
+
describe('waitForState errors', () => {
|
|
1100
|
+
it('throws immediately when thread is already in errored state', async () => {
|
|
1101
|
+
const bus = new MessageBus()
|
|
1102
|
+
const mod = makeModule()
|
|
1103
|
+
|
|
1104
|
+
const manager = new RemittanceManager(
|
|
1105
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
1106
|
+
makeWallet('k1'),
|
|
1107
|
+
new TestComms('k1', bus)
|
|
1108
|
+
)
|
|
1109
|
+
|
|
1110
|
+
const handle = await manager.sendInvoice('k2', makeInvoiceInput())
|
|
1111
|
+
const thread = manager.getThreadOrThrow(handle.threadId)
|
|
1112
|
+
thread.state = 'errored'
|
|
1113
|
+
|
|
1114
|
+
await expect(
|
|
1115
|
+
manager.waitForState(handle.threadId, 'receipted', { timeoutMs: 50 })
|
|
1116
|
+
).rejects.toThrow('Thread entered terminal state: errored')
|
|
1117
|
+
})
|
|
1118
|
+
|
|
1119
|
+
it('throws immediately when thread is already in terminated state', async () => {
|
|
1120
|
+
const bus = new MessageBus()
|
|
1121
|
+
const mod = makeModule()
|
|
1122
|
+
|
|
1123
|
+
const manager = new RemittanceManager(
|
|
1124
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
1125
|
+
makeWallet('k1'),
|
|
1126
|
+
new TestComms('k1', bus)
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
const handle = await manager.sendInvoice('k2', makeInvoiceInput())
|
|
1130
|
+
const thread = manager.getThreadOrThrow(handle.threadId)
|
|
1131
|
+
thread.state = 'terminated'
|
|
1132
|
+
|
|
1133
|
+
await expect(
|
|
1134
|
+
manager.waitForState(handle.threadId, 'receipted', { timeoutMs: 50 })
|
|
1135
|
+
).rejects.toThrow('Thread entered terminal state: terminated')
|
|
1136
|
+
})
|
|
1137
|
+
})
|
|
1138
|
+
|
|
1139
|
+
describe('shouldRequestIdentity missing identityLayer', () => {
|
|
1140
|
+
it('throws when identityOptions requires identity but no identityLayer is set', async () => {
|
|
1141
|
+
const bus = new MessageBus()
|
|
1142
|
+
const mod = makeModule()
|
|
1143
|
+
|
|
1144
|
+
const maker = new RemittanceManager(
|
|
1145
|
+
{
|
|
1146
|
+
remittanceModules: [mod],
|
|
1147
|
+
// NO identityLayer
|
|
1148
|
+
options: {
|
|
1149
|
+
identityOptions: { makerRequestIdentity: 'beforeInvoicing' }
|
|
1150
|
+
},
|
|
1151
|
+
threadIdFactory: makeThreadIdFactory()
|
|
1152
|
+
},
|
|
1153
|
+
makeWallet('maker-key'),
|
|
1154
|
+
new TestComms('maker-key', bus)
|
|
1155
|
+
)
|
|
1156
|
+
|
|
1157
|
+
await expect(maker.sendInvoice('taker-key', makeInvoiceInput())).rejects.toThrow(
|
|
1158
|
+
'Identity layer is required by runtime options but is not configured'
|
|
1159
|
+
)
|
|
1160
|
+
})
|
|
1161
|
+
})
|
|
1162
|
+
|
|
1163
|
+
describe('constructor with initial threads', () => {
|
|
1164
|
+
it('accepts pre-hydrated threads and restores their state', async () => {
|
|
1165
|
+
const bus = new MessageBus()
|
|
1166
|
+
const mod = makeModule()
|
|
1167
|
+
|
|
1168
|
+
const manager1 = new RemittanceManager(
|
|
1169
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
1170
|
+
makeWallet('k1'),
|
|
1171
|
+
new TestComms('k1', bus)
|
|
1172
|
+
)
|
|
1173
|
+
await manager1.sendInvoice('k2', makeInvoiceInput())
|
|
1174
|
+
const { threads } = manager1.saveState()
|
|
1175
|
+
|
|
1176
|
+
const manager2 = new RemittanceManager(
|
|
1177
|
+
{ remittanceModules: [mod], threadIdFactory: makeThreadIdFactory() },
|
|
1178
|
+
makeWallet('k1'),
|
|
1179
|
+
new TestComms('k1', bus),
|
|
1180
|
+
threads
|
|
1181
|
+
)
|
|
1182
|
+
expect(manager2.threads).toHaveLength(1)
|
|
1183
|
+
expect(manager2.threads[0].state).toBe('invoiced')
|
|
1184
|
+
})
|
|
1185
|
+
})
|
|
1186
|
+
|
|
1187
|
+
describe('custom messageBox', () => {
|
|
1188
|
+
it('uses the configured messageBox for sending and listing', async () => {
|
|
1189
|
+
const bus = new MessageBus()
|
|
1190
|
+
const mod = makeModule()
|
|
1191
|
+
|
|
1192
|
+
const customBox = 'custom_inbox'
|
|
1193
|
+
const sendMessage = jest.fn(async (args: any, _hostOverride?: string) => {
|
|
1194
|
+
bus.send('k1', args.recipient, args.messageBox, args.body)
|
|
1195
|
+
return 'mid-custom'
|
|
1196
|
+
})
|
|
1197
|
+
const listMessages = jest.fn(async () => [])
|
|
1198
|
+
const acknowledgeMessage = jest.fn(async () => undefined)
|
|
1199
|
+
const comms: CommsLayer = { sendMessage, listMessages, acknowledgeMessage }
|
|
1200
|
+
|
|
1201
|
+
const manager = new RemittanceManager(
|
|
1202
|
+
{ remittanceModules: [mod], messageBox: customBox, threadIdFactory: makeThreadIdFactory() },
|
|
1203
|
+
makeWallet('k1'),
|
|
1204
|
+
comms
|
|
1205
|
+
)
|
|
1206
|
+
|
|
1207
|
+
await manager.sendInvoice('k2', makeInvoiceInput())
|
|
1208
|
+
expect(sendMessage).toHaveBeenCalledWith(expect.objectContaining({ messageBox: customBox }), undefined)
|
|
1209
|
+
})
|
|
1210
|
+
})
|
|
1211
|
+
|
|
1212
|
+
describe('identity verification response – assessReceivedCertificateSufficiency rejects', () => {
|
|
1213
|
+
it('sends termination when certificate assessment fails', async () => {
|
|
1214
|
+
const bus = new MessageBus()
|
|
1215
|
+
const mod = makeModule({ createOption: async () => ({}) })
|
|
1216
|
+
|
|
1217
|
+
const rejectingIdentityLayer: IdentityLayer = {
|
|
1218
|
+
...makeIdentityLayer(),
|
|
1219
|
+
assessReceivedCertificateSufficiency: async () => ({
|
|
1220
|
+
code: 'cert.insufficient',
|
|
1221
|
+
message: 'Certs not sufficient',
|
|
1222
|
+
details: {}
|
|
1223
|
+
} as any)
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const maker = new RemittanceManager(
|
|
1227
|
+
{
|
|
1228
|
+
remittanceModules: [mod],
|
|
1229
|
+
identityLayer: rejectingIdentityLayer,
|
|
1230
|
+
options: {
|
|
1231
|
+
identityOptions: { makerRequestIdentity: 'beforeInvoicing' },
|
|
1232
|
+
identityTimeoutMs: 200,
|
|
1233
|
+
identityPollIntervalMs: 5
|
|
1234
|
+
},
|
|
1235
|
+
threadIdFactory: makeThreadIdFactory()
|
|
1236
|
+
},
|
|
1237
|
+
makeWallet('maker-key'),
|
|
1238
|
+
new TestComms('maker-key', bus)
|
|
1239
|
+
)
|
|
1240
|
+
const taker = new RemittanceManager(
|
|
1241
|
+
{
|
|
1242
|
+
remittanceModules: [mod],
|
|
1243
|
+
identityLayer: makeIdentityLayer(),
|
|
1244
|
+
options: { identityOptions: { makerRequestIdentity: 'beforeInvoicing' }, identityTimeoutMs: 200, identityPollIntervalMs: 5 },
|
|
1245
|
+
threadIdFactory: makeThreadIdFactory()
|
|
1246
|
+
},
|
|
1247
|
+
makeWallet('taker-key'),
|
|
1248
|
+
new TestComms('taker-key', bus)
|
|
1249
|
+
)
|
|
1250
|
+
|
|
1251
|
+
const sendPromise = maker.sendInvoice('taker-key', makeInvoiceInput())
|
|
1252
|
+
await tick()
|
|
1253
|
+
|
|
1254
|
+
// Taker responds to identity request
|
|
1255
|
+
await taker.syncThreads()
|
|
1256
|
+
|
|
1257
|
+
// Maker receives response and rejects via assessReceivedCertificateSufficiency
|
|
1258
|
+
await maker.syncThreads()
|
|
1259
|
+
|
|
1260
|
+
// The taker should receive a termination
|
|
1261
|
+
const takerMsgs = bus.list('taker-key', DEFAULT_REMITTANCE_MESSAGEBOX)
|
|
1262
|
+
const termMsg = takerMsgs.find(m => {
|
|
1263
|
+
const env = JSON.parse(m.body) as RemittanceEnvelope
|
|
1264
|
+
return env.kind === 'termination'
|
|
1265
|
+
})
|
|
1266
|
+
expect(termMsg).toBeDefined()
|
|
1267
|
+
|
|
1268
|
+
// sendInvoice should timeout because identity was rejected
|
|
1269
|
+
await expect(sendPromise).rejects.toThrow()
|
|
1270
|
+
})
|
|
1271
|
+
})
|
|
1272
|
+
})
|