@bankofai/x402-cli 1.0.1-beta.4 → 1.0.1-beta.6
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 +38 -2
- package/dist/cli.js +59 -10
- package/dist/gateway/config.js +12 -6
- package/dist/gateway/server.js +67 -19
- package/dist/x402.js +17 -9
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -13,6 +13,15 @@ from the payment token, so the payer does not need TRX.
|
|
|
13
13
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
16
|
+
Install the CLI package:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g @bankofai/x402-cli
|
|
20
|
+
x402-cli --version
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For repository development:
|
|
24
|
+
|
|
16
25
|
```bash
|
|
17
26
|
npm install
|
|
18
27
|
npm run build
|
|
@@ -61,8 +70,8 @@ The server exposes:
|
|
|
61
70
|
|
|
62
71
|
- `GET /health`
|
|
63
72
|
- `GET /.well-known/x402`
|
|
64
|
-
-
|
|
65
|
-
-
|
|
73
|
+
- `/pay` returns `402 Payment Required` without a payment signature
|
|
74
|
+
- The signed retry uses the same HTTP method, then verifies and settles with the facilitator
|
|
66
75
|
|
|
67
76
|
### Pay
|
|
68
77
|
|
|
@@ -75,6 +84,9 @@ node dist/cli.js pay http://127.0.0.1:4020/pay \
|
|
|
75
84
|
--token USDT
|
|
76
85
|
```
|
|
77
86
|
|
|
87
|
+
For automated or unfamiliar endpoints, set `--max-amount` or
|
|
88
|
+
`--max-raw-amount` before allowing the CLI to sign a payment.
|
|
89
|
+
|
|
78
90
|
Pay a TRON GasFree endpoint (the CLI normally selects this automatically from
|
|
79
91
|
the server challenge):
|
|
80
92
|
|
|
@@ -89,7 +101,31 @@ node dist/cli.js pay https://api.example.com/pay \
|
|
|
89
101
|
Use `--gasfree-api-url <url>` or `X402_GASFREE_API_URL` to override the SDK's
|
|
90
102
|
default relayer endpoint.
|
|
91
103
|
|
|
104
|
+
GasFree fees are separate from the advertised payment amount. Set a fee limit
|
|
105
|
+
so the CLI estimates the relayer fee and rejects the payment before signing if
|
|
106
|
+
the estimate is too high:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
x402-cli pay https://api.example.com/pay \
|
|
110
|
+
--scheme exact_gasfree \
|
|
111
|
+
--max-amount 0.01 \
|
|
112
|
+
--max-gasfree-fee 0.5 \
|
|
113
|
+
--json
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Use `--max-gasfree-fee-raw` to express the fee limit in the token's smallest
|
|
117
|
+
unit. Successful and failed paid responses distinguish `settled` (payment
|
|
118
|
+
completed) from `delivered` (HTTP business response succeeded). A settled
|
|
119
|
+
upstream failure has `paid=true`, `settled=true`, and `delivered=false` and
|
|
120
|
+
includes its transaction information.
|
|
121
|
+
|
|
92
122
|
For EVM networks use `EVM_PRIVATE_KEY` or `PRIVATE_KEY`.
|
|
123
|
+
Prefer environment variables over `--private-key` in shared environments,
|
|
124
|
+
because command-line arguments may be visible to other local processes.
|
|
125
|
+
|
|
126
|
+
If the gateway settles a payment but the upstream request fails, JSON error
|
|
127
|
+
output includes `error.details.paymentResponse` for reconciliation. Do not retry
|
|
128
|
+
such a request blindly; inspect the transaction and provider behavior first.
|
|
93
129
|
|
|
94
130
|
### Roundtrip
|
|
95
131
|
|
package/dist/cli.js
CHANGED
|
@@ -18,11 +18,13 @@ class CliError extends Error {
|
|
|
18
18
|
code;
|
|
19
19
|
hint;
|
|
20
20
|
exitCode;
|
|
21
|
-
|
|
21
|
+
details;
|
|
22
|
+
constructor(code, message, hint, exitCode = 1, details) {
|
|
22
23
|
super(message);
|
|
23
24
|
this.code = code;
|
|
24
25
|
this.hint = hint;
|
|
25
26
|
this.exitCode = exitCode;
|
|
27
|
+
this.details = details;
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
30
|
function parseArgs(argv) {
|
|
@@ -144,6 +146,8 @@ function emit(args) {
|
|
|
144
146
|
process.stderr.write(` ${args.error.message}\n`);
|
|
145
147
|
if (args.error.hint)
|
|
146
148
|
process.stderr.write(` hint: ${args.error.hint}\n`);
|
|
149
|
+
if (args.error.details !== undefined)
|
|
150
|
+
process.stderr.write(` details: ${JSON.stringify(args.error.details)}\n`);
|
|
147
151
|
return;
|
|
148
152
|
}
|
|
149
153
|
const suffix = [args.network, args.scheme].filter(Boolean).join(" ");
|
|
@@ -171,6 +175,7 @@ function classify(error) {
|
|
|
171
175
|
code: error.code,
|
|
172
176
|
message,
|
|
173
177
|
hint: error.hint,
|
|
178
|
+
details: error.details,
|
|
174
179
|
};
|
|
175
180
|
}
|
|
176
181
|
const lower = message.toLowerCase();
|
|
@@ -316,6 +321,8 @@ Options:
|
|
|
316
321
|
--token <symbol> Require a specific token
|
|
317
322
|
--scheme <scheme> Require a specific x402 scheme
|
|
318
323
|
--gasfree-api-url <url> Override the TRON GasFree relayer API URL
|
|
324
|
+
--max-gasfree-fee <amt> Maximum GasFree relayer fee in token units
|
|
325
|
+
--max-gasfree-fee-raw <n> Maximum GasFree relayer fee in smallest units
|
|
319
326
|
--max-amount <amount> Maximum human-readable payment amount
|
|
320
327
|
--max-raw-amount <amount> Maximum smallest-unit payment amount
|
|
321
328
|
--dry-run Read requirements but do not sign or pay
|
|
@@ -737,7 +744,7 @@ async function fetchWithTimeout(input, init = {}, timeout = DEFAULT_TIMEOUT_MS,
|
|
|
737
744
|
return await fetch(input, { ...init, signal: controller.signal });
|
|
738
745
|
}
|
|
739
746
|
catch (error) {
|
|
740
|
-
if (error
|
|
747
|
+
if (error instanceof Error && error.name === "AbortError")
|
|
741
748
|
throw new Error(`${label} timed out after ${timeout}ms`);
|
|
742
749
|
throw error;
|
|
743
750
|
}
|
|
@@ -1079,6 +1086,31 @@ function validateAmountLimits(selected, options) {
|
|
|
1079
1086
|
}
|
|
1080
1087
|
}
|
|
1081
1088
|
}
|
|
1089
|
+
function gasfreeFeeLimitRaw(selected, options) {
|
|
1090
|
+
const maxRaw = opt(options, "max-gasfree-fee-raw");
|
|
1091
|
+
const maxHuman = opt(options, "max-gasfree-fee");
|
|
1092
|
+
if (maxRaw && maxHuman) {
|
|
1093
|
+
throw new CliError("INVALID_ARGUMENT", "--max-gasfree-fee and --max-gasfree-fee-raw are mutually exclusive", "Pass one GasFree fee limit.", 2);
|
|
1094
|
+
}
|
|
1095
|
+
if (selected.scheme !== "exact_gasfree") {
|
|
1096
|
+
if (maxRaw || maxHuman)
|
|
1097
|
+
throw new Error("GasFree fee limits require an exact_gasfree payment requirement");
|
|
1098
|
+
return undefined;
|
|
1099
|
+
}
|
|
1100
|
+
if (maxRaw)
|
|
1101
|
+
return assertRawAmount(maxRaw, "--max-gasfree-fee-raw");
|
|
1102
|
+
if (!maxHuman)
|
|
1103
|
+
return undefined;
|
|
1104
|
+
const token = findTokenByAddress(selected.network, selected.asset);
|
|
1105
|
+
const decimalsOption = opt(options, "decimals");
|
|
1106
|
+
if (!token && decimalsOption === undefined) {
|
|
1107
|
+
throw new Error("cannot evaluate --max-gasfree-fee for an unknown asset; pass --max-gasfree-fee-raw or --decimals");
|
|
1108
|
+
}
|
|
1109
|
+
const decimals = decimalsOption !== undefined ? Number(decimalsOption) : token.decimals;
|
|
1110
|
+
if (!Number.isInteger(decimals) || decimals < 0)
|
|
1111
|
+
throw new Error("--decimals must be a non-negative integer");
|
|
1112
|
+
return toSmallestUnit(maxHuman, decimals);
|
|
1113
|
+
}
|
|
1082
1114
|
async function serve(options) {
|
|
1083
1115
|
const host = opt(options, "host", "127.0.0.1");
|
|
1084
1116
|
const port = Number(opt(options, "port", "4020"));
|
|
@@ -1182,7 +1214,13 @@ function selectRequirement(accepts, options) {
|
|
|
1182
1214
|
if (scheme && scheme !== req.scheme)
|
|
1183
1215
|
return false;
|
|
1184
1216
|
if (token) {
|
|
1185
|
-
|
|
1217
|
+
let tokenInfo;
|
|
1218
|
+
try {
|
|
1219
|
+
tokenInfo = getToken(req.network, token);
|
|
1220
|
+
}
|
|
1221
|
+
catch {
|
|
1222
|
+
return false;
|
|
1223
|
+
}
|
|
1186
1224
|
if (tokenInfo.address.toLowerCase() !== req.asset.toLowerCase())
|
|
1187
1225
|
return false;
|
|
1188
1226
|
}
|
|
@@ -1225,6 +1263,7 @@ async function pay(url, options) {
|
|
|
1225
1263
|
const required = decodeRequired(header);
|
|
1226
1264
|
const selected = selectRequirement(required.accepts ?? [], options);
|
|
1227
1265
|
validateAmountLimits(selected, options);
|
|
1266
|
+
const maxGasfreeFeeRaw = gasfreeFeeLimitRaw(selected, options);
|
|
1228
1267
|
if (options["dry-run"]) {
|
|
1229
1268
|
emit({
|
|
1230
1269
|
command: "client",
|
|
@@ -1240,16 +1279,17 @@ async function pay(url, options) {
|
|
|
1240
1279
|
});
|
|
1241
1280
|
return;
|
|
1242
1281
|
}
|
|
1243
|
-
const
|
|
1282
|
+
const creation = await withSdkStdoutRedirect(outputMode(options) === "json", () => createPaymentPayload({
|
|
1244
1283
|
selected,
|
|
1245
1284
|
resource: required.resource?.url ?? url,
|
|
1246
1285
|
extensions: required.extensions,
|
|
1247
1286
|
rpcUrl: opt(options, "rpc-url"),
|
|
1248
1287
|
privateKey: opt(options, "private-key"),
|
|
1249
1288
|
gasfreeApiUrl: opt(options, "gasfree-api-url"),
|
|
1289
|
+
maxGasfreeFeeRaw,
|
|
1250
1290
|
}));
|
|
1251
1291
|
const retryHeaders = new Headers(baseHeaders);
|
|
1252
|
-
retryHeaders.set(headers.signature, encodeSignature(payload));
|
|
1292
|
+
retryHeaders.set(headers.signature, encodeSignature(creation.payload));
|
|
1253
1293
|
const paid = await fetchWithTimeout(url, {
|
|
1254
1294
|
method,
|
|
1255
1295
|
headers: retryHeaders,
|
|
@@ -1257,16 +1297,26 @@ async function pay(url, options) {
|
|
|
1257
1297
|
}, timeoutMs(options), `fetch ${url}`);
|
|
1258
1298
|
const body = await responsePayload(paid);
|
|
1259
1299
|
const paymentResponse = paid.headers.get(headers.response);
|
|
1300
|
+
const settlement = paymentResponse ? decodeResponse(paymentResponse) : undefined;
|
|
1301
|
+
const settled = settlement !== undefined;
|
|
1260
1302
|
const result = {
|
|
1261
1303
|
url,
|
|
1262
1304
|
status: paid.status,
|
|
1263
|
-
paid:
|
|
1305
|
+
paid: settled,
|
|
1306
|
+
settled,
|
|
1307
|
+
delivered: paid.ok,
|
|
1264
1308
|
response: body,
|
|
1265
|
-
...(
|
|
1309
|
+
...(settlement !== undefined ? {
|
|
1310
|
+
paymentResponse: settlement,
|
|
1311
|
+
transaction: settlement?.transaction ?? settlement?.txHash ?? null,
|
|
1312
|
+
} : {}),
|
|
1313
|
+
...(creation.gasfreeEstimate ? { gasfreeEstimate: creation.gasfreeEstimate } : {}),
|
|
1266
1314
|
};
|
|
1267
1315
|
if (!paid.ok) {
|
|
1268
1316
|
const retryAfter = paid.headers.get("retry-after");
|
|
1269
|
-
throw new
|
|
1317
|
+
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
|
|
1318
|
+
? "The gateway reports that payment was settled; retain paymentResponse for support or reconciliation."
|
|
1319
|
+
: "Inspect the HTTP response and retry only when it is safe to do so.", 1, result);
|
|
1270
1320
|
}
|
|
1271
1321
|
emit({
|
|
1272
1322
|
command: "client",
|
|
@@ -1278,8 +1328,7 @@ async function pay(url, options) {
|
|
|
1278
1328
|
}
|
|
1279
1329
|
async function roundtrip(options) {
|
|
1280
1330
|
const port = Number(opt(options, "port", "4020"));
|
|
1281
|
-
serve(options);
|
|
1282
|
-
await delay(500);
|
|
1331
|
+
await serve(options);
|
|
1283
1332
|
await pay(`http://127.0.0.1:${port}/pay`, options);
|
|
1284
1333
|
process.exit(0);
|
|
1285
1334
|
}
|
package/dist/gateway/config.js
CHANGED
|
@@ -104,15 +104,21 @@ export function loadProvider(file) {
|
|
|
104
104
|
validateProvider(config, file);
|
|
105
105
|
config.operator.network = normalizeNetwork(config.operator.network);
|
|
106
106
|
normalizePaymentProtocol(config, file);
|
|
107
|
+
const facilitatorUrl = config.operator.facilitator_url ||
|
|
108
|
+
process.env.X402_FACILITATOR_URL ||
|
|
109
|
+
process.env.FACILITATOR_URL ||
|
|
110
|
+
"https://facilitator.bankofai.io";
|
|
111
|
+
assertHttpUrl(facilitatorUrl, `${file}: facilitator URL`);
|
|
112
|
+
const configuredApiKeyEnv = config.operator.facilitator_api_key_env;
|
|
113
|
+
if (configuredApiKeyEnv && !process.env[configuredApiKeyEnv]) {
|
|
114
|
+
throw new Error(`${file}: environment variable ${configuredApiKeyEnv} is not set`);
|
|
115
|
+
}
|
|
107
116
|
return {
|
|
108
117
|
config,
|
|
109
|
-
facilitatorUrl
|
|
110
|
-
process.env.X402_FACILITATOR_URL ||
|
|
111
|
-
process.env.FACILITATOR_URL ||
|
|
112
|
-
"https://facilitator.bankofai.io",
|
|
118
|
+
facilitatorUrl,
|
|
113
119
|
facilitatorApiKey: config.operator.facilitator_api_key ||
|
|
114
|
-
(
|
|
115
|
-
? process.env[
|
|
120
|
+
(configuredApiKeyEnv
|
|
121
|
+
? process.env[configuredApiKeyEnv]
|
|
116
122
|
: undefined) ||
|
|
117
123
|
process.env.X402_FACILITATOR_API_KEY ||
|
|
118
124
|
process.env.FACILITATOR_API_KEY,
|
package/dist/gateway/server.js
CHANGED
|
@@ -18,9 +18,20 @@ class RequestTooLargeError extends HttpError {
|
|
|
18
18
|
super(413, "request body too large");
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
21
|
+
function positiveIntegerEnv(name, fallback) {
|
|
22
|
+
const raw = process.env[name];
|
|
23
|
+
if (raw === undefined || raw === "")
|
|
24
|
+
return fallback;
|
|
25
|
+
if (!/^\d+$/.test(raw))
|
|
26
|
+
throw new Error(`${name} must be a positive integer`);
|
|
27
|
+
const value = Number(raw);
|
|
28
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
29
|
+
throw new Error(`${name} must be a positive integer`);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
const MAX_BODY_BYTES = positiveIntegerEnv("X402_GATEWAY_MAX_BODY_BYTES", 1_000_000);
|
|
33
|
+
const FACILITATOR_TIMEOUT_MS = positiveIntegerEnv("X402_GATEWAY_FACILITATOR_TIMEOUT_MS", 10_000);
|
|
34
|
+
const UPSTREAM_TIMEOUT_MS = positiveIntegerEnv("X402_GATEWAY_UPSTREAM_TIMEOUT_MS", 30_000);
|
|
24
35
|
const STRIP_REQUEST_HEADERS = new Set([
|
|
25
36
|
"host",
|
|
26
37
|
"connection",
|
|
@@ -48,14 +59,22 @@ const STRIP_RESPONSE_HEADERS = new Set([
|
|
|
48
59
|
"authorization",
|
|
49
60
|
"proxy-authorization",
|
|
50
61
|
"set-cookie",
|
|
62
|
+
"payment-required",
|
|
63
|
+
"x-payment-required",
|
|
64
|
+
"payment-signature",
|
|
65
|
+
"x-payment",
|
|
66
|
+
"payment-response",
|
|
67
|
+
"x-payment-response",
|
|
51
68
|
]);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
function createMetrics() {
|
|
70
|
+
return {
|
|
71
|
+
requests: 0,
|
|
72
|
+
paidRequests: 0,
|
|
73
|
+
verifyFailures: 0,
|
|
74
|
+
settleFailures: 0,
|
|
75
|
+
upstreamFailures: 0,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
59
78
|
async function readBody(request) {
|
|
60
79
|
const chunks = [];
|
|
61
80
|
let total = 0;
|
|
@@ -79,7 +98,7 @@ async function fetchWithTimeout(url, init, timeoutMs, label = "upstream") {
|
|
|
79
98
|
return await fetch(url, { ...init, signal: controller.signal });
|
|
80
99
|
}
|
|
81
100
|
catch (error) {
|
|
82
|
-
if (error
|
|
101
|
+
if (error instanceof Error && error.name === "AbortError")
|
|
83
102
|
throw new HttpError(504, `${label} request timed out`);
|
|
84
103
|
throw error;
|
|
85
104
|
}
|
|
@@ -112,12 +131,25 @@ async function facilitatorPost(entry, path, body) {
|
|
|
112
131
|
}
|
|
113
132
|
return data;
|
|
114
133
|
}
|
|
115
|
-
function
|
|
134
|
+
function configuredPublicBaseUrl() {
|
|
135
|
+
const value = process.env.X402_GATEWAY_PUBLIC_BASE_URL?.trim();
|
|
136
|
+
if (!value)
|
|
137
|
+
return undefined;
|
|
138
|
+
try {
|
|
139
|
+
const url = new URL(value);
|
|
140
|
+
if (!["http:", "https:"].includes(url.protocol))
|
|
141
|
+
throw new Error("unsupported protocol");
|
|
142
|
+
return `${value.replace(/\/+$/, "")}/`;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
throw new Error("X402_GATEWAY_PUBLIC_BASE_URL must be a valid http(s) URL");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function resourceUrl(url, publicBaseUrl) {
|
|
116
149
|
const path = `${url.pathname}${url.search}`;
|
|
117
|
-
const publicBaseUrl = process.env.X402_GATEWAY_PUBLIC_BASE_URL?.trim();
|
|
118
150
|
if (!publicBaseUrl)
|
|
119
151
|
return path;
|
|
120
|
-
return new URL(path,
|
|
152
|
+
return new URL(path, publicBaseUrl).toString();
|
|
121
153
|
}
|
|
122
154
|
function logFacilitatorFailure(entry, path, response, body, data) {
|
|
123
155
|
const requirement = body?.paymentRequirements;
|
|
@@ -205,7 +237,7 @@ function upstreamUrl(entry, request, routePath) {
|
|
|
205
237
|
}
|
|
206
238
|
return upstream;
|
|
207
239
|
}
|
|
208
|
-
async function forward(entry, request, response, routePath, body, paymentResponse) {
|
|
240
|
+
async function forward(metrics, entry, request, response, routePath, body, paymentResponse) {
|
|
209
241
|
const upstream = upstreamUrl(entry, request, routePath);
|
|
210
242
|
let upstreamResponse;
|
|
211
243
|
try {
|
|
@@ -229,10 +261,13 @@ async function forward(entry, request, response, routePath, body, paymentRespons
|
|
|
229
261
|
});
|
|
230
262
|
if (paymentResponse)
|
|
231
263
|
responseHeaders[headers.response] = encodeResponse(paymentResponse);
|
|
264
|
+
const responseBody = Buffer.from(await upstreamResponse.arrayBuffer());
|
|
232
265
|
response.writeHead(upstreamResponse.status, responseHeaders);
|
|
233
|
-
response.end(
|
|
266
|
+
response.end(responseBody);
|
|
234
267
|
}
|
|
235
268
|
export function createGatewayServer(providers) {
|
|
269
|
+
const publicBaseUrl = configuredPublicBaseUrl();
|
|
270
|
+
const metrics = createMetrics();
|
|
236
271
|
return http.createServer(async (request, response) => {
|
|
237
272
|
try {
|
|
238
273
|
metrics.requests += 1;
|
|
@@ -302,7 +337,7 @@ export function createGatewayServer(providers) {
|
|
|
302
337
|
const body = await readBody(request);
|
|
303
338
|
const requirements = paymentRequirements(entry.config, priceUsd(endpoint, requestParams(url, body, request)));
|
|
304
339
|
if (!requirements.length) {
|
|
305
|
-
await forward(entry, request, response, routePath, body);
|
|
340
|
+
await forward(metrics, entry, request, response, routePath, body);
|
|
306
341
|
return;
|
|
307
342
|
}
|
|
308
343
|
const paymentHeader = request.headers[headers.signature.toLowerCase()];
|
|
@@ -311,7 +346,7 @@ export function createGatewayServer(providers) {
|
|
|
311
346
|
const challenge = {
|
|
312
347
|
x402Version: 2,
|
|
313
348
|
error: "Payment required",
|
|
314
|
-
resource: { url: resourceUrl(url) },
|
|
349
|
+
resource: { url: resourceUrl(url, publicBaseUrl) },
|
|
315
350
|
accepts,
|
|
316
351
|
};
|
|
317
352
|
json(response, 402, challenge, { [headers.required]: encodeRequired(challenge) });
|
|
@@ -356,7 +391,20 @@ export function createGatewayServer(providers) {
|
|
|
356
391
|
return;
|
|
357
392
|
}
|
|
358
393
|
metrics.paidRequests += 1;
|
|
359
|
-
|
|
394
|
+
try {
|
|
395
|
+
await forward(metrics, entry, request, response, routePath, body, settle);
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
const status = error instanceof HttpError ? error.status : 502;
|
|
399
|
+
const extraHeaders = error instanceof HttpError ? error.responseHeaders : {};
|
|
400
|
+
json(response, status, {
|
|
401
|
+
error: "upstream failed after payment settlement",
|
|
402
|
+
settled: true,
|
|
403
|
+
}, {
|
|
404
|
+
...extraHeaders,
|
|
405
|
+
[headers.response]: encodeResponse(settle),
|
|
406
|
+
});
|
|
407
|
+
}
|
|
360
408
|
}
|
|
361
409
|
catch (error) {
|
|
362
410
|
if (error instanceof HttpError) {
|
package/dist/x402.js
CHANGED
|
@@ -2,7 +2,7 @@ import { decodePaymentRequiredHeader, decodePaymentResponseHeader, decodePayment
|
|
|
2
2
|
import { x402Client } from "@bankofai/x402-core/client";
|
|
3
3
|
import { ExactEvmScheme, toClientEvmSigner } from "@bankofai/x402-evm";
|
|
4
4
|
import { ExactTronScheme, createClientTronSigner } from "@bankofai/x402-tron";
|
|
5
|
-
import {
|
|
5
|
+
import { ExactGasFreeTronScheme, createGasFreeApiClients, getGasFreeApiBaseUrl } from "@bankofai/x402-tron/gasfree";
|
|
6
6
|
import { createPublicClient, http } from "viem";
|
|
7
7
|
import { privateKeyToAccount } from "viem/accounts";
|
|
8
8
|
import { TronWeb } from "tronweb";
|
|
@@ -142,9 +142,10 @@ export async function createPaymentPayload(args) {
|
|
|
142
142
|
const publicClient = rpcUrl ? createPublicClient({ transport: http(rpcUrl) }) : undefined;
|
|
143
143
|
const signer = toClientEvmSigner(account, publicClient);
|
|
144
144
|
const scheme = new ExactEvmScheme(signer, rpcUrl ? { rpcUrl } : undefined);
|
|
145
|
-
|
|
145
|
+
const payload = await new x402Client()
|
|
146
146
|
.register(selected.network, scheme)
|
|
147
147
|
.createPaymentPayload(required);
|
|
148
|
+
return { payload };
|
|
148
149
|
}
|
|
149
150
|
if (selected.network.startsWith("tron:")) {
|
|
150
151
|
const privateKey = privateKeyFrom(["TRON_PRIVATE_KEY", "AGENT_WALLET_PRIVATE_KEY", "PRIVATE_KEY"], args.privateKey, ["tron_client", "payer", "default"]);
|
|
@@ -156,14 +157,20 @@ export async function createPaymentPayload(args) {
|
|
|
156
157
|
allowanceMode: args.allowanceMode || process.env.X402_TRON_ALLOWANCE_MODE || "auto",
|
|
157
158
|
});
|
|
158
159
|
const client = new x402Client();
|
|
160
|
+
let gasfreeEstimate;
|
|
159
161
|
if (selected.scheme === "exact_gasfree") {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
...(args.gasfreeApiUrl || process.env.X402_GASFREE_API_URL
|
|
164
|
-
? { schemeOptions: { apiBaseUrls: { [selected.network]: args.gasfreeApiUrl || process.env.X402_GASFREE_API_URL } } }
|
|
165
|
-
: {}),
|
|
162
|
+
const apiUrl = args.gasfreeApiUrl || process.env.X402_GASFREE_API_URL || getGasFreeApiBaseUrl(selected.network);
|
|
163
|
+
const scheme = new ExactGasFreeTronScheme(signer, {
|
|
164
|
+
apiClients: createGasFreeApiClients({ [selected.network]: apiUrl }),
|
|
166
165
|
});
|
|
166
|
+
const total = await scheme.estimateCost(selected);
|
|
167
|
+
const amount = BigInt(selected.amount);
|
|
168
|
+
const fee = total - amount;
|
|
169
|
+
if (args.maxGasfreeFeeRaw !== undefined && fee > BigInt(args.maxGasfreeFeeRaw)) {
|
|
170
|
+
throw new Error(`estimated GasFree fee ${fee} exceeds --max-gasfree-fee limit ${args.maxGasfreeFeeRaw}`);
|
|
171
|
+
}
|
|
172
|
+
gasfreeEstimate = { fee: fee.toString(), total: total.toString() };
|
|
173
|
+
client.register(selected.network, scheme);
|
|
167
174
|
}
|
|
168
175
|
else if (selected.scheme === "exact") {
|
|
169
176
|
client.register(selected.network, new ExactTronScheme(signer));
|
|
@@ -171,7 +178,8 @@ export async function createPaymentPayload(args) {
|
|
|
171
178
|
else {
|
|
172
179
|
throw new Error(`unsupported scheme ${selected.scheme} on ${selected.network}`);
|
|
173
180
|
}
|
|
174
|
-
|
|
181
|
+
const payload = await client.createPaymentPayload(required);
|
|
182
|
+
return { payload, ...(gasfreeEstimate ? { gasfreeEstimate } : {}) };
|
|
175
183
|
}
|
|
176
184
|
throw new Error(`unsupported network ${selected.network}`);
|
|
177
185
|
}
|
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.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@bankofai/x402-core": "1.0.1-beta.4",
|
|
24
24
|
"@bankofai/x402-evm": "1.0.1-beta.4",
|
|
25
|
-
"@bankofai/x402-gateway": "1.0.1-beta.
|
|
25
|
+
"@bankofai/x402-gateway": "1.0.1-beta.5",
|
|
26
26
|
"@bankofai/x402-tron": "1.0.1-beta.4",
|
|
27
27
|
"tronweb": "6.4.0",
|
|
28
28
|
"viem": "^2.55.0",
|
|
@@ -32,5 +32,8 @@
|
|
|
32
32
|
"@types/node": "^24.10.1",
|
|
33
33
|
"tsx": "^4.20.6",
|
|
34
34
|
"typescript": "^5.9.3"
|
|
35
|
+
},
|
|
36
|
+
"overrides": {
|
|
37
|
+
"ws": "8.21.0"
|
|
35
38
|
}
|
|
36
39
|
}
|