Skip to content

Commit e8d54a0

Browse files
committed
fix(x402): validate server payment requirements in pay command before signing
The pay CLI signed the server-supplied 402 payment requirements directly via signX402Payment without validation. Apply the existing validatePaymentRequirements guard (now exported from x402-payment) on the CLI path before signing: reject burn/zero payTo addresses and, with the new --max-amount flag, reject amounts above the caller's cap. Adds focused tests for the guard.
1 parent 1a87df8 commit e8d54a0

3 files changed

Lines changed: 65 additions & 3 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { describe, expect, it } from "vitest"
2+
import {
3+
type PaymentRequirements,
4+
validatePaymentRequirements,
5+
} from "../lib/client/x402-payment.js"
6+
7+
const base: PaymentRequirements = {
8+
scheme: "exact",
9+
network: "base",
10+
maxAmountRequired: "1000000",
11+
payTo: "0x1111111111111111111111111111111111111111",
12+
// USDC on Base; validatePaymentRequirements rejects any other asset when
13+
// allowedAssets is not provided.
14+
asset: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
15+
}
16+
17+
describe("validatePaymentRequirements", () => {
18+
it("passes for a well-formed requirement", () => {
19+
expect(() => validatePaymentRequirements(base, {})).not.toThrow()
20+
})
21+
22+
it("rejects a burn/zero payTo address", () => {
23+
expect(() =>
24+
validatePaymentRequirements(
25+
{ ...base, payTo: "0x0000000000000000000000000000000000000000" },
26+
{},
27+
),
28+
).toThrow(/burn\/zero address/)
29+
})
30+
31+
it("rejects when the server amount exceeds maxAmount", () => {
32+
expect(() =>
33+
validatePaymentRequirements(base, { maxAmount: "999999" }),
34+
).toThrow(/maxAmount/)
35+
})
36+
37+
it("passes when the server amount is within maxAmount", () => {
38+
expect(() =>
39+
validatePaymentRequirements(base, { maxAmount: "1000000" }),
40+
).not.toThrow()
41+
})
42+
})

src/cli/commands/pay.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { Command } from "commander"
22
import pc from "picocolors"
33
import type { PaymentRequirements } from "../../lib/client/x402-payment.js"
4-
import { signX402Payment } from "../../lib/client/x402-payment.js"
4+
import {
5+
signX402Payment,
6+
validatePaymentRequirements,
7+
} from "../../lib/client/x402-payment.js"
58
import {
69
createWalletForProvider,
710
createWalletFromEnv,
@@ -14,6 +17,7 @@ import { readInput } from "./read-input.js"
1417
interface PayOptions {
1518
body?: string
1619
walletProvider?: string
20+
maxAmount?: string
1721
}
1822

1923
export const payCommand = new Command("pay")
@@ -26,6 +30,10 @@ export const payCommand = new Command("pay")
2630
"--wallet-provider <provider>",
2731
`Wallet provider: ${WALLET_PROVIDERS.join(", ")}`,
2832
)
33+
.option(
34+
"--max-amount <atomic>",
35+
"Maximum payment amount in atomic units the CLI will sign; the server's 402 requirements are rejected if they exceed it",
36+
)
2937
.action(async (url: string, options: PayOptions) => {
3038
let adapter: WalletAdapter
3139
try {
@@ -65,13 +73,14 @@ export const payCommand = new Command("pay")
6573
process.exit(1)
6674
}
6775

68-
await runPaymentOnly(url, inputBody, adapter)
76+
await runPaymentOnly(url, inputBody, adapter, options.maxAmount)
6977
})
7078

7179
async function runPaymentOnly(
7280
url: string,
7381
inputBody: string,
7482
adapter: WalletAdapter,
83+
maxAmount?: string,
7584
): Promise<void> {
7685
console.log(pc.cyan("Probing endpoint for payment requirements..."))
7786

@@ -125,6 +134,17 @@ async function runPaymentOnly(
125134
console.log(` Pay To: ${requirements.payTo}`)
126135
console.log(` Asset: ${requirements.asset}`)
127136

137+
try {
138+
validatePaymentRequirements(requirements, { maxAmount })
139+
} catch (err) {
140+
console.error(
141+
pc.red(
142+
`Refusing to sign: ${err instanceof Error ? err.message : String(err)}`,
143+
),
144+
)
145+
process.exit(1)
146+
}
147+
128148
console.log(pc.cyan("\nSigning EIP-3009 transferWithAuthorization..."))
129149

130150
const xPayment = await signX402Payment({

src/lib/client/x402-payment.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ export async function paidFetch(
233233
return paidRes
234234
}
235235

236-
function validatePaymentRequirements(
236+
export function validatePaymentRequirements(
237237
reqs: PaymentRequirements,
238238
opts: {
239239
maxAmount?: string

0 commit comments

Comments
 (0)