@bankofai/x402-cli 1.0.1-beta.6 → 1.0.1-beta.8
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 +8 -8
- package/dist/cli.js +112 -22
- package/dist/gateway/config.js +48 -3
- package/dist/gateway/server.js +95 -16
- package/dist/tokens.js +2 -2
- package/dist/x402.js +8 -2
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
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.1
|
|
7
|
-
- `@bankofai/x402-evm@1.0.1
|
|
8
|
-
- `@bankofai/x402-tron@1.0.1
|
|
6
|
+
- `@bankofai/x402-core@1.0.1`
|
|
7
|
+
- `@bankofai/x402-evm@1.0.1`
|
|
8
|
+
- `@bankofai/x402-tron@1.0.1`
|
|
9
9
|
|
|
10
10
|
Stablecoin payments support `scheme=exact` and TRON `scheme=exact_gasfree`.
|
|
11
11
|
The GasFree flow lets the relayer pay network energy while deducting its fee
|
|
@@ -16,7 +16,7 @@ from the payment token, so the payer does not need TRX.
|
|
|
16
16
|
Install the CLI package:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
npm install -g @bankofai/x402-cli
|
|
19
|
+
npm install -g @bankofai/x402-cli@beta
|
|
20
20
|
x402-cli --version
|
|
21
21
|
```
|
|
22
22
|
|
|
@@ -58,7 +58,7 @@ envelope with `ok`, `command`, `result`, or structured `error` fields.
|
|
|
58
58
|
Start a local x402 paywall endpoint:
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
|
-
|
|
61
|
+
x402-cli serve \
|
|
62
62
|
--pay-to <recipient> \
|
|
63
63
|
--amount 0.0001 \
|
|
64
64
|
--network tron:0xcd8690dc \
|
|
@@ -79,7 +79,7 @@ Pay an x402-protected URL:
|
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
81
|
TRON_PRIVATE_KEY=<hex> \
|
|
82
|
-
|
|
82
|
+
x402-cli pay http://127.0.0.1:4020/pay \
|
|
83
83
|
--network tron:0xcd8690dc \
|
|
84
84
|
--token USDT
|
|
85
85
|
```
|
|
@@ -92,7 +92,7 @@ the server challenge):
|
|
|
92
92
|
|
|
93
93
|
```bash
|
|
94
94
|
TRON_PRIVATE_KEY=<hex> \
|
|
95
|
-
|
|
95
|
+
x402-cli pay https://api.example.com/pay \
|
|
96
96
|
--network tron:0xcd8690dc \
|
|
97
97
|
--token USDT \
|
|
98
98
|
--scheme exact_gasfree
|
|
@@ -133,7 +133,7 @@ Start a temporary local server and immediately pay it:
|
|
|
133
133
|
|
|
134
134
|
```bash
|
|
135
135
|
TRON_PRIVATE_KEY=<hex> \
|
|
136
|
-
|
|
136
|
+
x402-cli roundtrip \
|
|
137
137
|
--pay-to <recipient> \
|
|
138
138
|
--amount 0.0001 \
|
|
139
139
|
--network tron:0xcd8690dc \
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import http from "node:http";
|
|
3
3
|
import { setTimeout as delay } from "node:timers/promises";
|
|
4
|
+
import net from "node:net";
|
|
4
5
|
import fs from "node:fs";
|
|
5
6
|
import os from "node:os";
|
|
6
7
|
import path from "node:path";
|
|
@@ -11,6 +12,7 @@ import YAML from "yaml";
|
|
|
11
12
|
import { createPaymentPayload, decodeRequired, decodeResponse, decodeSignature, encodeRequired, encodeResponse, encodeSignature, headers } from "./x402.js";
|
|
12
13
|
import { assertRawAmount, findTokenByAddress, getToken, normalizeNetwork, toSmallestUnit } from "./tokens.js";
|
|
13
14
|
const BOOLEAN_FLAGS = new Set(["daemon", "dry-run", "force", "help", "human", "include-blocked", "json", "raw", "version"]);
|
|
15
|
+
const MAX_HTTP_BODY_BYTES = 10 * 1024 * 1024;
|
|
14
16
|
const require = createRequire(import.meta.url);
|
|
15
17
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
16
18
|
const CATALOG_UPDATE_RETRIES = 3;
|
|
@@ -71,7 +73,7 @@ function parseArgs(argv) {
|
|
|
71
73
|
options[key] = true;
|
|
72
74
|
}
|
|
73
75
|
else if (!next || next.startsWith("--")) {
|
|
74
|
-
|
|
76
|
+
throw new CliError("MISSING_ARGUMENT", `--${key} requires a value`, `Pass --${key} <value>.`, 2);
|
|
75
77
|
}
|
|
76
78
|
else {
|
|
77
79
|
if (key === "header") {
|
|
@@ -653,7 +655,36 @@ async function readText(source, options) {
|
|
|
653
655
|
const response = await fetchWithTimeout(source, {}, timeoutMs(options), `fetch ${source}`);
|
|
654
656
|
if (!response.ok)
|
|
655
657
|
throw new Error(`failed to fetch ${source}: ${response.status}`);
|
|
656
|
-
return response
|
|
658
|
+
return readBoundedText(response, `response from ${source}`);
|
|
659
|
+
}
|
|
660
|
+
async function readBoundedText(response, label) {
|
|
661
|
+
const declared = Number(response.headers.get("content-length"));
|
|
662
|
+
if (Number.isFinite(declared) && declared > MAX_HTTP_BODY_BYTES) {
|
|
663
|
+
throw new Error(`${label} exceeds ${MAX_HTTP_BODY_BYTES} byte limit`);
|
|
664
|
+
}
|
|
665
|
+
if (!response.body)
|
|
666
|
+
return "";
|
|
667
|
+
const reader = response.body.getReader();
|
|
668
|
+
const chunks = [];
|
|
669
|
+
let size = 0;
|
|
670
|
+
for (;;) {
|
|
671
|
+
const { done, value } = await reader.read();
|
|
672
|
+
if (done)
|
|
673
|
+
break;
|
|
674
|
+
size += value.byteLength;
|
|
675
|
+
if (size > MAX_HTTP_BODY_BYTES) {
|
|
676
|
+
await reader.cancel();
|
|
677
|
+
throw new Error(`${label} exceeds ${MAX_HTTP_BODY_BYTES} byte limit`);
|
|
678
|
+
}
|
|
679
|
+
chunks.push(value);
|
|
680
|
+
}
|
|
681
|
+
const bytes = new Uint8Array(size);
|
|
682
|
+
let offset = 0;
|
|
683
|
+
for (const chunk of chunks) {
|
|
684
|
+
bytes.set(chunk, offset);
|
|
685
|
+
offset += chunk.byteLength;
|
|
686
|
+
}
|
|
687
|
+
return new TextDecoder().decode(bytes);
|
|
657
688
|
}
|
|
658
689
|
async function readJson(source, options) {
|
|
659
690
|
return JSON.parse(await readText(source, options));
|
|
@@ -663,7 +694,7 @@ function writeJson(file, value) {
|
|
|
663
694
|
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
664
695
|
}
|
|
665
696
|
async function responsePayload(response) {
|
|
666
|
-
const text = await response
|
|
697
|
+
const text = await readBoundedText(response, "HTTP response");
|
|
667
698
|
const contentType = response.headers.get("content-type") ?? "";
|
|
668
699
|
if (contentType.toLowerCase().includes("json")) {
|
|
669
700
|
try {
|
|
@@ -696,7 +727,7 @@ function cachedCatalogPath() {
|
|
|
696
727
|
return path.join(cacheDir(), "catalog.json");
|
|
697
728
|
}
|
|
698
729
|
function providerFilename(fqn) {
|
|
699
|
-
return `${fqn.replace(/\//g, "__")}.json`;
|
|
730
|
+
return `${sanitizeProviderName(fqn).replace(/\//g, "__")}.json`;
|
|
700
731
|
}
|
|
701
732
|
function sanitizeProviderName(name) {
|
|
702
733
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,127}$/.test(name) || name.includes("..")) {
|
|
@@ -758,8 +789,11 @@ function remoteBaseFromCatalogPayload(payload) {
|
|
|
758
789
|
}
|
|
759
790
|
async function remoteBaseFromSource(source, payload, options) {
|
|
760
791
|
const fromPayload = payload ? remoteBaseFromCatalogPayload(payload) : undefined;
|
|
761
|
-
if (fromPayload)
|
|
792
|
+
if (fromPayload) {
|
|
793
|
+
if (/^https?:\/\//.test(source) && new URL(fromPayload).origin !== new URL(source).origin)
|
|
794
|
+
throw new Error("catalog base_url must use the same origin as the catalog source");
|
|
762
795
|
return fromPayload;
|
|
796
|
+
}
|
|
763
797
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
764
798
|
return `${source.slice(0, source.lastIndexOf("/") + 1)}`;
|
|
765
799
|
}
|
|
@@ -771,6 +805,7 @@ async function remoteBaseFromSource(source, payload, options) {
|
|
|
771
805
|
}
|
|
772
806
|
}
|
|
773
807
|
function catalogDetailSource(source, section, name) {
|
|
808
|
+
name = sanitizeProviderName(name);
|
|
774
809
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
775
810
|
const base = new URL(source);
|
|
776
811
|
const pathname = base.pathname.endsWith("/catalog.json")
|
|
@@ -994,8 +1029,8 @@ function buildRequirement(options) {
|
|
|
994
1029
|
if (!explicitAsset && !registryToken)
|
|
995
1030
|
throw new Error(`unknown token ${tokenSymbol} on ${network}`);
|
|
996
1031
|
const decimals = decimalsOption !== undefined ? Number(decimalsOption) : registryToken.decimals;
|
|
997
|
-
if (!Number.isInteger(decimals) || decimals < 0)
|
|
998
|
-
throw new Error("--decimals must be
|
|
1032
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 255)
|
|
1033
|
+
throw new Error("--decimals must be an integer between 0 and 255");
|
|
999
1034
|
const rawAmount = opt(options, "rawAmount") ?? opt(options, "raw-amount");
|
|
1000
1035
|
const humanAmount = opt(options, "amount");
|
|
1001
1036
|
if (rawAmount && humanAmount)
|
|
@@ -1003,13 +1038,17 @@ function buildRequirement(options) {
|
|
|
1003
1038
|
const amount = rawAmount ? assertRawAmount(rawAmount, "--raw-amount") : toSmallestUnit(humanAmount ?? "0.0001", decimals);
|
|
1004
1039
|
const assetAddress = explicitAsset ?? registryToken.address;
|
|
1005
1040
|
const assetTransferMethod = registryToken?.assetTransferMethod ?? "permit2";
|
|
1041
|
+
const maxTimeoutSeconds = Number(opt(options, "valid-for-seconds", "300"));
|
|
1042
|
+
if (!Number.isInteger(maxTimeoutSeconds) || maxTimeoutSeconds <= 0 || maxTimeoutSeconds > 86400) {
|
|
1043
|
+
throw new Error("--valid-for-seconds must be an integer between 1 and 86400");
|
|
1044
|
+
}
|
|
1006
1045
|
return {
|
|
1007
1046
|
scheme,
|
|
1008
1047
|
network,
|
|
1009
1048
|
amount,
|
|
1010
1049
|
asset: assetAddress,
|
|
1011
1050
|
payTo: opt(options, "pay-to") ?? opt(options, "payTo") ?? "",
|
|
1012
|
-
maxTimeoutSeconds
|
|
1051
|
+
maxTimeoutSeconds,
|
|
1013
1052
|
extra: scheme === "exact" && assetTransferMethod ? { assetTransferMethod } : {},
|
|
1014
1053
|
};
|
|
1015
1054
|
}
|
|
@@ -1019,10 +1058,16 @@ async function facilitatorPost(baseUrl, path, body, options) {
|
|
|
1019
1058
|
headers: { "content-type": "application/json" },
|
|
1020
1059
|
body: JSON.stringify(body),
|
|
1021
1060
|
}, timeoutMs(options), `facilitator ${path}`);
|
|
1022
|
-
const text = await response
|
|
1023
|
-
|
|
1061
|
+
const text = await readBoundedText(response, `facilitator ${path} response`);
|
|
1062
|
+
let data = {};
|
|
1063
|
+
try {
|
|
1064
|
+
data = text ? JSON.parse(text) : {};
|
|
1065
|
+
}
|
|
1066
|
+
catch {
|
|
1067
|
+
throw new Error(`facilitator ${path} returned invalid JSON`);
|
|
1068
|
+
}
|
|
1024
1069
|
if (!response.ok)
|
|
1025
|
-
throw new Error(`facilitator ${path} failed
|
|
1070
|
+
throw new Error(`facilitator ${path} failed with HTTP ${response.status}`);
|
|
1026
1071
|
return data;
|
|
1027
1072
|
}
|
|
1028
1073
|
function requestHeaders(options) {
|
|
@@ -1038,7 +1083,23 @@ function requestHeaders(options) {
|
|
|
1038
1083
|
function stripFlag(argv, flag) {
|
|
1039
1084
|
return argv.filter(item => item !== flag);
|
|
1040
1085
|
}
|
|
1041
|
-
function
|
|
1086
|
+
async function waitForPort(host, port, timeout = 5_000) {
|
|
1087
|
+
const deadline = Date.now() + timeout;
|
|
1088
|
+
while (Date.now() < deadline) {
|
|
1089
|
+
const connected = await new Promise(resolve => {
|
|
1090
|
+
const socket = net.createConnection({ host, port });
|
|
1091
|
+
socket.setTimeout(250);
|
|
1092
|
+
socket.once("connect", () => { socket.destroy(); resolve(true); });
|
|
1093
|
+
socket.once("error", () => resolve(false));
|
|
1094
|
+
socket.once("timeout", () => { socket.destroy(); resolve(false); });
|
|
1095
|
+
});
|
|
1096
|
+
if (connected)
|
|
1097
|
+
return;
|
|
1098
|
+
await delay(50);
|
|
1099
|
+
}
|
|
1100
|
+
throw new Error(`daemon did not become ready on ${host}:${port}`);
|
|
1101
|
+
}
|
|
1102
|
+
async function serveDaemon(argv, options) {
|
|
1042
1103
|
const requirement = buildRequirement(options);
|
|
1043
1104
|
if (!requirement.payTo)
|
|
1044
1105
|
throw new Error("--pay-to is required");
|
|
@@ -1053,6 +1114,18 @@ function serveDaemon(argv, options) {
|
|
|
1053
1114
|
const host = opt(options, "host", "127.0.0.1");
|
|
1054
1115
|
const port = Number(opt(options, "port", "4020"));
|
|
1055
1116
|
const resourceUrl = opt(options, "resource-url", `http://${host}:${port}/pay`);
|
|
1117
|
+
try {
|
|
1118
|
+
await waitForPort(host === "0.0.0.0" ? "127.0.0.1" : host === "::" ? "::1" : host, port);
|
|
1119
|
+
}
|
|
1120
|
+
catch (error) {
|
|
1121
|
+
if (child.pid) {
|
|
1122
|
+
try {
|
|
1123
|
+
process.kill(child.pid);
|
|
1124
|
+
}
|
|
1125
|
+
catch { /* child already exited */ }
|
|
1126
|
+
}
|
|
1127
|
+
throw error;
|
|
1128
|
+
}
|
|
1056
1129
|
emit({
|
|
1057
1130
|
command: "server",
|
|
1058
1131
|
mode: outputMode(options),
|
|
@@ -1079,8 +1152,8 @@ function validateAmountLimits(selected, options) {
|
|
|
1079
1152
|
throw new Error("cannot evaluate --max-amount for an unknown asset; pass --max-raw-amount or --decimals");
|
|
1080
1153
|
}
|
|
1081
1154
|
const decimals = decimalsOption !== undefined ? Number(decimalsOption) : token.decimals;
|
|
1082
|
-
if (!Number.isInteger(decimals) || decimals < 0)
|
|
1083
|
-
throw new Error("--decimals must be
|
|
1155
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 255)
|
|
1156
|
+
throw new Error("--decimals must be an integer between 0 and 255");
|
|
1084
1157
|
if (BigInt(selected.amount) > BigInt(toSmallestUnit(maxAmount, decimals))) {
|
|
1085
1158
|
throw new Error(`payment amount exceeds --max-amount ${maxAmount}`);
|
|
1086
1159
|
}
|
|
@@ -1107,8 +1180,8 @@ function gasfreeFeeLimitRaw(selected, options) {
|
|
|
1107
1180
|
throw new Error("cannot evaluate --max-gasfree-fee for an unknown asset; pass --max-gasfree-fee-raw or --decimals");
|
|
1108
1181
|
}
|
|
1109
1182
|
const decimals = decimalsOption !== undefined ? Number(decimalsOption) : token.decimals;
|
|
1110
|
-
if (!Number.isInteger(decimals) || decimals < 0)
|
|
1111
|
-
throw new Error("--decimals must be
|
|
1183
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 255)
|
|
1184
|
+
throw new Error("--decimals must be an integer between 0 and 255");
|
|
1112
1185
|
return toSmallestUnit(maxHuman, decimals);
|
|
1113
1186
|
}
|
|
1114
1187
|
async function serve(options) {
|
|
@@ -1173,7 +1246,7 @@ async function serve(options) {
|
|
|
1173
1246
|
paymentPayload: payload,
|
|
1174
1247
|
paymentRequirements: requirement,
|
|
1175
1248
|
}, options);
|
|
1176
|
-
if (!(settle?.success === true
|
|
1249
|
+
if (!(settle?.success === true && typeof settle?.transaction === "string" && settle.transaction.length > 0 && typeof settle?.network === "string" && settle.network.length > 0)) {
|
|
1177
1250
|
response.writeHead(502, { "content-type": "application/json" });
|
|
1178
1251
|
response.end(JSON.stringify({ error: "settlement failed" }));
|
|
1179
1252
|
return;
|
|
@@ -1182,7 +1255,7 @@ async function serve(options) {
|
|
|
1182
1255
|
"content-type": "application/json",
|
|
1183
1256
|
[headers.response]: encodeResponse(settle),
|
|
1184
1257
|
});
|
|
1185
|
-
response.end(JSON.stringify({ success: true, network: requirement.network, scheme: requirement.scheme, transaction: settle.transaction
|
|
1258
|
+
response.end(JSON.stringify({ success: true, network: requirement.network, scheme: requirement.scheme, transaction: settle.transaction }));
|
|
1186
1259
|
}
|
|
1187
1260
|
catch (error) {
|
|
1188
1261
|
response.writeHead(500, { "content-type": "application/json" });
|
|
@@ -1209,6 +1282,17 @@ function selectRequirement(accepts, options) {
|
|
|
1209
1282
|
const scheme = opt(options, "scheme");
|
|
1210
1283
|
const token = opt(options, "token");
|
|
1211
1284
|
const selected = accepts.find(req => {
|
|
1285
|
+
if (!req || !["exact", "exact_gasfree"].includes(req.scheme) || typeof req.network !== "string" || typeof req.asset !== "string")
|
|
1286
|
+
return false;
|
|
1287
|
+
if (req.scheme === "exact_gasfree" && !req.network.startsWith("tron:"))
|
|
1288
|
+
return false;
|
|
1289
|
+
try {
|
|
1290
|
+
if (!findTokenByAddress(req.network, req.asset))
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
catch {
|
|
1294
|
+
return false;
|
|
1295
|
+
}
|
|
1212
1296
|
if (network && normalizeNetwork(network) !== req.network)
|
|
1213
1297
|
return false;
|
|
1214
1298
|
if (scheme && scheme !== req.scheme)
|
|
@@ -1233,6 +1317,9 @@ function selectRequirement(accepts, options) {
|
|
|
1233
1317
|
async function pay(url, options) {
|
|
1234
1318
|
requireArgument(url, "URL", "x402-cli pay <url> [options]");
|
|
1235
1319
|
const method = opt(options, "method", "GET");
|
|
1320
|
+
if (!/^[A-Z]+$/.test(method) || !["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"].includes(method)) {
|
|
1321
|
+
throw new CliError("INVALID_ARGUMENT", `unsupported HTTP method ${method}`, "Use an uppercase standard HTTP method.", 2);
|
|
1322
|
+
}
|
|
1236
1323
|
const baseHeaders = requestHeaders(options);
|
|
1237
1324
|
const probe = await fetchWithTimeout(url, {
|
|
1238
1325
|
method,
|
|
@@ -1298,7 +1385,7 @@ async function pay(url, options) {
|
|
|
1298
1385
|
const body = await responsePayload(paid);
|
|
1299
1386
|
const paymentResponse = paid.headers.get(headers.response);
|
|
1300
1387
|
const settlement = paymentResponse ? decodeResponse(paymentResponse) : undefined;
|
|
1301
|
-
const settled = settlement
|
|
1388
|
+
const settled = settlement?.success === true && typeof settlement?.transaction === "string" && settlement.transaction.length > 0 && typeof settlement?.network === "string" && settlement.network.length > 0;
|
|
1302
1389
|
const result = {
|
|
1303
1390
|
url,
|
|
1304
1391
|
status: paid.status,
|
|
@@ -1312,6 +1399,9 @@ async function pay(url, options) {
|
|
|
1312
1399
|
} : {}),
|
|
1313
1400
|
...(creation.gasfreeEstimate ? { gasfreeEstimate: creation.gasfreeEstimate } : {}),
|
|
1314
1401
|
};
|
|
1402
|
+
if (paymentResponse && !settled) {
|
|
1403
|
+
throw new CliError("INVALID_SETTLEMENT", "gateway returned an invalid or unsuccessful PAYMENT-RESPONSE", "Do not treat this request as paid; contact the gateway operator.", 1, result);
|
|
1404
|
+
}
|
|
1315
1405
|
if (!paid.ok) {
|
|
1316
1406
|
const retryAfter = paid.headers.get("retry-after");
|
|
1317
1407
|
throw new CliError(paid.status === 429 ? "RATE_LIMITED" : "HTTP_ERROR", `HTTP ${paid.status} from ${url}${retryAfter ? ` (retry after ${retryAfter}s)` : ""}: ${typeof body === "string" ? body.slice(0, 500) : JSON.stringify(body).slice(0, 500)}`, paymentResponse
|
|
@@ -1664,7 +1754,7 @@ async function catalogPayJson(source, name, options) {
|
|
|
1664
1754
|
}
|
|
1665
1755
|
async function handleGateway(args) {
|
|
1666
1756
|
const { command, positional, options } = parseArgs(args);
|
|
1667
|
-
if (hasFlag(options, "help") ||
|
|
1757
|
+
if (hasFlag(options, "help") || ["help", "--help", "-h"].includes(command)) {
|
|
1668
1758
|
process.stdout.write(helpText(command === "catalog" || positional[0] === "catalog" ? "gateway-catalog" : "gateway"));
|
|
1669
1759
|
return;
|
|
1670
1760
|
}
|
|
@@ -1697,7 +1787,7 @@ async function handleGatewayCatalog(positional, options) {
|
|
|
1697
1787
|
}
|
|
1698
1788
|
async function handleCatalog(args) {
|
|
1699
1789
|
const { command, positional, options } = parseArgs(args);
|
|
1700
|
-
if (hasFlag(options, "help") ||
|
|
1790
|
+
if (hasFlag(options, "help") || ["help", "--help", "-h"].includes(command)) {
|
|
1701
1791
|
const topic = command === "help" ? positional[0] : command;
|
|
1702
1792
|
process.stdout.write(helpText(topic ? `catalog-${topic}` : "catalog"));
|
|
1703
1793
|
return;
|
|
@@ -1742,7 +1832,7 @@ async function main() {
|
|
|
1742
1832
|
}
|
|
1743
1833
|
if (command === "serve") {
|
|
1744
1834
|
if (hasFlag(options, "daemon"))
|
|
1745
|
-
serveDaemon(argv, options);
|
|
1835
|
+
await serveDaemon(argv, options);
|
|
1746
1836
|
else
|
|
1747
1837
|
await serve(options);
|
|
1748
1838
|
}
|
package/dist/gateway/config.js
CHANGED
|
@@ -30,6 +30,11 @@ function assertHttpUrl(value, name) {
|
|
|
30
30
|
const url = new URL(value);
|
|
31
31
|
if (!["http:", "https:"].includes(url.protocol))
|
|
32
32
|
throw new Error("unsupported protocol");
|
|
33
|
+
if (url.username || url.password || url.search || url.hash)
|
|
34
|
+
throw new Error("credentials, query, and fragment are not allowed");
|
|
35
|
+
if (url.protocol === "http:" && !["localhost", "127.0.0.1", "::1"].includes(url.hostname) && process.env.X402_GATEWAY_ALLOW_INSECURE_HTTP !== "true") {
|
|
36
|
+
throw new Error("remote HTTP is not allowed");
|
|
37
|
+
}
|
|
33
38
|
}
|
|
34
39
|
catch {
|
|
35
40
|
throw new Error(`${name} must be a valid http(s) URL`);
|
|
@@ -80,6 +85,13 @@ function validateProvider(config, file) {
|
|
|
80
85
|
if (authMethod && !["header", "query_param", "access_token", "oauth2"].includes(authMethod)) {
|
|
81
86
|
throw new Error(`${file}: unsupported routing.auth.method ${authMethod}`);
|
|
82
87
|
}
|
|
88
|
+
const auth = config.routing?.auth;
|
|
89
|
+
if (auth) {
|
|
90
|
+
if (!auth.value && !auth.value_from_env)
|
|
91
|
+
throw new Error(`${file}: routing.auth requires value or value_from_env`);
|
|
92
|
+
if (auth.value_from_env && !process.env[auth.value_from_env])
|
|
93
|
+
throw new Error(`${file}: environment variable ${auth.value_from_env} is not set`);
|
|
94
|
+
}
|
|
83
95
|
if (!config.endpoints?.length)
|
|
84
96
|
throw new Error(`${file}: endpoints must contain at least one endpoint`);
|
|
85
97
|
const seen = new Set();
|
|
@@ -104,6 +116,7 @@ export function loadProvider(file) {
|
|
|
104
116
|
validateProvider(config, file);
|
|
105
117
|
config.operator.network = normalizeNetwork(config.operator.network);
|
|
106
118
|
normalizePaymentProtocol(config, file);
|
|
119
|
+
validatePaymentCapabilities(config, file);
|
|
107
120
|
const facilitatorUrl = config.operator.facilitator_url ||
|
|
108
121
|
process.env.X402_FACILITATOR_URL ||
|
|
109
122
|
process.env.FACILITATOR_URL ||
|
|
@@ -125,10 +138,13 @@ export function loadProvider(file) {
|
|
|
125
138
|
};
|
|
126
139
|
}
|
|
127
140
|
function normalizePaymentProtocol(config, file) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
141
|
+
if (config.operator.schemes !== undefined && (!Array.isArray(config.operator.schemes) || !config.operator.schemes.length)) {
|
|
142
|
+
throw new Error(`${file}: operator.schemes must be a non-empty string array`);
|
|
143
|
+
}
|
|
144
|
+
const rawSchemes = config.operator.schemes ?? [config.operator.protocol || config.operator.scheme || "exact"];
|
|
131
145
|
const schemes = [...new Set(rawSchemes.map(value => {
|
|
146
|
+
if (typeof value !== "string" || !value.trim())
|
|
147
|
+
throw new Error(`${file}: operator.schemes must contain non-empty strings`);
|
|
132
148
|
const raw = String(value).toLowerCase();
|
|
133
149
|
const normalized = raw.replace(/[-:\s]/g, "_");
|
|
134
150
|
if (!["exact", "exact_gasfree", "exact_permit", "permit2", "exact_permit2"].includes(normalized)) {
|
|
@@ -151,6 +167,33 @@ function normalizePaymentProtocol(config, file) {
|
|
|
151
167
|
delete config.operator.assetTransferMethod;
|
|
152
168
|
}
|
|
153
169
|
}
|
|
170
|
+
function validatePaymentCapabilities(config, file) {
|
|
171
|
+
const symbols = config.operator.currencies?.usd ?? ["USDT"];
|
|
172
|
+
if (!Array.isArray(symbols) || !symbols.length || symbols.some(symbol => typeof symbol !== "string" || !symbol.trim())) {
|
|
173
|
+
throw new Error(`${file}: operator.currencies.usd must be a non-empty string array`);
|
|
174
|
+
}
|
|
175
|
+
if (new Set(symbols.map(symbol => symbol.toUpperCase())).size !== symbols.length)
|
|
176
|
+
throw new Error(`${file}: operator.currencies.usd must not contain duplicates`);
|
|
177
|
+
for (const symbol of symbols)
|
|
178
|
+
getToken(config.operator.network, symbol);
|
|
179
|
+
const recipient = config.recipients?.[config.operator.recipient]?.account ?? config.operator.recipient;
|
|
180
|
+
assertString(recipient, `${file}: resolved recipient`);
|
|
181
|
+
const validRecipient = config.operator.network.startsWith("tron:")
|
|
182
|
+
? /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(recipient)
|
|
183
|
+
: /^0x[0-9a-fA-F]{40}$/.test(recipient);
|
|
184
|
+
if (!validRecipient)
|
|
185
|
+
throw new Error(`${file}: operator.recipient must be a valid address or a resolvable recipient alias`);
|
|
186
|
+
for (const endpoint of config.endpoints ?? []) {
|
|
187
|
+
if (!endpoint.metering)
|
|
188
|
+
continue;
|
|
189
|
+
const prices = [endpoint.metering.dimensions?.[0]?.tiers?.[0]?.price_usd,
|
|
190
|
+
...(endpoint.metering.variants ?? []).map(variant => variant.dimensions?.[0]?.tiers?.[0]?.price_usd)]
|
|
191
|
+
.filter((price) => typeof price === "number" && price > 0);
|
|
192
|
+
for (const price of prices)
|
|
193
|
+
if (!paymentRequirements(config, price).length)
|
|
194
|
+
throw new Error(`${file}: paid endpoint cannot generate payment requirements`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
154
197
|
export function loadProviders(providerPath) {
|
|
155
198
|
const stat = fs.statSync(providerPath);
|
|
156
199
|
const files = stat.isDirectory()
|
|
@@ -174,6 +217,8 @@ export function endpointFor(provider, method, routePath) {
|
|
|
174
217
|
return provider.endpoints?.find(endpoint => endpoint.method.toUpperCase() === method.toUpperCase() && pathMatches(endpoint.path, routePath));
|
|
175
218
|
}
|
|
176
219
|
function pathMatches(template, routePath) {
|
|
220
|
+
if (!routePath.startsWith("/") || routePath.startsWith("//") || routePath.includes("\\") || routePath.includes("\0") || /%(?:2f|5c)/i.test(routePath))
|
|
221
|
+
return false;
|
|
177
222
|
const templateParts = template.split("/").filter(Boolean);
|
|
178
223
|
const routeParts = routePath.split("/").filter(Boolean);
|
|
179
224
|
if (templateParts.length !== routeParts.length)
|
package/dist/gateway/server.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
3
|
import { URL } from "node:url";
|
|
3
4
|
import { endpointFor, paymentRequirements, priceUsd } from "./config.js";
|
|
4
5
|
import { decodeSignature, encodeRequired, encodeResponse, headers, matchRequirement } from "./x402.js";
|
|
@@ -25,13 +26,16 @@ function positiveIntegerEnv(name, fallback) {
|
|
|
25
26
|
if (!/^\d+$/.test(raw))
|
|
26
27
|
throw new Error(`${name} must be a positive integer`);
|
|
27
28
|
const value = Number(raw);
|
|
28
|
-
if (!Number.isSafeInteger(value) || value <= 0)
|
|
29
|
-
throw new Error(`${name} must be
|
|
29
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > 2_147_483_647)
|
|
30
|
+
throw new Error(`${name} must be an integer between 1 and 2147483647`);
|
|
30
31
|
return value;
|
|
31
32
|
}
|
|
32
33
|
const MAX_BODY_BYTES = positiveIntegerEnv("X402_GATEWAY_MAX_BODY_BYTES", 1_000_000);
|
|
33
34
|
const FACILITATOR_TIMEOUT_MS = positiveIntegerEnv("X402_GATEWAY_FACILITATOR_TIMEOUT_MS", 10_000);
|
|
34
35
|
const UPSTREAM_TIMEOUT_MS = positiveIntegerEnv("X402_GATEWAY_UPSTREAM_TIMEOUT_MS", 30_000);
|
|
36
|
+
const MAX_RESPONSE_BYTES = positiveIntegerEnv("X402_GATEWAY_MAX_RESPONSE_BYTES", 10_000_000);
|
|
37
|
+
const MAX_CONCURRENT_REQUESTS = positiveIntegerEnv("X402_GATEWAY_MAX_CONCURRENT_REQUESTS", 100);
|
|
38
|
+
const RATE_LIMIT_PER_MINUTE = positiveIntegerEnv("X402_GATEWAY_RATE_LIMIT_PER_MINUTE", 300);
|
|
35
39
|
const STRIP_REQUEST_HEADERS = new Set([
|
|
36
40
|
"host",
|
|
37
41
|
"connection",
|
|
@@ -49,6 +53,8 @@ const STRIP_REQUEST_HEADERS = new Set([
|
|
|
49
53
|
"payment-signature",
|
|
50
54
|
"payment-required",
|
|
51
55
|
"x-payment-required",
|
|
56
|
+
"payment-response",
|
|
57
|
+
"x-payment-response",
|
|
52
58
|
"accept-encoding",
|
|
53
59
|
]);
|
|
54
60
|
const STRIP_RESPONSE_HEADERS = new Set([
|
|
@@ -73,6 +79,7 @@ function createMetrics() {
|
|
|
73
79
|
verifyFailures: 0,
|
|
74
80
|
settleFailures: 0,
|
|
75
81
|
upstreamFailures: 0,
|
|
82
|
+
rejectedRequests: 0,
|
|
76
83
|
};
|
|
77
84
|
}
|
|
78
85
|
async function readBody(request) {
|
|
@@ -88,14 +95,17 @@ async function readBody(request) {
|
|
|
88
95
|
return Buffer.concat(chunks);
|
|
89
96
|
}
|
|
90
97
|
function json(response, status, body, extraHeaders = {}) {
|
|
91
|
-
response.writeHead(status, { "content-type": "application/json", ...extraHeaders });
|
|
98
|
+
response.writeHead(status, { "content-type": "application/json", "cache-control": "no-store", "x-content-type-options": "nosniff", ...extraHeaders });
|
|
92
99
|
response.end(JSON.stringify(body));
|
|
93
100
|
}
|
|
94
101
|
async function fetchWithTimeout(url, init, timeoutMs, label = "upstream") {
|
|
95
102
|
const controller = new AbortController();
|
|
96
103
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
97
104
|
try {
|
|
98
|
-
|
|
105
|
+
const response = await fetch(url, { ...init, redirect: "manual", signal: controller.signal });
|
|
106
|
+
if (response.status >= 300 && response.status < 400)
|
|
107
|
+
throw new HttpError(502, `${label} redirect refused`);
|
|
108
|
+
return response;
|
|
99
109
|
}
|
|
100
110
|
catch (error) {
|
|
101
111
|
if (error instanceof Error && error.name === "AbortError")
|
|
@@ -106,13 +116,30 @@ async function fetchWithTimeout(url, init, timeoutMs, label = "upstream") {
|
|
|
106
116
|
clearTimeout(timer);
|
|
107
117
|
}
|
|
108
118
|
}
|
|
119
|
+
async function readResponseBytes(response, limit = MAX_RESPONSE_BYTES) {
|
|
120
|
+
const declared = Number(response.headers.get("content-length"));
|
|
121
|
+
if (Number.isFinite(declared) && declared > limit)
|
|
122
|
+
throw new HttpError(502, "upstream response too large");
|
|
123
|
+
if (!response.body)
|
|
124
|
+
return Buffer.alloc(0);
|
|
125
|
+
const chunks = [];
|
|
126
|
+
let total = 0;
|
|
127
|
+
for await (const chunk of response.body) {
|
|
128
|
+
const buffer = Buffer.from(chunk);
|
|
129
|
+
total += buffer.length;
|
|
130
|
+
if (total > limit)
|
|
131
|
+
throw new HttpError(502, "upstream response too large");
|
|
132
|
+
chunks.push(buffer);
|
|
133
|
+
}
|
|
134
|
+
return Buffer.concat(chunks);
|
|
135
|
+
}
|
|
109
136
|
async function facilitatorPost(entry, path, body) {
|
|
110
137
|
const headers = { "content-type": "application/json" };
|
|
111
138
|
if (entry.facilitatorApiKey) {
|
|
112
139
|
headers["x-api-key"] = entry.facilitatorApiKey;
|
|
113
140
|
}
|
|
114
|
-
const response = await fetchWithTimeout(new URL(path, entry.facilitatorUrl), { method: "POST", headers, body: JSON.stringify(body) }, FACILITATOR_TIMEOUT_MS, "facilitator");
|
|
115
|
-
const text = await response.
|
|
141
|
+
const response = await fetchWithTimeout(new URL(path.replace(/^\/+/, ""), `${entry.facilitatorUrl.replace(/\/+$/, "")}/`), { method: "POST", headers, body: JSON.stringify(body) }, FACILITATOR_TIMEOUT_MS, "facilitator");
|
|
142
|
+
const text = (await readResponseBytes(response, Math.min(MAX_RESPONSE_BYTES, 1_000_000))).toString("utf8");
|
|
116
143
|
let data = {};
|
|
117
144
|
try {
|
|
118
145
|
data = text ? JSON.parse(text) : {};
|
|
@@ -173,17 +200,19 @@ function isVerifySuccess(verify) {
|
|
|
173
200
|
return verify?.valid === true || verify?.isValid === true;
|
|
174
201
|
}
|
|
175
202
|
function isSettleSuccess(settle) {
|
|
176
|
-
return
|
|
177
|
-
settle?.settled === true ||
|
|
178
|
-
(typeof settle?.transaction === "string" && settle.transaction.length > 0) ||
|
|
179
|
-
(typeof settle?.txHash === "string" && settle.txHash.length > 0));
|
|
203
|
+
return settle?.success === true && typeof settle?.transaction === "string" && settle.transaction.length > 0 && typeof settle?.network === "string" && settle.network.length > 0;
|
|
180
204
|
}
|
|
181
205
|
function isAdminAllowed(request) {
|
|
182
206
|
const token = process.env.X402_GATEWAY_ADMIN_TOKEN;
|
|
183
207
|
if (!token)
|
|
184
208
|
return process.env.X402_GATEWAY_ADMIN_ALLOW_PUBLIC === "true";
|
|
185
209
|
const auth = request.headers.authorization ?? "";
|
|
186
|
-
|
|
210
|
+
const expected = Buffer.from(`Bearer ${token}`);
|
|
211
|
+
const actual = Buffer.from(auth);
|
|
212
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
213
|
+
}
|
|
214
|
+
function clientAddress(request) {
|
|
215
|
+
return request.socket.remoteAddress ?? "unknown";
|
|
187
216
|
}
|
|
188
217
|
function requestParams(url, body, request) {
|
|
189
218
|
const params = {};
|
|
@@ -210,11 +239,12 @@ function requestParams(url, body, request) {
|
|
|
210
239
|
}
|
|
211
240
|
function upstreamHeaders(request, entry) {
|
|
212
241
|
const headersOut = new Headers();
|
|
242
|
+
const connectionHeaders = new Set(String(request.headers.connection ?? "").split(",").map(value => value.trim().toLowerCase()).filter(Boolean));
|
|
213
243
|
for (const [key, value] of Object.entries(request.headers)) {
|
|
214
244
|
if (!value)
|
|
215
245
|
continue;
|
|
216
246
|
const lower = key.toLowerCase();
|
|
217
|
-
if (STRIP_REQUEST_HEADERS.has(lower))
|
|
247
|
+
if (STRIP_REQUEST_HEADERS.has(lower) || connectionHeaders.has(lower))
|
|
218
248
|
continue;
|
|
219
249
|
headersOut.set(key, Array.isArray(value) ? value.join(",") : value);
|
|
220
250
|
}
|
|
@@ -229,7 +259,14 @@ function upstreamHeaders(request, entry) {
|
|
|
229
259
|
}
|
|
230
260
|
function upstreamUrl(entry, request, routePath) {
|
|
231
261
|
const sourceUrl = new URL(request.url ?? "/", "http://local");
|
|
232
|
-
|
|
262
|
+
if (!routePath.startsWith("/") || routePath.startsWith("//") || routePath.includes("\\") || routePath.includes("\0") || /%(?:2f|5c)/i.test(routePath))
|
|
263
|
+
throw new HttpError(400, "invalid provider path");
|
|
264
|
+
const base = new URL(entry.config.forward_url);
|
|
265
|
+
const upstream = new URL(base);
|
|
266
|
+
upstream.pathname = routePath;
|
|
267
|
+
upstream.search = sourceUrl.search;
|
|
268
|
+
if (upstream.origin !== base.origin)
|
|
269
|
+
throw new HttpError(400, "invalid provider path");
|
|
233
270
|
const auth = entry.config.routing?.auth;
|
|
234
271
|
const value = auth?.value ?? (auth?.value_from_env ? process.env[auth.value_from_env] : undefined);
|
|
235
272
|
if (auth?.method === "query_param" && value) {
|
|
@@ -261,14 +298,24 @@ async function forward(metrics, entry, request, response, routePath, body, payme
|
|
|
261
298
|
});
|
|
262
299
|
if (paymentResponse)
|
|
263
300
|
responseHeaders[headers.response] = encodeResponse(paymentResponse);
|
|
264
|
-
|
|
301
|
+
let responseBody;
|
|
302
|
+
try {
|
|
303
|
+
responseBody = await readResponseBytes(upstreamResponse);
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
metrics.upstreamFailures += 1;
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
265
309
|
response.writeHead(upstreamResponse.status, responseHeaders);
|
|
266
310
|
response.end(responseBody);
|
|
267
311
|
}
|
|
268
312
|
export function createGatewayServer(providers) {
|
|
269
313
|
const publicBaseUrl = configuredPublicBaseUrl();
|
|
270
314
|
const metrics = createMetrics();
|
|
271
|
-
|
|
315
|
+
const rateLimits = new Map();
|
|
316
|
+
let activeRequests = 0;
|
|
317
|
+
const server = http.createServer(async (request, response) => {
|
|
318
|
+
let countedActive = false;
|
|
272
319
|
try {
|
|
273
320
|
metrics.requests += 1;
|
|
274
321
|
const url = new URL(request.url ?? "/", "http://local");
|
|
@@ -314,6 +361,7 @@ export function createGatewayServer(providers) {
|
|
|
314
361
|
`x402_gateway_verify_failures_total ${metrics.verifyFailures}`,
|
|
315
362
|
`x402_gateway_settle_failures_total ${metrics.settleFailures}`,
|
|
316
363
|
`x402_gateway_upstream_failures_total ${metrics.upstreamFailures}`,
|
|
364
|
+
`x402_gateway_rejected_requests_total ${metrics.rejectedRequests}`,
|
|
317
365
|
"",
|
|
318
366
|
].join("\n"));
|
|
319
367
|
return;
|
|
@@ -334,8 +382,30 @@ export function createGatewayServer(providers) {
|
|
|
334
382
|
json(response, 404, { error: "endpoint not found" });
|
|
335
383
|
return;
|
|
336
384
|
}
|
|
385
|
+
const now = Date.now();
|
|
386
|
+
const address = clientAddress(request);
|
|
387
|
+
let rate = rateLimits.get(address);
|
|
388
|
+
if (!rate || rate.resetAt <= now) {
|
|
389
|
+
rate = { count: 0, resetAt: now + 60_000 };
|
|
390
|
+
rateLimits.set(address, rate);
|
|
391
|
+
}
|
|
392
|
+
rate.count += 1;
|
|
393
|
+
if (rate.count > RATE_LIMIT_PER_MINUTE) {
|
|
394
|
+
metrics.rejectedRequests += 1;
|
|
395
|
+
const retryAfter = Math.max(1, Math.ceil((rate.resetAt - now) / 1000));
|
|
396
|
+
throw new HttpError(429, "gateway rate limited", undefined, { "retry-after": String(retryAfter) });
|
|
397
|
+
}
|
|
398
|
+
if (activeRequests >= MAX_CONCURRENT_REQUESTS) {
|
|
399
|
+
metrics.rejectedRequests += 1;
|
|
400
|
+
throw new HttpError(503, "gateway is busy", undefined, { "retry-after": "1" });
|
|
401
|
+
}
|
|
402
|
+
activeRequests += 1;
|
|
403
|
+
countedActive = true;
|
|
337
404
|
const body = await readBody(request);
|
|
338
|
-
const
|
|
405
|
+
const price = priceUsd(endpoint, requestParams(url, body, request));
|
|
406
|
+
const requirements = paymentRequirements(entry.config, price);
|
|
407
|
+
if (price > 0 && !requirements.length)
|
|
408
|
+
throw new HttpError(500, "paid endpoint has no payment requirements");
|
|
339
409
|
if (!requirements.length) {
|
|
340
410
|
await forward(metrics, entry, request, response, routePath, body);
|
|
341
411
|
return;
|
|
@@ -414,5 +484,14 @@ export function createGatewayServer(providers) {
|
|
|
414
484
|
console.error(error);
|
|
415
485
|
json(response, 500, { error: "internal server error" });
|
|
416
486
|
}
|
|
487
|
+
finally {
|
|
488
|
+
if (countedActive)
|
|
489
|
+
activeRequests -= 1;
|
|
490
|
+
}
|
|
417
491
|
});
|
|
492
|
+
server.requestTimeout = UPSTREAM_TIMEOUT_MS + FACILITATOR_TIMEOUT_MS * 2 + 5_000;
|
|
493
|
+
server.headersTimeout = Math.min(server.requestTimeout, 60_000);
|
|
494
|
+
server.keepAliveTimeout = 5_000;
|
|
495
|
+
server.maxConnections = MAX_CONCURRENT_REQUESTS * 2;
|
|
496
|
+
return server;
|
|
418
497
|
}
|
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.8",
|
|
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
|
},
|
|
@@ -20,10 +21,10 @@
|
|
|
20
21
|
"node": ">=20"
|
|
21
22
|
},
|
|
22
23
|
"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
|
|
24
|
+
"@bankofai/x402-core": "1.0.1",
|
|
25
|
+
"@bankofai/x402-evm": "1.0.1",
|
|
26
|
+
"@bankofai/x402-gateway": "1.0.1-beta.7",
|
|
27
|
+
"@bankofai/x402-tron": "1.0.1",
|
|
27
28
|
"tronweb": "6.4.0",
|
|
28
29
|
"viem": "^2.55.0",
|
|
29
30
|
"yaml": "^2.8.2"
|