x402-bch-axios 2.0.0 → 2.1.1
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/index.js +99 -26
- package/package.json +2 -2
- package/test/unit/index-unit.js +388 -7
package/index.js
CHANGED
|
@@ -123,13 +123,16 @@ export async function createPaymentHeader (
|
|
|
123
123
|
// Support both v1 (minAmountRequired) and v2 (amount) field names
|
|
124
124
|
const amountRequired = paymentRequirements.amount || paymentRequirements.minAmountRequired
|
|
125
125
|
|
|
126
|
+
// Check if this is "check my tab" mode (txid === "*")
|
|
127
|
+
const isCheckMyTabMode = txid === '*'
|
|
128
|
+
|
|
126
129
|
const authorization = {
|
|
127
130
|
from: signer.address,
|
|
128
131
|
to: paymentRequirements.payTo,
|
|
129
132
|
value: amountRequired,
|
|
130
|
-
txid,
|
|
131
|
-
vout,
|
|
132
|
-
amount: signer.paymentAmountSats
|
|
133
|
+
txid: isCheckMyTabMode ? '*' : txid,
|
|
134
|
+
vout: isCheckMyTabMode ? null : vout,
|
|
135
|
+
amount: isCheckMyTabMode ? null : signer.paymentAmountSats
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
const messageToSign = JSON.stringify(authorization)
|
|
@@ -162,33 +165,57 @@ export async function createPaymentHeader (
|
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
async function sendPayment (signer, paymentRequirements, bchServerConfig = {}) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
try {
|
|
169
|
+
const { apiType, bchServerURL } = bchServerConfig
|
|
170
|
+
// Support both v1 (minAmountRequired) and v2 (amount) field names
|
|
171
|
+
const amountRequired = paymentRequirements.amount || paymentRequirements.minAmountRequired
|
|
172
|
+
const paymentAmountSats = signer.paymentAmountSats || amountRequired
|
|
173
|
+
|
|
174
|
+
const bchWallet = new dependencies.BCHWallet(signer.wif, {
|
|
175
|
+
interface: apiType,
|
|
176
|
+
restURL: bchServerURL
|
|
177
|
+
})
|
|
178
|
+
// console.log(`sendPayment() - interface: ${apiType}, restURL: ${bchServerURL}, wif: ${signer.wif}, payTo: ${paymentRequirements.payTo}, paymentAmountSats: ${paymentAmountSats}`)
|
|
179
|
+
console.log(`Sending ${paymentAmountSats} for x402 API payment to ${paymentRequirements.payTo}`)
|
|
180
|
+
await bchWallet.initialize()
|
|
181
|
+
|
|
182
|
+
const retryQueue = new dependencies.RetryQueue()
|
|
183
|
+
const receivers = [
|
|
184
|
+
{
|
|
185
|
+
address: paymentRequirements.payTo,
|
|
186
|
+
amountSat: paymentAmountSats
|
|
187
|
+
}
|
|
188
|
+
]
|
|
169
189
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
190
|
+
// Wrap send function to detect "Insufficient balance" errors
|
|
191
|
+
const sendWithRetry = async (receivers) => {
|
|
192
|
+
try {
|
|
193
|
+
return await bchWallet.send(receivers)
|
|
194
|
+
} catch (error) {
|
|
195
|
+
// Check if error message contains "Insufficient balance"
|
|
196
|
+
if (error.message && error.message.includes('Insufficient balance')) {
|
|
197
|
+
return null
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Re-throw other errors normally (will be retried)
|
|
201
|
+
throw error
|
|
202
|
+
}
|
|
183
203
|
}
|
|
184
|
-
]
|
|
185
204
|
|
|
186
|
-
|
|
205
|
+
const txid = await retryQueue.addToQueue(sendWithRetry, receivers)
|
|
187
206
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
207
|
+
if (txid === null) {
|
|
208
|
+
throw new Error('Insufficient balance')
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
txid,
|
|
213
|
+
vout: 0,
|
|
214
|
+
satsSent: paymentAmountSats
|
|
215
|
+
}
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.error('Error in x402-bch-axios/sendPayment(): ', err.message)
|
|
218
|
+
throw err
|
|
192
219
|
}
|
|
193
220
|
}
|
|
194
221
|
|
|
@@ -232,10 +259,17 @@ export function withPaymentInterceptor (
|
|
|
232
259
|
return Promise.reject(new Error('Missing axios request configuration'))
|
|
233
260
|
}
|
|
234
261
|
|
|
262
|
+
// Prevent infinite loops
|
|
235
263
|
if (originalConfig.__is402Retry) {
|
|
236
264
|
return Promise.reject(error)
|
|
237
265
|
}
|
|
238
266
|
|
|
267
|
+
// If this is a "check my tab" retry that failed, fall back to UTXO generation
|
|
268
|
+
if (originalConfig.__is402CheckMyTab) {
|
|
269
|
+
// Clear the flag and proceed with UTXO generation
|
|
270
|
+
originalConfig.__is402CheckMyTab = false
|
|
271
|
+
}
|
|
272
|
+
|
|
239
273
|
// Parse payment requirements - v2 can come from header, v1 from body
|
|
240
274
|
let paymentRequired = null
|
|
241
275
|
let x402Version = 1
|
|
@@ -277,6 +311,45 @@ export function withPaymentInterceptor (
|
|
|
277
311
|
// Convert to number for calculations (v2 uses strings, v1 uses numbers)
|
|
278
312
|
const cost = Number(paymentRequirements.amount || paymentRequirements.minAmountRequired)
|
|
279
313
|
|
|
314
|
+
// Try "check my tab" mode first if no UTXO is tracked
|
|
315
|
+
if (!currentUtxo.txid && !originalConfig.__is402CheckMyTab) {
|
|
316
|
+
// Attempt "check my tab" mode
|
|
317
|
+
const checkMyTabHeader = await createPaymentHeader(
|
|
318
|
+
signer,
|
|
319
|
+
paymentRequirements,
|
|
320
|
+
x402Version || 2,
|
|
321
|
+
'*', // txid = "*" for check my tab mode
|
|
322
|
+
null, // vout = null for check my tab mode
|
|
323
|
+
resource,
|
|
324
|
+
extensions
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
originalConfig.__is402CheckMyTab = true
|
|
328
|
+
originalConfig.__is402Retry = true
|
|
329
|
+
originalConfig.headers['PAYMENT-SIGNATURE'] = checkMyTabHeader
|
|
330
|
+
originalConfig.headers['Access-Control-Expose-Headers'] = 'PAYMENT-RESPONSE'
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
const checkMyTabResponse = await axiosInstance.request(originalConfig)
|
|
334
|
+
// "Check my tab" succeeded - return response and continue using check my tab mode
|
|
335
|
+
// Don't update currentUtxo since we're using check my tab mode
|
|
336
|
+
return checkMyTabResponse
|
|
337
|
+
} catch (checkMyTabError) {
|
|
338
|
+
// "Check my tab" failed - check if it's a 402 error
|
|
339
|
+
if (checkMyTabError.response && checkMyTabError.response.status === 402) {
|
|
340
|
+
// 402 error from "check my tab" - fall back to UTXO generation
|
|
341
|
+
// Reset flags and continue with standard flow
|
|
342
|
+
originalConfig.__is402CheckMyTab = false
|
|
343
|
+
originalConfig.__is402Retry = false
|
|
344
|
+
// Continue to UTXO generation logic below
|
|
345
|
+
} else {
|
|
346
|
+
// Non-402 error (network error, etc.) - reject it
|
|
347
|
+
return Promise.reject(checkMyTabError)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Standard mode: use existing UTXO or generate new one
|
|
280
353
|
let txid = null
|
|
281
354
|
let vout = null
|
|
282
355
|
let satsLeft = null
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x402-bch-axios",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Axios wrapper for x402 payment protocol with Bitcoin Cash (BCH) support.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"repository": "x402-bch/x402-bch-axios",
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@chris.troutner/retry-queue": "1.0.11",
|
|
29
|
-
"minimal-slp-wallet": "7.0.
|
|
29
|
+
"minimal-slp-wallet": "7.0.5"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"c8": "10.1.3",
|
package/test/unit/index-unit.js
CHANGED
|
@@ -315,7 +315,12 @@ describe('#index.js', () => {
|
|
|
315
315
|
.resolves({ txid: 'tx123', vout: 0, satsSent: 2000 })
|
|
316
316
|
__internals.sendPayment = sendPaymentStub
|
|
317
317
|
|
|
318
|
-
|
|
318
|
+
// First call (check my tab) returns 402, second call (with UTXO) succeeds
|
|
319
|
+
axiosInstance.request
|
|
320
|
+
.onFirstCall()
|
|
321
|
+
.rejects(create402Error())
|
|
322
|
+
.onSecondCall()
|
|
323
|
+
.resolves({ data: 'ok' })
|
|
319
324
|
|
|
320
325
|
withPaymentInterceptor(axiosInstance, signer)
|
|
321
326
|
|
|
@@ -326,9 +331,10 @@ describe('#index.js', () => {
|
|
|
326
331
|
|
|
327
332
|
assert.deepEqual(response, { data: 'ok' })
|
|
328
333
|
assert.isTrue(sendPaymentStub.calledOnce)
|
|
329
|
-
assert.isTrue(axiosInstance.request.
|
|
334
|
+
assert.isTrue(axiosInstance.request.calledTwice)
|
|
330
335
|
|
|
331
|
-
|
|
336
|
+
// Check the second call (with UTXO) - first call was check my tab
|
|
337
|
+
const updatedConfig = axiosInstance.request.secondCall.args[0]
|
|
332
338
|
assert.isTrue(updatedConfig.__is402Retry)
|
|
333
339
|
assert.property(updatedConfig.headers, 'PAYMENT-SIGNATURE')
|
|
334
340
|
assert.propertyVal(
|
|
@@ -382,7 +388,22 @@ describe('#index.js', () => {
|
|
|
382
388
|
.resolves({ txid: 'tx123', vout: 0, satsSent: 2000 })
|
|
383
389
|
__internals.sendPayment = sendPaymentStub
|
|
384
390
|
|
|
385
|
-
|
|
391
|
+
// First call (check my tab) returns 402, second call (with UTXO) succeeds
|
|
392
|
+
axiosInstance.request
|
|
393
|
+
.onFirstCall()
|
|
394
|
+
.rejects({
|
|
395
|
+
response: {
|
|
396
|
+
status: 402,
|
|
397
|
+
headers: {},
|
|
398
|
+
data: {
|
|
399
|
+
x402Version: 2,
|
|
400
|
+
accepts: [cloneDeep(basePaymentRequirements)]
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
config: { headers: {} }
|
|
404
|
+
})
|
|
405
|
+
.onSecondCall()
|
|
406
|
+
.resolves({ data: 'ok' })
|
|
386
407
|
|
|
387
408
|
withPaymentInterceptor(axiosInstance, signer)
|
|
388
409
|
|
|
@@ -414,9 +435,10 @@ describe('#index.js', () => {
|
|
|
414
435
|
|
|
415
436
|
assert.deepEqual(response, { data: 'ok' })
|
|
416
437
|
assert.isTrue(sendPaymentStub.calledOnce)
|
|
417
|
-
assert.isTrue(axiosInstance.request.
|
|
438
|
+
assert.isTrue(axiosInstance.request.calledTwice)
|
|
418
439
|
|
|
419
|
-
|
|
440
|
+
// Check the second call (with UTXO) - first call was check my tab
|
|
441
|
+
const updatedConfig = axiosInstance.request.secondCall.args[0]
|
|
420
442
|
const headerPayload = JSON.parse(updatedConfig.headers['PAYMENT-SIGNATURE'])
|
|
421
443
|
assert.equal(headerPayload.x402Version, 2)
|
|
422
444
|
assert.deepEqual(headerPayload.resource, baseResource)
|
|
@@ -432,7 +454,27 @@ describe('#index.js', () => {
|
|
|
432
454
|
.resolves({ txid: 'tx123', vout: 0, satsSent: 2000 })
|
|
433
455
|
__internals.sendPayment = sendPaymentStub
|
|
434
456
|
|
|
435
|
-
|
|
457
|
+
// First call (check my tab) returns 402, second call (with UTXO) succeeds
|
|
458
|
+
axiosInstance.request
|
|
459
|
+
.onFirstCall()
|
|
460
|
+
.rejects({
|
|
461
|
+
response: {
|
|
462
|
+
status: 402,
|
|
463
|
+
headers: {},
|
|
464
|
+
data: {
|
|
465
|
+
x402Version: 1,
|
|
466
|
+
accepts: [{
|
|
467
|
+
network: 'bch',
|
|
468
|
+
scheme: 'utxo',
|
|
469
|
+
payTo: 'bitcoincash:qprecv',
|
|
470
|
+
minAmountRequired: 1500
|
|
471
|
+
}]
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
config: { headers: {} }
|
|
475
|
+
})
|
|
476
|
+
.onSecondCall()
|
|
477
|
+
.resolves({ data: 'ok' })
|
|
436
478
|
|
|
437
479
|
withPaymentInterceptor(axiosInstance, signer)
|
|
438
480
|
|
|
@@ -464,4 +506,343 @@ describe('#index.js', () => {
|
|
|
464
506
|
assert.isTrue(sendPaymentStub.calledOnce)
|
|
465
507
|
})
|
|
466
508
|
})
|
|
509
|
+
|
|
510
|
+
describe('#sendPayment', () => {
|
|
511
|
+
function createSignerStub () {
|
|
512
|
+
return {
|
|
513
|
+
wif: 'test-wif',
|
|
514
|
+
paymentAmountSats: 2000,
|
|
515
|
+
address: 'bitcoincash:qptest'
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function createPaymentRequirementsStub () {
|
|
520
|
+
return {
|
|
521
|
+
payTo: 'bitcoincash:qprecv',
|
|
522
|
+
amount: '1500',
|
|
523
|
+
scheme: 'utxo',
|
|
524
|
+
network: 'bip122:000000000000000000651ef99cb9fcbe'
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
it('should successfully send payment and return txid, vout, and satsSent', async () => {
|
|
529
|
+
const signer = createSignerStub()
|
|
530
|
+
const paymentRequirements = createPaymentRequirementsStub()
|
|
531
|
+
const bchServerConfig = {
|
|
532
|
+
apiType: 'rest-api',
|
|
533
|
+
bchServerURL: 'https://api.example.com'
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const mockBchWallet = {
|
|
537
|
+
initialize: sandbox.stub().resolves(),
|
|
538
|
+
send: sandbox.stub().resolves('tx123')
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const mockRetryQueue = {
|
|
542
|
+
addToQueue: sandbox.stub().resolves('tx123')
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const BCHWalletStub = sandbox.stub().returns(mockBchWallet)
|
|
546
|
+
const RetryQueueStub = sandbox.stub().returns(mockRetryQueue)
|
|
547
|
+
|
|
548
|
+
__setDependencies({
|
|
549
|
+
BCHWallet: BCHWalletStub,
|
|
550
|
+
RetryQueue: RetryQueueStub
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
const result = await __internals.sendPayment(signer, paymentRequirements, bchServerConfig)
|
|
554
|
+
|
|
555
|
+
assert.deepEqual(result, {
|
|
556
|
+
txid: 'tx123',
|
|
557
|
+
vout: 0,
|
|
558
|
+
satsSent: 2000
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
assert.isTrue(BCHWalletStub.calledOnce)
|
|
562
|
+
assert.deepEqual(BCHWalletStub.firstCall.args[0], 'test-wif')
|
|
563
|
+
assert.deepEqual(BCHWalletStub.firstCall.args[1], {
|
|
564
|
+
interface: 'rest-api',
|
|
565
|
+
restURL: 'https://api.example.com'
|
|
566
|
+
})
|
|
567
|
+
assert.isTrue(mockBchWallet.initialize.calledOnce)
|
|
568
|
+
assert.isTrue(RetryQueueStub.calledOnce)
|
|
569
|
+
assert.isTrue(mockRetryQueue.addToQueue.calledOnce)
|
|
570
|
+
|
|
571
|
+
// Verify sendWithRetry was called with receivers
|
|
572
|
+
const sendWithRetry = mockRetryQueue.addToQueue.firstCall.args[0]
|
|
573
|
+
const receivers = mockRetryQueue.addToQueue.firstCall.args[1]
|
|
574
|
+
assert.isFunction(sendWithRetry)
|
|
575
|
+
assert.deepEqual(receivers, [{
|
|
576
|
+
address: 'bitcoincash:qprecv',
|
|
577
|
+
amountSat: 2000
|
|
578
|
+
}])
|
|
579
|
+
|
|
580
|
+
__resetDependencies()
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
it('should throw "Insufficient balance" error when sendWithRetry returns null', async () => {
|
|
584
|
+
const signer = createSignerStub()
|
|
585
|
+
const paymentRequirements = createPaymentRequirementsStub()
|
|
586
|
+
const bchServerConfig = {}
|
|
587
|
+
|
|
588
|
+
const mockBchWallet = {
|
|
589
|
+
initialize: sandbox.stub().resolves(),
|
|
590
|
+
send: sandbox.stub().rejects(new Error('Insufficient balance'))
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const mockRetryQueue = {
|
|
594
|
+
addToQueue: sandbox.stub().resolves(null) // sendWithRetry returns null for insufficient balance
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const BCHWalletStub = sandbox.stub().returns(mockBchWallet)
|
|
598
|
+
const RetryQueueStub = sandbox.stub().returns(mockRetryQueue)
|
|
599
|
+
|
|
600
|
+
__setDependencies({
|
|
601
|
+
BCHWallet: BCHWalletStub,
|
|
602
|
+
RetryQueue: RetryQueueStub
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
try {
|
|
606
|
+
await __internals.sendPayment(signer, paymentRequirements, bchServerConfig)
|
|
607
|
+
assert.fail('Expected "Insufficient balance" error to be thrown')
|
|
608
|
+
} catch (err) {
|
|
609
|
+
assert.equal(err.message, 'Insufficient balance')
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Verify sendWithRetry was called and handled the error
|
|
613
|
+
assert.isTrue(mockRetryQueue.addToQueue.calledOnce)
|
|
614
|
+
const sendWithRetry = mockRetryQueue.addToQueue.firstCall.args[0]
|
|
615
|
+
|
|
616
|
+
// Test sendWithRetry directly to verify it returns null for insufficient balance
|
|
617
|
+
try {
|
|
618
|
+
const result = await sendWithRetry([{ address: 'test', amountSat: 1000 }])
|
|
619
|
+
assert.strictEqual(result, null)
|
|
620
|
+
} catch (err) {
|
|
621
|
+
assert.fail('sendWithRetry should return null, not throw')
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
__resetDependencies()
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
it('should handle "Insufficient balance" error in sendWithRetry wrapper', async () => {
|
|
628
|
+
const signer = createSignerStub()
|
|
629
|
+
const paymentRequirements = createPaymentRequirementsStub()
|
|
630
|
+
|
|
631
|
+
const insufficientBalanceError = new Error('Insufficient balance')
|
|
632
|
+
const mockBchWallet = {
|
|
633
|
+
initialize: sandbox.stub().resolves(),
|
|
634
|
+
send: sandbox.stub().rejects(insufficientBalanceError)
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const mockRetryQueue = {
|
|
638
|
+
addToQueue: sandbox.stub().callsFake(async (fn, args) => {
|
|
639
|
+
// Simulate what RetryQueue does - call the function
|
|
640
|
+
return await fn(args)
|
|
641
|
+
})
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const BCHWalletStub = sandbox.stub().returns(mockBchWallet)
|
|
645
|
+
const RetryQueueStub = sandbox.stub().returns(mockRetryQueue)
|
|
646
|
+
|
|
647
|
+
__setDependencies({
|
|
648
|
+
BCHWallet: BCHWalletStub,
|
|
649
|
+
RetryQueue: RetryQueueStub
|
|
650
|
+
})
|
|
651
|
+
|
|
652
|
+
try {
|
|
653
|
+
await __internals.sendPayment(signer, paymentRequirements)
|
|
654
|
+
assert.fail('Expected "Insufficient balance" error to be thrown')
|
|
655
|
+
} catch (err) {
|
|
656
|
+
assert.equal(err.message, 'Insufficient balance')
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Verify that sendWithRetry was called and returned null
|
|
660
|
+
assert.isTrue(mockRetryQueue.addToQueue.calledOnce)
|
|
661
|
+
const sendWithRetry = mockRetryQueue.addToQueue.firstCall.args[0]
|
|
662
|
+
const receivers = mockRetryQueue.addToQueue.firstCall.args[1]
|
|
663
|
+
|
|
664
|
+
// Call sendWithRetry directly to verify it returns null
|
|
665
|
+
const result = await sendWithRetry(receivers)
|
|
666
|
+
assert.strictEqual(result, null)
|
|
667
|
+
|
|
668
|
+
__resetDependencies()
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
it('should re-throw other errors from sendWithRetry for retry queue to handle', async () => {
|
|
672
|
+
const networkError = new Error('Network timeout')
|
|
673
|
+
const mockBchWallet = {
|
|
674
|
+
initialize: sandbox.stub().resolves(),
|
|
675
|
+
send: sandbox.stub().rejects(networkError)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// RetryQueue should eventually succeed after retries
|
|
679
|
+
const mockRetryQueue = {
|
|
680
|
+
addToQueue: sandbox.stub().resolves('tx456')
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const BCHWalletStub = sandbox.stub().returns(mockBchWallet)
|
|
684
|
+
const RetryQueueStub = sandbox.stub().returns(mockRetryQueue)
|
|
685
|
+
|
|
686
|
+
__setDependencies({
|
|
687
|
+
BCHWallet: BCHWalletStub,
|
|
688
|
+
RetryQueue: RetryQueueStub
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
// Test sendWithRetry directly to verify it re-throws non-insufficient-balance errors
|
|
692
|
+
const receivers = [{ address: 'test', amountSat: 1000 }]
|
|
693
|
+
|
|
694
|
+
// Extract sendWithRetry by simulating what happens in sendPayment
|
|
695
|
+
const sendWithRetry = async (receivers) => {
|
|
696
|
+
try {
|
|
697
|
+
return await mockBchWallet.send(receivers)
|
|
698
|
+
} catch (error) {
|
|
699
|
+
if (error.message && error.message.includes('Insufficient balance')) {
|
|
700
|
+
return null
|
|
701
|
+
}
|
|
702
|
+
throw error
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Verify that sendWithRetry re-throws non-insufficient-balance errors
|
|
707
|
+
try {
|
|
708
|
+
await sendWithRetry(receivers)
|
|
709
|
+
assert.fail('Expected network error to be thrown')
|
|
710
|
+
} catch (err) {
|
|
711
|
+
assert.equal(err.message, 'Network timeout')
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Verify that send was called
|
|
715
|
+
assert.isTrue(mockBchWallet.send.calledOnce)
|
|
716
|
+
|
|
717
|
+
__resetDependencies()
|
|
718
|
+
})
|
|
719
|
+
|
|
720
|
+
it('should use paymentAmountSats from signer when available', async () => {
|
|
721
|
+
const signer = createSignerStub()
|
|
722
|
+
signer.paymentAmountSats = 5000
|
|
723
|
+
const paymentRequirements = createPaymentRequirementsStub()
|
|
724
|
+
|
|
725
|
+
const mockBchWallet = {
|
|
726
|
+
initialize: sandbox.stub().resolves(),
|
|
727
|
+
send: sandbox.stub().resolves('tx789')
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const mockRetryQueue = {
|
|
731
|
+
addToQueue: sandbox.stub().resolves('tx789')
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const BCHWalletStub = sandbox.stub().returns(mockBchWallet)
|
|
735
|
+
const RetryQueueStub = sandbox.stub().returns(mockRetryQueue)
|
|
736
|
+
|
|
737
|
+
__setDependencies({
|
|
738
|
+
BCHWallet: BCHWalletStub,
|
|
739
|
+
RetryQueue: RetryQueueStub
|
|
740
|
+
})
|
|
741
|
+
|
|
742
|
+
await __internals.sendPayment(signer, paymentRequirements)
|
|
743
|
+
|
|
744
|
+
const receivers = mockRetryQueue.addToQueue.firstCall.args[1]
|
|
745
|
+
assert.equal(receivers[0].amountSat, 5000)
|
|
746
|
+
|
|
747
|
+
__resetDependencies()
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
it('should use amountRequired from paymentRequirements when signer.paymentAmountSats is not set', async () => {
|
|
751
|
+
const signer = createSignerStub()
|
|
752
|
+
delete signer.paymentAmountSats
|
|
753
|
+
const paymentRequirements = createPaymentRequirementsStub()
|
|
754
|
+
paymentRequirements.amount = '3000'
|
|
755
|
+
|
|
756
|
+
const mockBchWallet = {
|
|
757
|
+
initialize: sandbox.stub().resolves(),
|
|
758
|
+
send: sandbox.stub().resolves('tx999')
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const mockRetryQueue = {
|
|
762
|
+
addToQueue: sandbox.stub().resolves('tx999')
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const BCHWalletStub = sandbox.stub().returns(mockBchWallet)
|
|
766
|
+
const RetryQueueStub = sandbox.stub().returns(mockRetryQueue)
|
|
767
|
+
|
|
768
|
+
__setDependencies({
|
|
769
|
+
BCHWallet: BCHWalletStub,
|
|
770
|
+
RetryQueue: RetryQueueStub
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
await __internals.sendPayment(signer, paymentRequirements)
|
|
774
|
+
|
|
775
|
+
const receivers = mockRetryQueue.addToQueue.firstCall.args[1]
|
|
776
|
+
assert.equal(receivers[0].amountSat, '3000')
|
|
777
|
+
|
|
778
|
+
__resetDependencies()
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
it('should only match "Insufficient balance" error message (case-sensitive)', async () => {
|
|
782
|
+
const mockBchWallet = {
|
|
783
|
+
initialize: sandbox.stub().resolves(),
|
|
784
|
+
send: sandbox.stub().rejects(new Error('INSUFFICIENT BALANCE'))
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Test sendWithRetry directly
|
|
788
|
+
const sendWithRetry = async (receivers) => {
|
|
789
|
+
try {
|
|
790
|
+
return await mockBchWallet.send(receivers)
|
|
791
|
+
} catch (error) {
|
|
792
|
+
if (error.message && error.message.includes('Insufficient balance')) {
|
|
793
|
+
return null
|
|
794
|
+
}
|
|
795
|
+
throw error
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const receivers = [{ address: 'test', amountSat: 1000 }]
|
|
800
|
+
|
|
801
|
+
// This should NOT return null because the error message doesn't match (case-sensitive check)
|
|
802
|
+
// The includes() method is case-sensitive, so 'INSUFFICIENT BALANCE' won't match 'Insufficient balance'
|
|
803
|
+
try {
|
|
804
|
+
const result = await sendWithRetry(receivers)
|
|
805
|
+
// If it returns null, that's unexpected
|
|
806
|
+
if (result === null) {
|
|
807
|
+
assert.fail('Should not return null for case-mismatched error message')
|
|
808
|
+
}
|
|
809
|
+
} catch (err) {
|
|
810
|
+
// Expected - error should be re-thrown because it doesn't match
|
|
811
|
+
assert.equal(err.message, 'INSUFFICIENT BALANCE')
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
__resetDependencies()
|
|
815
|
+
})
|
|
816
|
+
|
|
817
|
+
it('should propagate errors from wallet initialization', async () => {
|
|
818
|
+
const signer = createSignerStub()
|
|
819
|
+
const paymentRequirements = createPaymentRequirementsStub()
|
|
820
|
+
|
|
821
|
+
const initError = new Error('Failed to initialize wallet')
|
|
822
|
+
const mockBchWallet = {
|
|
823
|
+
initialize: sandbox.stub().rejects(initError),
|
|
824
|
+
send: sandbox.stub()
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const BCHWalletStub = sandbox.stub().returns(mockBchWallet)
|
|
828
|
+
const RetryQueueStub = sandbox.stub()
|
|
829
|
+
|
|
830
|
+
__setDependencies({
|
|
831
|
+
BCHWallet: BCHWalletStub,
|
|
832
|
+
RetryQueue: RetryQueueStub
|
|
833
|
+
})
|
|
834
|
+
|
|
835
|
+
try {
|
|
836
|
+
await __internals.sendPayment(signer, paymentRequirements)
|
|
837
|
+
assert.fail('Expected initialization error to be thrown')
|
|
838
|
+
} catch (err) {
|
|
839
|
+
assert.equal(err.message, 'Failed to initialize wallet')
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
assert.isTrue(mockBchWallet.initialize.calledOnce)
|
|
843
|
+
assert.isTrue(RetryQueueStub.notCalled)
|
|
844
|
+
|
|
845
|
+
__resetDependencies()
|
|
846
|
+
})
|
|
847
|
+
})
|
|
467
848
|
})
|