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 CHANGED
@@ -165,33 +165,57 @@ export async function createPaymentHeader (
165
165
  }
166
166
 
167
167
  async function sendPayment (signer, paymentRequirements, bchServerConfig = {}) {
168
- const { apiType, bchServerURL } = bchServerConfig
169
- // Support both v1 (minAmountRequired) and v2 (amount) field names
170
- const amountRequired = paymentRequirements.amount || paymentRequirements.minAmountRequired
171
- const paymentAmountSats = signer.paymentAmountSats || amountRequired
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
- const bchWallet = new dependencies.BCHWallet(signer.wif, {
174
- interface: apiType,
175
- restURL: bchServerURL
176
- })
177
- // console.log(`sendPayment() - interface: ${apiType}, restURL: ${bchServerURL}, wif: ${signer.wif}, payTo: ${paymentRequirements.payTo}, paymentAmountSats: ${paymentAmountSats}`)
178
- console.log(`Sending ${paymentAmountSats} for x402 API payment to ${paymentRequirements.payTo}`)
179
- await bchWallet.initialize()
180
-
181
- const retryQueue = new dependencies.RetryQueue()
182
- const receivers = [
183
- {
184
- address: paymentRequirements.payTo,
185
- amountSat: paymentAmountSats
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
- const txid = await retryQueue.addToQueue(bchWallet.send.bind(bchWallet), receivers)
205
+ const txid = await retryQueue.addToQueue(sendWithRetry, receivers)
190
206
 
191
- return {
192
- txid,
193
- vout: 0,
194
- satsSent: paymentAmountSats
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.0",
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.2"
29
+ "minimal-slp-wallet": "7.0.5"
30
30
  },
31
31
  "devDependencies": {
32
32
  "c8": "10.1.3",
@@ -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
  })