@mingderwang/x402 0.1.1 → 0.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.
Files changed (4) hide show
  1. package/README.md +23 -2
  2. package/dist/cli.js +24596 -0
  3. package/package.json +6 -3
  4. package/src/cli.ts +342 -0
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@mingderwang/x402",
3
- "version": "0.1.1",
4
- "description": "x402 (HTTP 402 Payment Required) client + server helpers: payment requirements, EIP-3009 signing, X-PAYMENT, verification and settlement.",
3
+ "version": "0.1.2",
4
+ "description": "x402 (HTTP 402 Payment Required) client + server helpers: EIP-3009 signing, X-PAYMENT, verification and settlement. Ships an `x402` CLI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
9
+ "bin": {
10
+ "x402": "./dist/cli.js"
11
+ },
9
12
  "exports": {
10
13
  ".": {
11
14
  "types": "./dist/index.d.ts",
@@ -24,7 +27,7 @@
24
27
  "build": "node ./scripts/build.mjs"
25
28
  },
26
29
  "dependencies": {
27
- "@mingderwang/wallet": "^0.1.0",
30
+ "@mingderwang/wallet": "^0.1.1",
28
31
  "ethers": "^6.13.4"
29
32
  },
30
33
  "keywords": [
package/src/cli.ts ADDED
@@ -0,0 +1,342 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, existsSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { readKeystore, loadWallet, processEnvPassword } from '@mingderwang/wallet';
6
+ import {
7
+ decodeJson,
8
+ detectPaymentRequired,
9
+ makePaymentPayload,
10
+ verifyPaymentPayload,
11
+ settlePayment,
12
+ xPaymentHeader,
13
+ PAYMENT_REQUIRED_HEADER,
14
+ type PaymentRequirements,
15
+ type PaymentRequirementsResponse,
16
+ type PaymentPayload,
17
+ } from './index';
18
+
19
+ const VERSION = '0.1.2';
20
+
21
+ const C = {
22
+ bold: (s: string) => `\x1b[1m${s}\x1b[0m`,
23
+ green: (s: string) => `\x1b[32m${s}\x1b[0m`,
24
+ yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
25
+ red: (s: string) => `\x1b[31m${s}\x1b[0m`,
26
+ gray: (s: string) => `\x1b[90m${s}\x1b[0m`,
27
+ };
28
+
29
+ function defaultKeystore(): string {
30
+ return process.env.X402_WALLET_PATH ?? join(homedir(), '.wallet', 'wallet.json');
31
+ }
32
+
33
+ function promptHidden(question: string): Promise<string> {
34
+ const env = processEnvPassword();
35
+ if (env) {
36
+ process.stderr.write(`${question}[using WALLET_PASSWORD env]\n`);
37
+ return Promise.resolve(env);
38
+ }
39
+ process.stderr.write(question);
40
+ return new Promise((resolve) => {
41
+ const stdin = process.stdin;
42
+ const wasRaw = !!stdin.isRaw;
43
+ stdin.setRawMode(true);
44
+ stdin.resume();
45
+ let input = '';
46
+ const onData = (buf: Buffer) => {
47
+ const code = buf[0];
48
+ if (code === 3) {
49
+ stdin.removeListener('data', onData);
50
+ stdin.setRawMode(false);
51
+ stdin.pause();
52
+ process.exit(130);
53
+ }
54
+ if (code === 13 || code === 10) {
55
+ stdin.removeListener('data', onData);
56
+ stdin.setRawMode(false);
57
+ stdin.pause();
58
+ process.stderr.write('\n');
59
+ resolve(input);
60
+ return;
61
+ }
62
+ if (code === 127 || code === 8) input = input.slice(0, -1);
63
+ else input += buf.toString('utf8');
64
+ };
65
+ stdin.on('data', onData);
66
+ });
67
+ }
68
+
69
+ function parseRequirement(spec: string): PaymentRequirements {
70
+ const raw = existsSync(spec) ? readFileSync(spec, 'utf8') : spec;
71
+ const parsed = JSON.parse(raw) as PaymentRequirementsResponse | PaymentRequirements;
72
+ if (Array.isArray((parsed as PaymentRequirementsResponse).accepts)) {
73
+ const first = (parsed as PaymentRequirementsResponse).accepts[0];
74
+ if (!first) throw new Error('PaymentRequirementsResponse has no accepts[] entries.');
75
+ return first;
76
+ }
77
+ return parsed as PaymentRequirements;
78
+ }
79
+
80
+ function parsePayload(spec: string): PaymentPayload {
81
+ if (/^[A-Za-z0-9+/=]+$/.test(spec) && spec.length > 64) return decodeJson<PaymentPayload>(spec);
82
+ const raw = existsSync(spec) ? readFileSync(spec, 'utf8') : spec;
83
+ return JSON.parse(raw) as PaymentPayload;
84
+ }
85
+
86
+ async function loadSigner(action: string, keystorePath: string): Promise<import('ethers').Wallet> {
87
+ if (!existsSync(keystorePath)) {
88
+ console.log(C.red(`No keystore at ${keystorePath}`) + ` — create one with the \`wallet\` CLI.`);
89
+ process.exit(2);
90
+ }
91
+ const keystore = readKeystore(keystorePath);
92
+ for (let attempt = 1; ; attempt++) {
93
+ const password = await promptHidden(`Keystore password (${action}): `);
94
+ try {
95
+ return loadWallet(keystore, password);
96
+ } catch {
97
+ if (attempt >= 3) {
98
+ console.log(C.red('Incorrect password. Exiting.'));
99
+ process.exit(1);
100
+ }
101
+ console.log(C.red('Incorrect password, try again.'));
102
+ }
103
+ }
104
+ }
105
+
106
+ function printHeaders(set: Headers): void {
107
+ const h = set.get(PAYMENT_REQUIRED_HEADER);
108
+ if (h) console.log(` ${C.gray('payment-required')} ${h.slice(0, 60)}…`);
109
+ const retry = set.get('retry-after');
110
+ if (retry) console.log(` ${C.gray('retry-after')} ${retry}`);
111
+ }
112
+
113
+ async function requestWithBody(url: string, method: string, headers: Record<string, string>): Promise<Response> {
114
+ return fetch(url, { method, headers });
115
+ }
116
+
117
+ // ---------- commands ----------
118
+
119
+ async function cmdRequirements(url: string, method: string, hdrs: Record<string, string>): Promise<void> {
120
+ const res = await requestWithBody(url, method, hdrs);
121
+ const text = await res.text();
122
+ if (res.status !== 402) {
123
+ console.log(C.yellow(`Response is ${res.status}, not 402 — no payment required.`));
124
+ console.log(text.slice(0, 400));
125
+ process.exit(0);
126
+ }
127
+ const reqs = detectPaymentRequired({ status: res.status, headers: res.headers, text });
128
+ if (res.headers && res.headers.size) printHeaders(res.headers);
129
+ console.log(JSON.stringify(reqs, null, 2));
130
+ }
131
+
132
+ async function cmdSign(requirementArg: string, keystorePath: string, json: boolean): Promise<void> {
133
+ const requirement = parseRequirement(requirementArg);
134
+ const wallet = await loadSigner('sign', keystorePath);
135
+ const payload = await makePaymentPayload({ wallet, requirement });
136
+ const header = xPaymentHeader(payload);
137
+ if (json) {
138
+ console.log(JSON.stringify({ payload, header }, null, 2));
139
+ return;
140
+ }
141
+ console.log(` ${C.bold('Signer')} ${wallet.address}`);
142
+ console.log(` ${C.bold('Network')} ${requirement.network}`);
143
+ console.log(` ${C.bold('Amount')} ${requirement.maxAmountRequired} (${requirement.asset})`);
144
+ console.log('');
145
+ console.log(` ${C.green('X-PAYMENT header')}:`);
146
+ console.log(' ' + header);
147
+ }
148
+
149
+ async function cmdPay(url: string, method: string, hdrs: Record<string, string>, keystorePath: string, json: boolean): Promise<void> {
150
+ const first = await requestWithBody(url, method, hdrs);
151
+ const firstText = await first.text();
152
+
153
+ if (first.status !== 402) {
154
+ console.log(C.yellow(`No payment required (${first.status}) — nothing to do.`));
155
+ console.log(firstText.slice(0, 400));
156
+ return;
157
+ }
158
+ const reqs = detectPaymentRequired({ status: first.status, headers: first.headers, text: firstText });
159
+ if (!reqs) {
160
+ console.log(C.red('402 response had no parseable payment requirements.'));
161
+ process.exit(1);
162
+ }
163
+ const requirement = reqs.accepts[0];
164
+ if (!requirement) {
165
+ console.log(C.red('PaymentRequirementsResponse has no accepts[] entries.'));
166
+ process.exit(1);
167
+ }
168
+ const wallet = await loadSigner('pay', keystorePath);
169
+ console.log(` ${C.bold('Paying')} ${requirement.maxAmountRequired} ${requirement.asset} on ${requirement.network}`);
170
+ console.log(` ${C.gray('from')} ${wallet.address} ${C.gray('→')} ${requirement.payTo}\n`);
171
+ const payload = await makePaymentPayload({ wallet, requirement });
172
+ const retried = await requestWithBody(url, method, { ...hdrs, 'X-PAYMENT': xPaymentHeader(payload) });
173
+ const body = await retried.text();
174
+ if (retried.status === 402) {
175
+ console.log(C.red(`Still 402 after payment:`));
176
+ console.log(body.slice(0, 400));
177
+ process.exit(1);
178
+ }
179
+ if (json) {
180
+ let parsed = null;
181
+ try { parsed = JSON.parse(body); } catch { /* non-json */ }
182
+ console.log(JSON.stringify({ status: retried.status, headers: Object.fromEntries(retried.headers.entries()), body: parsed ?? body }, null, 2));
183
+ return;
184
+ }
185
+ console.log(C.green(`${retried.status} ${retried.statusText}`));
186
+ try {
187
+ console.log(JSON.stringify(JSON.parse(body), null, 2));
188
+ } catch {
189
+ console.log(body.slice(0, 800));
190
+ }
191
+ }
192
+
193
+ async function cmdDecode(spec: string): Promise<void> {
194
+ const payload = parsePayload(spec);
195
+ console.log(JSON.stringify(payload, null, 2));
196
+ }
197
+
198
+ async function cmdVerify(payloadArg: string, requirementArg: string): Promise<void> {
199
+ const payload = parsePayload(payloadArg);
200
+ const requirement = parseRequirement(requirementArg);
201
+ const result = verifyPaymentPayload(payload, requirement);
202
+ if (result.isValid) console.log(JSON.stringify({ isValid: true, payer: result.payer }, null, 2));
203
+ else console.log(JSON.stringify({ isValid: false, invalidReason: result.invalidReason }, null, 2));
204
+ }
205
+
206
+ async function cmdSettle(payloadArg: string, requirementArg: string, gasKey?: string, rpcUrl?: string): Promise<void> {
207
+ const payload = parsePayload(payloadArg);
208
+ const requirement = parseRequirement(requirementArg);
209
+ if (gasKey) process.env.X402_GAS_PRIVATE_KEY = gasKey;
210
+ if (rpcUrl) process.env.X402_RPC_URL = rpcUrl;
211
+ const result = await settlePayment(payload, requirement, {});
212
+ console.log(JSON.stringify(result, null, 2));
213
+ }
214
+
215
+ // ---------- help ----------
216
+
217
+ const HELP = `x402 v${VERSION}
218
+
219
+ x402 (HTTP 402 Payment Required) toolkit: sign, pay, verify and settle
220
+ EIP-3009 USDC payments.
221
+
222
+ Usage:
223
+ x402 requirements <url> [--method GET] [--header K:V]...
224
+ Print the PaymentRequirementsResponse if the URL responds 402.
225
+ x402 sign <requirements.json> [--wallet <keystore>]
226
+ Sign a payment for the first accepts[] entry; print the X-PAYMENT header base64.
227
+ x402 pay <url> [--method GET] [--header K:V]... [--wallet <keystore>]
228
+ Full round trip: hit the URL, pay the 402, retry with X-PAYMENT, print the result.
229
+ x402 decode <base64-or-file | payload.json>
230
+ Decode an X-PAYMENT header value into the PaymentPayload JSON.
231
+ x402 verify <payload> <requirements.json>
232
+ Offline signature/amount/time sanity check. payload = base64 header or JSON file.
233
+ x402 settle <payload> <requirements.json> [--gas-key <private>] [--rpc <url>]
234
+ Broadcast transferWithAuthorization (uses X402_GAS_PRIVATE_KEY / X402_RPC_URL env if set).
235
+
236
+ Options:
237
+ --wallet <keystore> Keystore file (default ~/.wallet/wallet.json)
238
+ --json Machine-readable output
239
+ --header K:V Extra HTTP header (repeatable)
240
+ --method <verB> HTTP method (default GET)
241
+
242
+ Password:
243
+ Prompts interactively, or set WALLET_PASSWORD / XSIFT_WALLET_PASSWORD in the environment.`;
244
+
245
+ function printHelp(): void {
246
+ console.log(HELP);
247
+ }
248
+
249
+ // ---------- entry ----------
250
+
251
+ interface Parsed {
252
+ cmd: string;
253
+ positional: string[];
254
+ opts: Record<string, string>;
255
+ headers: Record<string, string>;
256
+ json: boolean;
257
+ }
258
+
259
+ function parseArgs(args: string[]): Parsed {
260
+ const positional: string[] = [];
261
+ const opts: Record<string, string> = {};
262
+ const headers: Record<string, string> = {};
263
+ let json = false;
264
+ for (let i = 0; i < args.length; i++) {
265
+ const a = args[i];
266
+ if (a === '--json') {
267
+ json = true;
268
+ continue;
269
+ }
270
+ if (a === '--header') {
271
+ const kv = args[++i];
272
+ const idx = kv ? kv.indexOf(':') : -1;
273
+ if (!kv || idx < 0) {
274
+ console.log(C.red('--header expects K:V'));
275
+ process.exit(1);
276
+ }
277
+ headers[kv.slice(0, idx).trim()] = kv.slice(idx + 1).trim();
278
+ continue;
279
+ }
280
+ if (a === '--wallet' || a === '--gas-key' || a === '--rpc' || a === '--method' || a === '--path') {
281
+ opts[a.slice(2)] = args[++i] ?? '';
282
+ continue;
283
+ }
284
+ if (!a.startsWith('--')) positional.push(a);
285
+ }
286
+ const cmd = positional[0] ?? 'help';
287
+ return { cmd, positional: positional.slice(1), opts, headers, json };
288
+ }
289
+
290
+ async function main(): Promise<void> {
291
+ const { cmd, positional, opts, headers, json } = parseArgs(process.argv.slice(2));
292
+ const keystore = opts.wallet ?? defaultKeystore();
293
+ const method = (opts.method ?? 'GET').toUpperCase();
294
+ switch (cmd) {
295
+ case 'requirements': {
296
+ const url = positional[0];
297
+ if (!url) throw new Error('usage: x402 requirements <url>');
298
+ return cmdRequirements(url, method, headers);
299
+ }
300
+ case 'sign': {
301
+ const spec = positional[0];
302
+ if (!spec) throw new Error('usage: x402 sign <requirements.json> [--wallet <keystore>]');
303
+ return cmdSign(spec, keystore, json);
304
+ }
305
+ case 'pay': {
306
+ const url = positional[0];
307
+ if (!url) throw new Error('usage: x402 pay <url> [--wallet <keystore>]');
308
+ return cmdPay(url, method, headers, keystore, json);
309
+ }
310
+ case 'decode': {
311
+ const spec = positional[0];
312
+ if (!spec) throw new Error('usage: x402 decode <base64-or-file>');
313
+ return cmdDecode(spec);
314
+ }
315
+ case 'verify': {
316
+ const [payloadArg, reqArg] = positional;
317
+ if (!payloadArg || !reqArg) throw new Error('usage: x402 verify <payload> <requirements.json>');
318
+ return cmdVerify(payloadArg, reqArg);
319
+ }
320
+ case 'settle': {
321
+ const [payloadArg, reqArg] = positional;
322
+ if (!payloadArg || !reqArg) throw new Error('usage: x402 settle <payload> <requirements.json>');
323
+ return cmdSettle(payloadArg, reqArg, opts['gas-key'], opts.rpc);
324
+ }
325
+ case 'help':
326
+ case '-h':
327
+ case '--help':
328
+ return printHelp();
329
+ case '-v':
330
+ case '--version':
331
+ return console.log(VERSION);
332
+ default:
333
+ console.log(C.red(`Unknown command: ${cmd}`) + '\n');
334
+ printHelp();
335
+ process.exit(1);
336
+ }
337
+ }
338
+
339
+ main().catch((e) => {
340
+ console.error(e instanceof Error ? e.message : e);
341
+ process.exit(1);
342
+ });