@bankofai/x402-cli 1.0.1-beta.1 → 1.0.1-beta.10
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 +56 -20
- package/dist/args.js +96 -0
- package/dist/catalog-commands.js +558 -0
- package/dist/cli.js +104 -1372
- package/dist/daemon.js +47 -0
- package/dist/gateway/catalog.js +56 -0
- package/dist/gateway/config.js +60 -9
- package/dist/gateway/index.js +3 -0
- package/dist/gateway/server.js +171 -31
- package/dist/gateway/tokens.js +11 -5
- package/dist/gateway-commands.js +145 -0
- package/dist/help.js +169 -0
- package/dist/http-client.js +82 -0
- package/dist/output.js +91 -0
- package/dist/tokens.js +13 -7
- package/dist/x402.js +25 -11
- package/package.json +12 -8
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { opt, outputMode } from "./args.js";
|
|
7
|
+
import { emit, printJson } from "./output.js";
|
|
8
|
+
import { buildGatewayCatalog, loadProviders, providerPaymentAssets } from "@bankofai/x402-gateway";
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
function executableInPath(name) {
|
|
11
|
+
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
12
|
+
if (!dir)
|
|
13
|
+
continue;
|
|
14
|
+
const candidate = path.join(dir, name);
|
|
15
|
+
try {
|
|
16
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
17
|
+
return candidate;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Try the next PATH entry.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
function resolveGatewayPackageRuntime() {
|
|
26
|
+
try {
|
|
27
|
+
return path.join(path.dirname(require.resolve("@bankofai/x402-gateway/package.json")), "dist", "cli.js");
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function gatewayCommand(options) {
|
|
34
|
+
const explicit = opt(options, "gateway-bin");
|
|
35
|
+
const gatewayPackageRuntime = resolveGatewayPackageRuntime();
|
|
36
|
+
const candidates = [
|
|
37
|
+
explicit ? { file: explicit, source: "--gateway-bin" } : undefined,
|
|
38
|
+
gatewayPackageRuntime ? { file: gatewayPackageRuntime, source: "@bankofai/x402-gateway dependency" } : undefined,
|
|
39
|
+
{ file: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "gateway", "cli.js"), source: "bundled gateway runtime" },
|
|
40
|
+
executableInPath("x402-gateway") ? { file: executableInPath("x402-gateway"), source: "PATH x402-gateway" } : undefined,
|
|
41
|
+
{ file: path.resolve(process.cwd(), "../x402-gateway/dist/cli.js"), source: "sibling ../x402-gateway" },
|
|
42
|
+
].filter(Boolean);
|
|
43
|
+
for (const candidate of candidates) {
|
|
44
|
+
try {
|
|
45
|
+
fs.accessSync(candidate.file, fs.constants.R_OK);
|
|
46
|
+
if (candidate.file.endsWith(".js")) {
|
|
47
|
+
return { command: process.execPath, argsPrefix: [candidate.file], source: candidate.source };
|
|
48
|
+
}
|
|
49
|
+
return { command: candidate.file, argsPrefix: [], source: candidate.source };
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Try the next candidate.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
throw new Error("x402-gateway runtime not found. Install @bankofai/x402-gateway, run from a checkout with ../x402-gateway/dist/cli.js, or pass --gateway-bin <path>.");
|
|
56
|
+
}
|
|
57
|
+
export async function gatewayStart(args, options) {
|
|
58
|
+
const gateway = gatewayCommand(options);
|
|
59
|
+
const child = spawn(gateway.command, [...gateway.argsPrefix, ...args], {
|
|
60
|
+
stdio: "inherit",
|
|
61
|
+
env: process.env,
|
|
62
|
+
});
|
|
63
|
+
await new Promise((resolve, reject) => {
|
|
64
|
+
child.on("exit", code => code === 0 ? resolve() : reject(new Error(`gateway exited with ${code}`)));
|
|
65
|
+
child.on("error", reject);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export function gatewayCheck(target, options) {
|
|
69
|
+
const providers = loadProviders(target);
|
|
70
|
+
emit({
|
|
71
|
+
command: "gateway check",
|
|
72
|
+
mode: outputMode(options),
|
|
73
|
+
result: { providers: [...providers.keys()], count: providers.size },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export function gatewayScaffold(name, options) {
|
|
77
|
+
const outputDir = opt(options, "output-dir", path.join("providers", name));
|
|
78
|
+
const forwardUrl = opt(options, "forward-url", "https://api.example.com");
|
|
79
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
80
|
+
const body = `name: ${name}
|
|
81
|
+
title: "${name}"
|
|
82
|
+
description: "x402 provider"
|
|
83
|
+
category: data
|
|
84
|
+
version: v1
|
|
85
|
+
|
|
86
|
+
forward_url: ${forwardUrl}
|
|
87
|
+
|
|
88
|
+
routing:
|
|
89
|
+
type: proxy
|
|
90
|
+
|
|
91
|
+
operator:
|
|
92
|
+
network: tron:0xcd8690dc
|
|
93
|
+
currencies:
|
|
94
|
+
usd: ["USDT"]
|
|
95
|
+
recipient: <provider-recipient-address>
|
|
96
|
+
scheme: exact
|
|
97
|
+
protocol: exact
|
|
98
|
+
asset_transfer_method: permit2
|
|
99
|
+
facilitator_url: https://facilitator.bankofai.io
|
|
100
|
+
facilitator_api_key: <facilitator-api-key>
|
|
101
|
+
valid_for_seconds: 300
|
|
102
|
+
|
|
103
|
+
endpoints:
|
|
104
|
+
- method: GET
|
|
105
|
+
path: /v1/ping
|
|
106
|
+
metering:
|
|
107
|
+
dimensions:
|
|
108
|
+
- tiers:
|
|
109
|
+
- price_usd: 0.0001
|
|
110
|
+
`;
|
|
111
|
+
fs.writeFileSync(path.join(outputDir, "provider.yml"), body);
|
|
112
|
+
emit({
|
|
113
|
+
command: "gateway scaffold",
|
|
114
|
+
mode: outputMode(options),
|
|
115
|
+
result: { file: path.join(outputDir, "provider.yml") },
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
export function catalogBuild(target, options) {
|
|
119
|
+
const providers = loadProviders(target);
|
|
120
|
+
const catalog = buildGatewayCatalog(providers);
|
|
121
|
+
const output = opt(options, "output", opt(options, "dist-dir") ? path.join(opt(options, "dist-dir"), "catalog.json") : undefined);
|
|
122
|
+
if (output) {
|
|
123
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
124
|
+
fs.writeFileSync(output, JSON.stringify(catalog, null, 2));
|
|
125
|
+
emit({
|
|
126
|
+
command: "catalog build",
|
|
127
|
+
mode: outputMode(options),
|
|
128
|
+
result: { output, count: providers.size },
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
else if (outputMode(options) === "json") {
|
|
132
|
+
emit({ command: "catalog build", mode: "json", result: catalog });
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
printJson(catalog);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export function catalogPayAssets(target, options) {
|
|
139
|
+
const rows = providerPaymentAssets(loadProviders(target));
|
|
140
|
+
emit({
|
|
141
|
+
command: "gateway catalog pay-assets",
|
|
142
|
+
mode: outputMode(options),
|
|
143
|
+
result: { count: rows.length, assets: rows },
|
|
144
|
+
});
|
|
145
|
+
}
|
package/dist/help.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
export function getVersion() {
|
|
3
|
+
try {
|
|
4
|
+
const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
5
|
+
return String(pkg.version ?? "0.0.0");
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return "0.0.0";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function helpText(topic = "root") {
|
|
12
|
+
const sections = {
|
|
13
|
+
root: `x402-cli ${getVersion()}
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
x402-cli <command> [options]
|
|
17
|
+
|
|
18
|
+
Commands:
|
|
19
|
+
pay <url> Pay an x402-protected URL
|
|
20
|
+
serve Run a local x402 paywall endpoint
|
|
21
|
+
roundtrip Start serve, pay it, then exit
|
|
22
|
+
gateway <command> Manage local gateway provider files
|
|
23
|
+
catalog <command> Search, cache, and export provider catalog assets
|
|
24
|
+
|
|
25
|
+
Global options:
|
|
26
|
+
-h, --help Show help
|
|
27
|
+
-V, --version Show version
|
|
28
|
+
--json Print machine-readable JSON envelope
|
|
29
|
+
--human Print human-readable output (default)
|
|
30
|
+
`,
|
|
31
|
+
pay: `Usage:
|
|
32
|
+
x402-cli pay <url> [options]
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
--method <method> HTTP method (default: GET)
|
|
36
|
+
--header "Name: Value" Request header, repeatable
|
|
37
|
+
--body <body> Request body for non-GET/HEAD methods
|
|
38
|
+
--network <caip2> Require a specific network
|
|
39
|
+
--token <symbol> Require a specific token
|
|
40
|
+
--scheme <scheme> Require a specific x402 scheme
|
|
41
|
+
--gasfree-api-url <url> Override the TRON GasFree relayer API URL
|
|
42
|
+
--max-gasfree-fee <amt> Maximum GasFree relayer fee in token units
|
|
43
|
+
--max-gasfree-fee-raw <n> Maximum GasFree relayer fee in smallest units
|
|
44
|
+
--max-amount <amount> Maximum human-readable payment amount
|
|
45
|
+
--max-raw-amount <amount> Maximum smallest-unit payment amount
|
|
46
|
+
--dry-run Read requirements but do not sign or pay
|
|
47
|
+
--private-key <hex> Explicit payer private key (or PRIVATE_KEY/TRON_PRIVATE_KEY/EVM_PRIVATE_KEY)
|
|
48
|
+
--rpc-url <url> Explicit network RPC URL
|
|
49
|
+
--timeout-ms <ms> Network timeout in milliseconds (default: 30000)
|
|
50
|
+
--json Print JSON envelope
|
|
51
|
+
|
|
52
|
+
Examples:
|
|
53
|
+
x402-cli pay https://api.example.com/paid --dry-run --json
|
|
54
|
+
x402-cli pay https://api.example.com/paid --max-amount 0.01
|
|
55
|
+
`,
|
|
56
|
+
serve: `Usage:
|
|
57
|
+
x402-cli serve --pay-to <address> [options]
|
|
58
|
+
|
|
59
|
+
Options:
|
|
60
|
+
--pay-to <address> Recipient wallet address
|
|
61
|
+
--amount <amount> Human-readable token amount (default: 0.0001)
|
|
62
|
+
--raw-amount <amount> Smallest-unit amount
|
|
63
|
+
--network <caip2> Payment network (default: tron:0xcd8690dc)
|
|
64
|
+
--scheme <scheme> Payment scheme: exact or exact_gasfree (default: exact)
|
|
65
|
+
--token <symbol> Token symbol (default: USDT)
|
|
66
|
+
--asset <address> Explicit token address
|
|
67
|
+
--decimals <count> Token decimals for unregistered --asset
|
|
68
|
+
--host <host> Bind host (default: 127.0.0.1)
|
|
69
|
+
--port <port> Bind port (default: 4020)
|
|
70
|
+
--resource-url <url> URL advertised in payment requirements
|
|
71
|
+
--facilitator-url <url> Facilitator base URL
|
|
72
|
+
--timeout-ms <ms> Facilitator timeout in milliseconds (default: 30000)
|
|
73
|
+
--daemon Run in background and print the child pid
|
|
74
|
+
--json Print JSON envelope
|
|
75
|
+
|
|
76
|
+
Examples:
|
|
77
|
+
x402-cli serve --pay-to T... --network tron:0xcd8690dc --token USDT
|
|
78
|
+
x402-cli serve --pay-to 0x... --network eip155:97 --token USDT --amount 0.0001
|
|
79
|
+
`,
|
|
80
|
+
roundtrip: `Usage:
|
|
81
|
+
x402-cli roundtrip --pay-to <address> [serve/pay options]
|
|
82
|
+
`,
|
|
83
|
+
gateway: `Usage:
|
|
84
|
+
x402-cli gateway <search|start|check|scaffold|catalog> [options]
|
|
85
|
+
|
|
86
|
+
Commands:
|
|
87
|
+
search <query> Search a gateway/catalog artifact
|
|
88
|
+
start Start a local x402 gateway process
|
|
89
|
+
check <providers> Validate provider.yml files
|
|
90
|
+
scaffold <name> Write a starter provider.yml
|
|
91
|
+
catalog <command> Build/check/search gateway catalog assets
|
|
92
|
+
`,
|
|
93
|
+
"gateway-catalog": `Usage:
|
|
94
|
+
x402-cli gateway catalog <build|check|pay-assets|search> [options]
|
|
95
|
+
|
|
96
|
+
Commands:
|
|
97
|
+
build <providers> Build a local catalog from provider.yml files
|
|
98
|
+
check <providers> Validate local provider.yml files
|
|
99
|
+
pay-assets <providers> List payable endpoint assets
|
|
100
|
+
search <query> Search a catalog artifact
|
|
101
|
+
`,
|
|
102
|
+
catalog: `Usage:
|
|
103
|
+
x402-cli catalog <update|search|show|endpoints|pay-json|export-gateway|build> [options]
|
|
104
|
+
|
|
105
|
+
Commands:
|
|
106
|
+
update Cache hosted/local catalog assets under ~/.cache
|
|
107
|
+
search <query> Search providers
|
|
108
|
+
show <provider> Show provider detail JSON
|
|
109
|
+
endpoints <provider> List provider endpoints
|
|
110
|
+
pay-json <provider> Print provider pay JSON
|
|
111
|
+
export-gateway <url> Export catalog.json and pay.md from a gateway
|
|
112
|
+
build <providers> Build catalog from provider.yml files
|
|
113
|
+
|
|
114
|
+
Options:
|
|
115
|
+
--catalog <source> catalog.json path or URL
|
|
116
|
+
--provider <fqn> Provider FQN for export-gateway
|
|
117
|
+
--output-dir <dir> Output directory for generated files
|
|
118
|
+
-n, --limit <count> Search result limit
|
|
119
|
+
--timeout-ms <ms> Network timeout in milliseconds (default: 30000)
|
|
120
|
+
--include-blocked Include blocked providers in search
|
|
121
|
+
--json Print JSON envelope
|
|
122
|
+
`,
|
|
123
|
+
"catalog-search": `Usage:
|
|
124
|
+
x402-cli catalog search <query> [--catalog <source>] [options]
|
|
125
|
+
|
|
126
|
+
Options:
|
|
127
|
+
--catalog <source> catalog.json path or URL
|
|
128
|
+
-n, --limit <count> Search result limit
|
|
129
|
+
--timeout-ms <ms> Network timeout in milliseconds (default: 30000)
|
|
130
|
+
--include-blocked Include blocked providers in search
|
|
131
|
+
--json Print JSON envelope
|
|
132
|
+
`,
|
|
133
|
+
"catalog-show": `Usage:
|
|
134
|
+
x402-cli catalog show <provider> [--catalog <source>] [options]
|
|
135
|
+
|
|
136
|
+
Options:
|
|
137
|
+
--catalog <source> catalog.json path or URL
|
|
138
|
+
--timeout-ms <ms> Network timeout in milliseconds (default: 30000)
|
|
139
|
+
--json Print JSON envelope
|
|
140
|
+
`,
|
|
141
|
+
"catalog-pay-json": `Usage:
|
|
142
|
+
x402-cli catalog pay-json <provider> [--catalog <source>] [options]
|
|
143
|
+
|
|
144
|
+
Options:
|
|
145
|
+
--catalog <source> catalog.json path or URL
|
|
146
|
+
--timeout-ms <ms> Network timeout in milliseconds (default: 30000)
|
|
147
|
+
--raw Print raw pay payload instead of JSON envelope
|
|
148
|
+
--json Print JSON envelope
|
|
149
|
+
`,
|
|
150
|
+
"catalog-endpoints": `Usage:
|
|
151
|
+
x402-cli catalog endpoints <provider> [--catalog <source>] [options]
|
|
152
|
+
|
|
153
|
+
Options:
|
|
154
|
+
--catalog <source> catalog.json path or URL
|
|
155
|
+
--timeout-ms <ms> Network timeout in milliseconds (default: 30000)
|
|
156
|
+
--json Print JSON envelope
|
|
157
|
+
`,
|
|
158
|
+
"catalog-export-gateway": `Usage:
|
|
159
|
+
x402-cli catalog export-gateway <gateway-url> --provider <fqn> [options]
|
|
160
|
+
|
|
161
|
+
Options:
|
|
162
|
+
--provider <fqn> Provider FQN to export
|
|
163
|
+
--output-dir <dir> Output directory for generated files
|
|
164
|
+
--force Overwrite existing files
|
|
165
|
+
--json Print JSON envelope
|
|
166
|
+
`,
|
|
167
|
+
};
|
|
168
|
+
return sections[topic] ?? sections.root;
|
|
169
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { CliError, opt } from "./args.js";
|
|
3
|
+
const MAX_HTTP_BODY_BYTES = 10 * 1024 * 1024;
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
5
|
+
export function positiveIntegerOption(options, key, fallback) {
|
|
6
|
+
const value = opt(options, key, String(fallback));
|
|
7
|
+
if (!/^\d+$/.test(value))
|
|
8
|
+
throw new CliError("INVALID_ARGUMENT", `--${key} must be a positive integer`, `Pass --${key} with a value greater than zero.`, 2);
|
|
9
|
+
const parsed = Number(value);
|
|
10
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0)
|
|
11
|
+
throw new CliError("INVALID_ARGUMENT", `--${key} must be a positive integer`, `Pass --${key} with a value greater than zero.`, 2);
|
|
12
|
+
return parsed;
|
|
13
|
+
}
|
|
14
|
+
export function timeoutMs(options) {
|
|
15
|
+
return options ? positiveIntegerOption(options, "timeout-ms", DEFAULT_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
|
|
16
|
+
}
|
|
17
|
+
export async function fetchWithTimeout(input, init = {}, timeout = DEFAULT_TIMEOUT_MS, label = "request") {
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
20
|
+
try {
|
|
21
|
+
return await fetch(input, { ...init, signal: controller.signal });
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error instanceof Error && error.name === "AbortError")
|
|
25
|
+
throw new Error(`${label} timed out after ${timeout}ms`);
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function readBoundedText(response, label) {
|
|
33
|
+
const declared = Number(response.headers.get("content-length"));
|
|
34
|
+
if (Number.isFinite(declared) && declared > MAX_HTTP_BODY_BYTES)
|
|
35
|
+
throw new Error(`${label} exceeds ${MAX_HTTP_BODY_BYTES} byte limit`);
|
|
36
|
+
if (!response.body)
|
|
37
|
+
return "";
|
|
38
|
+
const reader = response.body.getReader();
|
|
39
|
+
const chunks = [];
|
|
40
|
+
let size = 0;
|
|
41
|
+
for (;;) {
|
|
42
|
+
const { done, value } = await reader.read();
|
|
43
|
+
if (done)
|
|
44
|
+
break;
|
|
45
|
+
size += value.byteLength;
|
|
46
|
+
if (size > MAX_HTTP_BODY_BYTES) {
|
|
47
|
+
await reader.cancel();
|
|
48
|
+
throw new Error(`${label} exceeds ${MAX_HTTP_BODY_BYTES} byte limit`);
|
|
49
|
+
}
|
|
50
|
+
chunks.push(value);
|
|
51
|
+
}
|
|
52
|
+
const bytes = new Uint8Array(size);
|
|
53
|
+
let offset = 0;
|
|
54
|
+
for (const chunk of chunks) {
|
|
55
|
+
bytes.set(chunk, offset);
|
|
56
|
+
offset += chunk.byteLength;
|
|
57
|
+
}
|
|
58
|
+
return new TextDecoder().decode(bytes);
|
|
59
|
+
}
|
|
60
|
+
export async function readText(source, options) {
|
|
61
|
+
if (!source.startsWith("http://") && !source.startsWith("https://"))
|
|
62
|
+
return fs.readFileSync(source, "utf8");
|
|
63
|
+
const response = await fetchWithTimeout(source, {}, timeoutMs(options), `fetch ${source}`);
|
|
64
|
+
if (!response.ok)
|
|
65
|
+
throw new Error(`failed to fetch ${source}: ${response.status}`);
|
|
66
|
+
return readBoundedText(response, `response from ${source}`);
|
|
67
|
+
}
|
|
68
|
+
export async function readJson(source, options) {
|
|
69
|
+
return JSON.parse(await readText(source, options));
|
|
70
|
+
}
|
|
71
|
+
export async function responsePayload(response) {
|
|
72
|
+
const text = await readBoundedText(response, "HTTP response");
|
|
73
|
+
if ((response.headers.get("content-type") ?? "").toLowerCase().includes("json")) {
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(text);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return text;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return text;
|
|
82
|
+
}
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { CliError } from "./args.js";
|
|
2
|
+
export function printJson(value) {
|
|
3
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
4
|
+
}
|
|
5
|
+
export function emit(args) {
|
|
6
|
+
const mode = args.mode ?? "human";
|
|
7
|
+
if (mode === "json") {
|
|
8
|
+
const envelope = { ok: !args.error, command: args.command };
|
|
9
|
+
if (args.network)
|
|
10
|
+
envelope.network = args.network;
|
|
11
|
+
if (args.scheme)
|
|
12
|
+
envelope.scheme = args.scheme;
|
|
13
|
+
if (args.error)
|
|
14
|
+
envelope.error = args.error;
|
|
15
|
+
else
|
|
16
|
+
envelope.result = args.result ?? null;
|
|
17
|
+
printJson(envelope);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (args.error) {
|
|
21
|
+
process.stderr.write(`ERROR ${args.command}: ${args.error.code}\n`);
|
|
22
|
+
process.stderr.write(` ${args.error.message}\n`);
|
|
23
|
+
if (args.error.hint)
|
|
24
|
+
process.stderr.write(` hint: ${args.error.hint}\n`);
|
|
25
|
+
if (args.error.details !== undefined)
|
|
26
|
+
process.stderr.write(` details: ${JSON.stringify(args.error.details)}\n`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const suffix = [args.network, args.scheme].filter(Boolean).join(" ");
|
|
30
|
+
process.stdout.write(`OK ${args.command}${suffix ? ` (${suffix})` : ""}\n`);
|
|
31
|
+
if (args.result && typeof args.result === "object" && !Array.isArray(args.result)) {
|
|
32
|
+
for (const [key, value] of Object.entries(args.result)) {
|
|
33
|
+
if (value === undefined)
|
|
34
|
+
continue;
|
|
35
|
+
process.stdout.write(value && typeof value === "object" ? ` ${key}: ${JSON.stringify(value)}\n` : ` ${key}: ${value}\n`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else if (args.result !== undefined)
|
|
39
|
+
process.stdout.write(` ${args.result}\n`);
|
|
40
|
+
}
|
|
41
|
+
export function classify(error) {
|
|
42
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
43
|
+
if (error instanceof CliError)
|
|
44
|
+
return { code: error.code, message, hint: error.hint, details: error.details };
|
|
45
|
+
const lower = message.toLowerCase();
|
|
46
|
+
if (lower.includes("missing private key") || lower.includes("could not find a wallet"))
|
|
47
|
+
return { code: "WALLET_NOT_CONFIGURED", message, hint: "Set PRIVATE_KEY, TRON_PRIVATE_KEY, EVM_PRIVATE_KEY, or configure agent-wallet with a payer wallet." };
|
|
48
|
+
if (lower.includes("wallets_config") || lower.includes("wallet config"))
|
|
49
|
+
return { code: "WALLET_CONFIG_CORRUPT", message, hint: "Check ~/.agent-wallet/wallets_config.json or recreate the local agent-wallet configuration." };
|
|
50
|
+
if (lower.includes("does not exist") && lower.includes("account [t"))
|
|
51
|
+
return { code: "TRON_ACCOUNT_NOT_ACTIVATED", message, hint: "Activate the TRON address by sending it a small amount of TRX before signing contract calls." };
|
|
52
|
+
if (lower.includes("permit2_insufficient_balance") || lower.includes("insufficient") && lower.includes("balance"))
|
|
53
|
+
return { code: "INSUFFICIENT_TOKEN_BALANCE", message, hint: "Fund the payer address with the exact token and network advertised by the provider, then retry." };
|
|
54
|
+
if (lower.includes("transfer_from_failed") || lower.includes("transferfrom failed"))
|
|
55
|
+
return { code: "TOKEN_TRANSFER_FAILED", message, hint: "Check token balance, token contract, payer address, and that the selected x402 route matches the provider requirement." };
|
|
56
|
+
if (lower.includes("insufficient funds for gas") || lower.includes("insufficient gas") || lower.includes("energy"))
|
|
57
|
+
return { code: "INSUFFICIENT_GAS", message, hint: "Fund the payer address with the native gas token for this network." };
|
|
58
|
+
if (lower.includes("deadline") || lower.includes("expired"))
|
|
59
|
+
return { code: "DEADLINE_OR_CLOCK_SKEW", message, hint: "Check local clock sync and retry with a fresh payment requirement." };
|
|
60
|
+
if (lower.includes("permittransferfrom") || lower.includes("invalid signature") || lower.includes("permit reverted"))
|
|
61
|
+
return { code: "PERMIT_REVERTED", message, hint: "The token or Permit2 contract rejected the signature; retry with a fresh requirement and verify token/network support." };
|
|
62
|
+
if (lower.includes("tokenregistry") && lower.includes("import"))
|
|
63
|
+
return { code: "SDK_API_DRIFT", message, hint: "Installed x402 SDK packages do not match this CLI; reinstall @bankofai/x402-cli and SDK dependencies." };
|
|
64
|
+
if (lower.includes("429") || lower.includes("too many requests") || lower.includes("rate limit"))
|
|
65
|
+
return { code: "RATE_LIMITED", message, hint: "Wait briefly and retry; the upstream service or RPC is rate limiting requests." };
|
|
66
|
+
if (lower.includes("402 response missing"))
|
|
67
|
+
return { code: "INVALID_X402_RESPONSE", message, hint: "The endpoint returned HTTP 402 without a PAYMENT-REQUIRED header." };
|
|
68
|
+
if (lower.includes("no matching payment requirement"))
|
|
69
|
+
return { code: "NO_MATCHING_PAYMENT_REQUIREMENT", message, hint: "Relax --network, --token, or --scheme, or use values offered by the provider." };
|
|
70
|
+
if (lower.includes("exceeds --max"))
|
|
71
|
+
return { code: "PAYMENT_AMOUNT_TOO_HIGH", message, hint: "Increase the max amount flag only if this provider price is expected." };
|
|
72
|
+
if (lower.includes("failed to fetch") || lower.includes("fetch failed") || lower.includes("econnrefused") || lower.includes("timed out"))
|
|
73
|
+
return { code: "NETWORK_ERROR", message, hint: "Check the URL, local server, proxy, and network connectivity." };
|
|
74
|
+
if (lower.includes(" is required") || lower.includes("must be") || lower.includes("invalid --") || lower.includes("mutually exclusive"))
|
|
75
|
+
return { code: lower.includes("required") ? "MISSING_ARGUMENT" : "INVALID_ARGUMENT", message, hint: "Run the command with --help to see valid usage and options." };
|
|
76
|
+
return { code: "IO_ERROR", message, hint: "Run with --json for structured output, and check the provider/gateway logs for details." };
|
|
77
|
+
}
|
|
78
|
+
export async function withSdkStdoutRedirect(enabled, fn) {
|
|
79
|
+
if (!enabled)
|
|
80
|
+
return fn();
|
|
81
|
+
const originalLog = console.log;
|
|
82
|
+
console.log = (...args) => {
|
|
83
|
+
process.stderr.write(`${args.map(arg => typeof arg === "string" ? arg : JSON.stringify(arg, null, 2)).join(" ")}\n`);
|
|
84
|
+
};
|
|
85
|
+
try {
|
|
86
|
+
return await fn();
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
console.log = originalLog;
|
|
90
|
+
}
|
|
91
|
+
}
|
package/dist/tokens.js
CHANGED
|
@@ -74,19 +74,25 @@ export const TOKENS = {
|
|
|
74
74
|
},
|
|
75
75
|
};
|
|
76
76
|
export function normalizeNetwork(network) {
|
|
77
|
-
|
|
77
|
+
const legacyTronIds = {
|
|
78
78
|
"tron-mainnet": "tron:0x2b6653dc",
|
|
79
79
|
"tron:mainnet": "tron:0x2b6653dc",
|
|
80
|
-
|
|
80
|
+
mainnet: "tron:0x2b6653dc",
|
|
81
81
|
"tron-shasta": "tron:0x94a9059e",
|
|
82
82
|
"tron:shasta": "tron:0x94a9059e",
|
|
83
|
-
|
|
83
|
+
shasta: "tron:0x94a9059e",
|
|
84
84
|
"tron-nile": "tron:0xcd8690dc",
|
|
85
85
|
"tron:nile": "tron:0xcd8690dc",
|
|
86
|
-
|
|
86
|
+
nile: "tron:0xcd8690dc",
|
|
87
|
+
};
|
|
88
|
+
const canonical = legacyTronIds[network];
|
|
89
|
+
if (canonical) {
|
|
90
|
+
throw new Error(`legacy TRON network identifier ${network} is not supported; use ${canonical}`);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
87
93
|
"bsc-mainnet": "eip155:56",
|
|
88
94
|
"bsc-testnet": "eip155:97",
|
|
89
|
-
}[network] ?? network
|
|
95
|
+
}[network] ?? network;
|
|
90
96
|
}
|
|
91
97
|
export function getToken(network, symbol) {
|
|
92
98
|
const token = TOKENS[normalizeNetwork(network)]?.[symbol.toUpperCase()];
|
|
@@ -104,8 +110,8 @@ export function assertRawAmount(value, name = "raw amount") {
|
|
|
104
110
|
return value.replace(/^0+(?=\d)/, "");
|
|
105
111
|
}
|
|
106
112
|
export function toSmallestUnit(amount, decimals) {
|
|
107
|
-
if (!Number.isInteger(decimals) || decimals < 0)
|
|
108
|
-
throw new Error("token decimals must be
|
|
113
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 255)
|
|
114
|
+
throw new Error("token decimals must be an integer between 0 and 255");
|
|
109
115
|
if (!/^\d+(\.\d+)?$/.test(amount))
|
|
110
116
|
throw new Error("amount must be a non-negative decimal string");
|
|
111
117
|
const [whole, fraction = ""] = amount.split(".");
|
package/dist/x402.js
CHANGED
|
@@ -2,7 +2,7 @@ import { decodePaymentRequiredHeader, decodePaymentResponseHeader, decodePayment
|
|
|
2
2
|
import { x402Client } from "@bankofai/x402-core/client";
|
|
3
3
|
import { ExactEvmScheme, toClientEvmSigner } from "@bankofai/x402-evm";
|
|
4
4
|
import { ExactTronScheme, createClientTronSigner } from "@bankofai/x402-tron";
|
|
5
|
-
import {
|
|
5
|
+
import { ExactGasFreeTronScheme, createGasFreeApiClients, getGasFreeApiBaseUrl } from "@bankofai/x402-tron/gasfree";
|
|
6
6
|
import { createPublicClient, http } from "viem";
|
|
7
7
|
import { privateKeyToAccount } from "viem/accounts";
|
|
8
8
|
import { TronWeb } from "tronweb";
|
|
@@ -106,7 +106,7 @@ function evmRpcUrl(network, explicit) {
|
|
|
106
106
|
(chainId === "56" ? "https://bsc-dataseed.binance.org" : undefined) ||
|
|
107
107
|
(chainId === "97" ? "https://data-seed-prebsc-1-s1.binance.org:8545" : undefined));
|
|
108
108
|
}
|
|
109
|
-
async function createTronWallet(privateKey) {
|
|
109
|
+
async function createTronWallet(privateKey, maxGasfreeFeeRaw) {
|
|
110
110
|
const rawKey = privateKey.replace(/^0x/, "");
|
|
111
111
|
const tronWeb = new TronWeb({ fullHost: "https://api.trongrid.io" });
|
|
112
112
|
const address = TronWeb.address.fromPrivateKey(rawKey);
|
|
@@ -115,6 +115,12 @@ async function createTronWallet(privateKey) {
|
|
|
115
115
|
return {
|
|
116
116
|
getAddress: () => address,
|
|
117
117
|
async signTypedData(args) {
|
|
118
|
+
if (maxGasfreeFeeRaw !== undefined && args?.primaryType === "PermitTransfer") {
|
|
119
|
+
const maxFee = BigInt(args?.message?.maxFee ?? -1);
|
|
120
|
+
if (maxFee < 0n || maxFee > BigInt(maxGasfreeFeeRaw)) {
|
|
121
|
+
throw new Error(`final GasFree maxFee ${maxFee} exceeds --max-gasfree-fee limit ${maxGasfreeFeeRaw}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
118
124
|
const signature = await signTronTypedData(tronWeb, args, rawKey);
|
|
119
125
|
return signature.startsWith("0x") ? signature : `0x${signature}`;
|
|
120
126
|
},
|
|
@@ -142,13 +148,14 @@ export async function createPaymentPayload(args) {
|
|
|
142
148
|
const publicClient = rpcUrl ? createPublicClient({ transport: http(rpcUrl) }) : undefined;
|
|
143
149
|
const signer = toClientEvmSigner(account, publicClient);
|
|
144
150
|
const scheme = new ExactEvmScheme(signer, rpcUrl ? { rpcUrl } : undefined);
|
|
145
|
-
|
|
151
|
+
const payload = await new x402Client()
|
|
146
152
|
.register(selected.network, scheme)
|
|
147
153
|
.createPaymentPayload(required);
|
|
154
|
+
return { payload };
|
|
148
155
|
}
|
|
149
156
|
if (selected.network.startsWith("tron:")) {
|
|
150
157
|
const privateKey = privateKeyFrom(["TRON_PRIVATE_KEY", "AGENT_WALLET_PRIVATE_KEY", "PRIVATE_KEY"], args.privateKey, ["tron_client", "payer", "default"]);
|
|
151
|
-
const wallet = await createTronWallet(privateKey);
|
|
158
|
+
const wallet = await createTronWallet(privateKey, selected.scheme === "exact_gasfree" ? args.maxGasfreeFeeRaw : undefined);
|
|
152
159
|
const signer = await createClientTronSigner(wallet, {
|
|
153
160
|
network: selected.network,
|
|
154
161
|
rpcUrl: args.rpcUrl || process.env.TRON_RPC_URL,
|
|
@@ -156,14 +163,20 @@ export async function createPaymentPayload(args) {
|
|
|
156
163
|
allowanceMode: args.allowanceMode || process.env.X402_TRON_ALLOWANCE_MODE || "auto",
|
|
157
164
|
});
|
|
158
165
|
const client = new x402Client();
|
|
166
|
+
let gasfreeEstimate;
|
|
159
167
|
if (selected.scheme === "exact_gasfree") {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
...(args.gasfreeApiUrl || process.env.X402_GASFREE_API_URL
|
|
164
|
-
? { schemeOptions: { apiBaseUrls: { [selected.network]: args.gasfreeApiUrl || process.env.X402_GASFREE_API_URL } } }
|
|
165
|
-
: {}),
|
|
168
|
+
const apiUrl = args.gasfreeApiUrl || process.env.X402_GASFREE_API_URL || getGasFreeApiBaseUrl(selected.network);
|
|
169
|
+
const scheme = new ExactGasFreeTronScheme(signer, {
|
|
170
|
+
apiClients: createGasFreeApiClients({ [selected.network]: apiUrl }),
|
|
166
171
|
});
|
|
172
|
+
const total = await scheme.estimateCost(selected);
|
|
173
|
+
const amount = BigInt(selected.amount);
|
|
174
|
+
const fee = total - amount;
|
|
175
|
+
if (args.maxGasfreeFeeRaw !== undefined && fee > BigInt(args.maxGasfreeFeeRaw)) {
|
|
176
|
+
throw new Error(`estimated GasFree fee ${fee} exceeds --max-gasfree-fee limit ${args.maxGasfreeFeeRaw}`);
|
|
177
|
+
}
|
|
178
|
+
gasfreeEstimate = { fee: fee.toString(), total: total.toString() };
|
|
179
|
+
client.register(selected.network, scheme);
|
|
167
180
|
}
|
|
168
181
|
else if (selected.scheme === "exact") {
|
|
169
182
|
client.register(selected.network, new ExactTronScheme(signer));
|
|
@@ -171,7 +184,8 @@ export async function createPaymentPayload(args) {
|
|
|
171
184
|
else {
|
|
172
185
|
throw new Error(`unsupported scheme ${selected.scheme} on ${selected.network}`);
|
|
173
186
|
}
|
|
174
|
-
|
|
187
|
+
const payload = await client.createPaymentPayload(required);
|
|
188
|
+
return { payload, ...(gasfreeEstimate ? { gasfreeEstimate } : {}) };
|
|
175
189
|
}
|
|
176
190
|
throw new Error(`unsupported network ${selected.network}`);
|
|
177
191
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bankofai/x402-cli",
|
|
3
|
-
"version": "1.0.1-beta.
|
|
3
|
+
"version": "1.0.1-beta.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
"x402-cli": "dist/cli.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"
|
|
12
|
+
"clean": "node scripts/clean.mjs",
|
|
13
|
+
"build": "npm run clean && tsc -p tsconfig.json",
|
|
13
14
|
"bundle:gateway": "node scripts/bundle-gateway.mjs",
|
|
14
15
|
"prepack": "npm run build && npm run bundle:gateway",
|
|
15
16
|
"test": "npm run build && node --test tests/*.test.mjs",
|
|
17
|
+
"test:package": "node scripts/test-package.mjs",
|
|
16
18
|
"start": "node dist/cli.js",
|
|
17
19
|
"dev": "tsx src/cli.ts"
|
|
18
20
|
},
|
|
@@ -20,17 +22,19 @@
|
|
|
20
22
|
"node": ">=20"
|
|
21
23
|
},
|
|
22
24
|
"dependencies": {
|
|
23
|
-
"@bankofai/x402-core": "1.0.1
|
|
24
|
-
"@bankofai/x402-evm": "1.0.1
|
|
25
|
-
"@bankofai/x402-gateway": "1.0.1-beta.
|
|
26
|
-
"@bankofai/x402-tron": "1.0.1
|
|
25
|
+
"@bankofai/x402-core": "1.0.1",
|
|
26
|
+
"@bankofai/x402-evm": "1.0.1",
|
|
27
|
+
"@bankofai/x402-gateway": "1.0.1-beta.10",
|
|
28
|
+
"@bankofai/x402-tron": "1.0.1",
|
|
27
29
|
"tronweb": "6.4.0",
|
|
28
|
-
"viem": "^2.55.0"
|
|
29
|
-
"yaml": "^2.8.2"
|
|
30
|
+
"viem": "^2.55.0"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@types/node": "^24.10.1",
|
|
33
34
|
"tsx": "^4.20.6",
|
|
34
35
|
"typescript": "^5.9.3"
|
|
36
|
+
},
|
|
37
|
+
"overrides": {
|
|
38
|
+
"ws": "8.21.0"
|
|
35
39
|
}
|
|
36
40
|
}
|