@x402r/evm 0.0.1

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.
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Escrow Scheme - Server
3
+ * Handles price parsing and requirement enhancement for resource servers.
4
+ *
5
+ * Implements x402's SchemeNetworkServer interface so it can be registered
6
+ * on an x402ResourceServer via server.register('eip155:84532', new EscrowServerScheme()).
7
+ */
8
+
9
+ import type { EscrowExtra } from "../../shared/types.js";
10
+
11
+ /**
12
+ * x402 PaymentRequirements (matches @x402/core/types PaymentRequirements)
13
+ */
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
+ }
30
+
31
+ /**
32
+ * x402 AssetAmount (matches @x402/core/types AssetAmount)
33
+ */
34
+ export interface AssetAmount {
35
+ asset: string;
36
+ amount: string;
37
+ extra?: Record<string, unknown>;
38
+ }
39
+
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
+ /**
56
+ * Server scheme - handles price parsing and requirement enhancement.
57
+ * Implements x402's SchemeNetworkServer interface.
58
+ */
59
+ export class EscrowServerScheme {
60
+ readonly scheme = "escrow";
61
+ private readonly decimals: number;
62
+
63
+ constructor(config?: { decimals?: number }) {
64
+ this.decimals = config?.decimals ?? 6; // USDC default
65
+ }
66
+
67
+ /**
68
+ * Parse a price into an x402 AssetAmount.
69
+ *
70
+ * Accepts x402's Price type:
71
+ * - string: "$0.01", "0.01", "10000"
72
+ * - number: 0.01
73
+ * - AssetAmount: { asset: "0x...", amount: "10000" }
74
+ */
75
+ async parsePrice(price: Price, network: Network): Promise<AssetAmount> {
76
+ // If already an AssetAmount, pass through
77
+ if (
78
+ typeof price === "object" &&
79
+ price !== null &&
80
+ "amount" in price &&
81
+ "asset" in price
82
+ ) {
83
+ return price as AssetAmount;
84
+ }
85
+
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);
93
+ }
94
+
95
+ if (isNaN(numericAmount)) {
96
+ throw new Error(`Cannot parse price: ${price}`);
97
+ }
98
+
99
+ const rawAmount = BigInt(Math.round(numericAmount * 10 ** this.decimals));
100
+ const asset = USDC_ADDRESSES[network];
101
+ if (!asset) {
102
+ throw new Error(`No USDC address configured for network: ${network}`);
103
+ }
104
+
105
+ return {
106
+ asset,
107
+ amount: rawAmount.toString(),
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Enhance payment requirements with facilitator's extra fields.
113
+ *
114
+ * Merges supportedKind.extra (from facilitator's /supported endpoint) into
115
+ * the requirements, so escrow addresses flow from facilitator → merchant
116
+ * requirements automatically.
117
+ */
118
+ async enhancePaymentRequirements(
119
+ requirements: PaymentRequirements,
120
+ supportedKind: SupportedKind,
121
+ _facilitatorExtensions: string[],
122
+ ): Promise<PaymentRequirements> {
123
+ return {
124
+ ...requirements,
125
+ extra: {
126
+ ...supportedKind.extra,
127
+ ...requirements.extra,
128
+ },
129
+ };
130
+ }
131
+ }
132
+
133
+ export type { EscrowExtra, EscrowPayload } from "../../shared/types.js";
@@ -0,0 +1,43 @@
1
+ export const ZERO_ADDRESS =
2
+ "0x0000000000000000000000000000000000000000" as const;
3
+ export const MAX_UINT48 = 281474976710655;
4
+ export const MAX_UINT32 = 4294967295;
5
+
6
+ // PaymentInfo struct for AuthCaptureEscrow (matches commerce-payments contract)
7
+ export const PAYMENT_INFO_COMPONENTS = [
8
+ { name: "operator", type: "address" },
9
+ { name: "payer", type: "address" },
10
+ { name: "receiver", type: "address" },
11
+ { name: "token", type: "address" },
12
+ { name: "maxAmount", type: "uint120" },
13
+ { name: "preApprovalExpiry", type: "uint48" },
14
+ { name: "authorizationExpiry", type: "uint48" },
15
+ { name: "refundExpiry", type: "uint48" },
16
+ { name: "minFeeBps", type: "uint16" },
17
+ { name: "maxFeeBps", type: "uint16" },
18
+ { name: "feeReceiver", type: "address" },
19
+ { name: "salt", type: "uint256" },
20
+ ] as const;
21
+
22
+ export const OPERATOR_ABI = [
23
+ {
24
+ name: "authorize",
25
+ type: "function",
26
+ stateMutability: "nonpayable",
27
+ inputs: [
28
+ {
29
+ name: "paymentInfo",
30
+ type: "tuple",
31
+ components: PAYMENT_INFO_COMPONENTS,
32
+ },
33
+ { name: "amount", type: "uint256" },
34
+ { name: "tokenCollector", type: "address" },
35
+ { name: "collectorData", type: "bytes" },
36
+ ],
37
+ outputs: [],
38
+ },
39
+ ] as const;
40
+
41
+ // ERC-3009 TransferWithAuthorization type hash
42
+ export const TRANSFER_WITH_AUTHORIZATION_TYPEHASH =
43
+ "0x7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a2267" as const;
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Nonce computation and ERC-3009 signing utilities
3
+ * Adapted from @agentokratia/x402-escrow (MIT)
4
+ */
5
+
6
+ import { encodeAbiParameters, keccak256, type WalletClient } from "viem";
7
+ import { ZERO_ADDRESS, PAYMENT_INFO_COMPONENTS } from "./constants.js";
8
+ import type { EscrowExtra, EscrowPayload } from "./types.js";
9
+
10
+ /**
11
+ * PaymentInfo typehash - must match AuthCaptureEscrow.PAYMENT_INFO_TYPEHASH
12
+ */
13
+ const PAYMENT_INFO_TYPEHASH = keccak256(
14
+ new TextEncoder().encode(
15
+ "PaymentInfo(address operator,address payer,address receiver,address token,uint120 maxAmount,uint48 preApprovalExpiry,uint48 authorizationExpiry,uint48 refundExpiry,uint16 minFeeBps,uint16 maxFeeBps,address feeReceiver,uint256 salt)",
16
+ ),
17
+ );
18
+
19
+ /**
20
+ * Compute escrow nonce for ERC-3009 authorization
21
+ * Must match AuthCaptureEscrow.getHash() with payer=address(0)
22
+ */
23
+ export function computeEscrowNonce(
24
+ chainId: number,
25
+ escrowAddress: `0x${string}`,
26
+ paymentInfo: EscrowPayload["paymentInfo"],
27
+ ): `0x${string}` {
28
+ // Step 1: Encode paymentInfo with payer=0 (payer-agnostic)
29
+ const paymentInfoEncoded = encodeAbiParameters(
30
+ [
31
+ { name: "typehash", type: "bytes32" },
32
+ { name: "operator", type: "address" },
33
+ { name: "payer", type: "address" },
34
+ { name: "receiver", type: "address" },
35
+ { name: "token", type: "address" },
36
+ { name: "maxAmount", type: "uint120" },
37
+ { name: "preApprovalExpiry", type: "uint48" },
38
+ { name: "authorizationExpiry", type: "uint48" },
39
+ { name: "refundExpiry", type: "uint48" },
40
+ { name: "minFeeBps", type: "uint16" },
41
+ { name: "maxFeeBps", type: "uint16" },
42
+ { name: "feeReceiver", type: "address" },
43
+ { name: "salt", type: "uint256" },
44
+ ],
45
+ [
46
+ PAYMENT_INFO_TYPEHASH,
47
+ paymentInfo.operator,
48
+ ZERO_ADDRESS, // payer-agnostic
49
+ paymentInfo.receiver,
50
+ paymentInfo.token,
51
+ BigInt(paymentInfo.maxAmount),
52
+ paymentInfo.preApprovalExpiry,
53
+ paymentInfo.authorizationExpiry,
54
+ paymentInfo.refundExpiry,
55
+ paymentInfo.minFeeBps,
56
+ paymentInfo.maxFeeBps,
57
+ paymentInfo.feeReceiver,
58
+ BigInt(paymentInfo.salt),
59
+ ],
60
+ );
61
+ const paymentInfoHash = keccak256(paymentInfoEncoded);
62
+
63
+ // Step 2: Encode (chainId, escrow, paymentInfoHash) and hash
64
+ const outerEncoded = encodeAbiParameters(
65
+ [
66
+ { name: "chainId", type: "uint256" },
67
+ { name: "escrow", type: "address" },
68
+ { name: "paymentInfoHash", type: "bytes32" },
69
+ ],
70
+ [BigInt(chainId), escrowAddress, paymentInfoHash],
71
+ );
72
+
73
+ return keccak256(outerEncoded);
74
+ }
75
+
76
+ /**
77
+ * Sign ERC-3009 ReceiveWithAuthorization
78
+ * Note: receiveWithAuthorization uses a different primary type than transferWithAuthorization
79
+ */
80
+ export async function signERC3009(
81
+ wallet: WalletClient,
82
+ authorization: EscrowPayload["authorization"],
83
+ extra: EscrowExtra,
84
+ tokenAddress: `0x${string}`,
85
+ ): Promise<`0x${string}`> {
86
+ // EIP-712 domain - name must match the token's EIP-712 domain
87
+ // (e.g., "USDC" for Base USDC, not "USD Coin")
88
+ const domain = {
89
+ name: extra.name,
90
+ version: extra.version ?? "2",
91
+ chainId: await wallet.getChainId(),
92
+ verifyingContract: tokenAddress,
93
+ };
94
+
95
+ // ERC-3009 uses ReceiveWithAuthorization for receiveWithAuthorization()
96
+ const types = {
97
+ ReceiveWithAuthorization: [
98
+ { name: "from", type: "address" },
99
+ { name: "to", type: "address" },
100
+ { name: "value", type: "uint256" },
101
+ { name: "validAfter", type: "uint256" },
102
+ { name: "validBefore", type: "uint256" },
103
+ { name: "nonce", type: "bytes32" },
104
+ ],
105
+ };
106
+
107
+ const message = {
108
+ from: authorization.from,
109
+ to: authorization.to,
110
+ value: BigInt(authorization.value),
111
+ validAfter: BigInt(authorization.validAfter),
112
+ validBefore: BigInt(authorization.validBefore),
113
+ nonce: authorization.nonce,
114
+ };
115
+
116
+ return wallet.signTypedData({
117
+ account: wallet.account!,
118
+ domain,
119
+ types,
120
+ primaryType: "ReceiveWithAuthorization",
121
+ message,
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Verify ERC-3009 signature (facilitator-side)
127
+ * @param signer - The signer with verifyTypedData method
128
+ * @param authorization - ERC-3009 authorization data
129
+ * @param signature - The signature to verify
130
+ * @param extra - Extra configuration including chainId
131
+ * @param tokenAddress - The token contract address (verifyingContract for EIP-712)
132
+ */
133
+ export async function verifyERC3009Signature(
134
+ signer: {
135
+ verifyTypedData: (args: {
136
+ address: `0x${string}`;
137
+ domain: Record<string, unknown>;
138
+ types: Record<string, unknown>;
139
+ primaryType: string;
140
+ message: Record<string, unknown>;
141
+ signature: `0x${string}`;
142
+ }) => Promise<boolean>;
143
+ },
144
+ authorization: EscrowPayload["authorization"],
145
+ signature: `0x${string}`,
146
+ extra: EscrowExtra & { chainId: number },
147
+ tokenAddress: `0x${string}`,
148
+ ): Promise<boolean> {
149
+ // EIP-712 domain - name must match the token's EIP-712 domain
150
+ // (e.g., "USDC" for Base USDC, not "USD Coin")
151
+ const domain = {
152
+ name: extra.name,
153
+ version: extra.version ?? "2",
154
+ chainId: extra.chainId,
155
+ verifyingContract: tokenAddress,
156
+ };
157
+
158
+ // Must use ReceiveWithAuthorization to match what was signed
159
+ const types = {
160
+ ReceiveWithAuthorization: [
161
+ { name: "from", type: "address" },
162
+ { name: "to", type: "address" },
163
+ { name: "value", type: "uint256" },
164
+ { name: "validAfter", type: "uint256" },
165
+ { name: "validBefore", type: "uint256" },
166
+ { name: "nonce", type: "bytes32" },
167
+ ],
168
+ };
169
+
170
+ const message = {
171
+ from: authorization.from,
172
+ to: authorization.to,
173
+ value: BigInt(authorization.value),
174
+ validAfter: BigInt(authorization.validAfter),
175
+ validBefore: BigInt(authorization.validBefore),
176
+ nonce: authorization.nonce,
177
+ };
178
+
179
+ try {
180
+ return await signer.verifyTypedData({
181
+ address: authorization.from,
182
+ domain,
183
+ types,
184
+ primaryType: "ReceiveWithAuthorization",
185
+ message,
186
+ signature,
187
+ });
188
+ } catch {
189
+ return false;
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Generate random salt for paymentInfo
195
+ */
196
+ export function generateSalt(): `0x${string}` {
197
+ const bytes = new Uint8Array(32);
198
+ crypto.getRandomValues(bytes);
199
+ return `0x${Array.from(bytes)
200
+ .map((b) => b.toString(16).padStart(2, "0"))
201
+ .join("")}` as `0x${string}`;
202
+ }
@@ -0,0 +1,43 @@
1
+ // EscrowExtra - fields in PaymentRequirements.extra
2
+ export interface EscrowExtra {
3
+ escrowAddress: `0x${string}`;
4
+ operatorAddress: `0x${string}`;
5
+ tokenCollector: `0x${string}`;
6
+ authorizeAddress?: `0x${string}`;
7
+ minDeposit?: string;
8
+ maxDeposit?: string;
9
+ preApprovalExpirySeconds?: number;
10
+ authorizationExpirySeconds?: number;
11
+ refundExpirySeconds?: number;
12
+ minFeeBps?: number;
13
+ maxFeeBps?: number;
14
+ feeReceiver?: `0x${string}`;
15
+ name: string; // EIP-712 domain name (e.g., "USDC" for Base USDC)
16
+ version?: string;
17
+ }
18
+
19
+ // EscrowPayload - the payload field in PaymentPayload
20
+ export interface EscrowPayload {
21
+ authorization: {
22
+ from: `0x${string}`;
23
+ to: `0x${string}`;
24
+ value: string;
25
+ validAfter: string;
26
+ validBefore: string;
27
+ nonce: `0x${string}`;
28
+ };
29
+ signature: `0x${string}`;
30
+ paymentInfo: {
31
+ operator: `0x${string}`;
32
+ receiver: `0x${string}`;
33
+ token: `0x${string}`;
34
+ maxAmount: string;
35
+ preApprovalExpiry: number;
36
+ authorizationExpiry: number;
37
+ refundExpiry: number;
38
+ minFeeBps: number;
39
+ maxFeeBps: number;
40
+ feeReceiver: `0x${string}`;
41
+ salt: `0x${string}`;
42
+ };
43
+ }