Skip to content

Commit b996e32

Browse files
authored
Merge pull request #1041 from tronprotocol/release_v4.15.1
Release v4.15.1 follow-up x402 issue
2 parents 178b894 + 7e6aa56 commit b996e32

17 files changed

Lines changed: 451 additions & 19 deletions

‎.gitignore‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,9 @@ graphify-out/
2222
# Scoped registry credentials for the private @sun-sdk packages; local-only until the SDK
2323
# is published to public npm (see ts/docs/superpowers/specs, D6).
2424
ts/.npmrc
25+
26+
# Codex
27+
.codex/
28+
29+
# MCP
30+
.mcp.json

‎ts/.gitignore‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ docs/superpowers
88
docs/qa
99
docs/adr
1010
CONTEXT.md
11-
.boundary
11+
.boundary
12+
audit-claude/
13+
.codegraph/

‎ts/docs/commands/x402/pay.md‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ wallet-cli x402 pay <url> [--method <m>] [--header "Name: value"]... [--body <s>
1616

1717
Sends the request. If the endpoint answers with a successful status (2xx), that response is returned as-is and nothing is paid; any other status except `402` fails with `provider_error`, carrying `httpStatus` and `phase: "request"`. If it answers `402 Payment Required`, `pay` reads the payment routes it offers, picks one that matches the selected `--network` and your filters, signs a payment authorization with the active account (or `--account`), and sends the request again with it. The endpoint's facilitator settles the payment on chain.
1818

19-
**Nothing is signed until a route matches.** Routes on other networks are ignored; `--token`, `--asset` and `--scheme` narrow the choice further, and `--max-amount` (whole tokens) or `--max-raw-amount` (smallest units) rules out a route priced above it. If no route matches, the command fails with `no_matching_requirement`; if the matching route is priced over the limit, with `amount_exceeds_limit`. Both are raised before signing, with `paymentStatus: "not_sent"`, and no password is asked for — with or without `--dry-run`. Without a limit of your own, a built-in ceiling of **$1 per payment** applies to the known stablecoins; a route priced above it also fails with `amount_exceeds_limit`, and passing `--max-amount` or `--max-raw-amount` replaces that ceiling with yours.
19+
**Nothing is signed until a route matches.** Routes on other networks are ignored; `--token`, `--asset` and `--scheme` narrow the choice further, and `--max-amount` (whole tokens) or `--max-raw-amount` (smallest units) rules out a route priced above it. If no route matches, the command fails with `no_matching_requirement`; if the matching route is priced over the limit, with `amount_exceeds_limit`. Both are raised before signing, with `paymentStatus: "not_sent"`, and no password is asked for — with or without `--dry-run`. A raw amount has no unit, so `--max-raw-amount` requires `--token` or `--asset` to name the token it counts (`invalid_option` otherwise). Without a limit of your own, only the known stablecoins are accepted, under a built-in ceiling of **$1 per payment**; a route priced above it also fails with `amount_exceeds_limit`. A limit of your own replaces that ceiling and the stablecoin restriction, but other tokens are still refused: `--max-amount` needs a token whose decimals are known (or `--asset` with `--decimals`), and `--max-raw-amount` pays only the token you pinned.
2020

2121
**Two payment schemes:**
2222

@@ -52,7 +52,7 @@ Requires an account, even for an endpoint that turns out to be free. The master
5252
| `--body <string>` | Request body; excludes `--body-file` |
5353
| `--body-file <path>` | Read the request body from a file, or from stdin with `-`; excludes `--body` |
5454
| `--max-amount <n>` | Refuse a route priced above this, in whole tokens; excludes `--max-raw-amount` |
55-
| `--max-raw-amount <n>` | The same limit in smallest units |
55+
| `--max-raw-amount <n>` | The same limit in smallest units; requires `--token` or `--asset`, since a raw amount has no unit |
5656
| `--token <symbol>` | Accept only routes paying in this token |
5757
| `--asset <address>` | Accept only routes paying in this token contract |
5858
| `--decimals <n>` | Decimals of the `--asset` token; requires `--asset` |
@@ -114,6 +114,8 @@ URL https://x402-gateway.bankofai.io/providers/dia-price-tron/v1/quotat
114114
Status 200
115115
Settled Yes
116116
Delivered Yes
117+
Amount 0.000001 (1 base units)
118+
Asset USDT
117119
From TWer2Ygk5TEheHp3TPuYeqxmB6SsGZmaL6
118120
Transaction 9b41c7e2d05f83a6e1c4b8d27f9a03e5c6d8b1f4a2e7c9d0b3f5a8e1c6d2b7f4
119121
Output btc-quote.json
@@ -124,10 +126,10 @@ printf '%s' "$PW" | wallet-cli x402 pay https://x402-gateway.bankofai.io/provide
124126
```
125127

126128
```json
127-
{"schema":"wallet-cli.result.v1","success":true,"command":"x402.pay","data":{"url":"https://x402-gateway.bankofai.io/providers/dia-price-tron/v1/quotation/BTC","status":200,"delivered":true,"settled":true,"payer":{"address":"TWer2Ygk5TEheHp3TPuYeqxmB6SsGZmaL6"},"paymentResponse":{"success":true,"transaction":"9b41c7e2d05f83a6e1c4b8d27f9a03e5c6d8b1f4a2e7c9d0b3f5a8e1c6d2b7f4","network":"tron:0x2b6653dc","payer":"0xe2e1a54926527fbb4e4420de4c6bab82beaee24d"},"output":{"path":"btc-quote.json","bytes":214}},"meta":{"durationMs":6412,"warnings":[]},"chain":{"family":"tron","network":"tron:728126428","chainId":"728126428"}}
129+
{"schema":"wallet-cli.result.v1","success":true,"command":"x402.pay","data":{"url":"https://x402-gateway.bankofai.io/providers/dia-price-tron/v1/quotation/BTC","status":200,"delivered":true,"settled":true,"payer":{"address":"TWer2Ygk5TEheHp3TPuYeqxmB6SsGZmaL6"},"paymentResponse":{"success":true,"transaction":"9b41c7e2d05f83a6e1c4b8d27f9a03e5c6d8b1f4a2e7c9d0b3f5a8e1c6d2b7f4","network":"tron:0x2b6653dc","payer":"0xe2e1a54926527fbb4e4420de4c6bab82beaee24d"},"output":{"path":"btc-quote.json","bytes":214},"payment":{"asset":"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t","token":"USDT","decimals":6,"amount":"0.000001","rawAmount":"1"}},"meta":{"durationMs":6412,"warnings":[]},"chain":{"family":"tron","network":"tron:728126428","chainId":"728126428"}}
128130
```
129131

130-
`Settled Yes` means the payment went through on chain; `Delivered Yes` means the paid response arrived and was written to `btc-quote.json`. `From` is the paying account and `Transaction` the payment's transaction ID (JSON: `data.payer.address` and `data.paymentResponse.transaction`). The facilitator submits the payment transaction and pays its energy, so the payment itself costs the account only the quoted price. This account already had a Permit2 allowance; on a first payment without one, see the Permit2 approve note above.
132+
`Settled Yes` means the payment went through on chain; `Delivered Yes` means the paid response arrived and was written to `btc-quote.json`. `Amount` and `Asset` are what was paid (JSON: `data.payment`). `From` is the paying account and `Transaction` the payment's transaction ID (JSON: `data.payer.address` and `data.paymentResponse.transaction`). The facilitator submits the payment transaction and pays its energy, so the payment itself costs the account only the quoted price. This account already had a Permit2 allowance; on a first payment without one, see the Permit2 approve note above.
131133

132134
## Output
133135

@@ -138,6 +140,7 @@ printf '%s' "$PW" | wallet-cli x402 pay https://x402-gateway.bankofai.io/provide
138140
| `delivered` | boolean | Whether the final response was successful (2xx) |
139141
| `settled` | boolean | Whether a valid settlement receipt for the selected network came back |
140142
| `payer` | object | `{address}` of the paying account; present when a payment was signed |
143+
| `payment` | object | What the signed payment pays, present when one was created: `asset` (token contract), `token` (symbol, when the token is registered), `decimals` and `amount` (whole tokens, when the decimals are known), and `rawAmount` (smallest units) |
141144
| `paymentResponse` | object | The facilitator's settlement receipt, when the endpoint sent one: `success`, `transaction` (the payment's transaction ID), `network`, and `payer` — both as the facilitator writes them, e.g. `tron:0xcd8690dc` and a hex address |
142145
| `approval` | object | TRON only, when the payment needed a one-time Permit2 approve: `{txId, token, spender, allowance: "unlimited", feeLimitSun, status}`. `status` is `confirmed` (broadcast and mined before the payment was signed) or `exported` (signed into the payment package for the endpoint to sponsor). The same object appears in `error.details.approval` when anything after the approve fails |
143146
| `response` | any | The response body — parsed JSON, or text; absent with `--out` |
@@ -146,7 +149,7 @@ printf '%s' "$PW" | wallet-cli x402 pay https://x402-gateway.bankofai.io/provide
146149

147150
## Exit status
148151

149-
`0` success, including a free response · `1` execution failure (`no_matching_requirement`, `amount_exceeds_limit` — both before signing, `paymentStatus: "not_sent"`; `gasfree_insufficient_balance` / `gasfree_not_activated`, `permit2_allowance_required` / `approval_reset_required`, `fee_cap_exceeded`, `payer_mismatch`, `invalid_settlement`, `invalid_x402_response`, `response_too_large`, `provider_rate_limited` — HTTP 429; `provider_error` — any other failed request or payment, with `error.details.phase` and `httpStatus`; `missing_wallet_address`; `auth_required` — a payment needs signing and no `--password-stdin` was given; `auth_failed`, `timeout`) · `2` usage error (`output_exists` — the `--out` file exists; `gasfree_credentials_missing`; `invalid_option` — a binary response without `--out`, two exclusive options, `--decimals` without `--asset`, or `--wait` / `--wait-timeout`, which this command does not take because the facilitator settles the payment; `invalid_amount` — a limit that is not a positive amount; `invalid_value` — e.g. a malformed `--header`, an unsupported `--method`, or a `--gasfree-relay` URL that is not HTTPS).
152+
`0` success, including a free response · `1` execution failure (`no_matching_requirement`, `amount_exceeds_limit` — both before signing, `paymentStatus: "not_sent"`; `gasfree_insufficient_balance` / `gasfree_not_activated`, `permit2_allowance_required` / `approval_reset_required`, `fee_cap_exceeded`, `payer_mismatch`, `invalid_settlement`, `invalid_x402_response`, `response_too_large`, `provider_rate_limited` — HTTP 429; `provider_error` — any other failed request or payment, with `error.details.phase` and `httpStatus`; `missing_wallet_address`; `auth_required` — a payment needs signing and no `--password-stdin` was given; `auth_failed`, `timeout`) · `2` usage error (`output_exists` — the `--out` file exists; `gasfree_credentials_missing`; `invalid_option` — a binary response without `--out`, two exclusive options, `--decimals` without `--asset`, `--max-raw-amount` without `--token` or `--asset`, or `--wait` / `--wait-timeout`, which this command does not take because the facilitator settles the payment; `invalid_amount` — a limit that is not a positive amount; `invalid_value` — e.g. a malformed `--header`, an unsupported `--method`, or a `--gasfree-relay` URL that is not HTTPS).
150153

151154
## See also
152155

‎ts/src/adapters/inbound/cli/commands/r4-validation.test.ts‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,34 @@ describe("R4 payment input contracts", () => {
1212
it.each([
1313
[{ maxAmount: "0" }, "invalid_amount"],
1414
[{ maxAmount: "1e3" }, "invalid_amount"],
15-
[{ maxRawAmount: "0" }, "invalid_amount"],
16-
[{ maxRawAmount: (1n << 256n).toString() }, "invalid_amount"],
17-
[{ maxAmount: "1", maxRawAmount: "1" }, "invalid_option"],
15+
[{ maxRawAmount: "0", token: "USDT" }, "invalid_amount"],
16+
[{ maxRawAmount: (1n << 256n).toString(), token: "USDT" }, "invalid_amount"],
17+
[{ maxAmount: "1", maxRawAmount: "1", token: "USDT" }, "invalid_option"],
1818
[{ body: "", bodyFile: "body.json" }, "invalid_option"],
1919
[{ maxGasfreeFee: "1", maxGasfreeFeeRaw: "1" }, "invalid_option"],
2020
])("rejects %j with %s", (input, code) => {
2121
expect(() => parse("pay", { url: "https://example.test", ...input })).toThrow(
2222
expect.objectContaining({ code }),
2323
);
2424
});
25+
it("rejects a raw payment limit that names no token, before anything is requested", () => {
26+
expect(() =>
27+
parse("pay", { url: "https://example.test", maxRawAmount: "1000000000000000000" }),
28+
).toThrow(
29+
expect.objectContaining({
30+
code: "invalid_option",
31+
message: expect.stringContaining("--max-raw-amount: requires --token or --asset"),
32+
}),
33+
);
34+
});
35+
it.each([{ token: "USDT" }, { asset: "0x55d398326f99059fF775485246999027B3197955" }])(
36+
"accepts a raw payment limit pinned by %j",
37+
(pin) => {
38+
expect(
39+
parse("pay", { url: "https://example.test", maxRawAmount: "1000000", ...pin }),
40+
).toMatchObject({ maxRawAmount: "1000000", ...pin });
41+
},
42+
);
2543
it.each(["1e3", "1.5", "-1"])("rejects port literal %s", (port) => {
2644
expect(() => parse("serve", { payTo: "x", port })).toThrow();
2745
});

‎ts/src/adapters/inbound/cli/commands/x402.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ describe("x402 command surface", () => {
5353
header: [],
5454
maxAmount: "1",
5555
maxRawAmount: "1",
56+
token: "USDT",
5657
}).success,
5758
).toBe(false);
5859
});

‎ts/src/adapters/inbound/cli/commands/x402.ts‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ const payFields = z.object({
3434
decimals: integerLiteral(0, 18).optional().describe("decimals for an explicit asset"),
3535
scheme: z.enum(["exact", "exact_gasfree"]).optional(),
3636
maxAmount: paymentAmount.optional().describe("maximum payment in whole tokens"),
37-
maxRawAmount: rawPaymentAmount.optional().describe("maximum payment in smallest units"),
37+
maxRawAmount: rawPaymentAmount
38+
.optional()
39+
.describe("maximum payment in smallest units; requires --token or --asset"),
3840
dryRun: z.boolean().default(false).describe("inspect the payment challenge without signing"),
3941
gasfreeRelay: z
4042
.string()
@@ -60,6 +62,16 @@ const payInput = payFields.superRefine((value, context) => {
6062
message: "cannot be combined with --max-raw-amount",
6163
});
6264
}
65+
// A raw limit has no unit: unpinned, it would cap whatever token the seller names.
66+
if (value.maxRawAmount !== undefined && !value.token && !value.asset) {
67+
context.addIssue({
68+
code: "custom",
69+
params: { errorCode: "invalid_option" },
70+
path: ["maxRawAmount"],
71+
message:
72+
"requires --token or --asset; a raw limit has no unit, so pin it to one token or use --max-amount",
73+
});
74+
}
6375
if (value.body !== undefined && value.bodyFile !== undefined) {
6476
context.addIssue({
6577
code: "custom",

‎ts/src/adapters/inbound/cli/render/x402.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,28 @@ it("separates response text while output-file receipts omit body", () => {
7272
"--- response ---",
7373
);
7474
});
75+
it("shows the paid asset and amount after a payment", () => {
76+
const rendered = paymentText({
77+
status: 200,
78+
settled: true,
79+
payment: {
80+
asset: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
81+
token: "USDT",
82+
decimals: 6,
83+
amount: "0.01",
84+
rawAmount: "10000",
85+
},
86+
});
87+
expect(rendered).toMatch(/^Amount\s+0\.01 \(10000 base units\)$/m);
88+
expect(rendered).toMatch(/^Asset\s+USDT$/m);
89+
const unregistered = paymentText({
90+
status: 200,
91+
payment: { asset: "0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c", rawAmount: "5" },
92+
});
93+
expect(unregistered).toMatch(/^Amount\s+5 base units$/m);
94+
expect(unregistered).toMatch(/^Asset\s+0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c$/m);
95+
expect(paymentText({ status: 200 })).not.toMatch(/Amount|Asset/);
96+
});
7597
it("wraps a wide-character summary with aligned continuation lines", () => {
7698
const original = Object.getOwnPropertyDescriptor(process.stdout, "columns");
7799
Object.defineProperty(process.stdout, "columns", { value: 32, configurable: true });

‎ts/src/adapters/inbound/cli/render/x402.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,16 @@ export function providerEndpointsText(value: unknown): string {
104104
);
105105
}
106106
export function paymentText(value: unknown): string {
107-
const p = asObj(value);
107+
const p = asObj(value),
108+
paid = asObj(p.payment);
109+
const raw = typeof paid.rawAmount === "string" ? `${text(paid.rawAmount)} base units` : "";
108110
const details = query([
109111
["URL", text(p.url)],
110112
["Status", text(p.status)],
111113
["Settled", p.settled === true ? "Yes" : "No"],
112114
["Delivered", p.delivered === true ? "Yes" : "No"],
115+
["Amount", typeof paid.amount === "string" && raw ? `${text(paid.amount)} (${raw})` : raw],
116+
["Asset", text(paid.token ?? paid.asset)],
113117
["From", text(asObj(p.payer).address)],
114118
["Transaction", text(asObj(p.paymentResponse).transaction)],
115119
["Approval", text(asObj(p.approval).txId)],

‎ts/src/adapters/outbound/x402/nile-compatibility.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ it("uses the selected relay to construct and sign the final GasFree permit", asy
165165
scheme: "exact_gasfree",
166166
gasfreeRelay: "https://relay.example/nile",
167167
maxRawAmount: "10000",
168+
asset,
168169
},
169170
),
170171
).resolves.toMatchObject({ delivered: true });

‎ts/src/adapters/outbound/x402/payment-client.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,7 @@ it.each(["tron:3448148188", "tron:0xcd8690dc"])(
594594
);
595595

596596
it.each([
597-
[{ maxRawAmount: "99" }, "amount_exceeds_limit"],
597+
[{ maxRawAmount: "99", token: "USDT" }, "amount_exceeds_limit"],
598598
[{ maxAmount: "0.000000000000000099" }, "amount_exceeds_limit"],
599599
[{ token: "USDD" }, "no_matching_requirement"],
600600
[{ scheme: "exact_gasfree" as const }, "no_matching_requirement"],

0 commit comments

Comments
 (0)