Skip to content

Commit 4e8a2ad

Browse files
committed
fix(wallet): filter malformed payTo addresses in server payment options
Servers returning Solana payment options with EVM-format payTo addresses (0x...) caused a base58 decode crash. Added address validation policy that filters out network/address mismatches and falls back to valid options. When all options are malformed, shows a clear error instead of a cryptic codec failure.
1 parent beeffa0 commit 4e8a2ad

File tree

4 files changed

+96
-2
lines changed

4 files changed

+96
-2
lines changed

packages/x402-proxy/CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.8.4] - 2026-03-24
11+
12+
### Fixed
13+
- Servers returning Solana payment options with EVM-format `payTo` addresses (e.g. `0x...`) no longer crash with a base58 decode error - malformed options are filtered out and valid options are used instead
14+
- When all payment options from a server have mismatched address formats, a clear error is shown instead of a cryptic codec failure
15+
1016
## [0.8.3] - 2026-03-24
1117

1218
### Added
@@ -230,7 +236,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
230236
- `appendHistory` / `readHistory` / `calcSpend` - JSONL transaction history
231237
- Re-exports from `@x402/fetch`, `@x402/svm`, `@x402/evm`
232238

233-
[Unreleased]: https://github.com/cascade-protocol/x402-proxy/compare/v0.8.3...HEAD
239+
[Unreleased]: https://github.com/cascade-protocol/x402-proxy/compare/v0.8.4...HEAD
240+
[0.8.4]: https://github.com/cascade-protocol/x402-proxy/compare/v0.8.3...v0.8.4
234241
[0.8.3]: https://github.com/cascade-protocol/x402-proxy/compare/v0.8.2...v0.8.3
235242
[0.8.2]: https://github.com/cascade-protocol/x402-proxy/compare/v0.8.1...v0.8.2
236243
[0.8.1]: https://github.com/cascade-protocol/x402-proxy/compare/v0.8.0...v0.8.1

packages/x402-proxy/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "x402-proxy",
3-
"version": "0.8.3",
3+
"version": "0.8.4",
44
"description": "curl for x402 paid APIs. Auto-pays any endpoint on Base, Solana, and Tempo. Also works as an OpenClaw plugin.",
55
"type": "module",
66
"sideEffects": false,

packages/x402-proxy/src/lib/resolve-wallet.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { PaymentRequirements } from "@x402/fetch";
33
import { describe, expect, it } from "vitest";
44
import { deriveSolanaKeypair } from "./derive.js";
55
import {
6+
createAddressValidationPolicy,
67
createNetworkFilter,
78
createNetworkPreference,
89
networkToCaipPrefix,
@@ -123,6 +124,63 @@ describe("createNetworkPreference", () => {
123124
});
124125
});
125126

127+
// --- createAddressValidationPolicy ---
128+
129+
describe("createAddressValidationPolicy", () => {
130+
const policy = createAddressValidationPolicy();
131+
132+
const validBaseReq = makeReq({
133+
network: "eip155:8453",
134+
payTo: "0x2cA6f53D5Fbc89d9a0658AEc0352a453ac991EC1",
135+
});
136+
const validSolanaReq = makeReq({
137+
network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
138+
payTo: "AepWpq3GQwL8CeKMtZyKtKPa7W91Coygh3ropAJapVdU",
139+
});
140+
const malformedSolanaReq = makeReq({
141+
network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
142+
payTo: "0x2cA6f53D5Fbc89d9a0658AEc0352a453ac991EC1",
143+
});
144+
const malformedEvmReq = makeReq({
145+
network: "eip155:8453",
146+
payTo: "AepWpq3GQwL8CeKMtZyKtKPa7W91Coygh3ropAJapVdU",
147+
});
148+
149+
it("passes valid EVM and Solana options", () => {
150+
expect(policy(2, [validBaseReq, validSolanaReq])).toEqual([validBaseReq, validSolanaReq]);
151+
});
152+
153+
it("filters out Solana option with EVM payTo", () => {
154+
expect(policy(2, [validBaseReq, malformedSolanaReq])).toEqual([validBaseReq]);
155+
});
156+
157+
it("filters out EVM option with Solana payTo", () => {
158+
expect(policy(2, [malformedEvmReq, validSolanaReq])).toEqual([validSolanaReq]);
159+
});
160+
161+
it("falls back to valid option when first is malformed", () => {
162+
expect(policy(2, [malformedSolanaReq, validBaseReq])).toEqual([validBaseReq]);
163+
});
164+
165+
it("throws with clear message when all options are malformed", () => {
166+
expect(() => policy(2, [malformedSolanaReq])).toThrow(
167+
"Server returned only malformed payment options",
168+
);
169+
expect(() => policy(2, [malformedSolanaReq])).toThrow("Solana option has EVM-format payTo");
170+
});
171+
172+
it("throws listing all malformed reasons when multiple bad options", () => {
173+
expect(() => policy(2, [malformedSolanaReq, malformedEvmReq])).toThrow(
174+
"payTo addresses don't match the advertised networks",
175+
);
176+
});
177+
178+
it("passes through unknown network types unchanged", () => {
179+
const unknownReq = makeReq({ network: "cosmos:cosmoshub-4", payTo: "cosmos1xyz" });
180+
expect(policy(2, [unknownReq])).toEqual([unknownReq]);
181+
});
182+
});
183+
126184
// --- resolveWallet with --solana-key ---
127185

128186
describe("resolveWallet with solana key", () => {

packages/x402-proxy/src/lib/resolve-wallet.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,33 @@ export function networkToCaipPrefix(name: string): string {
125125
}
126126
}
127127

128+
/**
129+
* Validate that payTo addresses match the network format.
130+
* Filters out malformed entries (e.g. EVM hex address on a Solana network).
131+
*/
132+
export function createAddressValidationPolicy(): PaymentPolicy {
133+
return (_version, reqs) => {
134+
const malformed: string[] = [];
135+
const valid = reqs.filter((r) => {
136+
if (r.network.startsWith("solana:") && r.payTo.startsWith("0x")) {
137+
malformed.push(`Solana option has EVM-format payTo (${r.payTo})`);
138+
return false;
139+
}
140+
if (r.network.startsWith("eip155:") && !r.payTo.startsWith("0x")) {
141+
malformed.push(`EVM option has non-EVM payTo (${r.payTo})`);
142+
return false;
143+
}
144+
return true;
145+
});
146+
if (valid.length === 0 && malformed.length > 0) {
147+
throw new Error(
148+
`Server returned only malformed payment options:\n ${malformed.join("\n ")}\nThe server's payTo addresses don't match the advertised networks.`,
149+
);
150+
}
151+
return valid;
152+
};
153+
}
154+
128155
export function createNetworkFilter(network: string): PaymentPolicy {
129156
const prefix = networkToCaipPrefix(network);
130157
return (_version, reqs) => {
@@ -179,6 +206,8 @@ export async function buildX402Client(
179206
registerExactSvmScheme(client, { signer });
180207
}
181208

209+
client.registerPolicy(createAddressValidationPolicy());
210+
182211
if (opts?.network) {
183212
client.registerPolicy(createNetworkFilter(opts.network));
184213
}

0 commit comments

Comments
 (0)