@x402r/evm 0.0.1 → 0.0.2

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.
@@ -6,62 +6,125 @@
6
6
  * on an x402ResourceServer via server.register('eip155:84532', new EscrowServerScheme()).
7
7
  */
8
8
 
9
- import type { EscrowExtra } from "../../shared/types.js";
9
+ import type {
10
+ AssetAmount,
11
+ MoneyParser,
12
+ Network,
13
+ PaymentRequirements,
14
+ Price,
15
+ SchemeNetworkServer,
16
+ } from "@x402/core/types";
17
+ import { x402ResourceServer } from "@x402/core/server";
10
18
 
11
19
  /**
12
- * x402 PaymentRequirements (matches @x402/core/types PaymentRequirements)
20
+ * Asset info including EIP-712 domain parameters per network
13
21
  */
14
- export interface PaymentRequirements {
15
- scheme: string;
16
- network: string;
17
- amount: string;
18
- asset: string;
19
- payTo: string;
20
- maxTimeoutSeconds: number;
21
- extra: Record<string, unknown>;
22
- }
23
-
24
- export interface SupportedKind {
25
- x402Version: number;
26
- scheme: string;
27
- network: string;
28
- extra?: Record<string, unknown>;
29
- }
22
+ const ASSET_INFO: Record<
23
+ string,
24
+ { address: string; name: string; version: string; decimals: number }
25
+ > = {
26
+ // Base Sepolia
27
+ "eip155:84532": {
28
+ address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
29
+ name: "USDC",
30
+ version: "2",
31
+ decimals: 6,
32
+ },
33
+ // Base mainnet
34
+ "eip155:8453": {
35
+ address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
36
+ name: "USD Coin",
37
+ version: "2",
38
+ decimals: 6,
39
+ },
40
+ // Ethereum Sepolia
41
+ "eip155:11155111": {
42
+ address: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
43
+ name: "USDC",
44
+ version: "2",
45
+ decimals: 6,
46
+ },
47
+ // Ethereum mainnet
48
+ "eip155:1": {
49
+ address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
50
+ name: "USD Coin",
51
+ version: "2",
52
+ decimals: 6,
53
+ },
54
+ // Polygon
55
+ "eip155:137": {
56
+ address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
57
+ name: "USD Coin",
58
+ version: "2",
59
+ decimals: 6,
60
+ },
61
+ // Arbitrum
62
+ "eip155:42161": {
63
+ address: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
64
+ name: "USD Coin",
65
+ version: "2",
66
+ decimals: 6,
67
+ },
68
+ // Celo
69
+ "eip155:42220": {
70
+ address: "0xcebA9300f2b948710d2653dD7B07f33A8B32118C",
71
+ name: "USD Coin",
72
+ version: "2",
73
+ decimals: 6,
74
+ },
75
+ // Monad
76
+ "eip155:143": {
77
+ address: "0x754704Bc059F8C67012fEd69BC8A327a5aafb603",
78
+ name: "USDC",
79
+ version: "2",
80
+ decimals: 6,
81
+ },
82
+ // Avalanche
83
+ "eip155:43114": {
84
+ address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
85
+ name: "USD Coin",
86
+ version: "2",
87
+ decimals: 6,
88
+ },
89
+ };
30
90
 
31
91
  /**
32
- * x402 AssetAmount (matches @x402/core/types AssetAmount)
92
+ * Convert decimal amount to token units using string-based conversion
93
+ * (e.g., 0.10 -> 100000 for 6-decimal tokens)
94
+ * Avoids floating-point precision issues from BigInt(Math.round(...))
33
95
  */
