x402-bch-axios 1.1.0 → 1.1.2

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/README.md CHANGED
@@ -36,7 +36,8 @@ const api = withPaymentInterceptor(
36
36
  }
37
37
  )
38
38
 
39
- const response = await api.get('/premium-endpoint')
39
+ // Get data from an endpoint that requires 402 payment for access.
40
+ const response = await api.get('/weather')
40
41
  console.log(response.data)
41
42
  ```
42
43
 
package/index.js CHANGED
@@ -15,6 +15,20 @@
15
15
  import BCHWallet from 'minimal-slp-wallet'
16
16
  import RetryQueue from '@chris.troutner/retry-queue'
17
17
 
18
+ const dependencies = {
19
+ BCHWallet,
20
+ RetryQueue
21
+ }
22
+
23
+ export function __setDependencies (overrides = {}) {
24
+ Object.assign(dependencies, overrides)
25
+ }
26
+
27
+ export function __resetDependencies () {
28
+ dependencies.BCHWallet = BCHWallet
29
+ dependencies.RetryQueue = RetryQueue
30
+ }
31
+
18
32
  const currentUtxo = {
19
33
  txid: null,
20
34
  vout: null,
@@ -29,7 +43,7 @@ const currentUtxo = {
29
43
  * @returns {{ ecpair: any, address: string, wif: string, paymentAmountSats: number, signMessage: (message: string) => string }}
30
44
  */
31
45
  export function createSigner (privateKeyWIF, paymentAmountSats) {
32
- const wallet = new BCHWallet()
46
+ const wallet = new dependencies.BCHWallet()
33
47
  const bchjs = wallet.bchjs
34
48
 
35
49
  const ecpair = bchjs.ECPair.fromWIF(privateKeyWIF)
@@ -83,9 +97,6 @@ export async function createPaymentHeader (
83
97
  txid = null,
84
98
  vout = null
85
99
  ) {
86
- const wallet = new BCHWallet()
87
- await wallet.walletInfoPromise
88
-
89
100
  const authorization = {
90
101
  from: signer.address,
91
102
  to: paymentRequirements.payTo,
@@ -115,13 +126,15 @@ async function sendPayment (signer, paymentRequirements, bchServerConfig = {}) {
115
126
  const { apiType, bchServerURL } = bchServerConfig
116
127
  const paymentAmountSats = signer.paymentAmountSats || paymentRequirements.minAmountRequired
117
128
 
118
- const bchWallet = new BCHWallet(signer.wif, {
129
+ const bchWallet = new dependencies.BCHWallet(signer.wif, {
119
130
  interface: apiType,
120
131
  restURL: bchServerURL
121
132
  })
133
+ // console.log(`sendPayment() - interface: ${apiType}, restURL: ${bchServerURL}, wif: ${signer.wif}, payTo: ${paymentRequirements.payTo}, paymentAmountSats: ${paymentAmountSats}`)
134
+ console.log(`Sending ${paymentAmountSats} for x402 API payment to ${paymentRequirements.payTo}`)
122
135
  await bchWallet.initialize()
123
136
 
124
- const retryQueue = new RetryQueue()
137
+ const retryQueue = new dependencies.RetryQueue()
125
138
  const receivers = [
126
139
  {
127
140
  address: paymentRequirements.payTo,
@@ -195,7 +208,11 @@ export function withPaymentInterceptor (
195
208
  let satsLeft = null
196
209
 
197
210
  if (!currentUtxo.txid || currentUtxo.satsLeft < cost) {
198
- const payment = await sendPayment(signer, paymentRequirements, bchServerConfig)
211
+ const payment = await internals.sendPayment(
212
+ signer,
213
+ paymentRequirements,
214
+ bchServerConfig
215
+ )
199
216
  txid = payment.txid
200
217
  vout = payment.vout
201
218
  satsLeft = payment.satsSent - cost
@@ -231,3 +248,23 @@ export function withPaymentInterceptor (
231
248
 
232
249
  return axiosInstance
233
250
  }
251
+
252
+ const internals = {
253
+ dependencies,
254
+ currentUtxo,
255
+ sendPayment
256
+ }
257
+
258
+ export function __resetCurrentUtxo () {
259
+ currentUtxo.txid = null
260
+ currentUtxo.vout = null
261
+ currentUtxo.satsLeft = 0
262
+ }
263
+
264
+ export function __resetInternals () {
265
+ internals.sendPayment = sendPayment
266
+ __resetDependencies()
267
+ __resetCurrentUtxo()
268
+ }
269
+
270
+ export const __internals = internals
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x402-bch-axios",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Axios wrapper for x402 payment protocol with Bitcoin Cash (BCH) support.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -25,8 +25,8 @@
25
25
  },
26
26
  "repository": "x402-bch/x402-bch-axios",
27
27
  "dependencies": {
28
- "@psf/bch-js": "6.8.3",
29
- "apidoc": "1.2.0"
28
+ "@chris.troutner/retry-queue": "1.0.11",
29
+ "minimal-slp-wallet": "7.0.2"
30
30
  },
31
31
  "devDependencies": {
32
32
  "c8": "10.1.3",
@@ -0,0 +1,302 @@
1
+ /*
2
+ Unit tests for the index.js BCH Axios interceptor.
3
+ */
4
+
5
+ // npm libraries
6
+ import { assert } from 'chai'
7
+ import sinon from 'sinon'
8
+ import cloneDeep from 'lodash.clonedeep'
9
+
10
+ // Unit under test
11
+ import {
12
+ createSigner,
13
+ selectPaymentRequirements,
14
+ createPaymentHeader,
15
+ withPaymentInterceptor,
16
+ __setDependencies,
17
+ __resetDependencies,
18
+ __resetInternals,
19
+ __internals
20
+ } from '../../index.js'
21
+
22
+ describe('#index.js', () => {
23
+ let sandbox
24
+
25
+ beforeEach(() => {
26
+ sandbox = sinon.createSandbox()
27
+ __resetInternals()
28
+ })
29
+
30
+ afterEach(() => {
31
+ sandbox.restore()
32
+ __resetInternals()
33
+ })
34
+
35
+ describe('#createSigner', () => {
36
+ it('should create a signer with derived address and signer method', () => {
37
+ const mockEcpair = { ecpair: true }
38
+ const fromWIFStub = sandbox.stub().returns(mockEcpair)
39
+ const toCashAddressStub = sandbox.stub().returns('bitcoincash:qptest')
40
+ const signMessageStub = sandbox.stub().returns('signed-message')
41
+
42
+ const walletStub = sandbox.stub().returns({
43
+ bchjs: {
44
+ ECPair: {
45
+ fromWIF: fromWIFStub,
46
+ toCashAddress: toCashAddressStub
47
+ },
48
+ BitcoinCash: {
49
+ signMessageWithPrivKey: signMessageStub
50
+ }
51
+ }
52
+ })
53
+
54
+ __setDependencies({ BCHWallet: walletStub })
55
+
56
+ const signer = createSigner('test-wif', 1500)
57
+
58
+ assert.isTrue(walletStub.calledOnce)
59
+ assert.strictEqual(fromWIFStub.firstCall.args[0], 'test-wif')
60
+ assert.strictEqual(toCashAddressStub.firstCall.args[0], mockEcpair)
61
+
62
+ assert.equal(signer.address, 'bitcoincash:qptest')
63
+ assert.equal(signer.paymentAmountSats, 1500)
64
+ assert.equal(signer.wif, 'test-wif')
65
+ assert.strictEqual(signer.ecpair, mockEcpair)
66
+
67
+ const signature = signer.signMessage('message-to-sign')
68
+ assert.equal(signature, 'signed-message')
69
+ assert.deepEqual(signMessageStub.firstCall.args, ['test-wif', 'message-to-sign'])
70
+
71
+ __resetDependencies()
72
+ })
73
+ })
74
+
75
+ describe('#selectPaymentRequirements', () => {
76
+ it('should select the first BCH utxo requirement', () => {
77
+ const accepts = [
78
+ { network: 'eth', scheme: 'account' },
79
+ { network: 'bch', scheme: 'utxo', payTo: 'addr1' },
80
+ { network: 'bch', scheme: 'account' }
81
+ ]
82
+
83
+ const req = selectPaymentRequirements(accepts)
84
+ assert.deepEqual(req, { network: 'bch', scheme: 'utxo', payTo: 'addr1' })
85
+ })
86
+
87
+ it('should throw if no BCH utxo requirement exists', () => {
88
+ assert.throws(
89
+ () => selectPaymentRequirements([{ network: 'btc', scheme: 'utxo' }]),
90
+ /No BCH payment requirements/
91
+ )
92
+ })
93
+ })
94
+
95
+ describe('#createPaymentHeader', () => {
96
+ it('should build a valid payment header payload', async () => {
97
+ const signer = {
98
+ address: 'bitcoincash:qptest',
99
+ paymentAmountSats: 2000,
100
+ signMessage: sandbox.stub().returns('mock-signature')
101
+ }
102
+
103
+ const paymentRequirements = {
104
+ payTo: 'bitcoincash:qprecv',
105
+ minAmountRequired: 1500,
106
+ scheme: 'utxo',
107
+ network: 'bch'
108
+ }
109
+
110
+ const header = await createPaymentHeader(
111
+ signer,
112
+ paymentRequirements,
113
+ 2,
114
+ 'tx123',
115
+ 0
116
+ )
117
+
118
+ const parsed = JSON.parse(header)
119
+ assert.deepEqual(parsed, {
120
+ x402Version: 2,
121
+ scheme: 'utxo',
122
+ network: 'bch',
123
+ payload: {
124
+ signature: 'mock-signature',
125
+ authorization: {
126
+ from: 'bitcoincash:qptest',
127
+ to: 'bitcoincash:qprecv',
128
+ value: 1500,
129
+ txid: 'tx123',
130
+ vout: 0,
131
+ amount: 2000
132
+ }
133
+ }
134
+ })
135
+ assert.isTrue(signer.signMessage.calledOnce)
136
+ })
137
+ })
138
+
139
+ describe('#withPaymentInterceptor', () => {
140
+ function createAxiosInstance () {
141
+ return {
142
+ interceptors: {
143
+ response: {
144
+ use: sandbox.stub()
145
+ }
146
+ },
147
+ request: sandbox.stub()
148
+ }
149
+ }
150
+
151
+ function createSignerStub () {
152
+ return {
153
+ address: 'bitcoincash:qptest',
154
+ paymentAmountSats: 2000,
155
+ signMessage: sandbox.stub().returns('signature')
156
+ }
157
+ }
158
+
159
+ const basePaymentRequirements = {
160
+ network: 'bch',
161
+ scheme: 'utxo',
162
+ payTo: 'bitcoincash:qprecv',
163
+ minAmountRequired: 1500
164
+ }
165
+
166
+ function create402Error (overrides = {}) {
167
+ const defaultError = {
168
+ response: {
169
+ status: 402,
170
+ data: {
171
+ x402Version: 1,
172
+ accepts: [cloneDeep(basePaymentRequirements)]
173
+ }
174
+ },
175
+ config: {
176
+ headers: {}
177
+ }
178
+ }
179
+ return Object.assign(defaultError, overrides)
180
+ }
181
+
182
+ it('should rethrow non-402 errors', async () => {
183
+ const axiosInstance = createAxiosInstance()
184
+ const signer = createSignerStub()
185
+
186
+ withPaymentInterceptor(axiosInstance, signer)
187
+
188
+ const [, errorHandler] = axiosInstance.interceptors.response.use.firstCall.args
189
+ const non402Error = { response: { status: 400 } }
190
+
191
+ try {
192
+ await errorHandler(non402Error)
193
+ assert.fail('Expected rejection')
194
+ } catch (err) {
195
+ assert.strictEqual(err, non402Error)
196
+ }
197
+ })
198
+
199
+ it('should reject when axios config headers are missing', async () => {
200
+ const axiosInstance = createAxiosInstance()
201
+ const signer = createSignerStub()
202
+
203
+ withPaymentInterceptor(axiosInstance, signer)
204
+
205
+ const [, errorHandler] = axiosInstance.interceptors.response.use.firstCall.args
206
+ const error = create402Error({
207
+ config: {}
208
+ })
209
+
210
+ try {
211
+ await errorHandler(error)
212
+ assert.fail('Expected rejection')
213
+ } catch (err) {
214
+ assert.match(err.message, /Missing axios request configuration/)
215
+ }
216
+ })
217
+
218
+ it('should reject when no payment requirements are provided', async () => {
219
+ const axiosInstance = createAxiosInstance()
220
+ const signer = createSignerStub()
221
+
222
+ withPaymentInterceptor(axiosInstance, signer)
223
+
224
+ const [, errorHandler] = axiosInstance.interceptors.response.use.firstCall.args
225
+ const error = create402Error({
226
+ response: { status: 402, data: { accepts: [] } }
227
+ })
228
+
229
+ try {
230
+ await errorHandler(error)
231
+ assert.fail('Expected rejection')
232
+ } catch (err) {
233
+ assert.match(err.message, /No payment requirements/)
234
+ }
235
+ })
236
+
237
+ it('should send payment, attach headers, and retry request', async () => {
238
+ const axiosInstance = createAxiosInstance()
239
+ const signer = createSignerStub()
240
+ signer.signMessage.returns('signed')
241
+
242
+ const sendPaymentStub = sandbox
243
+ .stub()
244
+ .resolves({ txid: 'tx123', vout: 0, satsSent: 2000 })
245
+ __internals.sendPayment = sendPaymentStub
246
+
247
+ axiosInstance.request.resolves({ data: 'ok' })
248
+
249
+ withPaymentInterceptor(axiosInstance, signer)
250
+
251
+ const [, errorHandler] = axiosInstance.interceptors.response.use.firstCall.args
252
+ const error = create402Error()
253
+
254
+ const response = await errorHandler(error)
255
+
256
+ assert.deepEqual(response, { data: 'ok' })
257
+ assert.isTrue(sendPaymentStub.calledOnce)
258
+ assert.isTrue(axiosInstance.request.calledOnce)
259
+
260
+ const updatedConfig = axiosInstance.request.firstCall.args[0]
261
+ assert.isTrue(updatedConfig.__is402Retry)
262
+ assert.property(updatedConfig.headers, 'X-PAYMENT')
263
+ assert.propertyVal(
264
+ updatedConfig.headers,
265
+ 'Access-Control-Expose-Headers',
266
+ 'X-PAYMENT-RESPONSE'
267
+ )
268
+
269
+ const headerPayload = JSON.parse(updatedConfig.headers['X-PAYMENT'])
270
+ assert.equal(headerPayload.payload.authorization.txid, 'tx123')
271
+ assert.equal(__internals.currentUtxo.txid, 'tx123')
272
+ assert.equal(__internals.currentUtxo.satsLeft, 500)
273
+ })
274
+
275
+ it('should reuse cached utxo when sufficient balance remains', async () => {
276
+ const axiosInstance = createAxiosInstance()
277
+ const signer = createSignerStub()
278
+ signer.signMessage.returns('signed')
279
+
280
+ __internals.currentUtxo.txid = 'cached'
281
+ __internals.currentUtxo.vout = 1
282
+ __internals.currentUtxo.satsLeft = 2_000
283
+
284
+ const sendPaymentStub = sandbox.stub()
285
+ __internals.sendPayment = sendPaymentStub
286
+
287
+ axiosInstance.request.resolves({ status: 200 })
288
+
289
+ withPaymentInterceptor(axiosInstance, signer)
290
+
291
+ const [, errorHandler] = axiosInstance.interceptors.response.use.firstCall.args
292
+ const error = create402Error()
293
+
294
+ const result = await errorHandler(error)
295
+ assert.deepEqual(result, { status: 200 })
296
+
297
+ assert.isTrue(sendPaymentStub.notCalled)
298
+ assert.equal(__internals.currentUtxo.txid, 'cached')
299
+ assert.equal(__internals.currentUtxo.satsLeft, 500)
300
+ })
301
+ })
302
+ })
package/lib/util.js DELETED
@@ -1,46 +0,0 @@
1
- /*
2
- An example of a typical utility library. Things to notice:
3
- - This library is exported as a Class.
4
- - External dependencies are embedded into the class 'this' object: this.bchjs
5
- */
6
-
7
- 'use strict'
8
-
9
- // Global npm libraries
10
- import BCHJS from '@psf/bch-js'
11
-
12
- class UtilLib {
13
- constructor () {
14
- // Encapsulate dependencies
15
- this.bchjs = new BCHJS()
16
-
17
- // Bind 'this' object to all class methods
18
- this.getBchData = this.getBchData.bind(this)
19
- }
20
-
21
- async getBchData (addr) {
22
- try {
23
- // Validate Input
24
- if (typeof addr !== 'string') throw new Error('Address must be a string')
25
-
26
- const balance = await this.bchjs.Electrumx.balance(addr)
27
-
28
- const utxos = await this.bchjs.Electrumx.utxo(addr)
29
-
30
- const bchData = {
31
- balance: balance.balance,
32
- utxos: utxos.utxos
33
- }
34
- // console.log(`bchData: ${JSON.stringify(bchData, null, 2)}`)
35
-
36
- return bchData
37
- } catch (err) {
38
- // Optional log to indicate the source of the error. This would normally
39
- // be written with a logging app like Winston.
40
- console.log('Error in util.js/getBalance()')
41
- throw err
42
- }
43
- }
44
- }
45
-
46
- export default UtilLib
@@ -1,33 +0,0 @@
1
- /*
2
- Integration tests for the util.js utility library.
3
- */
4
-
5
- // npm libraries
6
- import chai from 'chai'
7
-
8
- // Unit under test
9
- import UtilLib from '../../lib/util.js'
10
-
11
- // Locally global variables.
12
- const assert = chai.assert
13
- const uut = new UtilLib()
14
-
15
- describe('#util.js', () => {
16
- describe('#getBchData', () => {
17
- it('should get BCH data on an address', async () => {
18
- const addr = 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'
19
-
20
- const bchData = await uut.getBchData(addr)
21
-
22
- // Assert that top-level properties exist.
23
- assert.property(bchData, 'balance')
24
- assert.property(bchData, 'utxos')
25
-
26
- // Assert essential UTXOs properties exist.
27
- assert.isArray(bchData.utxos)
28
- assert.property(bchData.utxos[0], 'tx_pos')
29
- assert.property(bchData.utxos[0], 'tx_hash')
30
- assert.property(bchData.utxos[0], 'value')
31
- })
32
- })
33
- })
@@ -1,30 +0,0 @@
1
- /*
2
- A mocking library for util.js unit tests.
3
- A mocking library contains data to use in place of the data that would come
4
- from an external dependency.
5
- */
6
-
7
- 'use strict'
8
-
9
- const mockBalance = {
10
- success: true,
11
- balance: {
12
- confirmed: 1000,
13
- unconfirmed: 0
14
- }
15
- }
16
-
17
- const mockUtxos = {
18
- success: true,
19
- utxos: [
20
- {
21
- height: 601861,
22
- tx_hash:
23
- '6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d',
24
- tx_pos: 0,
25
- value: 1000
26
- }
27
- ]
28
- }
29
-
30
- export default { mockBalance, mockUtxos }
@@ -1,69 +0,0 @@
1
- /*
2
- Unit tests for the util.js utility library.
3
- */
4
-
5
- // npm libraries
6
- import { assert } from 'chai'
7
- import sinon from 'sinon'
8
- import cloneDeep from 'lodash.clonedeep'
9
-
10
- // Mocking data libraries.
11
- import mockDataLib from './mocks/util-mocks.js'
12
-
13
- // Unit under test
14
- import UtilLib from '../../lib/util.js'
15
-
16
- describe('#util.js', () => {
17
- let sandbox
18
- let mockData
19
- let uut
20
-
21
- beforeEach(() => {
22
- // Restore the sandbox before each test.
23
- sandbox = sinon.createSandbox()
24
-
25
- // Clone the mock data.
26
- mockData = cloneDeep(mockDataLib)
27
-
28
- uut = new UtilLib()
29
- })
30
-
31
- afterEach(() => sandbox.restore())
32
-
33
- describe('#getBchData', () => {
34
- it('should throw error if address is not a string', async () => {
35
- try {
36
- const addr = 1234
37
-
38
- await uut.getBchData(addr)
39
-
40
- assert.equal(true, false, 'unexpected result')
41
- } catch (err) {
42
- assert.include(err.message, 'Address must be a string')
43
- }
44
- })
45
-
46
- it('should get BCH data on an address', async () => {
47
- // Mock external dependencies.
48
- sandbox
49
- .stub(uut.bchjs.Electrumx, 'balance')
50
- .resolves(mockData.mockBalance)
51
- sandbox.stub(uut.bchjs.Electrumx, 'utxo').resolves(mockData.mockUtxos)
52
-
53
- const addr = 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'
54
-
55
- const bchData = await uut.getBchData(addr)
56
- // console.log(`bchData: ${JSON.stringify(bchData, null, 2)}`)
57
-
58
- // Assert that top-level properties exist.
59
- assert.property(bchData, 'balance')
60
- assert.property(bchData, 'utxos')
61
-
62
- // Assert essential UTXOs properties exist.
63
- assert.isArray(bchData.utxos)
64
- assert.property(bchData.utxos[0], 'tx_pos')
65
- assert.property(bchData.utxos[0], 'tx_hash')
66
- assert.property(bchData.utxos[0], 'value')
67
- })
68
- })
69
- })