x402-bch-axios 1.1.1 → 2.0.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 +81 -4
- package/index.js +93 -15
- package/package.json +2 -2
- package/test/unit/index-unit.js +192 -27
package/README.md
CHANGED
|
@@ -4,6 +4,10 @@ JavaScript helpers for handling HTTP 402 responses against Bitcoin Cash powered
|
|
|
4
4
|
x402 endpoints. This package ports the `withPaymentInterceptor` experience from
|
|
5
5
|
the TypeScript `x402-axios` client and adapts it for BCH UTXO payments.
|
|
6
6
|
|
|
7
|
+
**Version 2.0+**: This library now supports x402-bch protocol v2, which includes
|
|
8
|
+
CAIP-2 network identifiers, updated header names, and restructured payment payloads.
|
|
9
|
+
Backward compatibility with v1 responses is maintained.
|
|
10
|
+
|
|
7
11
|
## Installation
|
|
8
12
|
|
|
9
13
|
```bash
|
|
@@ -50,11 +54,84 @@ console.log(response.data)
|
|
|
50
54
|
- waits for a 402 response,
|
|
51
55
|
- selects the BCH `utxo` payment requirement (or uses your selector),
|
|
52
56
|
- funds or reuses a tracked UTXO,
|
|
53
|
-
- replays the request with the `X-PAYMENT` header.
|
|
57
|
+
- replays the request with the `PAYMENT-SIGNATURE` header (v2) or `X-PAYMENT` header (v1).
|
|
54
58
|
- `selectPaymentRequirements(accepts)` — utility for filtering BCH
|
|
55
|
-
requirements.
|
|
56
|
-
- `createPaymentHeader(
|
|
57
|
-
direct x402 payload handling.
|
|
59
|
+
requirements. Supports both v1 (`bch`) and v2 CAIP-2 (`bip122:*`) network formats.
|
|
60
|
+
- `createPaymentHeader(signer, paymentRequirements, x402Version, txid, vout, resource?, extensions?)` — exposed for advanced integrations that need
|
|
61
|
+
direct x402 payload handling. Returns v2 format by default.
|
|
62
|
+
|
|
63
|
+
## Protocol Version 2 Changes
|
|
64
|
+
|
|
65
|
+
This library supports x402-bch protocol v2 with the following changes:
|
|
66
|
+
|
|
67
|
+
### Header Names
|
|
68
|
+
- **v2**: `PAYMENT-SIGNATURE` (replaces `X-PAYMENT`)
|
|
69
|
+
- **v2**: `PAYMENT-RESPONSE` (replaces `X-PAYMENT-RESPONSE`)
|
|
70
|
+
|
|
71
|
+
### Network Identifiers
|
|
72
|
+
- **v1**: `bch` (simple string)
|
|
73
|
+
- **v2**: `bip122:000000000000000000651ef99cb9fcbe` (CAIP-2 format for BCH mainnet)
|
|
74
|
+
|
|
75
|
+
The library automatically detects and supports both formats.
|
|
76
|
+
|
|
77
|
+
### Payment Payload Structure
|
|
78
|
+
|
|
79
|
+
**v2 Format:**
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"x402Version": 2,
|
|
83
|
+
"resource": {
|
|
84
|
+
"url": "http://localhost:4021/weather",
|
|
85
|
+
"description": "Access to weather data",
|
|
86
|
+
"mimeType": "application/json"
|
|
87
|
+
},
|
|
88
|
+
"accepted": {
|
|
89
|
+
"scheme": "utxo",
|
|
90
|
+
"network": "bip122:000000000000000000651ef99cb9fcbe",
|
|
91
|
+
"amount": "1000",
|
|
92
|
+
"asset": "0x0000000000000000000000000000000000000001",
|
|
93
|
+
"payTo": "bitcoincash:...",
|
|
94
|
+
"maxTimeoutSeconds": 60,
|
|
95
|
+
"extra": {}
|
|
96
|
+
},
|
|
97
|
+
"payload": {
|
|
98
|
+
"signature": "...",
|
|
99
|
+
"authorization": {
|
|
100
|
+
"from": "bitcoincash:...",
|
|
101
|
+
"to": "bitcoincash:...",
|
|
102
|
+
"value": "1000",
|
|
103
|
+
"txid": "...",
|
|
104
|
+
"vout": 0,
|
|
105
|
+
"amount": "2000"
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
"extensions": {}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Key differences from v1:**
|
|
113
|
+
- Removed top-level `scheme` and `network` fields
|
|
114
|
+
- Added `accepted` field containing the selected PaymentRequirements
|
|
115
|
+
- Added optional `resource` and `extensions` fields
|
|
116
|
+
- Field name change: `minAmountRequired` → `amount` (library supports both for compatibility)
|
|
117
|
+
|
|
118
|
+
### Response Parsing
|
|
119
|
+
|
|
120
|
+
The library supports both v1 and v2 response formats:
|
|
121
|
+
- **v2**: Parses from `PAYMENT-REQUIRED` header (base64-encoded JSON)
|
|
122
|
+
- **v1**: Falls back to response body format
|
|
123
|
+
|
|
124
|
+
## Migration from v1
|
|
125
|
+
|
|
126
|
+
If you're upgrading from v1, the library maintains backward compatibility:
|
|
127
|
+
- v1 responses are automatically detected and handled
|
|
128
|
+
- v1 field names (`minAmountRequired`, `bch` network) are supported
|
|
129
|
+
- No code changes required for basic usage
|
|
130
|
+
|
|
131
|
+
However, for full v2 support:
|
|
132
|
+
- Update your server to send v2 responses
|
|
133
|
+
- Use CAIP-2 network identifiers in payment requirements
|
|
134
|
+
- Expect `PAYMENT-SIGNATURE` and `PAYMENT-RESPONSE` headers
|
|
58
135
|
|
|
59
136
|
## Licence
|
|
60
137
|
|
package/index.js
CHANGED
|
@@ -62,15 +62,34 @@ export function createSigner (privateKeyWIF, paymentAmountSats) {
|
|
|
62
62
|
|
|
63
63
|
export const createBCHSigner = createSigner
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Normalizes network identifier to support both v1 and v2 formats.
|
|
67
|
+
* v1 uses 'bch', v2 uses CAIP-2 format 'bip122:000000000000000000651ef99cb9fcbe'
|
|
68
|
+
*
|
|
69
|
+
* @param {string} network - Network identifier
|
|
70
|
+
* @returns {boolean} True if the network is BCH (mainnet)
|
|
71
|
+
*/
|
|
72
|
+
function isBCHNetwork (network) {
|
|
73
|
+
if (!network) return false
|
|
74
|
+
// v1 format
|
|
75
|
+
if (network === 'bch') return true
|
|
76
|
+
// v2 CAIP-2 format for BCH mainnet
|
|
77
|
+
if (network === 'bip122:000000000000000000651ef99cb9fcbe') return true
|
|
78
|
+
// v2 CAIP-2 format pattern matching (bip122:*)
|
|
79
|
+
if (network.startsWith('bip122:')) return true
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
65
83
|
/**
|
|
66
84
|
* Selects BCH `utxo` payment requirements from a 402 accepts array.
|
|
85
|
+
* Supports both v1 ('bch') and v2 (CAIP-2 'bip122:*') network formats.
|
|
67
86
|
*
|
|
68
87
|
* @param {Array} accepts - Array of payment requirements objects
|
|
69
88
|
* @returns {Object} First BCH `utxo` payment requirement
|
|
70
89
|
*/
|
|
71
90
|
export function selectPaymentRequirements (accepts = []) {
|
|
72
91
|
const bchRequirements = accepts.filter(req => {
|
|
73
|
-
return req?.network
|
|
92
|
+
return isBCHNetwork(req?.network) && req?.scheme === 'utxo'
|
|
74
93
|
})
|
|
75
94
|
|
|
76
95
|
if (bchRequirements.length === 0) {
|
|
@@ -81,26 +100,33 @@ export function selectPaymentRequirements (accepts = []) {
|
|
|
81
100
|
}
|
|
82
101
|
|
|
83
102
|
/**
|
|
84
|
-
* Builds the
|
|
103
|
+
* Builds the PAYMENT-SIGNATURE header payload for BCH transfers.
|
|
85
104
|
*
|
|
86
105
|
* @param {ReturnType<typeof createSigner>} signer
|
|
87
106
|
* @param {Object} paymentRequirements
|
|
88
107
|
* @param {number} x402Version
|
|
89
108
|
* @param {string|null} txid
|
|
90
109
|
* @param {number|null} vout
|
|
110
|
+
* @param {Object|null} resource - Optional ResourceInfo object
|
|
111
|
+
* @param {Object|null} extensions - Optional extensions object
|
|
91
112
|
* @returns {Promise<string>}
|
|
92
113
|
*/
|
|
93
114
|
export async function createPaymentHeader (
|
|
94
115
|
signer,
|
|
95
116
|
paymentRequirements,
|
|
96
|
-
x402Version =
|
|
117
|
+
x402Version = 2,
|
|
97
118
|
txid = null,
|
|
98
|
-
vout = null
|
|
119
|
+
vout = null,
|
|
120
|
+
resource = null,
|
|
121
|
+
extensions = null
|
|
99
122
|
) {
|
|
123
|
+
// Support both v1 (minAmountRequired) and v2 (amount) field names
|
|
124
|
+
const amountRequired = paymentRequirements.amount || paymentRequirements.minAmountRequired
|
|
125
|
+
|
|
100
126
|
const authorization = {
|
|
101
127
|
from: signer.address,
|
|
102
128
|
to: paymentRequirements.payTo,
|
|
103
|
-
value:
|
|
129
|
+
value: amountRequired,
|
|
104
130
|
txid,
|
|
105
131
|
vout,
|
|
106
132
|
amount: signer.paymentAmountSats
|
|
@@ -109,14 +135,27 @@ export async function createPaymentHeader (
|
|
|
109
135
|
const messageToSign = JSON.stringify(authorization)
|
|
110
136
|
const signature = signer.signMessage(messageToSign)
|
|
111
137
|
|
|
138
|
+
// Build accepted PaymentRequirements object
|
|
139
|
+
const accepted = {
|
|
140
|
+
scheme: paymentRequirements.scheme || 'utxo',
|
|
141
|
+
network: paymentRequirements.network || 'bip122:000000000000000000651ef99cb9fcbe',
|
|
142
|
+
amount: amountRequired,
|
|
143
|
+
asset: paymentRequirements.asset,
|
|
144
|
+
payTo: paymentRequirements.payTo,
|
|
145
|
+
maxTimeoutSeconds: paymentRequirements.maxTimeoutSeconds,
|
|
146
|
+
extra: paymentRequirements.extra || {}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Build v2 PaymentPayload structure
|
|
112
150
|
const paymentHeader = {
|
|
113
151
|
x402Version,
|
|
114
|
-
|
|
115
|
-
|
|
152
|
+
...(resource && { resource }),
|
|
153
|
+
accepted,
|
|
116
154
|
payload: {
|
|
117
155
|
signature,
|
|
118
156
|
authorization
|
|
119
|
-
}
|
|
157
|
+
},
|
|
158
|
+
...(extensions && { extensions })
|
|
120
159
|
}
|
|
121
160
|
|
|
122
161
|
return JSON.stringify(paymentHeader)
|
|
@@ -124,12 +163,16 @@ export async function createPaymentHeader (
|
|
|
124
163
|
|
|
125
164
|
async function sendPayment (signer, paymentRequirements, bchServerConfig = {}) {
|
|
126
165
|
const { apiType, bchServerURL } = bchServerConfig
|
|
127
|
-
|
|
166
|
+
// Support both v1 (minAmountRequired) and v2 (amount) field names
|
|
167
|
+
const amountRequired = paymentRequirements.amount || paymentRequirements.minAmountRequired
|
|
168
|
+
const paymentAmountSats = signer.paymentAmountSats || amountRequired
|
|
128
169
|
|
|
129
170
|
const bchWallet = new dependencies.BCHWallet(signer.wif, {
|
|
130
171
|
interface: apiType,
|
|
131
172
|
restURL: bchServerURL
|
|
132
173
|
})
|
|
174
|
+
// console.log(`sendPayment() - interface: ${apiType}, restURL: ${bchServerURL}, wif: ${signer.wif}, payTo: ${paymentRequirements.payTo}, paymentAmountSats: ${paymentAmountSats}`)
|
|
175
|
+
console.log(`Sending ${paymentAmountSats} for x402 API payment to ${paymentRequirements.payTo}`)
|
|
133
176
|
await bchWallet.initialize()
|
|
134
177
|
|
|
135
178
|
const retryQueue = new dependencies.RetryQueue()
|
|
@@ -193,13 +236,46 @@ export function withPaymentInterceptor (
|
|
|
193
236
|
return Promise.reject(error)
|
|
194
237
|
}
|
|
195
238
|
|
|
196
|
-
|
|
239
|
+
// Parse payment requirements - v2 can come from header, v1 from body
|
|
240
|
+
let paymentRequired = null
|
|
241
|
+
let x402Version = 1
|
|
242
|
+
let accepts = []
|
|
243
|
+
let resource = null
|
|
244
|
+
let extensions = null
|
|
245
|
+
|
|
246
|
+
// Try v2 PAYMENT-REQUIRED header first (base64-encoded)
|
|
247
|
+
const paymentRequiredHeader = error.response.headers['payment-required'] ||
|
|
248
|
+
error.response.headers['PAYMENT-REQUIRED']
|
|
249
|
+
if (paymentRequiredHeader) {
|
|
250
|
+
try {
|
|
251
|
+
const decoded = Buffer.from(paymentRequiredHeader, 'base64').toString('utf-8')
|
|
252
|
+
paymentRequired = JSON.parse(decoded)
|
|
253
|
+
x402Version = paymentRequired.x402Version || 2
|
|
254
|
+
accepts = paymentRequired.accepts || []
|
|
255
|
+
resource = paymentRequired.resource
|
|
256
|
+
extensions = paymentRequired.extensions
|
|
257
|
+
} catch (parseError) {
|
|
258
|
+
// If header parsing fails, fall back to body
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Fall back to body format (v1 or v2)
|
|
263
|
+
if (!paymentRequired) {
|
|
264
|
+
const body = error.response.data || {}
|
|
265
|
+
x402Version = body.x402Version || 1
|
|
266
|
+
accepts = body.accepts || []
|
|
267
|
+
resource = body.resource
|
|
268
|
+
extensions = body.extensions
|
|
269
|
+
}
|
|
270
|
+
|
|
197
271
|
if (!accepts || !Array.isArray(accepts) || accepts.length === 0) {
|
|
198
272
|
return Promise.reject(new Error('No payment requirements found in 402 response'))
|
|
199
273
|
}
|
|
200
274
|
|
|
201
275
|
const paymentRequirements = paymentRequirementsSelector(accepts)
|
|
202
|
-
|
|
276
|
+
// Support both v1 (minAmountRequired) and v2 (amount) field names
|
|
277
|
+
// Convert to number for calculations (v2 uses strings, v1 uses numbers)
|
|
278
|
+
const cost = Number(paymentRequirements.amount || paymentRequirements.minAmountRequired)
|
|
203
279
|
|
|
204
280
|
let txid = null
|
|
205
281
|
let vout = null
|
|
@@ -227,14 +303,16 @@ export function withPaymentInterceptor (
|
|
|
227
303
|
const paymentHeader = await createPaymentHeader(
|
|
228
304
|
signer,
|
|
229
305
|
paymentRequirements,
|
|
230
|
-
x402Version ||
|
|
306
|
+
x402Version || 2,
|
|
231
307
|
txid,
|
|
232
|
-
vout
|
|
308
|
+
vout,
|
|
309
|
+
resource,
|
|
310
|
+
extensions
|
|
233
311
|
)
|
|
234
312
|
|
|
235
313
|
originalConfig.__is402Retry = true
|
|
236
|
-
originalConfig.headers['
|
|
237
|
-
originalConfig.headers['Access-Control-Expose-Headers'] = '
|
|
314
|
+
originalConfig.headers['PAYMENT-SIGNATURE'] = paymentHeader
|
|
315
|
+
originalConfig.headers['Access-Control-Expose-Headers'] = 'PAYMENT-RESPONSE'
|
|
238
316
|
|
|
239
317
|
const secondResponse = await axiosInstance.request(originalConfig)
|
|
240
318
|
return secondResponse
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x402-bch-axios",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
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": "
|
|
29
|
+
"minimal-slp-wallet": "7.0.2"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"c8": "10.1.3",
|
package/test/unit/index-unit.js
CHANGED
|
@@ -73,7 +73,7 @@ describe('#index.js', () => {
|
|
|
73
73
|
})
|
|
74
74
|
|
|
75
75
|
describe('#selectPaymentRequirements', () => {
|
|
76
|
-
it('should select the first BCH utxo requirement', () => {
|
|
76
|
+
it('should select the first BCH utxo requirement (v1 format)', () => {
|
|
77
77
|
const accepts = [
|
|
78
78
|
{ network: 'eth', scheme: 'account' },
|
|
79
79
|
{ network: 'bch', scheme: 'utxo', payTo: 'addr1' },
|
|
@@ -84,6 +84,17 @@ describe('#index.js', () => {
|
|
|
84
84
|
assert.deepEqual(req, { network: 'bch', scheme: 'utxo', payTo: 'addr1' })
|
|
85
85
|
})
|
|
86
86
|
|
|
87
|
+
it('should select the first BCH utxo requirement (v2 CAIP-2 format)', () => {
|
|
88
|
+
const accepts = [
|
|
89
|
+
{ network: 'eip155:84532', scheme: 'exact' },
|
|
90
|
+
{ network: 'bip122:000000000000000000651ef99cb9fcbe', scheme: 'utxo', payTo: 'addr1' },
|
|
91
|
+
{ network: 'bch', scheme: 'utxo', payTo: 'addr2' }
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
const req = selectPaymentRequirements(accepts)
|
|
95
|
+
assert.deepEqual(req, { network: 'bip122:000000000000000000651ef99cb9fcbe', scheme: 'utxo', payTo: 'addr1' })
|
|
96
|
+
})
|
|
97
|
+
|
|
87
98
|
it('should throw if no BCH utxo requirement exists', () => {
|
|
88
99
|
assert.throws(
|
|
89
100
|
() => selectPaymentRequirements([{ network: 'btc', scheme: 'utxo' }]),
|
|
@@ -93,7 +104,7 @@ describe('#index.js', () => {
|
|
|
93
104
|
})
|
|
94
105
|
|
|
95
106
|
describe('#createPaymentHeader', () => {
|
|
96
|
-
it('should build a valid payment header payload', async () => {
|
|
107
|
+
it('should build a valid v2 payment header payload', async () => {
|
|
97
108
|
const signer = {
|
|
98
109
|
address: 'bitcoincash:qptest',
|
|
99
110
|
paymentAmountSats: 2000,
|
|
@@ -102,9 +113,18 @@ describe('#index.js', () => {
|
|
|
102
113
|
|
|
103
114
|
const paymentRequirements = {
|
|
104
115
|
payTo: 'bitcoincash:qprecv',
|
|
105
|
-
|
|
116
|
+
amount: '1500',
|
|
106
117
|
scheme: 'utxo',
|
|
107
|
-
network: '
|
|
118
|
+
network: 'bip122:000000000000000000651ef99cb9fcbe',
|
|
119
|
+
asset: '0x0000000000000000000000000000000000000001',
|
|
120
|
+
maxTimeoutSeconds: 60,
|
|
121
|
+
extra: {}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const resource = {
|
|
125
|
+
url: 'http://localhost:4021/weather',
|
|
126
|
+
description: 'Access to weather data',
|
|
127
|
+
mimeType: 'application/json'
|
|
108
128
|
}
|
|
109
129
|
|
|
110
130
|
const header = await createPaymentHeader(
|
|
@@ -112,28 +132,65 @@ describe('#index.js', () => {
|
|
|
112
132
|
paymentRequirements,
|
|
113
133
|
2,
|
|
114
134
|
'tx123',
|
|
115
|
-
0
|
|
135
|
+
0,
|
|
136
|
+
resource
|
|
116
137
|
)
|
|
117
138
|
|
|
118
139
|
const parsed = JSON.parse(header)
|
|
119
|
-
assert.
|
|
120
|
-
|
|
140
|
+
assert.equal(parsed.x402Version, 2)
|
|
141
|
+
assert.deepEqual(parsed.resource, resource)
|
|
142
|
+
assert.deepEqual(parsed.accepted, {
|
|
121
143
|
scheme: 'utxo',
|
|
122
|
-
network: '
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
144
|
+
network: 'bip122:000000000000000000651ef99cb9fcbe',
|
|
145
|
+
amount: '1500',
|
|
146
|
+
asset: '0x0000000000000000000000000000000000000001',
|
|
147
|
+
payTo: 'bitcoincash:qprecv',
|
|
148
|
+
maxTimeoutSeconds: 60,
|
|
149
|
+
extra: {}
|
|
150
|
+
})
|
|
151
|
+
assert.deepEqual(parsed.payload, {
|
|
152
|
+
signature: 'mock-signature',
|
|
153
|
+
authorization: {
|
|
154
|
+
from: 'bitcoincash:qptest',
|
|
155
|
+
to: 'bitcoincash:qprecv',
|
|
156
|
+
value: '1500',
|
|
157
|
+
txid: 'tx123',
|
|
158
|
+
vout: 0,
|
|
159
|
+
amount: 2000
|
|
133
160
|
}
|
|
134
161
|
})
|
|
162
|
+
// v2 should not have top-level scheme/network
|
|
163
|
+
assert.isUndefined(parsed.scheme)
|
|
164
|
+
assert.isUndefined(parsed.network)
|
|
135
165
|
assert.isTrue(signer.signMessage.calledOnce)
|
|
136
166
|
})
|
|
167
|
+
|
|
168
|
+
it('should support v1 minAmountRequired field for backward compatibility', async () => {
|
|
169
|
+
const signer = {
|
|
170
|
+
address: 'bitcoincash:qptest',
|
|
171
|
+
paymentAmountSats: 2000,
|
|
172
|
+
signMessage: sandbox.stub().returns('mock-signature')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const paymentRequirements = {
|
|
176
|
+
payTo: 'bitcoincash:qprecv',
|
|
177
|
+
minAmountRequired: 1500, // v1 field name
|
|
178
|
+
scheme: 'utxo',
|
|
179
|
+
network: 'bch'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const header = await createPaymentHeader(
|
|
183
|
+
signer,
|
|
184
|
+
paymentRequirements,
|
|
185
|
+
2,
|
|
186
|
+
'tx123',
|
|
187
|
+
0
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
const parsed = JSON.parse(header)
|
|
191
|
+
assert.equal(parsed.accepted.amount, 1500)
|
|
192
|
+
assert.equal(parsed.payload.authorization.value, 1500)
|
|
193
|
+
})
|
|
137
194
|
})
|
|
138
195
|
|
|
139
196
|
describe('#withPaymentInterceptor', () => {
|
|
@@ -157,19 +214,31 @@ describe('#index.js', () => {
|
|
|
157
214
|
}
|
|
158
215
|
|
|
159
216
|
const basePaymentRequirements = {
|
|
160
|
-
network: '
|
|
217
|
+
network: 'bip122:000000000000000000651ef99cb9fcbe',
|
|
161
218
|
scheme: 'utxo',
|
|
162
219
|
payTo: 'bitcoincash:qprecv',
|
|
163
|
-
|
|
220
|
+
amount: '1500',
|
|
221
|
+
asset: '0x0000000000000000000000000000000000000001',
|
|
222
|
+
maxTimeoutSeconds: 60,
|
|
223
|
+
extra: {}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const baseResource = {
|
|
227
|
+
url: 'http://localhost:4021/weather',
|
|
228
|
+
description: 'Access to weather data',
|
|
229
|
+
mimeType: 'application/json'
|
|
164
230
|
}
|
|
165
231
|
|
|
166
232
|
function create402Error (overrides = {}) {
|
|
167
233
|
const defaultError = {
|
|
168
234
|
response: {
|
|
169
235
|
status: 402,
|
|
236
|
+
headers: {},
|
|
170
237
|
data: {
|
|
171
|
-
x402Version:
|
|
172
|
-
|
|
238
|
+
x402Version: 2,
|
|
239
|
+
resource: cloneDeep(baseResource),
|
|
240
|
+
accepts: [cloneDeep(basePaymentRequirements)],
|
|
241
|
+
extensions: {}
|
|
173
242
|
}
|
|
174
243
|
},
|
|
175
244
|
config: {
|
|
@@ -223,14 +292,16 @@ describe('#index.js', () => {
|
|
|
223
292
|
|
|
224
293
|
const [, errorHandler] = axiosInstance.interceptors.response.use.firstCall.args
|
|
225
294
|
const error = create402Error({
|
|
226
|
-
response: { status: 402, data: { accepts: [] } }
|
|
295
|
+
response: { status: 402, headers: {}, data: { accepts: [] } }
|
|
227
296
|
})
|
|
228
297
|
|
|
229
298
|
try {
|
|
230
299
|
await errorHandler(error)
|
|
231
300
|
assert.fail('Expected rejection')
|
|
232
301
|
} catch (err) {
|
|
233
|
-
|
|
302
|
+
// Should reject with "No payment requirements found in 402 response"
|
|
303
|
+
// or "No BCH payment requirements found in 402 response" from selector
|
|
304
|
+
assert.match(err.message, /No.*payment requirements/)
|
|
234
305
|
}
|
|
235
306
|
})
|
|
236
307
|
|
|
@@ -259,14 +330,16 @@ describe('#index.js', () => {
|
|
|
259
330
|
|
|
260
331
|
const updatedConfig = axiosInstance.request.firstCall.args[0]
|
|
261
332
|
assert.isTrue(updatedConfig.__is402Retry)
|
|
262
|
-
assert.property(updatedConfig.headers, '
|
|
333
|
+
assert.property(updatedConfig.headers, 'PAYMENT-SIGNATURE')
|
|
263
334
|
assert.propertyVal(
|
|
264
335
|
updatedConfig.headers,
|
|
265
336
|
'Access-Control-Expose-Headers',
|
|
266
|
-
'
|
|
337
|
+
'PAYMENT-RESPONSE'
|
|
267
338
|
)
|
|
268
339
|
|
|
269
|
-
const headerPayload = JSON.parse(updatedConfig.headers['
|
|
340
|
+
const headerPayload = JSON.parse(updatedConfig.headers['PAYMENT-SIGNATURE'])
|
|
341
|
+
assert.equal(headerPayload.x402Version, 2)
|
|
342
|
+
assert.deepEqual(headerPayload.accepted, basePaymentRequirements)
|
|
270
343
|
assert.equal(headerPayload.payload.authorization.txid, 'tx123')
|
|
271
344
|
assert.equal(__internals.currentUtxo.txid, 'tx123')
|
|
272
345
|
assert.equal(__internals.currentUtxo.satsLeft, 500)
|
|
@@ -298,5 +371,97 @@ describe('#index.js', () => {
|
|
|
298
371
|
assert.equal(__internals.currentUtxo.txid, 'cached')
|
|
299
372
|
assert.equal(__internals.currentUtxo.satsLeft, 500)
|
|
300
373
|
})
|
|
374
|
+
|
|
375
|
+
it('should parse v2 response from PAYMENT-REQUIRED header', async () => {
|
|
376
|
+
const axiosInstance = createAxiosInstance()
|
|
377
|
+
const signer = createSignerStub()
|
|
378
|
+
signer.signMessage.returns('signed')
|
|
379
|
+
|
|
380
|
+
const sendPaymentStub = sandbox
|
|
381
|
+
.stub()
|
|
382
|
+
.resolves({ txid: 'tx123', vout: 0, satsSent: 2000 })
|
|
383
|
+
__internals.sendPayment = sendPaymentStub
|
|
384
|
+
|
|
385
|
+
axiosInstance.request.resolves({ data: 'ok' })
|
|
386
|
+
|
|
387
|
+
withPaymentInterceptor(axiosInstance, signer)
|
|
388
|
+
|
|
389
|
+
const [, errorHandler] = axiosInstance.interceptors.response.use.firstCall.args
|
|
390
|
+
|
|
391
|
+
// Create v2 response with PAYMENT-REQUIRED header
|
|
392
|
+
const paymentRequired = {
|
|
393
|
+
x402Version: 2,
|
|
394
|
+
resource: baseResource,
|
|
395
|
+
accepts: [cloneDeep(basePaymentRequirements)],
|
|
396
|
+
extensions: {}
|
|
397
|
+
}
|
|
398
|
+
const headerValue = Buffer.from(JSON.stringify(paymentRequired)).toString('base64')
|
|
399
|
+
|
|
400
|
+
const error = {
|
|
401
|
+
response: {
|
|
402
|
+
status: 402,
|
|
403
|
+
headers: {
|
|
404
|
+
'payment-required': headerValue
|
|
405
|
+
},
|
|
406
|
+
data: {}
|
|
407
|
+
},
|
|
408
|
+
config: {
|
|
409
|
+
headers: {}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const response = await errorHandler(error)
|
|
414
|
+
|
|
415
|
+
assert.deepEqual(response, { data: 'ok' })
|
|
416
|
+
assert.isTrue(sendPaymentStub.calledOnce)
|
|
417
|
+
assert.isTrue(axiosInstance.request.calledOnce)
|
|
418
|
+
|
|
419
|
+
const updatedConfig = axiosInstance.request.firstCall.args[0]
|
|
420
|
+
const headerPayload = JSON.parse(updatedConfig.headers['PAYMENT-SIGNATURE'])
|
|
421
|
+
assert.equal(headerPayload.x402Version, 2)
|
|
422
|
+
assert.deepEqual(headerPayload.resource, baseResource)
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it('should support v1 response format for backward compatibility', async () => {
|
|
426
|
+
const axiosInstance = createAxiosInstance()
|
|
427
|
+
const signer = createSignerStub()
|
|
428
|
+
signer.signMessage.returns('signed')
|
|
429
|
+
|
|
430
|
+
const sendPaymentStub = sandbox
|
|
431
|
+
.stub()
|
|
432
|
+
.resolves({ txid: 'tx123', vout: 0, satsSent: 2000 })
|
|
433
|
+
__internals.sendPayment = sendPaymentStub
|
|
434
|
+
|
|
435
|
+
axiosInstance.request.resolves({ data: 'ok' })
|
|
436
|
+
|
|
437
|
+
withPaymentInterceptor(axiosInstance, signer)
|
|
438
|
+
|
|
439
|
+
const [, errorHandler] = axiosInstance.interceptors.response.use.firstCall.args
|
|
440
|
+
|
|
441
|
+
// Create v1 response format
|
|
442
|
+
const error = {
|
|
443
|
+
response: {
|
|
444
|
+
status: 402,
|
|
445
|
+
headers: {},
|
|
446
|
+
data: {
|
|
447
|
+
x402Version: 1,
|
|
448
|
+
accepts: [{
|
|
449
|
+
network: 'bch',
|
|
450
|
+
scheme: 'utxo',
|
|
451
|
+
payTo: 'bitcoincash:qprecv',
|
|
452
|
+
minAmountRequired: 1500
|
|
453
|
+
}]
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
config: {
|
|
457
|
+
headers: {}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const response = await errorHandler(error)
|
|
462
|
+
|
|
463
|
+
assert.deepEqual(response, { data: 'ok' })
|
|
464
|
+
assert.isTrue(sendPaymentStub.calledOnce)
|
|
465
|
+
})
|
|
301
466
|
})
|
|
302
467
|
})
|