@bankofai/x402-cli 1.0.0 → 1.0.1-beta.1
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 +23 -7
- package/dist/cli.js +24 -12
- package/dist/gateway/cli.js +71 -26
- package/dist/gateway/config.js +31 -12
- package/dist/gateway/server.js +24 -25
- package/dist/gateway/tokens.js +11 -5
- package/dist/tokens.js +12 -6
- package/dist/x402.js +23 -4
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
TypeScript command-line client for BankofAI x402 payments. This version uses
|
|
4
4
|
the npm TypeScript SDK packages only:
|
|
5
5
|
|
|
6
|
-
- `@bankofai/x402-core@1.0.
|
|
7
|
-
- `@bankofai/x402-evm@1.0.
|
|
8
|
-
- `@bankofai/x402-tron@1.0.
|
|
6
|
+
- `@bankofai/x402-core@1.0.1-beta.2`
|
|
7
|
+
- `@bankofai/x402-evm@1.0.1-beta.2`
|
|
8
|
+
- `@bankofai/x402-tron@1.0.1-beta.2`
|
|
9
9
|
|
|
10
|
-
Stablecoin payments
|
|
11
|
-
|
|
10
|
+
Stablecoin payments support `scheme=exact` and TRON `scheme=exact_gasfree`.
|
|
11
|
+
The GasFree flow lets the relayer pay network energy while deducting its fee
|
|
12
|
+
from the payment token, so the payer does not need TRX.
|
|
12
13
|
|
|
13
14
|
## Install
|
|
14
15
|
|
|
@@ -74,6 +75,20 @@ node dist/cli.js pay http://127.0.0.1:4020/pay \
|
|
|
74
75
|
--token USDT
|
|
75
76
|
```
|
|
76
77
|
|
|
78
|
+
Pay a TRON GasFree endpoint (the CLI normally selects this automatically from
|
|
79
|
+
the server challenge):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
TRON_PRIVATE_KEY=<hex> \
|
|
83
|
+
node dist/cli.js pay https://api.example.com/pay \
|
|
84
|
+
--network tron:nile \
|
|
85
|
+
--token USDT \
|
|
86
|
+
--scheme exact_gasfree
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Use `--gasfree-api-url <url>` or `X402_GASFREE_API_URL` to override the SDK's
|
|
90
|
+
default relayer endpoint.
|
|
91
|
+
|
|
77
92
|
For EVM networks use `EVM_PRIVATE_KEY` or `PRIVATE_KEY`.
|
|
78
93
|
|
|
79
94
|
### Roundtrip
|
|
@@ -114,5 +129,6 @@ Pass a facilitator URL when needed:
|
|
|
114
129
|
x402-cli serve --facilitator-url https://facilitator.bankofai.io ...
|
|
115
130
|
```
|
|
116
131
|
|
|
117
|
-
|
|
118
|
-
|
|
132
|
+
`serve --scheme exact_gasfree` advertises a TRON GasFree requirement. The
|
|
133
|
+
configured facilitator must advertise and settle `exact_gasfree` for that
|
|
134
|
+
network and token.
|
package/dist/cli.js
CHANGED
|
@@ -315,6 +315,7 @@ Options:
|
|
|
315
315
|
--network <caip2> Require a specific network
|
|
316
316
|
--token <symbol> Require a specific token
|
|
317
317
|
--scheme <scheme> Require a specific x402 scheme
|
|
318
|
+
--gasfree-api-url <url> Override the TRON GasFree relayer API URL
|
|
318
319
|
--max-amount <amount> Maximum human-readable payment amount
|
|
319
320
|
--max-raw-amount <amount> Maximum smallest-unit payment amount
|
|
320
321
|
--dry-run Read requirements but do not sign or pay
|
|
@@ -335,6 +336,7 @@ Options:
|
|
|
335
336
|
--amount <amount> Human-readable token amount (default: 0.0001)
|
|
336
337
|
--raw-amount <amount> Smallest-unit amount
|
|
337
338
|
--network <caip2> Payment network (default: tron:nile)
|
|
339
|
+
--scheme <scheme> Payment scheme: exact or exact_gasfree (default: exact)
|
|
338
340
|
--token <symbol> Token symbol (default: USDT)
|
|
339
341
|
--asset <address> Explicit token address
|
|
340
342
|
--decimals <count> Token decimals for unregistered --asset
|
|
@@ -476,7 +478,7 @@ function loadProviderFile(file) {
|
|
|
476
478
|
const provider = expandDeep(readYaml(file));
|
|
477
479
|
validateProvider(provider, file);
|
|
478
480
|
provider.operator.network = normalizeNetwork(provider.operator.network);
|
|
479
|
-
provider.operator.scheme = "exact";
|
|
481
|
+
provider.operator.scheme = provider.operator.scheme ?? "exact";
|
|
480
482
|
return provider;
|
|
481
483
|
}
|
|
482
484
|
function validateProvider(provider, file = "provider.yml") {
|
|
@@ -524,6 +526,9 @@ function providerPrice(endpoint) {
|
|
|
524
526
|
function providerAssetTransferMethod(provider) {
|
|
525
527
|
return provider.operator?.asset_transfer_method ?? provider.operator?.assetTransferMethod ?? "permit2";
|
|
526
528
|
}
|
|
529
|
+
function providerScheme(provider) {
|
|
530
|
+
return provider.operator?.scheme ?? "exact";
|
|
531
|
+
}
|
|
527
532
|
function providerCatalog(provider) {
|
|
528
533
|
return {
|
|
529
534
|
name: provider.name,
|
|
@@ -540,7 +545,7 @@ function providerCatalog(provider) {
|
|
|
540
545
|
upstream_path: endpoint.path,
|
|
541
546
|
description: endpoint.description ?? "",
|
|
542
547
|
paid: providerPrice(endpoint) > 0 ? {
|
|
543
|
-
scheme:
|
|
548
|
+
scheme: providerScheme(provider),
|
|
544
549
|
network: normalizeNetwork(provider.operator.network),
|
|
545
550
|
currency: provider.operator.currencies?.usd?.[0] ?? "USDT",
|
|
546
551
|
price_usd: providerPrice(endpoint),
|
|
@@ -548,7 +553,7 @@ function providerCatalog(provider) {
|
|
|
548
553
|
x402_routes: providerPrice(endpoint) > 0 ? [{
|
|
549
554
|
provider: provider.name,
|
|
550
555
|
network: normalizeNetwork(provider.operator.network),
|
|
551
|
-
scheme:
|
|
556
|
+
scheme: providerScheme(provider),
|
|
552
557
|
assetTransferMethod: providerAssetTransferMethod(provider),
|
|
553
558
|
url: `/providers/${provider.name}${endpoint.path}`,
|
|
554
559
|
}] : [],
|
|
@@ -964,6 +969,12 @@ async function catalogExportGateway(gatewayUrl, options) {
|
|
|
964
969
|
}
|
|
965
970
|
function buildRequirement(options) {
|
|
966
971
|
const network = normalizeNetwork(opt(options, "network", "tron:nile"));
|
|
972
|
+
const scheme = opt(options, "scheme", "exact");
|
|
973
|
+
if (!["exact", "exact_gasfree"].includes(scheme))
|
|
974
|
+
throw new Error(`unsupported scheme ${scheme}`);
|
|
975
|
+
if (scheme === "exact_gasfree" && !network.startsWith("tron:")) {
|
|
976
|
+
throw new Error("exact_gasfree is supported only on TRON networks");
|
|
977
|
+
}
|
|
967
978
|
const tokenSymbol = opt(options, "token", "USDT");
|
|
968
979
|
const explicitAsset = opt(options, "asset");
|
|
969
980
|
const registryToken = explicitAsset
|
|
@@ -986,13 +997,13 @@ function buildRequirement(options) {
|
|
|
986
997
|
const assetAddress = explicitAsset ?? registryToken.address;
|
|
987
998
|
const assetTransferMethod = registryToken?.assetTransferMethod ?? "permit2";
|
|
988
999
|
return {
|
|
989
|
-
scheme
|
|
1000
|
+
scheme,
|
|
990
1001
|
network,
|
|
991
1002
|
amount,
|
|
992
1003
|
asset: assetAddress,
|
|
993
1004
|
payTo: opt(options, "pay-to") ?? opt(options, "payTo") ?? "",
|
|
994
1005
|
maxTimeoutSeconds: Number(opt(options, "valid-for-seconds", "300")),
|
|
995
|
-
extra: assetTransferMethod ? { assetTransferMethod } : {},
|
|
1006
|
+
extra: scheme === "exact" && assetTransferMethod ? { assetTransferMethod } : {},
|
|
996
1007
|
};
|
|
997
1008
|
}
|
|
998
1009
|
async function facilitatorPost(baseUrl, path, body, options) {
|
|
@@ -1039,7 +1050,7 @@ function serveDaemon(argv, options) {
|
|
|
1039
1050
|
command: "server",
|
|
1040
1051
|
mode: outputMode(options),
|
|
1041
1052
|
network: requirement.network,
|
|
1042
|
-
scheme:
|
|
1053
|
+
scheme: requirement.scheme,
|
|
1043
1054
|
result: {
|
|
1044
1055
|
pid: child.pid,
|
|
1045
1056
|
pay_url: resourceUrl,
|
|
@@ -1092,7 +1103,7 @@ async function serve(options) {
|
|
|
1092
1103
|
}
|
|
1093
1104
|
if (pathname === "/.well-known/x402") {
|
|
1094
1105
|
response.writeHead(200, { "content-type": "application/json" });
|
|
1095
|
-
response.end(JSON.stringify({ network: requirement.network, scheme:
|
|
1106
|
+
response.end(JSON.stringify({ network: requirement.network, scheme: requirement.scheme, asset: requirement.asset, rawAmount: requirement.amount, amount: requirement.amount, payTo: requirement.payTo, pay_url: resourceUrl }));
|
|
1096
1107
|
return;
|
|
1097
1108
|
}
|
|
1098
1109
|
if (pathname !== "/pay") {
|
|
@@ -1139,7 +1150,7 @@ async function serve(options) {
|
|
|
1139
1150
|
"content-type": "application/json",
|
|
1140
1151
|
[headers.response]: encodeResponse(settle),
|
|
1141
1152
|
});
|
|
1142
|
-
response.end(JSON.stringify({ success: true, network: requirement.network, scheme:
|
|
1153
|
+
response.end(JSON.stringify({ success: true, network: requirement.network, scheme: requirement.scheme, transaction: settle.transaction ?? settle.txHash ?? null }));
|
|
1143
1154
|
}
|
|
1144
1155
|
catch (error) {
|
|
1145
1156
|
response.writeHead(500, { "content-type": "application/json" });
|
|
@@ -1153,7 +1164,7 @@ async function serve(options) {
|
|
|
1153
1164
|
emit({
|
|
1154
1165
|
command: "server",
|
|
1155
1166
|
network: requirement.network,
|
|
1156
|
-
scheme:
|
|
1167
|
+
scheme: requirement.scheme,
|
|
1157
1168
|
mode: outputMode(options),
|
|
1158
1169
|
result: { pay_url: resourceUrl, token: opt(options, "token", "USDT"), rawAmount: requirement.amount, pay_to: requirement.payTo },
|
|
1159
1170
|
});
|
|
@@ -1213,7 +1224,7 @@ async function pay(url, options) {
|
|
|
1213
1224
|
emit({
|
|
1214
1225
|
command: "client",
|
|
1215
1226
|
network: selected.network,
|
|
1216
|
-
scheme:
|
|
1227
|
+
scheme: selected.scheme,
|
|
1217
1228
|
mode: outputMode(options),
|
|
1218
1229
|
result: {
|
|
1219
1230
|
url,
|
|
@@ -1230,6 +1241,7 @@ async function pay(url, options) {
|
|
|
1230
1241
|
extensions: required.extensions,
|
|
1231
1242
|
rpcUrl: opt(options, "rpc-url"),
|
|
1232
1243
|
privateKey: opt(options, "private-key"),
|
|
1244
|
+
gasfreeApiUrl: opt(options, "gasfree-api-url"),
|
|
1233
1245
|
}));
|
|
1234
1246
|
const retryHeaders = new Headers(baseHeaders);
|
|
1235
1247
|
retryHeaders.set(headers.signature, encodeSignature(payload));
|
|
@@ -1253,7 +1265,7 @@ async function pay(url, options) {
|
|
|
1253
1265
|
emit({
|
|
1254
1266
|
command: "client",
|
|
1255
1267
|
network: selected.network,
|
|
1256
|
-
scheme:
|
|
1268
|
+
scheme: selected.scheme,
|
|
1257
1269
|
mode: outputMode(options),
|
|
1258
1270
|
result,
|
|
1259
1271
|
});
|
|
@@ -1408,7 +1420,7 @@ function catalogPayAssets(target, options) {
|
|
|
1408
1420
|
network: normalizeNetwork(provider.operator.network),
|
|
1409
1421
|
currency: provider.operator.currencies?.usd?.[0] ?? "USDT",
|
|
1410
1422
|
price_usd: providerPrice(endpoint),
|
|
1411
|
-
scheme:
|
|
1423
|
+
scheme: providerScheme(provider),
|
|
1412
1424
|
assetTransferMethod: providerAssetTransferMethod(provider),
|
|
1413
1425
|
})));
|
|
1414
1426
|
emit({
|
package/dist/gateway/cli.js
CHANGED
|
@@ -65,17 +65,21 @@ function opt(options, key, fallback) {
|
|
|
65
65
|
function flag(options, key) {
|
|
66
66
|
return options[key] === true;
|
|
67
67
|
}
|
|
68
|
-
function
|
|
68
|
+
function rawHasFlag(argv, name, short) {
|
|
69
|
+
return argv.some(item => item === `--${name}` || item.startsWith(`--${name}=`) || (short ? item === short : false));
|
|
70
|
+
}
|
|
71
|
+
function readVersion() {
|
|
69
72
|
try {
|
|
70
73
|
const url = new URL("../package.json", import.meta.url);
|
|
71
|
-
|
|
74
|
+
const version = JSON.parse(fs.readFileSync(url, "utf8")).version;
|
|
75
|
+
return typeof version === "string" && version ? version : undefined;
|
|
72
76
|
}
|
|
73
77
|
catch {
|
|
74
|
-
return
|
|
78
|
+
return undefined;
|
|
75
79
|
}
|
|
76
80
|
}
|
|
77
81
|
function help() {
|
|
78
|
-
return `x402-gateway ${
|
|
82
|
+
return `x402-gateway ${readVersion() ?? "unknown"}
|
|
79
83
|
|
|
80
84
|
Usage:
|
|
81
85
|
x402-gateway start --providers <dir> [options]
|
|
@@ -105,6 +109,9 @@ Examples:
|
|
|
105
109
|
`;
|
|
106
110
|
}
|
|
107
111
|
function providerPath(options) {
|
|
112
|
+
if (opt(options, "provider") && opt(options, "providers")) {
|
|
113
|
+
throw new CliError("Options --provider and --providers are mutually exclusive.");
|
|
114
|
+
}
|
|
108
115
|
const source = opt(options, "providers", opt(options, "provider", process.env.X402_GATEWAY_PROVIDERS_DIR));
|
|
109
116
|
if (!source)
|
|
110
117
|
throw new CliError("No provider source configured. Use --provider <file> or --providers <dir>.");
|
|
@@ -120,25 +127,56 @@ function parsePort(value) {
|
|
|
120
127
|
}
|
|
121
128
|
return port;
|
|
122
129
|
}
|
|
130
|
+
function parseHost(value) {
|
|
131
|
+
const host = value ?? process.env.X402_GATEWAY_HOST ?? "127.0.0.1";
|
|
132
|
+
if (!host.trim())
|
|
133
|
+
throw new CliError("Invalid --host; expected a non-empty host.");
|
|
134
|
+
return host;
|
|
135
|
+
}
|
|
136
|
+
function listenErrorMessage(error, host, port) {
|
|
137
|
+
const err = error;
|
|
138
|
+
if (err?.code === "EADDRINUSE")
|
|
139
|
+
return `Port ${port} is already in use on ${host}. Choose another --port or stop the existing process.`;
|
|
140
|
+
if (err?.code === "EADDRNOTAVAIL")
|
|
141
|
+
return `Host ${host} is not available on this machine. Use --host 127.0.0.1 for local runs or --host 0.0.0.0 in containers.`;
|
|
142
|
+
return error instanceof Error ? error.message : String(error);
|
|
143
|
+
}
|
|
123
144
|
function publicHost(host) {
|
|
124
145
|
return host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
|
|
125
146
|
}
|
|
126
|
-
function
|
|
147
|
+
function localUrls(host, port) {
|
|
148
|
+
const base = `http://${publicHost(host)}:${port}`;
|
|
149
|
+
return {
|
|
150
|
+
base,
|
|
151
|
+
health: `${base}/__402/health`,
|
|
152
|
+
ready: `${base}/__402/ready`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function printStartup(options, source, host, port, providers) {
|
|
127
156
|
if (flag(options, "quiet"))
|
|
128
157
|
return;
|
|
158
|
+
const urls = localUrls(host, port);
|
|
129
159
|
if (flag(options, "json")) {
|
|
130
|
-
process.stdout.write(JSON.stringify({ ok: true, host, port, providers }, null, 2) + "\n");
|
|
160
|
+
process.stdout.write(JSON.stringify({ ok: true, source, host, port, count: providers.length, providers, health: urls.health, ready: urls.ready }, null, 2) + "\n");
|
|
131
161
|
return;
|
|
132
162
|
}
|
|
133
|
-
|
|
134
|
-
process.stdout.write(`x402-gateway listening on ${base}\n`);
|
|
163
|
+
process.stdout.write(`x402-gateway listening on ${urls.base}\n`);
|
|
135
164
|
process.stdout.write(`providers: ${providers.length} loaded\n`);
|
|
136
|
-
process.stdout.write(`health: ${
|
|
137
|
-
process.stdout.write(`ready: ${
|
|
165
|
+
process.stdout.write(`health: ${urls.health}\n`);
|
|
166
|
+
process.stdout.write(`ready: ${urls.ready}\n`);
|
|
167
|
+
}
|
|
168
|
+
function loadProvidersForCli(action, source) {
|
|
169
|
+
try {
|
|
170
|
+
return loadProviders(source);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
174
|
+
throw new Error(`failed to load providers from ${source} for ${action}: ${message}`);
|
|
175
|
+
}
|
|
138
176
|
}
|
|
139
177
|
function check(options) {
|
|
140
178
|
const source = providerPath(options);
|
|
141
|
-
const providers =
|
|
179
|
+
const providers = loadProvidersForCli("check", source);
|
|
142
180
|
const summary = [...providers.values()].map(entry => ({
|
|
143
181
|
name: entry.config.name,
|
|
144
182
|
network: entry.config.operator.network,
|
|
@@ -148,6 +186,8 @@ function check(options) {
|
|
|
148
186
|
process.stdout.write(JSON.stringify({ ok: true, source, count: summary.length, providers: summary }, null, 2) + "\n");
|
|
149
187
|
return;
|
|
150
188
|
}
|
|
189
|
+
if (flag(options, "quiet"))
|
|
190
|
+
return;
|
|
151
191
|
process.stdout.write(`ok: ${summary.length} provider${summary.length === 1 ? "" : "s"} loaded from ${source}\n`);
|
|
152
192
|
for (const item of summary) {
|
|
153
193
|
process.stdout.write(` ${item.name} (${item.network}) endpoints=${item.endpoints}\n`);
|
|
@@ -155,22 +195,29 @@ function check(options) {
|
|
|
155
195
|
}
|
|
156
196
|
async function start(options) {
|
|
157
197
|
const source = providerPath(options);
|
|
158
|
-
const host = opt(options, "host"
|
|
198
|
+
const host = parseHost(opt(options, "host"));
|
|
159
199
|
const port = parsePort(opt(options, "port"));
|
|
160
|
-
const providers =
|
|
200
|
+
const providers = loadProvidersForCli("start", source);
|
|
161
201
|
const server = createGatewayServer(providers);
|
|
162
202
|
await new Promise((resolve, reject) => {
|
|
163
|
-
server.once("error", reject);
|
|
203
|
+
server.once("error", error => reject(new Error(listenErrorMessage(error, host, port))));
|
|
164
204
|
server.listen(port, host, () => {
|
|
165
205
|
server.off("error", reject);
|
|
166
|
-
printStartup(options, host, port, [...providers.keys()]);
|
|
206
|
+
printStartup(options, source, host, port, [...providers.keys()]);
|
|
167
207
|
resolve();
|
|
168
208
|
});
|
|
169
209
|
});
|
|
170
210
|
const shutdown = (signal) => {
|
|
171
211
|
if (!flag(options, "quiet") && !flag(options, "json"))
|
|
172
212
|
process.stderr.write(`received ${signal}, shutting down...\n`);
|
|
213
|
+
const timeout = setTimeout(() => {
|
|
214
|
+
if (!flag(options, "quiet") && !flag(options, "json"))
|
|
215
|
+
process.stderr.write("server close timed out; exiting\n");
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}, 10_000);
|
|
218
|
+
timeout.unref();
|
|
173
219
|
server.close(() => {
|
|
220
|
+
clearTimeout(timeout);
|
|
174
221
|
if (!flag(options, "quiet") && !flag(options, "json"))
|
|
175
222
|
process.stderr.write("server closed\n");
|
|
176
223
|
process.exit(0);
|
|
@@ -186,7 +233,10 @@ async function main() {
|
|
|
186
233
|
return;
|
|
187
234
|
}
|
|
188
235
|
if (flag(parsed.options, "version")) {
|
|
189
|
-
|
|
236
|
+
const version = readVersion();
|
|
237
|
+
if (!version)
|
|
238
|
+
throw new Error("Unable to read package version.");
|
|
239
|
+
process.stdout.write(`${version}\n`);
|
|
190
240
|
return;
|
|
191
241
|
}
|
|
192
242
|
if (parsed.command === "check")
|
|
@@ -195,22 +245,17 @@ async function main() {
|
|
|
195
245
|
await start(parsed.options);
|
|
196
246
|
}
|
|
197
247
|
main().catch(error => {
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
catch {
|
|
203
|
-
return { options: {} };
|
|
204
|
-
}
|
|
205
|
-
})();
|
|
248
|
+
const argv = process.argv.slice(2);
|
|
249
|
+
const json = rawHasFlag(argv, "json");
|
|
250
|
+
const debug = rawHasFlag(argv, "debug");
|
|
206
251
|
const message = error instanceof Error ? error.message : String(error);
|
|
207
|
-
if (
|
|
252
|
+
if (json) {
|
|
208
253
|
process.stdout.write(JSON.stringify({ ok: false, error: { message } }, null, 2) + "\n");
|
|
209
254
|
}
|
|
210
255
|
else {
|
|
211
256
|
process.stderr.write(`x402-gateway: ${message}\n`);
|
|
212
257
|
process.stderr.write("Run `x402-gateway --help` for usage.\n");
|
|
213
|
-
if (
|
|
258
|
+
if (debug && error instanceof Error && error.stack) {
|
|
214
259
|
process.stderr.write(`${error.stack}\n`);
|
|
215
260
|
}
|
|
216
261
|
}
|
package/dist/gateway/config.js
CHANGED
|
@@ -119,15 +119,31 @@ export function loadProvider(file) {
|
|
|
119
119
|
};
|
|
120
120
|
}
|
|
121
121
|
function normalizePaymentProtocol(config, file) {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
const rawSchemes = config.operator.schemes?.length
|
|
123
|
+
? config.operator.schemes
|
|
124
|
+
: [config.operator.protocol || config.operator.scheme || "exact"];
|
|
125
|
+
const schemes = [...new Set(rawSchemes.map(value => {
|
|
126
|
+
const raw = String(value).toLowerCase();
|
|
127
|
+
const normalized = raw.replace(/[-:\s]/g, "_");
|
|
128
|
+
if (!["exact", "exact_gasfree", "exact_permit", "permit2", "exact_permit2"].includes(normalized)) {
|
|
129
|
+
throw new Error(`${file}: unsupported x402 protocol ${raw}; use exact or exact_gasfree`);
|
|
130
|
+
}
|
|
131
|
+
return normalized === "exact_gasfree" ? "exact_gasfree" : "exact";
|
|
132
|
+
}))];
|
|
133
|
+
if (schemes.includes("exact_gasfree") && !config.operator.network.startsWith("tron:")) {
|
|
134
|
+
throw new Error(`${file}: exact_gasfree is supported only on TRON networks`);
|
|
135
|
+
}
|
|
136
|
+
config.operator.schemes = schemes;
|
|
137
|
+
config.operator.scheme = schemes[0];
|
|
138
|
+
config.operator.protocol = schemes[0];
|
|
139
|
+
if (schemes.includes("exact")) {
|
|
140
|
+
config.operator.asset_transfer_method = "permit2";
|
|
141
|
+
config.operator.assetTransferMethod = "permit2";
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
delete config.operator.asset_transfer_method;
|
|
145
|
+
delete config.operator.assetTransferMethod;
|
|
126
146
|
}
|
|
127
|
-
config.operator.scheme = "exact";
|
|
128
|
-
config.operator.protocol = "exact";
|
|
129
|
-
config.operator.asset_transfer_method = "permit2";
|
|
130
|
-
config.operator.assetTransferMethod = "permit2";
|
|
131
147
|
}
|
|
132
148
|
export function loadProviders(providerPath) {
|
|
133
149
|
const stat = fs.statSync(providerPath);
|
|
@@ -178,20 +194,23 @@ export function paymentRequirements(provider, price) {
|
|
|
178
194
|
const network = normalizeNetwork(provider.operator.network);
|
|
179
195
|
const symbols = provider.operator.currencies?.usd ?? ["USDT"];
|
|
180
196
|
const payTo = provider.recipients?.[provider.operator.recipient]?.account ?? provider.operator.recipient;
|
|
181
|
-
|
|
197
|
+
const schemes = provider.operator.schemes?.length
|
|
198
|
+
? provider.operator.schemes.map(scheme => scheme === "exact_gasfree" ? "exact_gasfree" : "exact")
|
|
199
|
+
: [provider.operator.scheme === "exact_gasfree" ? "exact_gasfree" : "exact"];
|
|
200
|
+
return schemes.flatMap(scheme => symbols.map(symbol => {
|
|
182
201
|
const token = getToken(network, symbol);
|
|
183
202
|
const transferMethod = provider.operator.assetTransferMethod || provider.operator.asset_transfer_method || token.assetTransferMethod;
|
|
184
203
|
const amount = toSmallestUnit(price, token.decimals);
|
|
185
204
|
if (amount === "0")
|
|
186
205
|
throw new Error(`positive price produced zero amount for ${symbol} on ${network}`);
|
|
187
206
|
return {
|
|
188
|
-
scheme
|
|
207
|
+
scheme,
|
|
189
208
|
network,
|
|
190
209
|
amount,
|
|
191
210
|
asset: token.address,
|
|
192
211
|
payTo,
|
|
193
212
|
maxTimeoutSeconds: provider.operator.valid_for_seconds ?? 300,
|
|
194
|
-
extra: transferMethod === "permit2" ? { assetTransferMethod: "permit2" } : {},
|
|
213
|
+
extra: scheme === "exact" && transferMethod === "permit2" ? { assetTransferMethod: "permit2" } : {},
|
|
195
214
|
};
|
|
196
|
-
});
|
|
215
|
+
}));
|
|
197
216
|
}
|
package/dist/gateway/server.js
CHANGED
|
@@ -52,7 +52,6 @@ const metrics = {
|
|
|
52
52
|
paidRequests: 0,
|
|
53
53
|
verifyFailures: 0,
|
|
54
54
|
settleFailures: 0,
|
|
55
|
-
feeQuoteFailures: 0,
|
|
56
55
|
upstreamFailures: 0,
|
|
57
56
|
};
|
|
58
57
|
async function readBody(request) {
|
|
@@ -89,7 +88,7 @@ async function fetchWithTimeout(url, init, timeoutMs, label = "upstream") {
|
|
|
89
88
|
async function facilitatorPost(entry, path, body) {
|
|
90
89
|
const headers = { "content-type": "application/json" };
|
|
91
90
|
if (entry.facilitatorApiKey) {
|
|
92
|
-
headers
|
|
91
|
+
headers["x-api-key"] = entry.facilitatorApiKey;
|
|
93
92
|
}
|
|
94
93
|
const response = await fetchWithTimeout(new URL(path, entry.facilitatorUrl), { method: "POST", headers, body: JSON.stringify(body) }, FACILITATOR_TIMEOUT_MS, "facilitator");
|
|
95
94
|
const text = await response.text();
|
|
@@ -98,31 +97,32 @@ async function facilitatorPost(entry, path, body) {
|
|
|
98
97
|
data = text ? JSON.parse(text) : {};
|
|
99
98
|
}
|
|
100
99
|
catch {
|
|
100
|
+
logFacilitatorFailure(entry, path, response, body, { code: "invalid_json" });
|
|
101
101
|
throw new HttpError(502, "facilitator returned invalid response");
|
|
102
102
|
}
|
|
103
|
-
if (!response.ok)
|
|
104
|
-
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
logFacilitatorFailure(entry, path, response, body, data);
|
|
105
|
+
throw new HttpError(502, "facilitator request failed", `facilitator ${path} failed: ${response.status}`);
|
|
106
|
+
}
|
|
105
107
|
return data;
|
|
106
108
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return requirements;
|
|
125
|
-
}
|
|
109
|
+
function logFacilitatorFailure(entry, path, response, body, data) {
|
|
110
|
+
const requirement = body?.paymentRequirements;
|
|
111
|
+
const nestedError = data?.error && typeof data.error === "object" ? data.error : undefined;
|
|
112
|
+
const message = nestedError?.message ?? data?.message ?? data?.detail ??
|
|
113
|
+
(typeof data?.error === "string" ? data.error : undefined);
|
|
114
|
+
console.error(JSON.stringify({
|
|
115
|
+
event: "facilitator_request_failed",
|
|
116
|
+
provider: entry.config.name,
|
|
117
|
+
endpoint: path,
|
|
118
|
+
status: response.status,
|
|
119
|
+
scheme: requirement?.scheme,
|
|
120
|
+
network: requirement?.network,
|
|
121
|
+
errorCode: nestedError?.code ?? data?.code,
|
|
122
|
+
errorMessage: typeof message === "string" ? message.slice(0, 200) : undefined,
|
|
123
|
+
retryAfter: response.headers.get("retry-after") ?? undefined,
|
|
124
|
+
cfRay: response.headers.get("cf-ray") ?? undefined,
|
|
125
|
+
}));
|
|
126
126
|
}
|
|
127
127
|
function isVerifySuccess(verify) {
|
|
128
128
|
return verify?.valid === true || verify?.isValid === true;
|
|
@@ -265,7 +265,6 @@ export function createGatewayServer(providers) {
|
|
|
265
265
|
`x402_gateway_paid_requests_total ${metrics.paidRequests}`,
|
|
266
266
|
`x402_gateway_verify_failures_total ${metrics.verifyFailures}`,
|
|
267
267
|
`x402_gateway_settle_failures_total ${metrics.settleFailures}`,
|
|
268
|
-
`x402_gateway_fee_quote_failures_total ${metrics.feeQuoteFailures}`,
|
|
269
268
|
`x402_gateway_upstream_failures_total ${metrics.upstreamFailures}`,
|
|
270
269
|
"",
|
|
271
270
|
].join("\n"));
|
|
@@ -295,7 +294,7 @@ export function createGatewayServer(providers) {
|
|
|
295
294
|
}
|
|
296
295
|
const paymentHeader = request.headers[headers.signature.toLowerCase()];
|
|
297
296
|
if (!paymentHeader || Array.isArray(paymentHeader)) {
|
|
298
|
-
const accepts =
|
|
297
|
+
const accepts = requirements;
|
|
299
298
|
const challenge = {
|
|
300
299
|
x402Version: 2,
|
|
301
300
|
error: "Payment required",
|
package/dist/gateway/tokens.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const TOKENS = {
|
|
2
|
-
"tron:
|
|
2
|
+
"tron:0x2b6653dc": {
|
|
3
3
|
USDT: { address: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", decimals: 6, name: "Tether USD", symbol: "USDT", assetTransferMethod: "permit2" },
|
|
4
4
|
USDD: { address: "TXDk8mbtRbXeYuMNS83CfKPaYYT8XWv9Hz", decimals: 18, name: "Decentralized USD", symbol: "USDD", assetTransferMethod: "permit2" },
|
|
5
5
|
},
|
|
6
|
-
"tron:
|
|
6
|
+
"tron:0xcd8690dc": {
|
|
7
7
|
USDT: { address: "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf", decimals: 6, name: "Tether USD", symbol: "USDT", assetTransferMethod: "permit2" },
|
|
8
8
|
USDD: { address: "TGjgvdTWWrybVLaVeFqSyVqJQWjxqRYbaK", decimals: 18, name: "Decentralized USD", symbol: "USDD", assetTransferMethod: "permit2" },
|
|
9
9
|
},
|
|
@@ -17,9 +17,15 @@ export const TOKENS = {
|
|
|
17
17
|
};
|
|
18
18
|
export function normalizeNetwork(network) {
|
|
19
19
|
return ({
|
|
20
|
-
"tron-mainnet": "tron:
|
|
21
|
-
"tron
|
|
22
|
-
"
|
|
20
|
+
"tron-mainnet": "tron:0x2b6653dc",
|
|
21
|
+
"tron:mainnet": "tron:0x2b6653dc",
|
|
22
|
+
"mainnet": "tron:0x2b6653dc",
|
|
23
|
+
"tron-shasta": "tron:0x94a9059e",
|
|
24
|
+
"tron:shasta": "tron:0x94a9059e",
|
|
25
|
+
"shasta": "tron:0x94a9059e",
|
|
26
|
+
"tron-nile": "tron:0xcd8690dc",
|
|
27
|
+
"tron:nile": "tron:0xcd8690dc",
|
|
28
|
+
"nile": "tron:0xcd8690dc",
|
|
23
29
|
"bsc-mainnet": "eip155:56",
|
|
24
30
|
"bsc-testnet": "eip155:97",
|
|
25
31
|
}[network] ?? network);
|
package/dist/tokens.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const TOKENS = {
|
|
2
|
-
"tron:
|
|
2
|
+
"tron:0x2b6653dc": {
|
|
3
3
|
USDT: {
|
|
4
4
|
address: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
|
5
5
|
decimals: 6,
|
|
@@ -17,7 +17,7 @@ export const TOKENS = {
|
|
|
17
17
|
assetTransferMethod: "permit2",
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
|
-
"tron:
|
|
20
|
+
"tron:0xcd8690dc": {
|
|
21
21
|
USDT: {
|
|
22
22
|
address: "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf",
|
|
23
23
|
decimals: 6,
|
|
@@ -35,7 +35,7 @@ export const TOKENS = {
|
|
|
35
35
|
assetTransferMethod: "permit2",
|
|
36
36
|
},
|
|
37
37
|
},
|
|
38
|
-
"tron:
|
|
38
|
+
"tron:0x94a9059e": {
|
|
39
39
|
USDT: {
|
|
40
40
|
address: "TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs",
|
|
41
41
|
decimals: 6,
|
|
@@ -75,9 +75,15 @@ export const TOKENS = {
|
|
|
75
75
|
};
|
|
76
76
|
export function normalizeNetwork(network) {
|
|
77
77
|
return ({
|
|
78
|
-
"tron-mainnet": "tron:
|
|
79
|
-
"tron
|
|
80
|
-
"
|
|
78
|
+
"tron-mainnet": "tron:0x2b6653dc",
|
|
79
|
+
"tron:mainnet": "tron:0x2b6653dc",
|
|
80
|
+
"mainnet": "tron:0x2b6653dc",
|
|
81
|
+
"tron-shasta": "tron:0x94a9059e",
|
|
82
|
+
"tron:shasta": "tron:0x94a9059e",
|
|
83
|
+
"shasta": "tron:0x94a9059e",
|
|
84
|
+
"tron-nile": "tron:0xcd8690dc",
|
|
85
|
+
"tron:nile": "tron:0xcd8690dc",
|
|
86
|
+
"nile": "tron:0xcd8690dc",
|
|
81
87
|
"bsc-mainnet": "eip155:56",
|
|
82
88
|
"bsc-testnet": "eip155:97",
|
|
83
89
|
}[network] ?? network);
|
package/dist/x402.js
CHANGED
|
@@ -2,6 +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 { registerExactGasFreeTronScheme } from "@bankofai/x402-tron/gasfree/client";
|
|
5
6
|
import { createPublicClient, http } from "viem";
|
|
6
7
|
import { privateKeyToAccount } from "viem/accounts";
|
|
7
8
|
import { TronWeb } from "tronweb";
|
|
@@ -33,6 +34,8 @@ export function decodeResponse(value) {
|
|
|
33
34
|
return decodePaymentResponseHeader(value);
|
|
34
35
|
}
|
|
35
36
|
export function ensurePermit2(requirement) {
|
|
37
|
+
if (requirement.scheme !== "exact")
|
|
38
|
+
return requirement;
|
|
36
39
|
const extra = { ...(requirement.extra ?? {}) };
|
|
37
40
|
if (!extra.assetTransferMethod) {
|
|
38
41
|
const token = findTokenByAddress(requirement.network, requirement.asset);
|
|
@@ -40,7 +43,7 @@ export function ensurePermit2(requirement) {
|
|
|
40
43
|
extra.assetTransferMethod = "permit2";
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
|
-
return { ...requirement,
|
|
46
|
+
return { ...requirement, extra };
|
|
44
47
|
}
|
|
45
48
|
export function paymentRequired(requirement, resource, extensions) {
|
|
46
49
|
return {
|
|
@@ -131,6 +134,8 @@ export async function createPaymentPayload(args) {
|
|
|
131
134
|
const selected = ensurePermit2(args.selected);
|
|
132
135
|
const required = paymentRequired(selected, args.resource, args.extensions);
|
|
133
136
|
if (selected.network.startsWith("eip155:")) {
|
|
137
|
+
if (selected.scheme !== "exact")
|
|
138
|
+
throw new Error(`unsupported scheme ${selected.scheme} on ${selected.network}`);
|
|
134
139
|
const privateKey = privateKeyFrom(["EVM_PRIVATE_KEY", "AGENT_WALLET_PRIVATE_KEY", "PRIVATE_KEY"], args.privateKey, ["evm_client", "payer", "default"]);
|
|
135
140
|
const account = privateKeyToAccount(privateKey);
|
|
136
141
|
const rpcUrl = evmRpcUrl(selected.network, args.rpcUrl);
|
|
@@ -150,9 +155,23 @@ export async function createPaymentPayload(args) {
|
|
|
150
155
|
apiKey: args.apiKey || process.env.TRON_GRID_API_KEY,
|
|
151
156
|
allowanceMode: args.allowanceMode || process.env.X402_TRON_ALLOWANCE_MODE || "auto",
|
|
152
157
|
});
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
158
|
+
const client = new x402Client();
|
|
159
|
+
if (selected.scheme === "exact_gasfree") {
|
|
160
|
+
registerExactGasFreeTronScheme(client, {
|
|
161
|
+
signer,
|
|
162
|
+
networks: [selected.network],
|
|
163
|
+
...(args.gasfreeApiUrl || process.env.X402_GASFREE_API_URL
|
|
164
|
+
? { schemeOptions: { apiBaseUrls: { [selected.network]: args.gasfreeApiUrl || process.env.X402_GASFREE_API_URL } } }
|
|
165
|
+
: {}),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
else if (selected.scheme === "exact") {
|
|
169
|
+
client.register(selected.network, new ExactTronScheme(signer));
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
throw new Error(`unsupported scheme ${selected.scheme} on ${selected.network}`);
|
|
173
|
+
}
|
|
174
|
+
return client.createPaymentPayload(required);
|
|
156
175
|
}
|
|
157
176
|
throw new Error(`unsupported network ${selected.network}`);
|
|
158
177
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bankofai/x402-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1-beta.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
"node": ">=20"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@bankofai/x402-core": "1.0.
|
|
24
|
-
"@bankofai/x402-evm": "1.0.
|
|
25
|
-
"@bankofai/x402-gateway": "
|
|
26
|
-
"@bankofai/x402-tron": "1.0.
|
|
23
|
+
"@bankofai/x402-core": "1.0.1-beta.4",
|
|
24
|
+
"@bankofai/x402-evm": "1.0.1-beta.4",
|
|
25
|
+
"@bankofai/x402-gateway": "1.0.1-beta.1",
|
|
26
|
+
"@bankofai/x402-tron": "1.0.1-beta.4",
|
|
27
27
|
"tronweb": "6.4.0",
|
|
28
28
|
"viem": "^2.55.0",
|
|
29
29
|
"yaml": "^2.8.2"
|