x402-bch-axios 2.1.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 +47 -23
- package/package.json +2 -2
- package/test/unit/index-unit.js +339 -0
package/index.js
CHANGED
|
@@ -165,33 +165,57 @@ export async function createPaymentHeader (
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
async function sendPayment (signer, paymentRequirements, bchServerConfig = {}) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
+
]
|
|
172
189
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
+
}
|
|
186
203
|
}
|
|
187
|
-
]
|
|
188
204
|
|
|
189
|
-
|
|
205
|
+
const txid = await retryQueue.addToQueue(sendWithRetry, receivers)
|
|
190
206
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
195
219
|
}
|
|
196
220
|
}
|
|
197
221
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x402-bch-axios",
|
|
3
|
-
"version": "2.1.
|
|
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
|
@@ -506,4 +506,343 @@ describe('#index.js', () => {
|
|
|
506
506
|
assert.isTrue(sendPaymentStub.calledOnce)
|
|
507
507
|
})
|
|
508
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
|
+
})
|
|
509
848
|
})
|