@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.
- package/README.md +44 -0
- package/dist/escrow/client/index.d.ts +24 -0
- package/dist/escrow/client/index.d.ts.map +1 -0
- package/dist/escrow/client/index.js +45 -0
- package/dist/escrow/client/index.js.map +1 -0
- package/dist/escrow/facilitator/index.d.ts +49 -0
- package/dist/escrow/facilitator/index.d.ts.map +1 -0
- package/dist/escrow/facilitator/index.js +146 -0
- package/dist/escrow/facilitator/index.js.map +1 -0
- package/dist/escrow/server/index.d.ts +68 -0
- package/dist/escrow/server/index.d.ts.map +1 -0
- package/dist/escrow/server/index.js +80 -0
- package/dist/escrow/server/index.js.map +1 -0
- package/dist/shared/constants.d.ts +98 -0
- package/dist/shared/constants.d.ts.map +1 -0
- package/dist/shared/constants.js +39 -0
- package/dist/shared/constants.js.map +1 -0
- package/dist/shared/nonce.d.ts +41 -0
- package/dist/shared/nonce.d.ts.map +1 -0
- package/dist/shared/nonce.js +155 -0
- package/dist/shared/nonce.js.map +1 -0
- package/dist/shared/types.d.ts +41 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +2 -0
- package/dist/shared/types.js.map +1 -0
- package/package.json +58 -0
- package/src/escrow/client/index.ts +88 -0
- package/src/escrow/facilitator/index.ts +204 -0
- package/src/escrow/server/index.ts +133 -0
- package/src/shared/constants.ts +43 -0
- package/src/shared/nonce.ts +202 -0
- package/src/shared/types.ts +43 -0
|
@@ -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
|
+
}
|