@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.
- package/README.md +21 -8
- package/dist/escrow/client/index.d.ts +29 -13
- package/dist/escrow/client/index.d.ts.map +1 -1
- package/dist/escrow/client/index.js +92 -33
- package/dist/escrow/client/index.js.map +1 -1
- package/dist/escrow/facilitator/index.d.ts +1 -1
- package/dist/escrow/facilitator/index.d.ts.map +1 -1
- package/dist/escrow/facilitator/index.js +163 -9
- package/dist/escrow/facilitator/index.js.map +1 -1
- package/dist/escrow/server/index.d.ts +41 -37
- package/dist/escrow/server/index.d.ts.map +1 -1
- package/dist/escrow/server/index.js +159 -25
- package/dist/escrow/server/index.js.map +1 -1
- package/dist/shared/constants.d.ts +14 -0
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +12 -0
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/nonce.d.ts +2 -2
- package/dist/shared/nonce.d.ts.map +1 -1
- package/dist/shared/nonce.js +3 -4
- package/dist/shared/nonce.js.map +1 -1
- package/dist/shared/types.d.ts +8 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +20 -1
- package/dist/shared/types.js.map +1 -1
- package/package.json +1 -1
- package/src/escrow/client/index.ts +140 -67
- package/src/escrow/facilitator/index.ts +196 -12
- package/src/escrow/server/index.ts +189 -61
- package/src/shared/constants.ts +15 -0
- package/src/shared/nonce.ts +6 -5
- package/src/shared/types.ts +26 -0
|
@@ -6,62 +6,125 @@
|
|
|
6
6
|
* on an x402ResourceServer via server.register('eip155:84532', new EscrowServerScheme()).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type {
|
|
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
|
-
*
|
|
20
|
+
* Asset info including EIP-712 domain parameters per network
|
|
13
21
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
amount
|
|
37
|
-
|
|
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
|
|
113
|
+
private moneyParsers: MoneyParser[] = [];
|
|
62
114
|
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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:
|
|
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:
|
|
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";
|
package/src/shared/constants.ts
CHANGED
|
@@ -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;
|
package/src/shared/nonce.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Adapted from @agentokratia/x402-escrow (MIT)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { encodeAbiParameters, keccak256
|
|
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
|
-
|
|
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
|
|
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
|
|
117
|
-
account: wallet.account!,
|
|
118
|
+
return signer.signTypedData({
|
|
118
119
|
domain,
|
|
119
120
|
types,
|
|
120
121
|
primaryType: "ReceiveWithAuthorization",
|
package/src/shared/types.ts
CHANGED
|
@@ -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}`;
|