@facilitator/server 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 +123 -0
- package/package.json +18 -0
- package/src/abi.ts +58 -0
- package/src/config.ts +54 -0
- package/src/index.ts +147 -0
- package/src/mechanism.ts +306 -0
- package/src/storage.ts +43 -0
- package/src/types.ts +81 -0
- package/test/Delegate.json +1 -0
- package/test/MockERC20.json +1 -0
- package/test/integration.test.ts +430 -0
- package/tsconfig.json +3 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
createPublicClient,
|
|
4
|
+
createWalletClient,
|
|
5
|
+
http,
|
|
6
|
+
parseEther,
|
|
7
|
+
type Hex,
|
|
8
|
+
} from "viem";
|
|
9
|
+
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
|
10
|
+
import { foundry } from "viem/chains";
|
|
11
|
+
|
|
12
|
+
// Artifacts
|
|
13
|
+
import delegateArtifact from "./Delegate.json";
|
|
14
|
+
import tokenArtifact from "./MockERC20.json";
|
|
15
|
+
|
|
16
|
+
const ANVIL_PORT = 8545;
|
|
17
|
+
const SERVER_PORT = 3000;
|
|
18
|
+
const CHAIN_ID = 31337;
|
|
19
|
+
|
|
20
|
+
const transport = http(`http://127.0.0.1:${ANVIL_PORT}`);
|
|
21
|
+
const publicClient = createPublicClient({ chain: foundry, transport });
|
|
22
|
+
const walletClient = createWalletClient({ chain: foundry, transport });
|
|
23
|
+
|
|
24
|
+
const deployerKey =
|
|
25
|
+
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
|
|
26
|
+
const deployer = privateKeyToAccount(deployerKey);
|
|
27
|
+
|
|
28
|
+
const relayerKey =
|
|
29
|
+
"0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d";
|
|
30
|
+
const relayer = privateKeyToAccount(relayerKey);
|
|
31
|
+
|
|
32
|
+
const userKey = generatePrivateKey();
|
|
33
|
+
const user = privateKeyToAccount(userKey);
|
|
34
|
+
|
|
35
|
+
let anvilProcess: any;
|
|
36
|
+
let serverProcess: any;
|
|
37
|
+
let delegateAddress: Hex;
|
|
38
|
+
let tokenAddress: Hex;
|
|
39
|
+
|
|
40
|
+
describe("x402 EIP-7702 Integration", () => {
|
|
41
|
+
beforeAll(async () => {
|
|
42
|
+
console.log("Starting Anvil...");
|
|
43
|
+
anvilProcess = Bun.spawn(["anvil", "--port", String(ANVIL_PORT)], {
|
|
44
|
+
stdout: "ignore",
|
|
45
|
+
stderr: "ignore",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
49
|
+
|
|
50
|
+
console.log("Deploying contracts...");
|
|
51
|
+
|
|
52
|
+
const deployDelegateHash = await walletClient.deployContract({
|
|
53
|
+
account: deployer,
|
|
54
|
+
abi: delegateArtifact.abi,
|
|
55
|
+
bytecode: delegateArtifact.bytecode.object as Hex,
|
|
56
|
+
});
|
|
57
|
+
const receipt1 = await publicClient.waitForTransactionReceipt({
|
|
58
|
+
hash: deployDelegateHash,
|
|
59
|
+
});
|
|
60
|
+
delegateAddress = receipt1.contractAddress!;
|
|
61
|
+
console.log("Delegate deployed at:", delegateAddress);
|
|
62
|
+
|
|
63
|
+
const deployTokenHash = await walletClient.deployContract({
|
|
64
|
+
account: deployer,
|
|
65
|
+
abi: tokenArtifact.abi,
|
|
66
|
+
bytecode: tokenArtifact.bytecode.object as Hex,
|
|
67
|
+
});
|
|
68
|
+
const receipt2 = await publicClient.waitForTransactionReceipt({
|
|
69
|
+
hash: deployTokenHash,
|
|
70
|
+
});
|
|
71
|
+
tokenAddress = receipt2.contractAddress!;
|
|
72
|
+
console.log("Token deployed at:", tokenAddress);
|
|
73
|
+
|
|
74
|
+
await walletClient.writeContract({
|
|
75
|
+
account: deployer,
|
|
76
|
+
address: tokenAddress,
|
|
77
|
+
abi: tokenArtifact.abi,
|
|
78
|
+
functionName: "mint",
|
|
79
|
+
args: [user.address, parseEther("1000")],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await walletClient.sendTransaction({
|
|
83
|
+
account: deployer,
|
|
84
|
+
to: relayer.address,
|
|
85
|
+
value: parseEther("10"),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Fund user with ETH for ETH transfer test
|
|
89
|
+
await walletClient.sendTransaction({
|
|
90
|
+
account: deployer,
|
|
91
|
+
to: user.address,
|
|
92
|
+
value: parseEther("10"),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
console.log("Starting Server...");
|
|
96
|
+
serverProcess = Bun.spawn(["bun", "run", "src/index.ts"], {
|
|
97
|
+
env: {
|
|
98
|
+
...process.env,
|
|
99
|
+
PORT: String(SERVER_PORT),
|
|
100
|
+
RELAYER_PRIVATE_KEY: relayerKey,
|
|
101
|
+
DELEGATE_ADDRESS: delegateAddress,
|
|
102
|
+
RPC_URL_31337: `http://127.0.0.1:${ANVIL_PORT}`,
|
|
103
|
+
},
|
|
104
|
+
stdout: "inherit",
|
|
105
|
+
stderr: "inherit",
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
afterAll(() => {
|
|
112
|
+
if (serverProcess) serverProcess.kill();
|
|
113
|
+
if (anvilProcess) anvilProcess.kill();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("ERC20 transfer with EIP-7702 + EIP-712", async () => {
|
|
117
|
+
const facilitatorUrl = `http://localhost:${SERVER_PORT}`;
|
|
118
|
+
|
|
119
|
+
const requirements = {
|
|
120
|
+
scheme: "eip7702",
|
|
121
|
+
network: `eip155:${CHAIN_ID}`,
|
|
122
|
+
asset: tokenAddress,
|
|
123
|
+
amount: parseEther("10").toString(),
|
|
124
|
+
payTo: deployer.address,
|
|
125
|
+
maxTimeoutSeconds: 300,
|
|
126
|
+
extra: {},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const intent = {
|
|
130
|
+
token: tokenAddress,
|
|
131
|
+
amount: requirements.amount,
|
|
132
|
+
to: requirements.payTo,
|
|
133
|
+
nonce: "0",
|
|
134
|
+
deadline: Math.floor(Date.now() / 1000) + 3600,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const signature = await user.signTypedData({
|
|
138
|
+
domain: {
|
|
139
|
+
name: "Delegate",
|
|
140
|
+
version: "1.0",
|
|
141
|
+
chainId: CHAIN_ID,
|
|
142
|
+
verifyingContract: user.address,
|
|
143
|
+
},
|
|
144
|
+
types: {
|
|
145
|
+
PaymentIntent: [
|
|
146
|
+
{ name: "token", type: "address" },
|
|
147
|
+
{ name: "amount", type: "uint256" },
|
|
148
|
+
{ name: "to", type: "address" },
|
|
149
|
+
{ name: "nonce", type: "uint256" },
|
|
150
|
+
{ name: "deadline", type: "uint256" },
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
primaryType: "PaymentIntent",
|
|
154
|
+
message: {
|
|
155
|
+
token: intent.token,
|
|
156
|
+
amount: BigInt(intent.amount),
|
|
157
|
+
to: intent.to as `0x${string}`,
|
|
158
|
+
nonce: BigInt(intent.nonce),
|
|
159
|
+
deadline: BigInt(intent.deadline),
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const authorization = await user.signAuthorization({
|
|
164
|
+
contractAddress: delegateAddress,
|
|
165
|
+
chainId: CHAIN_ID,
|
|
166
|
+
nonce: 0,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const paymentPayload = {
|
|
170
|
+
x402Version: 2,
|
|
171
|
+
resource: {
|
|
172
|
+
url: "http://example.com/resource",
|
|
173
|
+
description: "Test Resource",
|
|
174
|
+
mimeType: "application/json",
|
|
175
|
+
},
|
|
176
|
+
accepted: requirements,
|
|
177
|
+
payload: {
|
|
178
|
+
authorization: {
|
|
179
|
+
contractAddress: authorization.address || delegateAddress,
|
|
180
|
+
chainId: authorization.chainId,
|
|
181
|
+
nonce: authorization.nonce,
|
|
182
|
+
r: authorization.r,
|
|
183
|
+
s: authorization.s,
|
|
184
|
+
yParity: authorization.yParity,
|
|
185
|
+
},
|
|
186
|
+
intent,
|
|
187
|
+
signature,
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Verify
|
|
192
|
+
console.log("Calling /verify...");
|
|
193
|
+
const verifyRes = await fetch(`${facilitatorUrl}/verify`, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
body: JSON.stringify({
|
|
196
|
+
paymentPayload,
|
|
197
|
+
paymentRequirements: requirements,
|
|
198
|
+
}),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const verifyJson = (await verifyRes.json()) as any;
|
|
202
|
+
console.log("Verify Result:", verifyJson);
|
|
203
|
+
expect(verifyJson.isValid).toBe(true);
|
|
204
|
+
expect(verifyJson.payer?.toLowerCase()).toBe(user.address.toLowerCase());
|
|
205
|
+
|
|
206
|
+
// Settle (New Nonce)
|
|
207
|
+
const intentSettle = { ...intent, nonce: "1" };
|
|
208
|
+
|
|
209
|
+
const signatureSettle = await user.signTypedData({
|
|
210
|
+
domain: {
|
|
211
|
+
name: "Delegate",
|
|
212
|
+
version: "1.0",
|
|
213
|
+
chainId: CHAIN_ID,
|
|
214
|
+
verifyingContract: user.address,
|
|
215
|
+
},
|
|
216
|
+
types: {
|
|
217
|
+
PaymentIntent: [
|
|
218
|
+
{ name: "token", type: "address" },
|
|
219
|
+
{ name: "amount", type: "uint256" },
|
|
220
|
+
{ name: "to", type: "address" },
|
|
221
|
+
{ name: "nonce", type: "uint256" },
|
|
222
|
+
{ name: "deadline", type: "uint256" },
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
primaryType: "PaymentIntent",
|
|
226
|
+
message: {
|
|
227
|
+
token: intentSettle.token,
|
|
228
|
+
amount: BigInt(intentSettle.amount),
|
|
229
|
+
to: intentSettle.to as `0x${string}`,
|
|
230
|
+
nonce: BigInt(intentSettle.nonce),
|
|
231
|
+
deadline: BigInt(intentSettle.deadline),
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const paymentPayloadSettle = {
|
|
236
|
+
...paymentPayload,
|
|
237
|
+
accepted: requirements,
|
|
238
|
+
payload: {
|
|
239
|
+
authorization: {
|
|
240
|
+
contractAddress: authorization.address || delegateAddress,
|
|
241
|
+
chainId: authorization.chainId,
|
|
242
|
+
nonce: authorization.nonce,
|
|
243
|
+
r: authorization.r,
|
|
244
|
+
s: authorization.s,
|
|
245
|
+
yParity: authorization.yParity,
|
|
246
|
+
},
|
|
247
|
+
intent: intentSettle,
|
|
248
|
+
signature: signatureSettle,
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
console.log("Calling /settle...");
|
|
253
|
+
const settleRes = await fetch(`${facilitatorUrl}/settle`, {
|
|
254
|
+
method: "POST",
|
|
255
|
+
body: JSON.stringify({
|
|
256
|
+
paymentPayload: paymentPayloadSettle,
|
|
257
|
+
paymentRequirements: requirements,
|
|
258
|
+
}),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const settleJson = (await settleRes.json()) as any;
|
|
262
|
+
console.log("Settle Result:", settleJson);
|
|
263
|
+
expect(settleJson.success).toBe(true);
|
|
264
|
+
|
|
265
|
+
// Verify On-Chain State
|
|
266
|
+
const balanceUser = await publicClient.readContract({
|
|
267
|
+
address: tokenAddress,
|
|
268
|
+
abi: tokenArtifact.abi,
|
|
269
|
+
functionName: "balanceOf",
|
|
270
|
+
args: [user.address],
|
|
271
|
+
});
|
|
272
|
+
const balancePayTo = await publicClient.readContract({
|
|
273
|
+
address: tokenAddress,
|
|
274
|
+
abi: tokenArtifact.abi,
|
|
275
|
+
functionName: "balanceOf",
|
|
276
|
+
args: [deployer.address],
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
console.log("User Balance:", balanceUser);
|
|
280
|
+
console.log("PayTo Balance:", balancePayTo);
|
|
281
|
+
|
|
282
|
+
expect(balanceUser).toBe(parseEther("990"));
|
|
283
|
+
expect(balancePayTo).toBe(parseEther("10"));
|
|
284
|
+
}, 30000);
|
|
285
|
+
|
|
286
|
+
test("ETH transfer with EIP-7702 + EIP-712", async () => {
|
|
287
|
+
const facilitatorUrl = `http://localhost:${SERVER_PORT}`;
|
|
288
|
+
const ADDRESS_ZERO = "0x0000000000000000000000000000000000000000";
|
|
289
|
+
|
|
290
|
+
const requirements = {
|
|
291
|
+
scheme: "eip7702",
|
|
292
|
+
network: `eip155:${CHAIN_ID}`,
|
|
293
|
+
asset: ADDRESS_ZERO,
|
|
294
|
+
amount: parseEther("1").toString(),
|
|
295
|
+
payTo: deployer.address,
|
|
296
|
+
maxTimeoutSeconds: 300,
|
|
297
|
+
extra: {},
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const intent = {
|
|
301
|
+
amount: requirements.amount,
|
|
302
|
+
to: requirements.payTo,
|
|
303
|
+
nonce: "100",
|
|
304
|
+
deadline: Math.floor(Date.now() / 1000) + 3600,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const signature = await user.signTypedData({
|
|
308
|
+
domain: {
|
|
309
|
+
name: "Delegate",
|
|
310
|
+
version: "1.0",
|
|
311
|
+
chainId: CHAIN_ID,
|
|
312
|
+
verifyingContract: user.address,
|
|
313
|
+
},
|
|
314
|
+
types: {
|
|
315
|
+
EthPaymentIntent: [
|
|
316
|
+
{ name: "amount", type: "uint256" },
|
|
317
|
+
{ name: "to", type: "address" },
|
|
318
|
+
{ name: "nonce", type: "uint256" },
|
|
319
|
+
{ name: "deadline", type: "uint256" },
|
|
320
|
+
],
|
|
321
|
+
},
|
|
322
|
+
primaryType: "EthPaymentIntent",
|
|
323
|
+
message: {
|
|
324
|
+
amount: BigInt(intent.amount),
|
|
325
|
+
to: intent.to as `0x${string}`,
|
|
326
|
+
nonce: BigInt(intent.nonce),
|
|
327
|
+
deadline: BigInt(intent.deadline),
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const authorization = await user.signAuthorization({
|
|
332
|
+
contractAddress: delegateAddress,
|
|
333
|
+
chainId: CHAIN_ID,
|
|
334
|
+
nonce: await publicClient.getTransactionCount({ address: user.address }),
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const paymentPayload = {
|
|
338
|
+
x402Version: 2,
|
|
339
|
+
resource: {
|
|
340
|
+
url: "http://example.com/resource",
|
|
341
|
+
description: "Test Resource",
|
|
342
|
+
mimeType: "application/json",
|
|
343
|
+
},
|
|
344
|
+
accepted: requirements,
|
|
345
|
+
payload: {
|
|
346
|
+
authorization: {
|
|
347
|
+
contractAddress: authorization.address || delegateAddress,
|
|
348
|
+
chainId: authorization.chainId,
|
|
349
|
+
nonce: authorization.nonce,
|
|
350
|
+
r: authorization.r,
|
|
351
|
+
s: authorization.s,
|
|
352
|
+
yParity: authorization.yParity,
|
|
353
|
+
},
|
|
354
|
+
intent,
|
|
355
|
+
signature,
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// Verify
|
|
360
|
+
console.log("Calling /verify for ETH...");
|
|
361
|
+
const verifyRes = await fetch(`${facilitatorUrl}/verify`, {
|
|
362
|
+
method: "POST",
|
|
363
|
+
body: JSON.stringify({
|
|
364
|
+
paymentPayload,
|
|
365
|
+
paymentRequirements: requirements,
|
|
366
|
+
}),
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const verifyJson = (await verifyRes.json()) as any;
|
|
370
|
+
console.log("ETH Verify Result:", verifyJson);
|
|
371
|
+
expect(verifyJson.isValid).toBe(true);
|
|
372
|
+
|
|
373
|
+
// Settle (New Nonce)
|
|
374
|
+
const intentSettle = { ...intent, nonce: "101" };
|
|
375
|
+
|
|
376
|
+
const signatureSettle = await user.signTypedData({
|
|
377
|
+
domain: {
|
|
378
|
+
name: "Delegate",
|
|
379
|
+
version: "1.0",
|
|
380
|
+
chainId: CHAIN_ID,
|
|
381
|
+
verifyingContract: user.address,
|
|
382
|
+
},
|
|
383
|
+
types: {
|
|
384
|
+
EthPaymentIntent: [
|
|
385
|
+
{ name: "amount", type: "uint256" },
|
|
386
|
+
{ name: "to", type: "address" },
|
|
387
|
+
{ name: "nonce", type: "uint256" },
|
|
388
|
+
{ name: "deadline", type: "uint256" },
|
|
389
|
+
],
|
|
390
|
+
},
|
|
391
|
+
primaryType: "EthPaymentIntent",
|
|
392
|
+
message: {
|
|
393
|
+
amount: BigInt(intentSettle.amount),
|
|
394
|
+
to: intentSettle.to as `0x${string}`,
|
|
395
|
+
nonce: BigInt(intentSettle.nonce),
|
|
396
|
+
deadline: BigInt(intentSettle.deadline),
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const paymentPayloadSettle = {
|
|
401
|
+
...paymentPayload,
|
|
402
|
+
accepted: requirements,
|
|
403
|
+
payload: {
|
|
404
|
+
authorization: {
|
|
405
|
+
contractAddress: authorization.address || delegateAddress,
|
|
406
|
+
chainId: authorization.chainId,
|
|
407
|
+
nonce: authorization.nonce,
|
|
408
|
+
r: authorization.r,
|
|
409
|
+
s: authorization.s,
|
|
410
|
+
yParity: authorization.yParity,
|
|
411
|
+
},
|
|
412
|
+
intent: intentSettle,
|
|
413
|
+
signature: signatureSettle,
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
console.log("Calling /settle for ETH...");
|
|
418
|
+
const settleRes = await fetch(`${facilitatorUrl}/settle`, {
|
|
419
|
+
method: "POST",
|
|
420
|
+
body: JSON.stringify({
|
|
421
|
+
paymentPayload: paymentPayloadSettle,
|
|
422
|
+
paymentRequirements: requirements,
|
|
423
|
+
}),
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const settleJson = (await settleRes.json()) as any;
|
|
427
|
+
console.log("ETH Settle Result:", settleJson);
|
|
428
|
+
expect(settleJson.success).toBe(true);
|
|
429
|
+
}, 30000);
|
|
430
|
+
});
|
package/tsconfig.json
ADDED