@bankofai/x402-cli 1.0.1-beta.7 → 1.0.1-beta.9
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/dist/args.js +96 -0
- package/dist/catalog-commands.js +558 -0
- package/dist/cli.js +49 -1372
- package/dist/daemon.js +47 -0
- package/dist/gateway/catalog.js +56 -0
- package/dist/gateway/cli.js +0 -0
- package/dist/gateway/config.js +48 -3
- package/dist/gateway/index.js +3 -0
- package/dist/gateway/server.js +95 -18
- 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/provider-config.js +94 -0
- package/dist/tokens.js +2 -2
- package/dist/x402.js +8 -2
- package/package.json +4 -4
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
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import { normalizeNetwork } from "./tokens.js";
|
|
5
|
+
function readYaml(file) {
|
|
6
|
+
return YAML.parse(fs.readFileSync(file, "utf8"));
|
|
7
|
+
}
|
|
8
|
+
function expandEnv(value) {
|
|
9
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, name) => {
|
|
10
|
+
if (!(name in process.env))
|
|
11
|
+
throw new Error(`environment variable \${${name}} is not set`);
|
|
12
|
+
return process.env[name] ?? "";
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function expandDeep(value) {
|
|
16
|
+
if (typeof value === "string")
|
|
17
|
+
return expandEnv(value);
|
|
18
|
+
if (Array.isArray(value))
|
|
19
|
+
return value.map(expandDeep);
|
|
20
|
+
if (value && typeof value === "object")
|
|
21
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, expandDeep(item)]));
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
export function providerFiles(root) {
|
|
25
|
+
const stat = fs.statSync(root);
|
|
26
|
+
if (stat.isFile())
|
|
27
|
+
return [root];
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const entry of fs.readdirSync(root, { recursive: true })) {
|
|
30
|
+
const file = path.join(root, String(entry));
|
|
31
|
+
if (file.endsWith("provider.yml") || file.endsWith("provider.yaml"))
|
|
32
|
+
out.push(file);
|
|
33
|
+
}
|
|
34
|
+
return out.sort();
|
|
35
|
+
}
|
|
36
|
+
export function providerPrice(endpoint) {
|
|
37
|
+
return endpoint?.metering?.dimensions?.[0]?.tiers?.[0]?.price_usd ?? 0;
|
|
38
|
+
}
|
|
39
|
+
export function validateProvider(provider, file = "provider.yml") {
|
|
40
|
+
const required = [["name", provider?.name], ["forward_url", provider?.forward_url], ["operator.network", provider?.operator?.network], ["operator.recipient", provider?.operator?.recipient]];
|
|
41
|
+
for (const [name, value] of required)
|
|
42
|
+
if (typeof value !== "string" || !value.trim())
|
|
43
|
+
throw new Error(`${file}: ${name} is required`);
|
|
44
|
+
try {
|
|
45
|
+
const url = new URL(provider.forward_url);
|
|
46
|
+
if (!["http:", "https:"].includes(url.protocol))
|
|
47
|
+
throw new Error("unsupported protocol");
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new Error(`${file}: forward_url must be a valid http(s) URL`);
|
|
51
|
+
}
|
|
52
|
+
if (!Array.isArray(provider.endpoints) || !provider.endpoints.length)
|
|
53
|
+
throw new Error(`${file}: endpoints must contain at least one endpoint`);
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
for (const endpoint of provider.endpoints) {
|
|
56
|
+
if (typeof endpoint.method !== "string" || typeof endpoint.path !== "string")
|
|
57
|
+
throw new Error(`${file}: each endpoint needs method and path`);
|
|
58
|
+
if (!endpoint.method.trim() || !endpoint.path.trim() || !endpoint.path.startsWith("/"))
|
|
59
|
+
throw new Error(`${file}: endpoint method/path must be non-empty and path must start with /`);
|
|
60
|
+
const price = providerPrice(endpoint);
|
|
61
|
+
if (!Number.isFinite(price) || price < 0)
|
|
62
|
+
throw new Error(`${file}: endpoint price_usd must be a finite number >= 0`);
|
|
63
|
+
const key = `${endpoint.method.toUpperCase()} ${endpoint.path}`;
|
|
64
|
+
if (seen.has(key))
|
|
65
|
+
throw new Error(`${file}: duplicate endpoint ${key}`);
|
|
66
|
+
seen.add(key);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function loadProviderFile(file) {
|
|
70
|
+
const provider = expandDeep(readYaml(file));
|
|
71
|
+
validateProvider(provider, file);
|
|
72
|
+
provider.operator.network = normalizeNetwork(provider.operator.network);
|
|
73
|
+
provider.operator.scheme = provider.operator.scheme ?? "exact";
|
|
74
|
+
return provider;
|
|
75
|
+
}
|
|
76
|
+
export function providerAssetTransferMethod(provider) {
|
|
77
|
+
return provider.operator?.asset_transfer_method ?? provider.operator?.assetTransferMethod ?? "permit2";
|
|
78
|
+
}
|
|
79
|
+
export function providerScheme(provider) {
|
|
80
|
+
return provider.operator?.scheme ?? "exact";
|
|
81
|
+
}
|
|
82
|
+
export function providerCatalog(provider) {
|
|
83
|
+
return {
|
|
84
|
+
name: provider.name, title: provider.title ?? provider.name, description: provider.description ?? "",
|
|
85
|
+
category: provider.category ?? "other", service_url: provider.display?.service_url, tags: provider.display?.tags ?? [],
|
|
86
|
+
network: normalizeNetwork(provider.operator.network), currency: provider.operator.currencies?.usd?.[0] ?? "USDT",
|
|
87
|
+
endpoints: (provider.endpoints ?? []).map((endpoint) => ({
|
|
88
|
+
method: endpoint.method.toUpperCase(), path: `/providers/${provider.name}${endpoint.path}`, upstream_path: endpoint.path,
|
|
89
|
+
description: endpoint.description ?? "",
|
|
90
|
+
paid: providerPrice(endpoint) > 0 ? { scheme: providerScheme(provider), network: normalizeNetwork(provider.operator.network), currency: provider.operator.currencies?.usd?.[0] ?? "USDT", price_usd: providerPrice(endpoint) } : null,
|
|
91
|
+
x402_routes: providerPrice(endpoint) > 0 ? [{ provider: provider.name, network: normalizeNetwork(provider.operator.network), scheme: providerScheme(provider), assetTransferMethod: providerAssetTransferMethod(provider), url: `/providers/${provider.name}${endpoint.path}` }] : [],
|
|
92
|
+
})),
|
|
93
|
+
};
|
|
94
|
+
}
|
package/dist/tokens.js
CHANGED
|
@@ -110,8 +110,8 @@ export function assertRawAmount(value, name = "raw amount") {
|
|
|
110
110
|
return value.replace(/^0+(?=\d)/, "");
|
|
111
111
|
}
|
|
112
112
|
export function toSmallestUnit(amount, decimals) {
|
|
113
|
-
if (!Number.isInteger(decimals) || decimals < 0)
|
|
114
|
-
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");
|
|
115
115
|
if (!/^\d+(\.\d+)?$/.test(amount))
|
|
116
116
|
throw new Error("amount must be a non-negative decimal string");
|
|
117
117
|
const [whole, fraction = ""] = amount.split(".");
|
package/dist/x402.js
CHANGED
|
@@ -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
|
},
|
|
@@ -149,7 +155,7 @@ export async function createPaymentPayload(args) {
|
|
|
149
155
|
}
|
|
150
156
|
if (selected.network.startsWith("tron:")) {
|
|
151
157
|
const privateKey = privateKeyFrom(["TRON_PRIVATE_KEY", "AGENT_WALLET_PRIVATE_KEY", "PRIVATE_KEY"], args.privateKey, ["tron_client", "payer", "default"]);
|
|
152
|
-
const wallet = await createTronWallet(privateKey);
|
|
158
|
+
const wallet = await createTronWallet(privateKey, selected.scheme === "exact_gasfree" ? args.maxGasfreeFeeRaw : undefined);
|
|
153
159
|
const signer = await createClientTronSigner(wallet, {
|
|
154
160
|
network: selected.network,
|
|
155
161
|
rpcUrl: args.rpcUrl || process.env.TRON_RPC_URL,
|
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.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"bundle:gateway": "node scripts/bundle-gateway.mjs",
|
|
14
14
|
"prepack": "npm run build && npm run bundle:gateway",
|
|
15
15
|
"test": "npm run build && node --test tests/*.test.mjs",
|
|
16
|
+
"test:package": "node scripts/test-package.mjs",
|
|
16
17
|
"start": "node dist/cli.js",
|
|
17
18
|
"dev": "tsx src/cli.ts"
|
|
18
19
|
},
|
|
@@ -22,11 +23,10 @@
|
|
|
22
23
|
"dependencies": {
|
|
23
24
|
"@bankofai/x402-core": "1.0.1",
|
|
24
25
|
"@bankofai/x402-evm": "1.0.1",
|
|
25
|
-
"@bankofai/x402-gateway": "1.0.1-beta.
|
|
26
|
+
"@bankofai/x402-gateway": "1.0.1-beta.10",
|
|
26
27
|
"@bankofai/x402-tron": "1.0.1",
|
|
27
28
|
"tronweb": "6.4.0",
|
|
28
|
-
"viem": "^2.55.0"
|
|
29
|
-
"yaml": "^2.8.2"
|
|
29
|
+
"viem": "^2.55.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^24.10.1",
|