x402-bch-axios 1.0.0 → 1.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/README.md CHANGED
@@ -1,7 +1,61 @@
1
1
  # x402-bch-axios
2
2
 
3
- This is a placeholder for what will become the x402-bch-axios library.
3
+ JavaScript helpers for handling HTTP 402 responses against Bitcoin Cash powered
4
+ x402 endpoints. This package ports the `withPaymentInterceptor` experience from
5
+ the TypeScript `x402-axios` client and adapts it for BCH UTXO payments.
4
6
 
5
- # Licence
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install x402-bch-axios minimal-slp-wallet axios
11
+ ```
12
+
13
+ The library depends on `minimal-slp-wallet` and assumes you are using ESM
14
+ (`type: module`) in your project.
15
+
16
+ ## Usage
17
+
18
+ ```javascript
19
+ import axios from 'axios'
20
+ import {
21
+ createSigner,
22
+ withPaymentInterceptor
23
+ } from 'x402-bch-axios'
24
+
25
+ const signer = createSigner(process.env.PRIVATE_KEY_WIF, 2000)
26
+
27
+ const api = withPaymentInterceptor(
28
+ axios.create({ baseURL: 'https://example.com' }),
29
+ signer,
30
+ // Optional payment requirements selector; defaults to BCH utxo
31
+ undefined,
32
+ // Optional BCH wallet config matching minimal-slp-wallet expectations
33
+ {
34
+ apiType: 'consumer-api',
35
+ bchServerURL: 'https://free-bch.fullstack.cash'
36
+ }
37
+ )
38
+
39
+ // Get data from an endpoint that requires 402 payment for access.
40
+ const response = await api.get('/weather')
41
+ console.log(response.data)
42
+ ```
43
+
44
+ ## API
45
+
46
+ - `createSigner(privateKeyWIF, paymentAmountSats)` — build a BCH signer used to
47
+ sign x402 payment payloads and control default spend amounts.
48
+ - `withPaymentInterceptor(axiosInstance, signer, selector?, config?)` — attach
49
+ an interceptor that:
50
+ - waits for a 402 response,
51
+ - selects the BCH `utxo` payment requirement (or uses your selector),
52
+ - funds or reuses a tracked UTXO,
53
+ - replays the request with the `X-PAYMENT` header.
54
+ - `selectPaymentRequirements(accepts)` — utility for filtering BCH
55
+ requirements.
56
+ - `createPaymentHeader(...)` — exposed for advanced integrations that need
57
+ direct x402 payload handling.
58
+
59
+ ## Licence
6
60
 
7
61
  [MIT](LICENSE.md)
package/index.js CHANGED
@@ -1,23 +1,268 @@
1
1
  /*
2
- An npm JavaScript library for front end web apps. Implements a minimal
3
- Bitcoin Cash wallet.
2
+ x402-bch-axios
3
+
4
+ A BCH-focused port of the x402 Axios interceptor. The API mirrors the
5
+ TypeScript `withPaymentInterceptor` experience but intentionally omits
6
+ multi-network support and config typings that are not relevant for BCH.
7
+
8
+ Differences from the TypeScript implementation:
9
+ - Only supports BCH `utxo` payment requirements and BCH signers.
10
+ - Accepts an optional BCH server config instead of the generic `X402Config`.
11
+ - Does not expose multi-network signer helpers or type exports.
4
12
  */
5
13
 
6
- /* eslint-disable no-async-promise-executor */
14
+ // External dependencies
15
+ import BCHWallet from 'minimal-slp-wallet'
16
+ import RetryQueue from '@chris.troutner/retry-queue'
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
+
32
+ const currentUtxo = {
33
+ txid: null,
34
+ vout: null,
35
+ satsLeft: 0
36
+ }
37
+
38
+ /**
39
+ * Creates a BCH signer from a private key in WIF format.
40
+ *
41
+ * @param {string} privateKeyWIF - Private key in Wallet Import Format (WIF)
42
+ * @param {number} paymentAmountSats - Default spend amount for queued payments
43
+ * @returns {{ ecpair: any, address: string, wif: string, paymentAmountSats: number, signMessage: (message: string) => string }}
44
+ */
45
+ export function createSigner (privateKeyWIF, paymentAmountSats) {
46
+ const wallet = new dependencies.BCHWallet()
47
+ const bchjs = wallet.bchjs
48
+
49
+ const ecpair = bchjs.ECPair.fromWIF(privateKeyWIF)
50
+ const address = bchjs.ECPair.toCashAddress(ecpair)
51
+
52
+ return {
53
+ ecpair,
54
+ address,
55
+ wif: privateKeyWIF,
56
+ paymentAmountSats,
57
+ signMessage (message) {
58
+ return bchjs.BitcoinCash.signMessageWithPrivKey(privateKeyWIF, message)
59
+ }
60
+ }
61
+ }
62
+
63
+ export const createBCHSigner = createSigner
64
+
65
+ /**
66
+ * Selects BCH `utxo` payment requirements from a 402 accepts array.
67
+ *
68
+ * @param {Array} accepts - Array of payment requirements objects
69
+ * @returns {Object} First BCH `utxo` payment requirement
70
+ */
71
+ export function selectPaymentRequirements (accepts = []) {
72
+ const bchRequirements = accepts.filter(req => {
73
+ return req?.network === 'bch' && req?.scheme === 'utxo'
74
+ })
75
+
76
+ if (bchRequirements.length === 0) {
77
+ throw new Error('No BCH payment requirements found in 402 response')
78
+ }
79
+
80
+ return bchRequirements[0]
81
+ }
82
+
83
+ /**
84
+ * Builds the X-PAYMENT header payload for BCH transfers.
85
+ *
86
+ * @param {ReturnType<typeof createSigner>} signer
87
+ * @param {Object} paymentRequirements
88
+ * @param {number} x402Version
89
+ * @param {string|null} txid
90
+ * @param {number|null} vout
91
+ * @returns {Promise<string>}
92
+ */
93
+ export async function createPaymentHeader (
94
+ signer,
95
+ paymentRequirements,
96
+ x402Version = 1,
97
+ txid = null,
98
+ vout = null
99
+ ) {
100
+ const authorization = {
101
+ from: signer.address,
102
+ to: paymentRequirements.payTo,
103
+ value: paymentRequirements.minAmountRequired,
104
+ txid,
105
+ vout,
106
+ amount: signer.paymentAmountSats
107
+ }
108
+
109
+ const messageToSign = JSON.stringify(authorization)
110
+ const signature = signer.signMessage(messageToSign)
111
+
112
+ const paymentHeader = {
113
+ x402Version,
114
+ scheme: paymentRequirements.scheme || 'utxo',
115
+ network: paymentRequirements.network || 'bch',
116
+ payload: {
117
+ signature,
118
+ authorization
119
+ }
120
+ }
121
+
122
+ return JSON.stringify(paymentHeader)
123
+ }
124
+
125
+ async function sendPayment (signer, paymentRequirements, bchServerConfig = {}) {
126
+ const { apiType, bchServerURL } = bchServerConfig
127
+ const paymentAmountSats = signer.paymentAmountSats || paymentRequirements.minAmountRequired
128
+
129
+ const bchWallet = new dependencies.BCHWallet(signer.wif, {
130
+ interface: apiType,
131
+ restURL: bchServerURL
132
+ })
133
+ await bchWallet.initialize()
7
134
 
8
- // Global npm libraries
9
- import BCHJS from '@psf/bch-js'
135
+ const retryQueue = new dependencies.RetryQueue()
136
+ const receivers = [
137
+ {
138
+ address: paymentRequirements.payTo,
139
+ amountSat: paymentAmountSats
140
+ }
141
+ ]
10
142
 
11
- // Local libraries
12
- import Util from './lib/util.js'
13
- const util = new Util()
143
+ const txid = await retryQueue.addToQueue(bchWallet.send.bind(bchWallet), receivers)
14
144
 
15
- class BoilerplateLib {
16
- constructor () {
17
- // Encapsulate dependencies
18
- this.bchjs = new BCHJS()
19
- this.util = util
145
+ return {
146
+ txid,
147
+ vout: 0,
148
+ satsSent: paymentAmountSats
20
149
  }
21
150
  }
22
151
 
23
- export default BoilerplateLib
152
+ /**
153
+ * Adds a payment interceptor to an axios instance.
154
+ *
155
+ * @param {import('axios').AxiosInstance} axiosInstance
156
+ * @param {ReturnType<typeof createSigner>} signer
157
+ * @param {Function|Object} paymentRequirementsSelectorOrConfig - Optional selector or BCH server config
158
+ * @param {Object} maybeConfig - Optional BCH server config when a selector is provided
159
+ * @returns {import('axios').AxiosInstance}
160
+ */
161
+ export function withPaymentInterceptor (
162
+ axiosInstance,
163
+ signer,
164
+ paymentRequirementsSelectorOrConfig,
165
+ maybeConfig
166
+ ) {
167
+ let paymentRequirementsSelector = selectPaymentRequirements
168
+ let bchServerConfig = {}
169
+
170
+ if (typeof paymentRequirementsSelectorOrConfig === 'function') {
171
+ paymentRequirementsSelector = paymentRequirementsSelectorOrConfig
172
+ if (maybeConfig) {
173
+ bchServerConfig = maybeConfig
174
+ }
175
+ } else if (paymentRequirementsSelectorOrConfig) {
176
+ bchServerConfig = paymentRequirementsSelectorOrConfig
177
+ }
178
+
179
+ axiosInstance.interceptors.response.use(
180
+ response => response,
181
+ async error => {
182
+ if (!error.response || error.response.status !== 402) {
183
+ return Promise.reject(error)
184
+ }
185
+
186
+ try {
187
+ const originalConfig = error.config
188
+ if (!originalConfig || !originalConfig.headers) {
189
+ return Promise.reject(new Error('Missing axios request configuration'))
190
+ }
191
+
192
+ if (originalConfig.__is402Retry) {
193
+ return Promise.reject(error)
194
+ }
195
+
196
+ const { x402Version, accepts } = error.response.data || {}
197
+ if (!accepts || !Array.isArray(accepts) || accepts.length === 0) {
198
+ return Promise.reject(new Error('No payment requirements found in 402 response'))
199
+ }
200
+
201
+ const paymentRequirements = paymentRequirementsSelector(accepts)
202
+ const cost = paymentRequirements.minAmountRequired
203
+
204
+ let txid = null
205
+ let vout = null
206
+ let satsLeft = null
207
+
208
+ if (!currentUtxo.txid || currentUtxo.satsLeft < cost) {
209
+ const payment = await internals.sendPayment(
210
+ signer,
211
+ paymentRequirements,
212
+ bchServerConfig
213
+ )
214
+ txid = payment.txid
215
+ vout = payment.vout
216
+ satsLeft = payment.satsSent - cost
217
+ } else {
218
+ txid = currentUtxo.txid
219
+ vout = currentUtxo.vout
220
+ satsLeft = currentUtxo.satsLeft - cost
221
+ }
222
+
223
+ currentUtxo.txid = txid
224
+ currentUtxo.vout = vout
225
+ currentUtxo.satsLeft = satsLeft
226
+
227
+ const paymentHeader = await createPaymentHeader(
228
+ signer,
229
+ paymentRequirements,
230
+ x402Version || 1,
231
+ txid,
232
+ vout
233
+ )
234
+
235
+ originalConfig.__is402Retry = true
236
+ originalConfig.headers['X-PAYMENT'] = paymentHeader
237
+ originalConfig.headers['Access-Control-Expose-Headers'] = 'X-PAYMENT-RESPONSE'
238
+
239
+ const secondResponse = await axiosInstance.request(originalConfig)
240
+ return secondResponse
241
+ } catch (paymentError) {
242
+ return Promise.reject(paymentError)
243
+ }
244
+ }
245
+ )
246
+
247
+ return axiosInstance
248
+ }
249
+
250
+ const internals = {
251
+ dependencies,
252
+ currentUtxo,
253
+ sendPayment
254
+ }
255
+
256
+ export function __resetCurrentUtxo () {
257
+ currentUtxo.txid = null
258
+ currentUtxo.vout = null
259
+ currentUtxo.satsLeft = 0
260
+ }
261
+
262
+ export function __resetInternals () {
263
+ internals.sendPayment = sendPayment
264
+ __resetDependencies()
265
+ __resetCurrentUtxo()
266
+ }
267
+
268
+ export const __internals = internals
package/package.json CHANGED
@@ -1,15 +1,13 @@
1
1
  {
2
2
  "name": "x402-bch-axios",
3
- "version": "1.0.0",
3
+ "version": "1.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",
7
7
  "scripts": {
8
8
  "start": "node index.js",
9
9
  "test": "npm run lint && TEST=unit c8 mocha test/unit/",
10
- "test:integration": "mocha --timeout 25000 test/integration/",
11
10
  "lint": "standard --env mocha --fix",
12
- "docs": "./node_modules/.bin/apidoc -i src/ -o docs",
13
11
  "coverage": "c8 --reporter=html mocha test/unit/ --exit"
14
12
  },
15
13
  "keywords": [
@@ -27,8 +25,8 @@
27
25
  },
28
26
  "repository": "x402-bch/x402-bch-axios",
29
27
  "dependencies": {
30
- "@psf/bch-js": "6.8.3",
31
- "apidoc": "1.2.0"
28
+ "@chris.troutner/retry-queue": "1.0.11",
29
+ "minimal-slp-wallet": "6.1.0"
32
30
  },
33
31
  "devDependencies": {
34
32
  "c8": "10.1.3",
@@ -36,7 +34,7 @@
36
34
  "husky": "9.1.7",
37
35
  "lodash.clonedeep": "4.5.0",
38
36
  "mocha": "11.7.5",
39
- "semantic-release": "25.0.2",
37
+ "semantic-release": "19.0.3",
40
38
  "sinon": "21.0.0",
41
39
  "standard": "17.1.2"
42
40
  },
@@ -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/apidoc.json DELETED
@@ -1,3 +0,0 @@
1
- {
2
- "sampleUrl": null
3
- }
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
- })