x402-bazaar 3.0.0 → 3.2.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 +67 -4
- package/bin/cli.js +109 -109
- package/package.json +50 -50
- package/src/commands/call.js +419 -305
- package/src/commands/init.js +566 -565
- package/src/commands/wallet.js +217 -216
- package/src/lib/payment.js +149 -23
package/src/commands/call.js
CHANGED
|
@@ -1,305 +1,419 @@
|
|
|
1
|
-
import ora from 'ora';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { log } from '../utils/logger.js';
|
|
6
|
-
|
|
7
|
-
export async function callCommand(endpoint, options) {
|
|
8
|
-
if (!endpoint || endpoint.trim().length === 0) {
|
|
9
|
-
log.error('Endpoint is required');
|
|
10
|
-
log.dim(' Usage: x402-bazaar call <endpoint> [--param key=value...]');
|
|
11
|
-
log.dim(' Example: x402-bazaar call /api/weather --param city=Paris');
|
|
12
|
-
log.dim(' Example: x402-bazaar call /api/hash --param text=hello --key 0x...');
|
|
13
|
-
console.log('');
|
|
14
|
-
process.exit(1);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const serverUrl = options.serverUrl || 'https://x402-api.onrender.com';
|
|
18
|
-
const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
|
19
|
-
const fullUrl = `${serverUrl}${normalizedEndpoint}`;
|
|
20
|
-
|
|
21
|
-
log.banner();
|
|
22
|
-
log.info(`Calling endpoint: ${chalk.bold(normalizedEndpoint)}`);
|
|
23
|
-
console.log('');
|
|
24
|
-
|
|
25
|
-
// Parse params
|
|
26
|
-
const params = {};
|
|
27
|
-
if (options.param) {
|
|
28
|
-
const paramArray = Array.isArray(options.param) ? options.param : [options.param];
|
|
29
|
-
for (const p of paramArray) {
|
|
30
|
-
const [key, ...valueParts] = p.split('=');
|
|
31
|
-
if (!key || valueParts.length === 0) {
|
|
32
|
-
log.warn(`Invalid param format: ${p} (expected key=value)`);
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
let value = valueParts.join('=');
|
|
36
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
37
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
38
|
-
value = value.slice(1, -1);
|
|
39
|
-
}
|
|
40
|
-
params[key.trim()] = value;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (Object.keys(params).length > 0) {
|
|
45
|
-
log.info('Parameters:');
|
|
46
|
-
for (const [k, v] of Object.entries(params)) {
|
|
47
|
-
log.dim(` ${k}: ${v}`);
|
|
48
|
-
}
|
|
49
|
-
console.log('');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Resolve private key for auto-payment
|
|
53
|
-
const privateKey = resolvePrivateKey(options);
|
|
54
|
-
const autoPay = !!privateKey;
|
|
55
|
-
|
|
56
|
-
if (autoPay) {
|
|
57
|
-
log.info(`Auto-payment: ${chalk.hex('#34D399').bold('enabled')}`);
|
|
58
|
-
try {
|
|
59
|
-
const { getAddressFromKey } = await import('../lib/payment.js');
|
|
60
|
-
const address = getAddressFromKey(privateKey);
|
|
61
|
-
log.dim(` Wallet: ${address.slice(0, 6)}...${address.slice(-4)}`);
|
|
62
|
-
} catch { /* ignore display errors */ }
|
|
63
|
-
console.log('');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Build URL with query params
|
|
67
|
-
let finalUrl = fullUrl;
|
|
68
|
-
if (Object.keys(params).length > 0) {
|
|
69
|
-
const queryString = new URLSearchParams(params).toString();
|
|
70
|
-
finalUrl = `${fullUrl}?${queryString}`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const spinner = ora(`GET ${finalUrl}...`).start();
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
const fetchOptions = {
|
|
77
|
-
method: 'GET',
|
|
78
|
-
headers: { 'Content-Type': 'application/json' },
|
|
79
|
-
signal: AbortSignal.timeout(30000),
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const res = await fetch(finalUrl, fetchOptions);
|
|
83
|
-
spinner.stop();
|
|
84
|
-
|
|
85
|
-
// Handle 402 Payment Required
|
|
86
|
-
if (res.status === 402) {
|
|
87
|
-
console.log('');
|
|
88
|
-
log.warn(chalk.bold('Payment Required (HTTP 402)'));
|
|
89
|
-
console.log('');
|
|
90
|
-
|
|
91
|
-
let paymentInfo;
|
|
92
|
-
try {
|
|
93
|
-
paymentInfo = await res.json();
|
|
94
|
-
} catch {
|
|
95
|
-
log.error('Could not parse payment details');
|
|
96
|
-
process.exit(1);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const price = paymentInfo.payment_details?.amount || paymentInfo.price;
|
|
100
|
-
const payTo = paymentInfo.payment_details?.recipient || paymentInfo.payment_details?.walletAddress || paymentInfo.paymentAddress;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
log
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
log.
|
|
133
|
-
console.log('');
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
console.log('');
|
|
140
|
-
log.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
log
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
.
|
|
305
|
-
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { log } from '../utils/logger.js';
|
|
6
|
+
|
|
7
|
+
export async function callCommand(endpoint, options) {
|
|
8
|
+
if (!endpoint || endpoint.trim().length === 0) {
|
|
9
|
+
log.error('Endpoint is required');
|
|
10
|
+
log.dim(' Usage: x402-bazaar call <endpoint> [--param key=value...]');
|
|
11
|
+
log.dim(' Example: x402-bazaar call /api/weather --param city=Paris');
|
|
12
|
+
log.dim(' Example: x402-bazaar call /api/hash --param text=hello --key 0x...');
|
|
13
|
+
console.log('');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const serverUrl = options.serverUrl || 'https://x402-api.onrender.com';
|
|
18
|
+
const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
|
19
|
+
const fullUrl = `${serverUrl}${normalizedEndpoint}`;
|
|
20
|
+
|
|
21
|
+
log.banner();
|
|
22
|
+
log.info(`Calling endpoint: ${chalk.bold(normalizedEndpoint)}`);
|
|
23
|
+
console.log('');
|
|
24
|
+
|
|
25
|
+
// Parse params
|
|
26
|
+
const params = {};
|
|
27
|
+
if (options.param) {
|
|
28
|
+
const paramArray = Array.isArray(options.param) ? options.param : [options.param];
|
|
29
|
+
for (const p of paramArray) {
|
|
30
|
+
const [key, ...valueParts] = p.split('=');
|
|
31
|
+
if (!key || valueParts.length === 0) {
|
|
32
|
+
log.warn(`Invalid param format: ${p} (expected key=value)`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
let value = valueParts.join('=');
|
|
36
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
37
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
38
|
+
value = value.slice(1, -1);
|
|
39
|
+
}
|
|
40
|
+
params[key.trim()] = value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (Object.keys(params).length > 0) {
|
|
45
|
+
log.info('Parameters:');
|
|
46
|
+
for (const [k, v] of Object.entries(params)) {
|
|
47
|
+
log.dim(` ${k}: ${v}`);
|
|
48
|
+
}
|
|
49
|
+
console.log('');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Resolve private key for auto-payment
|
|
53
|
+
const privateKey = resolvePrivateKey(options);
|
|
54
|
+
const autoPay = !!privateKey;
|
|
55
|
+
|
|
56
|
+
if (autoPay) {
|
|
57
|
+
log.info(`Auto-payment: ${chalk.hex('#34D399').bold('enabled')}`);
|
|
58
|
+
try {
|
|
59
|
+
const { getAddressFromKey } = await import('../lib/payment.js');
|
|
60
|
+
const address = getAddressFromKey(privateKey);
|
|
61
|
+
log.dim(` Wallet: ${address.slice(0, 6)}...${address.slice(-4)}`);
|
|
62
|
+
} catch { /* ignore display errors */ }
|
|
63
|
+
console.log('');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Build URL with query params
|
|
67
|
+
let finalUrl = fullUrl;
|
|
68
|
+
if (Object.keys(params).length > 0) {
|
|
69
|
+
const queryString = new URLSearchParams(params).toString();
|
|
70
|
+
finalUrl = `${fullUrl}?${queryString}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const spinner = ora(`GET ${finalUrl}...`).start();
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const fetchOptions = {
|
|
77
|
+
method: 'GET',
|
|
78
|
+
headers: { 'Content-Type': 'application/json' },
|
|
79
|
+
signal: AbortSignal.timeout(30000),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const res = await fetch(finalUrl, fetchOptions);
|
|
83
|
+
spinner.stop();
|
|
84
|
+
|
|
85
|
+
// Handle 402 Payment Required
|
|
86
|
+
if (res.status === 402) {
|
|
87
|
+
console.log('');
|
|
88
|
+
log.warn(chalk.bold('Payment Required (HTTP 402)'));
|
|
89
|
+
console.log('');
|
|
90
|
+
|
|
91
|
+
let paymentInfo;
|
|
92
|
+
try {
|
|
93
|
+
paymentInfo = await res.json();
|
|
94
|
+
} catch {
|
|
95
|
+
log.error('Could not parse payment details');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const price = paymentInfo.payment_details?.amount || paymentInfo.price;
|
|
100
|
+
const payTo = paymentInfo.payment_details?.recipient || paymentInfo.payment_details?.walletAddress || paymentInfo.paymentAddress;
|
|
101
|
+
|
|
102
|
+
// Split-native mode: provider_wallet is present in payment_details
|
|
103
|
+
const providerWallet = paymentInfo.payment_details?.provider_wallet || null;
|
|
104
|
+
const serverSplit = paymentInfo.payment_details?.split || null;
|
|
105
|
+
const isSplitMode = !!(providerWallet);
|
|
106
|
+
|
|
107
|
+
if (price) {
|
|
108
|
+
log.info(`Price: ${chalk.cyan.bold(`${price} USDC`)}`);
|
|
109
|
+
}
|
|
110
|
+
if (isSplitMode) {
|
|
111
|
+
log.dim(` Mode: split native (95% provider / 5% platform)`);
|
|
112
|
+
log.dim(` Provider wallet: ${providerWallet}`);
|
|
113
|
+
log.dim(` Platform wallet: ${payTo}`);
|
|
114
|
+
} else if (payTo) {
|
|
115
|
+
log.dim(` Pay to: ${payTo}`);
|
|
116
|
+
}
|
|
117
|
+
console.log('');
|
|
118
|
+
|
|
119
|
+
// Auto-pay if key is available
|
|
120
|
+
if (autoPay && price && payTo) {
|
|
121
|
+
if (isSplitMode) {
|
|
122
|
+
await handleSplitAutoPayment(
|
|
123
|
+
privateKey, price, providerWallet, payTo, serverSplit, finalUrl, fetchOptions
|
|
124
|
+
);
|
|
125
|
+
} else {
|
|
126
|
+
await handleAutoPayment(privateKey, payTo, price, finalUrl, fetchOptions);
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// No auto-pay — show instructions
|
|
132
|
+
log.separator();
|
|
133
|
+
console.log('');
|
|
134
|
+
log.info('To pay automatically, provide your private key:');
|
|
135
|
+
console.log('');
|
|
136
|
+
log.dim(' Option 1: Environment variable (recommended)');
|
|
137
|
+
log.dim(' export X402_PRIVATE_KEY=0xYourPrivateKey');
|
|
138
|
+
log.dim(' npx x402-bazaar call /api/weather --param city=Paris');
|
|
139
|
+
console.log('');
|
|
140
|
+
log.dim(' Option 2: Generate a wallet file');
|
|
141
|
+
log.dim(' npx x402-bazaar wallet --setup');
|
|
142
|
+
console.log('');
|
|
143
|
+
log.dim(' Option 3: --key flag (reads from wallet.json file)');
|
|
144
|
+
log.dim(' npx x402-bazaar call /api/weather --param city=Paris --key ~/.x402-bazaar/wallet.json');
|
|
145
|
+
console.log('');
|
|
146
|
+
log.dim(' Option 4: Use the MCP server (via Claude/Cursor)');
|
|
147
|
+
log.dim(' npx x402-bazaar init');
|
|
148
|
+
console.log('');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Handle other errors
|
|
153
|
+
if (!res.ok) {
|
|
154
|
+
console.log('');
|
|
155
|
+
log.error(`HTTP ${res.status}: ${res.statusText}`);
|
|
156
|
+
try {
|
|
157
|
+
const errorBody = await res.text();
|
|
158
|
+
if (errorBody) {
|
|
159
|
+
console.log('');
|
|
160
|
+
log.dim('Response:');
|
|
161
|
+
console.log(chalk.red(errorBody));
|
|
162
|
+
}
|
|
163
|
+
} catch { /* ignore */ }
|
|
164
|
+
console.log('');
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Success
|
|
169
|
+
await displayResponse(res);
|
|
170
|
+
|
|
171
|
+
} catch (err) {
|
|
172
|
+
spinner.fail('Request failed');
|
|
173
|
+
console.log('');
|
|
174
|
+
|
|
175
|
+
if (err.name === 'AbortError') {
|
|
176
|
+
log.error('Request timeout (30s)');
|
|
177
|
+
log.dim(' Try again or check status: npx x402-bazaar status');
|
|
178
|
+
} else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
|
|
179
|
+
log.error('Cannot connect to server');
|
|
180
|
+
log.dim(` Server URL: ${serverUrl}`);
|
|
181
|
+
} else {
|
|
182
|
+
log.error(err.message);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.log('');
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Resolve private key from: --key flag > X402_PRIVATE_KEY env > ~/.x402-bazaar/wallet.json
|
|
192
|
+
*/
|
|
193
|
+
function resolvePrivateKey(options) {
|
|
194
|
+
if (options.key) {
|
|
195
|
+
const raw = options.key.trim();
|
|
196
|
+
// Warn if a raw private key is passed directly as CLI argument (visible in ps aux, shell history)
|
|
197
|
+
const hex = raw.startsWith('0x') ? raw.slice(2) : raw;
|
|
198
|
+
if (/^[0-9a-fA-F]{64}$/.test(hex)) {
|
|
199
|
+
console.log('');
|
|
200
|
+
console.warn(chalk.yellow('⚠️ Warning: Using --key on the command line exposes your private key in shell history.'));
|
|
201
|
+
console.warn(chalk.yellow('⚠️ Prefer setting the X402_PRIVATE_KEY environment variable instead.'));
|
|
202
|
+
console.log('');
|
|
203
|
+
}
|
|
204
|
+
return normalizeKey(options.key);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (process.env.X402_PRIVATE_KEY) {
|
|
208
|
+
return normalizeKey(process.env.X402_PRIVATE_KEY);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Try local wallet file
|
|
212
|
+
try {
|
|
213
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
214
|
+
const walletPath = path.join(home, '.x402-bazaar', 'wallet.json');
|
|
215
|
+
if (fs.existsSync(walletPath)) {
|
|
216
|
+
const data = JSON.parse(fs.readFileSync(walletPath, 'utf-8'));
|
|
217
|
+
if (data.privateKey) {
|
|
218
|
+
return normalizeKey(data.privateKey);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} catch { /* ignore */ }
|
|
222
|
+
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function normalizeKey(key) {
|
|
227
|
+
key = key.trim();
|
|
228
|
+
if (!key.startsWith('0x')) key = '0x' + key;
|
|
229
|
+
if (!/^0x[a-fA-F0-9]{64}$/.test(key)) return null;
|
|
230
|
+
return key;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Handle split native payment (95% to provider, 5% to platform) and retry.
|
|
235
|
+
*
|
|
236
|
+
* On success the retry request carries two separate tx-hash headers:
|
|
237
|
+
* X-Payment-TxHash-Provider — hash of the 95% transfer to the provider
|
|
238
|
+
* X-Payment-TxHash-Platform — hash of the 5% transfer to the platform
|
|
239
|
+
*
|
|
240
|
+
* @param {string} privateKey - Agent private key (hex, with 0x)
|
|
241
|
+
* @param {number} totalPrice - Full price in USDC
|
|
242
|
+
* @param {string} providerWallet - Provider wallet address (95% recipient)
|
|
243
|
+
* @param {string} platformWallet - Platform wallet address (5% recipient)
|
|
244
|
+
* @param {object|null} serverSplit - Optional split amounts from 402 payment_details.split
|
|
245
|
+
* @param {string} url - API endpoint URL
|
|
246
|
+
* @param {object} fetchOptions - Fetch options passed to the retry request
|
|
247
|
+
*/
|
|
248
|
+
async function handleSplitAutoPayment(
|
|
249
|
+
privateKey, totalPrice, providerWallet, platformWallet, serverSplit, url, fetchOptions
|
|
250
|
+
) {
|
|
251
|
+
const spinner = ora(
|
|
252
|
+
`Sending ${totalPrice} USDC (split: 95% provider / 5% platform)...`
|
|
253
|
+
).start();
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const { sendSplitUsdcPayment } = await import('../lib/payment.js');
|
|
257
|
+
|
|
258
|
+
const result = await sendSplitUsdcPayment(privateKey, {
|
|
259
|
+
totalAmountUsdc: totalPrice,
|
|
260
|
+
providerWallet,
|
|
261
|
+
platformWallet,
|
|
262
|
+
serverSplit,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
spinner.succeed(
|
|
266
|
+
`Split payment confirmed: ` +
|
|
267
|
+
chalk.hex('#34D399').bold(`${result.providerAmountUsdc.toFixed(6)} USDC`) +
|
|
268
|
+
` to provider + ` +
|
|
269
|
+
chalk.hex('#34D399').bold(`${result.platformAmountUsdc.toFixed(6)} USDC`) +
|
|
270
|
+
` to platform`
|
|
271
|
+
);
|
|
272
|
+
log.dim(` Provider tx: ${result.explorerProvider}`);
|
|
273
|
+
log.dim(` Platform tx: ${result.explorerPlatform}`);
|
|
274
|
+
console.log('');
|
|
275
|
+
|
|
276
|
+
// Retry with both payment proofs
|
|
277
|
+
const retrySpinner = ora('Retrying with split payment proof...').start();
|
|
278
|
+
|
|
279
|
+
const retryRes = await fetch(url, {
|
|
280
|
+
...fetchOptions,
|
|
281
|
+
headers: {
|
|
282
|
+
...fetchOptions.headers,
|
|
283
|
+
'X-Payment-TxHash-Provider': result.txHashProvider,
|
|
284
|
+
'X-Payment-TxHash-Platform': result.txHashPlatform,
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
retrySpinner.stop();
|
|
289
|
+
|
|
290
|
+
if (!retryRes.ok) {
|
|
291
|
+
console.log('');
|
|
292
|
+
log.error(`HTTP ${retryRes.status}: ${retryRes.statusText}`);
|
|
293
|
+
try {
|
|
294
|
+
const body = await retryRes.text();
|
|
295
|
+
if (body) console.log(chalk.red(body));
|
|
296
|
+
} catch { /* ignore */ }
|
|
297
|
+
console.log('');
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
await displayResponse(retryRes);
|
|
302
|
+
|
|
303
|
+
} catch (err) {
|
|
304
|
+
spinner.fail('Split payment failed');
|
|
305
|
+
console.log('');
|
|
306
|
+
|
|
307
|
+
if (err.message.includes('Insufficient USDC')) {
|
|
308
|
+
log.error(err.message);
|
|
309
|
+
log.dim(' Fund your wallet with USDC on Base.');
|
|
310
|
+
log.dim(' Check balance: npx x402-bazaar wallet --address <your-address>');
|
|
311
|
+
} else if (err.message.includes('Amount too small')) {
|
|
312
|
+
log.error(err.message);
|
|
313
|
+
log.dim(' The service price is too low for a split payment (minimum 0.0001 USDC).');
|
|
314
|
+
} else {
|
|
315
|
+
log.error(err.message);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
console.log('');
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Handle automatic x402 payment and retry
|
|
325
|
+
*/
|
|
326
|
+
async function handleAutoPayment(privateKey, payTo, price, url, fetchOptions) {
|
|
327
|
+
const spinner = ora(`Sending ${price} USDC on Base mainnet...`).start();
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const { sendUsdcPayment } = await import('../lib/payment.js');
|
|
331
|
+
const payment = await sendUsdcPayment(privateKey, payTo, price);
|
|
332
|
+
|
|
333
|
+
spinner.succeed(`Payment confirmed: ${chalk.hex('#34D399').bold(`${price} USDC`)}`);
|
|
334
|
+
log.dim(` Tx: ${payment.explorer}`);
|
|
335
|
+
console.log('');
|
|
336
|
+
|
|
337
|
+
// Retry with payment proof
|
|
338
|
+
const retrySpinner = ora('Retrying with payment proof...').start();
|
|
339
|
+
|
|
340
|
+
const retryRes = await fetch(url, {
|
|
341
|
+
...fetchOptions,
|
|
342
|
+
headers: {
|
|
343
|
+
...fetchOptions.headers,
|
|
344
|
+
'X-Payment-TxHash': payment.txHash,
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
retrySpinner.stop();
|
|
349
|
+
|
|
350
|
+
if (!retryRes.ok) {
|
|
351
|
+
console.log('');
|
|
352
|
+
log.error(`HTTP ${retryRes.status}: ${retryRes.statusText}`);
|
|
353
|
+
try {
|
|
354
|
+
const body = await retryRes.text();
|
|
355
|
+
if (body) console.log(chalk.red(body));
|
|
356
|
+
} catch { /* ignore */ }
|
|
357
|
+
console.log('');
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
await displayResponse(retryRes);
|
|
362
|
+
|
|
363
|
+
} catch (err) {
|
|
364
|
+
spinner.fail('Payment failed');
|
|
365
|
+
console.log('');
|
|
366
|
+
|
|
367
|
+
if (err.message.includes('Insufficient USDC')) {
|
|
368
|
+
log.error(err.message);
|
|
369
|
+
log.dim(' Fund your wallet with USDC on Base.');
|
|
370
|
+
log.dim(' Check balance: npx x402-bazaar wallet --address <your-address>');
|
|
371
|
+
} else {
|
|
372
|
+
log.error(err.message);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
console.log('');
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Display API response with JSON highlighting
|
|
382
|
+
*/
|
|
383
|
+
async function displayResponse(res) {
|
|
384
|
+
console.log('');
|
|
385
|
+
log.success(`${chalk.bold(res.status)} ${res.statusText}`);
|
|
386
|
+
console.log('');
|
|
387
|
+
|
|
388
|
+
const contentType = res.headers.get('content-type') || '';
|
|
389
|
+
|
|
390
|
+
if (contentType.includes('application/json')) {
|
|
391
|
+
const responseData = await res.json();
|
|
392
|
+
log.separator();
|
|
393
|
+
console.log('');
|
|
394
|
+
log.info('Response (JSON):');
|
|
395
|
+
console.log('');
|
|
396
|
+
console.log(highlightJson(JSON.stringify(responseData, null, 2)));
|
|
397
|
+
console.log('');
|
|
398
|
+
log.separator();
|
|
399
|
+
} else {
|
|
400
|
+
const responseText = await res.text();
|
|
401
|
+
log.separator();
|
|
402
|
+
console.log('');
|
|
403
|
+
log.info('Response:');
|
|
404
|
+
console.log('');
|
|
405
|
+
console.log(chalk.white(responseText));
|
|
406
|
+
console.log('');
|
|
407
|
+
log.separator();
|
|
408
|
+
}
|
|
409
|
+
console.log('');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function highlightJson(jsonString) {
|
|
413
|
+
return jsonString
|
|
414
|
+
.replace(/"([^"]+)":/g, chalk.hex('#60A5FA')('"$1"') + ':')
|
|
415
|
+
.replace(/: "([^"]+)"/g, ': ' + chalk.hex('#34D399')('"$1"'))
|
|
416
|
+
.replace(/: (\d+\.?\d*)/g, ': ' + chalk.hex('#FBBF24')('$1'))
|
|
417
|
+
.replace(/: (true|false)/g, ': ' + chalk.hex('#9333EA')('$1'))
|
|
418
|
+
.replace(/: null/g, ': ' + chalk.hex('#6B7280')('null'));
|
|
419
|
+
}
|