x402-bch-axios 1.0.0 → 1.1.0

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,60 @@
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
+ const response = await api.get('/premium-endpoint')
40
+ console.log(response.data)
41
+ ```
42
+
43
+ ## API
44
+
45
+ - `createSigner(privateKeyWIF, paymentAmountSats)` — build a BCH signer used to
46
+ sign x402 payment payloads and control default spend amounts.
47
+ - `withPaymentInterceptor(axiosInstance, signer, selector?, config?)` — attach
48
+ an interceptor that:
49
+ - waits for a 402 response,
50
+ - selects the BCH `utxo` payment requirement (or uses your selector),
51
+ - funds or reuses a tracked UTXO,
52
+ - replays the request with the `X-PAYMENT` header.
53
+ - `selectPaymentRequirements(accepts)` — utility for filtering BCH
54
+ requirements.
55
+ - `createPaymentHeader(...)` — exposed for advanced integrations that need
56
+ direct x402 payload handling.
57
+
58
+ ## Licence
6
59
 
7
60
  [MIT](LICENSE.md)
package/index.js CHANGED
@@ -1,23 +1,233 @@
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 currentUtxo = {
19
+ txid: null,
20
+ vout: null,
21
+ satsLeft: 0
22
+ }
23
+
24
+ /**
25
+ * Creates a BCH signer from a private key in WIF format.
26
+ *
27
+ * @param {string} privateKeyWIF - Private key in Wallet Import Format (WIF)
28
+ * @param {number} paymentAmountSats - Default spend amount for queued payments
29
+ * @returns {{ ecpair: any, address: string, wif: string, paymentAmountSats: number, signMessage: (message: string) => string }}
30
+ */
31
+ export function createSigner (privateKeyWIF, paymentAmountSats) {
32
+ const wallet = new BCHWallet()
33
+ const bchjs = wallet.bchjs
34
+
35
+ const ecpair = bchjs.ECPair.fromWIF(privateKeyWIF)
36
+ const address = bchjs.ECPair.toCashAddress(ecpair)
37
+
38
+ return {
39
+ ecpair,
40
+ address,
41
+ wif: privateKeyWIF,
42
+ paymentAmountSats,
43
+ signMessage (message) {
44
+ return bchjs.BitcoinCash.signMessageWithPrivKey(privateKeyWIF, message)
45
+ }
46
+ }
47
+ }
48
+
49
+ export const createBCHSigner = createSigner
50
+
51
+ /**
52
+ * Selects BCH `utxo` payment requirements from a 402 accepts array.
53
+ *
54
+ * @param {Array} accepts - Array of payment requirements objects
55
+ * @returns {Object} First BCH `utxo` payment requirement
56
+ */
57
+ export function selectPaymentRequirements (accepts = []) {
58
+ const bchRequirements = accepts.filter(req => {
59
+ return req?.network === 'bch' && req?.scheme === 'utxo'
60
+ })
61
+
62
+ if (bchRequirements.length === 0) {
63
+ throw new Error('No BCH payment requirements found in 402 response')
64
+ }
65
+
66
+ return bchRequirements[0]
67
+ }
68
+
69
+ /**
70
+ * Builds the X-PAYMENT header payload for BCH transfers.
71
+ *
72
+ * @param {ReturnType<typeof createSigner>} signer
73
+ * @param {Object} paymentRequirements
74
+ * @param {number} x402Version
75
+ * @param {string|null} txid
76
+ * @param {number|null} vout
77
+ * @returns {Promise<string>}
78
+ */
79
+ export async function createPaymentHeader (
80
+ signer,
81
+ paymentRequirements,
82
+ x402Version = 1,
83
+ txid = null,
84
+ vout = null
85
+ ) {
86
+ const wallet = new BCHWallet()
87
+ await wallet.walletInfoPromise
88
+
89
+ const authorization = {
90
+ from: signer.address,
91
+ to: paymentRequirements.payTo,
92
+ value: paymentRequirements.minAmountRequired,
93
+ txid,
94
+ vout,
95
+ amount: signer.paymentAmountSats
96
+ }
97
+
98
+ const messageToSign = JSON.stringify(authorization)
99
+ const signature = signer.signMessage(messageToSign)
100
+
101
+ const paymentHeader = {
102
+ x402Version,
103
+ scheme: paymentRequirements.scheme || 'utxo',
104
+ network: paymentRequirements.network || 'bch',
105
+ payload: {
106
+ signature,
107
+ authorization
108
+ }
109
+ }
110
+
111
+ return JSON.stringify(paymentHeader)
112
+ }
113
+
114
+ async function sendPayment (signer, paymentRequirements, bchServerConfig = {}) {
115
+ const { apiType, bchServerURL } = bchServerConfig
116
+ const paymentAmountSats = signer.paymentAmountSats || paymentRequirements.minAmountRequired
117
+
118
+ const bchWallet = new BCHWallet(signer.wif, {
119
+ interface: apiType,
120
+ restURL: bchServerURL
121
+ })
122
+ await bchWallet.initialize()
7
123
 
8
- // Global npm libraries
9
- import BCHJS from '@psf/bch-js'
124
+ const retryQueue = new RetryQueue()
125
+ const receivers = [
126
+ {
127
+ address: paymentRequirements.payTo,
128
+ amountSat: paymentAmountSats
129
+ }
130
+ ]
10
131
 
11
- // Local libraries
12
- import Util from './lib/util.js'
13
- const util = new Util()
132
+ const txid = await retryQueue.addToQueue(bchWallet.send.bind(bchWallet), receivers)
14
133
 
15
- class BoilerplateLib {
16
- constructor () {
17
- // Encapsulate dependencies
18
- this.bchjs = new BCHJS()
19
- this.util = util
134
+ return {
135
+ txid,
136
+ vout: 0,
137
+ satsSent: paymentAmountSats
20
138
  }
21
139
  }
22
140
 
23
- export default BoilerplateLib
141
+ /**
142
+ * Adds a payment interceptor to an axios instance.
143
+ *
144
+ * @param {import('axios').AxiosInstance} axiosInstance
145
+ * @param {ReturnType<typeof createSigner>} signer
146
+ * @param {Function|Object} paymentRequirementsSelectorOrConfig - Optional selector or BCH server config
147
+ * @param {Object} maybeConfig - Optional BCH server config when a selector is provided
148
+ * @returns {import('axios').AxiosInstance}
149
+ */
150
+ export function withPaymentInterceptor (
151
+ axiosInstance,
152
+ signer,
153
+ paymentRequirementsSelectorOrConfig,
154
+ maybeConfig
155
+ ) {
156
+ let paymentRequirementsSelector = selectPaymentRequirements
157
+ let bchServerConfig = {}
158
+
159
+ if (typeof paymentRequirementsSelectorOrConfig === 'function') {
160
+ paymentRequirementsSelector = paymentRequirementsSelectorOrConfig
161
+ if (maybeConfig) {
162
+ bchServerConfig = maybeConfig
163
+ }
164
+ } else if (paymentRequirementsSelectorOrConfig) {
165
+ bchServerConfig = paymentRequirementsSelectorOrConfig
166
+ }
167
+
168
+ axiosInstance.interceptors.response.use(
169
+ response => response,
170
+ async error => {
171
+ if (!error.response || error.response.status !== 402) {
172
+ return Promise.reject(error)
173
+ }
174
+
175
+ try {
176
+ const originalConfig = error.config
177
+ if (!originalConfig || !originalConfig.headers) {
178
+ return Promise.reject(new Error('Missing axios request configuration'))
179
+ }
180
+
181
+ if (originalConfig.__is402Retry) {
182
+ return Promise.reject(error)
183
+ }
184
+
185
+ const { x402Version, accepts } = error.response.data || {}
186
+ if (!accepts || !Array.isArray(accepts) || accepts.length === 0) {
187
+ return Promise.reject(new Error('No payment requirements found in 402 response'))
188
+ }
189
+
190
+ const paymentRequirements = paymentRequirementsSelector(accepts)
191
+ const cost = paymentRequirements.minAmountRequired
192
+
193
+ let txid = null
194
+ let vout = null
195
+ let satsLeft = null
196
+
197
+ if (!currentUtxo.txid || currentUtxo.satsLeft < cost) {
198
+ const payment = await sendPayment(signer, paymentRequirements, bchServerConfig)
199
+ txid = payment.txid
200
+ vout = payment.vout
201
+ satsLeft = payment.satsSent - cost
202
+ } else {
203
+ txid = currentUtxo.txid
204
+ vout = currentUtxo.vout
205
+ satsLeft = currentUtxo.satsLeft - cost
206
+ }
207
+
208
+ currentUtxo.txid = txid
209
+ currentUtxo.vout = vout
210
+ currentUtxo.satsLeft = satsLeft
211
+
212
+ const paymentHeader = await createPaymentHeader(
213
+ signer,
214
+ paymentRequirements,
215
+ x402Version || 1,
216
+ txid,
217
+ vout
218
+ )
219
+
220
+ originalConfig.__is402Retry = true
221
+ originalConfig.headers['X-PAYMENT'] = paymentHeader
222
+ originalConfig.headers['Access-Control-Expose-Headers'] = 'X-PAYMENT-RESPONSE'
223
+
224
+ const secondResponse = await axiosInstance.request(originalConfig)
225
+ return secondResponse
226
+ } catch (paymentError) {
227
+ return Promise.reject(paymentError)
228
+ }
229
+ }
230
+ )
231
+
232
+ return axiosInstance
233
+ }
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.0",
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": [
@@ -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
  },
package/apidoc.json DELETED
@@ -1,3 +0,0 @@
1
- {
2
- "sampleUrl": null
3
- }