34
- export interface AssetAmount {
35
- asset: string;
36
- amount: string;
37
- extra?: Record<string, unknown>;
96
+ function convertToTokenAmount(decimalAmount: string, decimals: number): string {
97
+ const amount = parseFloat(decimalAmount);
98
+ if (isNaN(amount)) {
99
+ throw new Error(`Invalid amount: ${decimalAmount}`);
100
+ }
101
+ const [intPart, decPart = ""] = String(amount).split(".");
102
+ const paddedDec = decPart.padEnd(decimals, "0").slice(0, decimals);
103
+ const tokenAmount = (intPart + paddedDec).replace(/^0+/, "") || "0";
104
+ return tokenAmount;
38
105
  }
39
106
 
40
- /**
41
- * x402 Price type (matches @x402/core/types Price)
42
- */
43
- export type Price = string | number | AssetAmount;
44
-
45
- export type Network = `${string}:${string}`;
46
-
47
- /**
48
- * Known USDC addresses per network
49
- */
50
- const USDC_ADDRESSES: Record<string, string> = {
51
- "eip155:84532": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
52
- "eip155:8453": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
53
- };
54
-
55
107
  /**
56
108
  * Server scheme - handles price parsing and requirement enhancement.
57
109
  * Implements x402's SchemeNetworkServer interface.
58
110
  */
59
- export class EscrowServerScheme {
111
+ export class EscrowServerScheme implements SchemeNetworkServer {
60
112
  readonly scheme = "escrow";
61
- private readonly decimals: number;
113
+ private moneyParsers: MoneyParser[] = [];
62
114
 
63
- constructor(config?: { decimals?: number }) {
64
- this.decimals = config?.decimals ?? 6; // USDC default
115
+ /**
116
+ * Register a custom money parser in the parser chain.
117
+ * Multiple parsers can be registered — they will be tried in registration order.
118
+ * Each parser receives a decimal amount (e.g., 1.50 for $1.50).
119
+ * If a parser returns null, the next parser in the chain will be tried.
120
+ * The default parser (USDC) is always the final fallback.
121
+ *
122
+ * @param parser - Custom function to convert amount to AssetAmount (or null to skip)
123
+ * @returns The server instance for chaining
124
+ */
125
+ registerMoneyParser(parser: MoneyParser): EscrowServerScheme {
126
+ this.moneyParsers.push(parser);
127
+ return this;
65
128
  }
66
129
 
67
130
  /**
@@ -73,38 +136,75 @@ export class EscrowServerScheme {
73
136
  * - AssetAmount: { asset: "0x...", amount: "10000" }
74
137
  */
75
138
  async parsePrice(price: Price, network: Network): Promise<AssetAmount> {
76
- // If already an AssetAmount, pass through
139
+ // If already an AssetAmount, pass through with validation
77
140
  if (
78
141
  typeof price === "object" &&
79
142
  price !== null &&
80
- "amount" in price &&
81
- "asset" in price
143
+ "amount" in price
82
144
  ) {
83
- return price as AssetAmount;
145
+ if (!price.asset) {
146
+ throw new Error(
147
+ `Asset address must be specified for AssetAmount on network ${network}`,
148
+ );
149
+ }
150
+ return {
151
+ amount: price.amount,
152
+ asset: price.asset,
153
+ extra: price.extra || {},
154
+ };
84
155
  }
85
156
 
86
- // Convert to number for calculation
87
- let numericAmount: number;
88
- if (typeof price === "number") {
89
- numericAmount = price;
90
- } else {
91
- const cleaned = String(price).replace(/[$,]/g, "").trim();
92
- numericAmount = parseFloat(cleaned);
157
+ // Parse Money to decimal number
158
+ const numericAmount = this.parseMoneyToDecimal(price);
159
+
160
+ // Try each custom money parser in order
161
+ for (const parser of this.moneyParsers) {
162
+ const result = await parser(numericAmount, network);
163
+ if (result !== null) {
164
+ return result;
165
+ }
93
166
  }
94
167
 
95
- if (isNaN(numericAmount)) {
96
- throw new Error(`Cannot parse price: ${price}`);
168
+ // All custom parsers returned null (or none registered), use default conversion
169
+ return this.defaultMoneyConversion(numericAmount, network);
170
+ }
171
+
172
+ /**
173
+ * Parse Money (string | number) to a decimal number.
174
+ */
175
+ private parseMoneyToDecimal(money: string | number): number {
176
+ if (typeof money === "number") {
177
+ return money;
178
+ }
179
+ const cleaned = String(money).replace(/[$,]/g, "").trim();
180
+ const amount = parseFloat(cleaned);
181
+ if (isNaN(amount)) {
182
+ throw new Error(`Cannot parse price: ${money}`);
97
183
  }
184
+ return amount;
185
+ }
98
186
 
99
- const rawAmount = BigInt(Math.round(numericAmount * 10 ** this.decimals));
100
- const asset = USDC_ADDRESSES[network];
101
- if (!asset) {
187
+ /**
188
+ * Default money conversion — converts decimal amount to the default stablecoin on the network.
189
+ */
190
+ private defaultMoneyConversion(amount: number, network: Network): AssetAmount {
191
+ const assetInfo = ASSET_INFO[network];
192
+ if (!assetInfo) {
102
193
  throw new Error(`No USDC address configured for network: ${network}`);
103
194
  }
104
195
 
196
+ const tokenAmount = convertToTokenAmount(
197
+ String(amount),
198
+ assetInfo.decimals,
199
+ );
200
+
105
201
  return {
106
- asset,
107
- amount: rawAmount.toString(),
202
+ asset: assetInfo.address,
203
+ amount: tokenAmount,
204
+ extra: {
205
+ name: assetInfo.name,
206
+ version: assetInfo.version,
207
+ },
108
208
  };
109
209
  }
110
210
 
@@ -117,7 +217,12 @@ export class EscrowServerScheme {
117
217
  */
118
218
  async enhancePaymentRequirements(
119
219
  requirements: PaymentRequirements,
120
- supportedKind: SupportedKind,
220
+ supportedKind: {
221
+ x402Version: number;
222
+ scheme: string;
223
+ network: Network;
224
+ extra?: Record<string, unknown>;
225
+ },
121
226
  _facilitatorExtensions: string[],
122
227
  ): Promise<PaymentRequirements> {
123
228
  return {
@@ -130,4 +235,27 @@ export class EscrowServerScheme {
130
235
  }
131
236
  }
132
237
 
238
+ /**
239
+ * Register escrow server scheme with x402ResourceServer
240
+ *
241
+ * @example
242
+ * ```typescript
243
+ * const server = new x402ResourceServer(facilitatorConfig);
244
+ * registerEscrowServerScheme(server, { networks: "eip155:84532" });
245
+ * ```
246
+ */
247
+ export function registerEscrowServerScheme(
248
+ server: x402ResourceServer,
249
+ config: { networks: Network | Network[] },
250
+ ): x402ResourceServer {
251
+ const scheme = new EscrowServerScheme();
252
+ const networks = Array.isArray(config.networks)
253
+ ? config.networks
254
+ : [config.networks];
255
+ for (const network of networks) {
256
+ server.register(network, scheme);
257
+ }
258
+ return server;
259
+ }
260
+
133
261
  export type { EscrowExtra, EscrowPayload } from "../../shared/types.js";
@@ -41,3 +41,18 @@ export const OPERATOR_ABI = [
41
41
  // ERC-3009 TransferWithAuthorization type hash
42
42
  export const TRANSFER_WITH_AUTHORIZATION_TYPEHASH =
43
43
  "0x7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a2267" as const;
44
+
45
+ // EIP-6492 magic suffix (32 bytes) — appended to signatures from counterfactual smart wallets
46
+ export const ERC6492_MAGIC_VALUE =
47
+ "0x6492649264926492649264926492649264926492649264926492649264926492" as const;
48
+
49
+ // ERC-20 balanceOf ABI for balance checks
50
+ export const ERC20_BALANCE_OF_ABI = [
51
+ {
52
+ name: "balanceOf",
53
+ type: "function",
54
+ stateMutability: "view",
55
+ inputs: [{ name: "account", type: "address" }],
56
+ outputs: [{ name: "balance", type: "uint256" }],
57
+ },
58
+ ] as const;
@@ -3,7 +3,8 @@
3
3
  * Adapted from @agentokratia/x402-escrow (MIT)
4
4
  */
5
5
 
6
- import { encodeAbiParameters, keccak256, type WalletClient } from "viem";
6
+ import { encodeAbiParameters, keccak256 } from "viem";
7
+ import type { ClientEvmSigner } from "@x402/evm";
7
8
  import { ZERO_ADDRESS, PAYMENT_INFO_COMPONENTS } from "./constants.js";
8
9
  import type { EscrowExtra, EscrowPayload } from "./types.js";
9
10
 
@@ -78,17 +79,18 @@ export function computeEscrowNonce(
78
79
  * Note: receiveWithAuthorization uses a different primary type than transferWithAuthorization
79
80
  */
80
81
  export async function signERC3009(
81
- wallet: WalletClient,
82
+ signer: ClientEvmSigner,
82
83
  authorization: EscrowPayload["authorization"],
83
84
  extra: EscrowExtra,
84
85
  tokenAddress: `0x${string}`,
86
+ chainId: number,
85
87
  ): Promise<`0x${string}`> {
86
88
  // EIP-712 domain - name must match the token's EIP-712 domain
87
89
  // (e.g., "USDC" for Base USDC, not "USD Coin")
88
90
  const domain = {
89
91
  name: extra.name,
90
92
  version: extra.version ?? "2",
91
- chainId: await wallet.getChainId(),
93
+ chainId,
92
94
  verifyingContract: tokenAddress,
93
95
  };
94
96
 
@@ -113,8 +115,7 @@ export async function signERC3009(
113
115
  nonce: authorization.nonce,
114
116
  };
115
117
 
116
- return wallet.signTypedData({
117
- account: wallet.account!,
118
+ return signer.signTypedData({
118
119
  domain,
119
120
  types,
120
121
  primaryType: "ReceiveWithAuthorization",
@@ -1,3 +1,29 @@
1
+ /**
2
+ * Type guard for EscrowPayload
3
+ */
4
+ export function isEscrowPayload(value: unknown): value is EscrowPayload {
5
+ return (
6
+ typeof value === "object" &&
7
+ value !== null &&
8
+ "authorization" in value &&
9
+ "signature" in value &&
10
+ "paymentInfo" in value
11
+ );
12
+ }
13
+
14
+ /**
15
+ * Type guard for EscrowExtra
16
+ */
17
+ export function isEscrowExtra(value: unknown): value is EscrowExtra {
18
+ return (
19
+ typeof value === "object" &&
20
+ value !== null &&
21
+ "escrowAddress" in value &&
22
+ "operatorAddress" in value &&
23
+ "tokenCollector" in value
24
+ );
25
+ }
26
+
1
27
  // EscrowExtra - fields in PaymentRequirements.extra
2
28
  export interface EscrowExtra {
3
29
  escrowAddress: `0x${string}`;