@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
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @facilitator/server
|
|
2
|
+
|
|
3
|
+
x402 payment facilitator server implementing gasless ERC-20 and native ETH transfers via [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) delegated transactions.
|
|
4
|
+
|
|
5
|
+
A **relayer** pays the gas on behalf of users. Users sign an EIP-712 payment intent and an EIP-7702 authorization that delegates their EOA to the [`Delegate`](../contracts/src/Delegate.sol) contract. The facilitator verifies both signatures off-chain, then submits a single Type 4 transaction to settle the payment on-chain.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- [Bun](https://bun.sh/) v1.1+
|
|
10
|
+
- Access to an EVM RPC endpoint (Anvil for local development)
|
|
11
|
+
- A deployed `Delegate` contract (see `packages/contracts`)
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
# Install dependencies
|
|
17
|
+
bun install
|
|
18
|
+
|
|
19
|
+
# Set environment variables (or create a .env file — Bun loads it automatically)
|
|
20
|
+
export RELAYER_PRIVATE_KEY="0x..."
|
|
21
|
+
export DELEGATE_ADDRESS="0x..."
|
|
22
|
+
export RPC_URL_31337="http://127.0.0.1:8545"
|
|
23
|
+
|
|
24
|
+
# Start the server
|
|
25
|
+
bun run start
|
|
26
|
+
|
|
27
|
+
# Or with file-watching for development
|
|
28
|
+
bun run dev
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The server starts on port `3000` by default. Override with the `PORT` environment variable.
|
|
32
|
+
|
|
33
|
+
## Environment Variables
|
|
34
|
+
|
|
35
|
+
| Variable | Required | Description |
|
|
36
|
+
| --------------------- | -------- | --------------------------------------------------------- |
|
|
37
|
+
| `RELAYER_PRIVATE_KEY` | Yes | Private key of the relayer account that pays gas |
|
|
38
|
+
| `DELEGATE_ADDRESS` | Yes | Deployed `Delegate` contract address |
|
|
39
|
+
| `RPC_URL_<chainId>` | Yes | RPC endpoint per chain (e.g. `RPC_URL_1`, `RPC_URL_8453`) |
|
|
40
|
+
| `PORT` | No | Server port (default: `3000`) |
|
|
41
|
+
|
|
42
|
+
## API
|
|
43
|
+
|
|
44
|
+
### `GET /healthcheck`
|
|
45
|
+
|
|
46
|
+
Returns server uptime and status.
|
|
47
|
+
|
|
48
|
+
### `GET /supported`
|
|
49
|
+
|
|
50
|
+
Returns supported payment schemes, networks, and the relayer signer address.
|
|
51
|
+
|
|
52
|
+
### `GET /discovery/resources?limit=100&offset=0`
|
|
53
|
+
|
|
54
|
+
Lists resources that have been settled through this facilitator (the "bazaar").
|
|
55
|
+
|
|
56
|
+
### `POST /verify`
|
|
57
|
+
|
|
58
|
+
Off-chain verification of a payment intent. Checks:
|
|
59
|
+
|
|
60
|
+
1. EIP-7702 authorization signature recovery
|
|
61
|
+
2. Delegate contract address trust
|
|
62
|
+
3. EIP-712 intent signature validity
|
|
63
|
+
4. Deadline expiration
|
|
64
|
+
5. Nonce uniqueness
|
|
65
|
+
6. Payer balance (ERC-20 `balanceOf` or native ETH `getBalance`)
|
|
66
|
+
|
|
67
|
+
**Request body:**
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"paymentPayload": { "..." },
|
|
72
|
+
"paymentRequirements": { "..." }
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `POST /settle`
|
|
77
|
+
|
|
78
|
+
Verifies and then submits the payment transaction on-chain. Sends a Type 4 (EIP-7702) transaction through the relayer. If the payer's EOA already has delegated code, the authorization list is omitted.
|
|
79
|
+
|
|
80
|
+
**Request body:** Same as `/verify`.
|
|
81
|
+
|
|
82
|
+
## Asset Types
|
|
83
|
+
|
|
84
|
+
- **ERC-20 tokens:** Set `asset` to the token contract address. Uses `Delegate.transfer()`.
|
|
85
|
+
- **Native ETH:** Set `asset` to `0x0000000000000000000000000000000000000000`. Uses `Delegate.transferEth()`.
|
|
86
|
+
|
|
87
|
+
## Architecture
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
src/
|
|
91
|
+
index.ts HTTP server (Bun.serve) and route handlers
|
|
92
|
+
config.ts Environment config, relayer account, RPC client factory
|
|
93
|
+
mechanism.ts EIP-7702 verification and settlement logic
|
|
94
|
+
storage.ts In-memory nonce tracker and discovery catalog
|
|
95
|
+
types.ts x402 protocol and EIP-7702 type definitions
|
|
96
|
+
abi.ts Solidity ABI fragments for Delegate and ERC-20
|
|
97
|
+
test/
|
|
98
|
+
integration.test.ts End-to-end test against Anvil
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Testing
|
|
102
|
+
|
|
103
|
+
Tests require [Foundry](https://getfoundry.sh/) (for Anvil) and compiled contract artifacts.
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
# Build contracts first (from the monorepo root)
|
|
107
|
+
cd packages/contracts && forge build
|
|
108
|
+
|
|
109
|
+
# Run integration tests
|
|
110
|
+
cd packages/server && bun test
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The integration test suite:
|
|
114
|
+
|
|
115
|
+
1. Starts a local Anvil instance
|
|
116
|
+
2. Deploys `Delegate` and `MockERC20` contracts
|
|
117
|
+
3. Starts the facilitator server
|
|
118
|
+
4. Executes full verify + settle flows for both ERC-20 and native ETH transfers
|
|
119
|
+
5. Asserts on-chain balances after settlement
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@facilitator/server",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "x402 payment facilitator server using EIP-7702 delegated transactions",
|
|
5
|
+
"module": "src/index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "bun run src/index.ts",
|
|
9
|
+
"dev": "bun --hot src/index.ts",
|
|
10
|
+
"test": "bun test",
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@x402/core": "^2.2.0",
|
|
15
|
+
"viem": "^2.45.1"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT"
|
|
18
|
+
}
|
package/src/abi.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const ERC20_ABI = [
|
|
2
|
+
{
|
|
3
|
+
name: "balanceOf",
|
|
4
|
+
type: "function",
|
|
5
|
+
stateMutability: "view",
|
|
6
|
+
inputs: [{ name: "account", type: "address" }],
|
|
7
|
+
outputs: [{ name: "", type: "uint256" }],
|
|
8
|
+
},
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
export const DELEGATE_ABI = [
|
|
12
|
+
{
|
|
13
|
+
name: "transfer",
|
|
14
|
+
type: "function",
|
|
15
|
+
stateMutability: "nonpayable",
|
|
16
|
+
inputs: [
|
|
17
|
+
{
|
|
18
|
+
name: "intent",
|
|
19
|
+
type: "tuple",
|
|
20
|
+
components: [
|
|
21
|
+
{ name: "token", type: "address" },
|
|
22
|
+
{ name: "amount", type: "uint256" },
|
|
23
|
+
{ name: "to", type: "address" },
|
|
24
|
+
{ name: "nonce", type: "uint256" },
|
|
25
|
+
{ name: "deadline", type: "uint256" },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{ name: "signature", type: "bytes" },
|
|
29
|
+
],
|
|
30
|
+
outputs: [],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "transferEth",
|
|
34
|
+
type: "function",
|
|
35
|
+
stateMutability: "nonpayable",
|
|
36
|
+
inputs: [
|
|
37
|
+
{
|
|
38
|
+
name: "intent",
|
|
39
|
+
type: "tuple",
|
|
40
|
+
components: [
|
|
41
|
+
{ name: "amount", type: "uint256" },
|
|
42
|
+
{ name: "to", type: "address" },
|
|
43
|
+
{ name: "nonce", type: "uint256" },
|
|
44
|
+
{ name: "deadline", type: "uint256" },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{ name: "signature", type: "bytes" },
|
|
48
|
+
],
|
|
49
|
+
outputs: [],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "invalidateNonce",
|
|
53
|
+
type: "function",
|
|
54
|
+
stateMutability: "nonpayable",
|
|
55
|
+
inputs: [{ name: "nonce", type: "uint256" }],
|
|
56
|
+
outputs: [],
|
|
57
|
+
},
|
|
58
|
+
] as const;
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createPublicClient, createWalletClient, defineChain, http } from "viem";
|
|
2
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
3
|
+
|
|
4
|
+
const RELAYER_KEY = process.env.RELAYER_PRIVATE_KEY as `0x${string}`;
|
|
5
|
+
if (!RELAYER_KEY) throw new Error("RELAYER_PRIVATE_KEY is required");
|
|
6
|
+
|
|
7
|
+
export const relayerAccount = privateKeyToAccount(RELAYER_KEY);
|
|
8
|
+
|
|
9
|
+
export const DELEGATE_CONTRACT_ADDRESS = (process.env.DELEGATE_ADDRESS ??
|
|
10
|
+
"") as `0x${string}`;
|
|
11
|
+
if (!DELEGATE_CONTRACT_ADDRESS || DELEGATE_CONTRACT_ADDRESS.length !== 42) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
"DELEGATE_ADDRESS must be a valid 42-character hex address (0x...)",
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildClients(chainId: number) {
|
|
18
|
+
const rpcUrl = process.env[`RPC_URL_${chainId}`];
|
|
19
|
+
if (!rpcUrl) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`RPC URL for chain ${chainId} not configured (set RPC_URL_${chainId})`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const chain = defineChain({
|
|
26
|
+
id: chainId,
|
|
27
|
+
name: `Chain ${chainId}`,
|
|
28
|
+
nativeCurrency: { decimals: 18, name: "Ether", symbol: "ETH" },
|
|
29
|
+
rpcUrls: { default: { http: [rpcUrl] } },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const transport = http(rpcUrl);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
publicClient: createPublicClient({ chain, transport }),
|
|
36
|
+
walletClient: createWalletClient({
|
|
37
|
+
account: relayerAccount,
|
|
38
|
+
chain,
|
|
39
|
+
transport,
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type Clients = ReturnType<typeof buildClients>;
|
|
45
|
+
const clientCache = new Map<number, Clients>();
|
|
46
|
+
|
|
47
|
+
export function getClients(chainId: number): Clients {
|
|
48
|
+
const cached = clientCache.get(chainId);
|
|
49
|
+
if (cached) return cached;
|
|
50
|
+
|
|
51
|
+
const clients = buildClients(chainId);
|
|
52
|
+
clientCache.set(chainId, clients);
|
|
53
|
+
return clients;
|
|
54
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { DELEGATE_CONTRACT_ADDRESS, relayerAccount, getClients } from "./config";
|
|
2
|
+
import { formatEther } from "viem";
|
|
3
|
+
import { mechanism } from "./mechanism";
|
|
4
|
+
import { bazaarManager } from "./storage";
|
|
5
|
+
import type {
|
|
6
|
+
DiscoveryResponse,
|
|
7
|
+
SettleRequest,
|
|
8
|
+
SettleResponse,
|
|
9
|
+
SupportedResponse,
|
|
10
|
+
VerifyRequest,
|
|
11
|
+
VerifyResponse,
|
|
12
|
+
} from "./types";
|
|
13
|
+
|
|
14
|
+
const PORT = Number(process.env.PORT) || 3000;
|
|
15
|
+
|
|
16
|
+
const CORS_HEADERS = {
|
|
17
|
+
"Access-Control-Allow-Origin": "*",
|
|
18
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
19
|
+
"Access-Control-Allow-Headers": "Content-Type, Payment-Signature",
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
function json(data: unknown, status = 200) {
|
|
23
|
+
return Response.json(data, { status, headers: CORS_HEADERS });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// --- Route Handlers ---
|
|
27
|
+
|
|
28
|
+
function handleHealthcheck() {
|
|
29
|
+
return json({
|
|
30
|
+
status: "ok",
|
|
31
|
+
uptime: process.uptime(),
|
|
32
|
+
timestamp: Date.now(),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function handleSupported() {
|
|
37
|
+
const response: SupportedResponse = {
|
|
38
|
+
kinds: [
|
|
39
|
+
{
|
|
40
|
+
x402Version: 2,
|
|
41
|
+
scheme: "eip7702",
|
|
42
|
+
network: "eip155:*",
|
|
43
|
+
extra: {},
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
extensions: ["bazaar"],
|
|
47
|
+
signers: { "eip155:*": [relayerAccount.address] },
|
|
48
|
+
};
|
|
49
|
+
return json({ ...response, delegateContract: DELEGATE_CONTRACT_ADDRESS });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function handleDiscovery(url: URL) {
|
|
53
|
+
const limit = Number(url.searchParams.get("limit")) || 100;
|
|
54
|
+
const offset = Number(url.searchParams.get("offset")) || 0;
|
|
55
|
+
const { items, total } = bazaarManager.list(limit, offset);
|
|
56
|
+
|
|
57
|
+
const response: DiscoveryResponse = {
|
|
58
|
+
x402Version: 2,
|
|
59
|
+
items,
|
|
60
|
+
pagination: { limit, offset, total },
|
|
61
|
+
};
|
|
62
|
+
return json(response);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function handleVerify(req: Request) {
|
|
66
|
+
const body = (await req.json()) as VerifyRequest;
|
|
67
|
+
const result: VerifyResponse = await mechanism.verify(
|
|
68
|
+
body.paymentPayload,
|
|
69
|
+
body.paymentRequirements,
|
|
70
|
+
);
|
|
71
|
+
return json(result);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function handleSettle(req: Request) {
|
|
75
|
+
const body = (await req.json()) as SettleRequest;
|
|
76
|
+
const result: SettleResponse = await mechanism.settle(
|
|
77
|
+
body.paymentPayload,
|
|
78
|
+
body.paymentRequirements,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (result.success && body.paymentPayload.resource?.url) {
|
|
82
|
+
bazaarManager.upsert({
|
|
83
|
+
resource: body.paymentPayload.resource.url,
|
|
84
|
+
type: "http",
|
|
85
|
+
x402Version: 2,
|
|
86
|
+
accepts: [body.paymentRequirements],
|
|
87
|
+
lastUpdated: new Date().toISOString(),
|
|
88
|
+
metadata: body.paymentPayload.extensions?.bazaar as
|
|
89
|
+
| Record<string, unknown>
|
|
90
|
+
| undefined,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return json(result);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function handleBalance() {
|
|
98
|
+
try {
|
|
99
|
+
// Default to Anvil chain ID 31337 for this demo
|
|
100
|
+
const chainId = 31337;
|
|
101
|
+
const { publicClient } = getClients(chainId);
|
|
102
|
+
const balance = await publicClient.getBalance({ address: relayerAccount.address });
|
|
103
|
+
|
|
104
|
+
return json({
|
|
105
|
+
address: relayerAccount.address,
|
|
106
|
+
eth: formatEther(balance),
|
|
107
|
+
chainId
|
|
108
|
+
});
|
|
109
|
+
} catch (e) {
|
|
110
|
+
return json({ error: "Failed to fetch balance", details: (e as Error).message }, 500);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// --- Server ---
|
|
115
|
+
|
|
116
|
+
Bun.serve({
|
|
117
|
+
port: PORT,
|
|
118
|
+
async fetch(req) {
|
|
119
|
+
if (req.method === "OPTIONS") {
|
|
120
|
+
return new Response(null, { headers: CORS_HEADERS });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const url = new URL(req.url);
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
if (req.method === "GET") {
|
|
127
|
+
if (url.pathname === "/healthcheck") return handleHealthcheck();
|
|
128
|
+
if (url.pathname === "/supported") return handleSupported();
|
|
129
|
+
if (url.pathname === "/discovery/resources")
|
|
130
|
+
return handleDiscovery(url);
|
|
131
|
+
if (url.pathname === "/balance") return await handleBalance();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (req.method === "POST") {
|
|
135
|
+
if (url.pathname === "/verify") return await handleVerify(req);
|
|
136
|
+
if (url.pathname === "/settle") return await handleSettle(req);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return new Response("Not Found", { status: 404, headers: CORS_HEADERS });
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.error(e);
|
|
142
|
+
return json({ error: (e as Error).message }, 500);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
console.log(`x402 EIP-7702 Facilitator running on port ${PORT}`);
|
package/src/mechanism.ts
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { type Address, encodeFunctionData, verifyTypedData } from "viem";
|
|
2
|
+
import { recoverAuthorizationAddress } from "viem/utils";
|
|
3
|
+
import { DELEGATE_ABI, ERC20_ABI } from "./abi";
|
|
4
|
+
import {
|
|
5
|
+
DELEGATE_CONTRACT_ADDRESS,
|
|
6
|
+
getClients,
|
|
7
|
+
relayerAccount,
|
|
8
|
+
} from "./config";
|
|
9
|
+
import { nonceManager } from "./storage";
|
|
10
|
+
import {
|
|
11
|
+
ADDRESS_ZERO,
|
|
12
|
+
type Eip7702Authorization,
|
|
13
|
+
type Eip7702EthPayloadData,
|
|
14
|
+
type Eip7702PayloadData,
|
|
15
|
+
type PaymentPayload,
|
|
16
|
+
type PaymentRequirements,
|
|
17
|
+
type SettleResponse,
|
|
18
|
+
type VerifyResponse,
|
|
19
|
+
} from "./types";
|
|
20
|
+
|
|
21
|
+
// --- EIP-712 Type Definitions ---
|
|
22
|
+
|
|
23
|
+
const EIP712_DOMAIN = {
|
|
24
|
+
name: "Delegate",
|
|
25
|
+
version: "1.0",
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
const ERC20_INTENT_TYPES = {
|
|
29
|
+
PaymentIntent: [
|
|
30
|
+
{ name: "token", type: "address" },
|
|
31
|
+
{ name: "amount", type: "uint256" },
|
|
32
|
+
{ name: "to", type: "address" },
|
|
33
|
+
{ name: "nonce", type: "uint256" },
|
|
34
|
+
{ name: "deadline", type: "uint256" },
|
|
35
|
+
],
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
const ETH_INTENT_TYPES = {
|
|
39
|
+
EthPaymentIntent: [
|
|
40
|
+
{ name: "amount", type: "uint256" },
|
|
41
|
+
{ name: "to", type: "address" },
|
|
42
|
+
{ name: "nonce", type: "uint256" },
|
|
43
|
+
{ name: "deadline", type: "uint256" },
|
|
44
|
+
],
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
// --- Helpers ---
|
|
48
|
+
|
|
49
|
+
function isEthPayment(reqs: PaymentRequirements): boolean {
|
|
50
|
+
return reqs.asset.toLowerCase() === ADDRESS_ZERO.toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseChainId(network: string): number {
|
|
54
|
+
const chainId = Number(network.split(":")[1]);
|
|
55
|
+
if (isNaN(chainId)) throw new Error(`Invalid network format: ${network}`);
|
|
56
|
+
return chainId;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function extractPayload<T extends Eip7702PayloadData | Eip7702EthPayloadData>(
|
|
60
|
+
payload: Record<string, unknown>,
|
|
61
|
+
): T {
|
|
62
|
+
if (!payload.authorization || !payload.intent || !payload.signature) {
|
|
63
|
+
throw new Error("Missing required EIP-7702 payload fields");
|
|
64
|
+
}
|
|
65
|
+
return payload as unknown as T;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildDomain(chainId: number, verifyingContract: Address) {
|
|
69
|
+
return { ...EIP712_DOMAIN, chainId, verifyingContract };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// --- Mechanism ---
|
|
73
|
+
|
|
74
|
+
export class Eip7702Mechanism {
|
|
75
|
+
private async recoverSigner(authorization: Eip7702Authorization) {
|
|
76
|
+
const signer = await recoverAuthorizationAddress({
|
|
77
|
+
authorization: {
|
|
78
|
+
contractAddress: authorization.contractAddress,
|
|
79
|
+
to: authorization.contractAddress,
|
|
80
|
+
chainId: authorization.chainId,
|
|
81
|
+
nonce: authorization.nonce,
|
|
82
|
+
},
|
|
83
|
+
signature: {
|
|
84
|
+
r: authorization.r,
|
|
85
|
+
s: authorization.s,
|
|
86
|
+
yParity: authorization.yParity,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (
|
|
91
|
+
authorization.contractAddress.toLowerCase() !==
|
|
92
|
+
DELEGATE_CONTRACT_ADDRESS.toLowerCase()
|
|
93
|
+
) {
|
|
94
|
+
throw new Error("Untrusted Delegate Contract");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return signer;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private async verifyIntentSignature(
|
|
101
|
+
payload: PaymentPayload,
|
|
102
|
+
ethPayment: boolean,
|
|
103
|
+
chainId: number,
|
|
104
|
+
signer: Address,
|
|
105
|
+
signature: `0x${string}`,
|
|
106
|
+
): Promise<boolean> {
|
|
107
|
+
const domain = buildDomain(chainId, signer);
|
|
108
|
+
|
|
109
|
+
if (ethPayment) {
|
|
110
|
+
const { intent } = extractPayload<Eip7702EthPayloadData>(payload.payload);
|
|
111
|
+
return verifyTypedData({
|
|
112
|
+
address: signer,
|
|
113
|
+
domain,
|
|
114
|
+
types: ETH_INTENT_TYPES,
|
|
115
|
+
primaryType: "EthPaymentIntent",
|
|
116
|
+
message: {
|
|
117
|
+
amount: BigInt(intent.amount),
|
|
118
|
+
to: intent.to,
|
|
119
|
+
nonce: BigInt(intent.nonce),
|
|
120
|
+
deadline: BigInt(intent.deadline),
|
|
121
|
+
},
|
|
122
|
+
signature,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const { intent } = extractPayload<Eip7702PayloadData>(payload.payload);
|
|
127
|
+
return verifyTypedData({
|
|
128
|
+
address: signer,
|
|
129
|
+
domain,
|
|
130
|
+
types: ERC20_INTENT_TYPES,
|
|
131
|
+
primaryType: "PaymentIntent",
|
|
132
|
+
message: {
|
|
133
|
+
token: intent.token,
|
|
134
|
+
amount: BigInt(intent.amount),
|
|
135
|
+
to: intent.to,
|
|
136
|
+
nonce: BigInt(intent.nonce),
|
|
137
|
+
deadline: BigInt(intent.deadline),
|
|
138
|
+
},
|
|
139
|
+
signature,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async verify(
|
|
144
|
+
payload: PaymentPayload,
|
|
145
|
+
reqs: PaymentRequirements,
|
|
146
|
+
): Promise<VerifyResponse> {
|
|
147
|
+
try {
|
|
148
|
+
const chainId = parseChainId(reqs.network);
|
|
149
|
+
const ethPayment = isEthPayment(reqs);
|
|
150
|
+
const { authorization, signature } = extractPayload<Eip7702PayloadData>(
|
|
151
|
+
payload.payload,
|
|
152
|
+
);
|
|
153
|
+
const { publicClient } = getClients(chainId);
|
|
154
|
+
|
|
155
|
+
// 1. Verify EIP-7702 authorization
|
|
156
|
+
const signer = await this.recoverSigner(authorization);
|
|
157
|
+
|
|
158
|
+
// 2. Verify EIP-712 intent signature
|
|
159
|
+
const valid = await this.verifyIntentSignature(
|
|
160
|
+
payload,
|
|
161
|
+
ethPayment,
|
|
162
|
+
chainId,
|
|
163
|
+
signer,
|
|
164
|
+
signature,
|
|
165
|
+
);
|
|
166
|
+
if (!valid) {
|
|
167
|
+
return { isValid: false, invalidReason: "Invalid Intent Signature" };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 3. Check deadline
|
|
171
|
+
const intent = extractPayload<Eip7702PayloadData>(payload.payload).intent;
|
|
172
|
+
if (BigInt(intent.deadline) < BigInt(Math.floor(Date.now() / 1000))) {
|
|
173
|
+
return { isValid: false, invalidReason: "Deadline Expired" };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 4. Check nonce
|
|
177
|
+
if (!nonceManager.checkAndMark(intent.nonce.toString())) {
|
|
178
|
+
return { isValid: false, invalidReason: "Nonce Used" };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 5. Check balance
|
|
182
|
+
if (ethPayment) {
|
|
183
|
+
const balance = await publicClient.getBalance({ address: signer });
|
|
184
|
+
if (balance < BigInt(intent.amount)) {
|
|
185
|
+
return { isValid: false, invalidReason: "Insufficient Balance" };
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
const balance = await publicClient.readContract({
|
|
189
|
+
address: intent.token,
|
|
190
|
+
abi: ERC20_ABI,
|
|
191
|
+
functionName: "balanceOf",
|
|
192
|
+
args: [signer],
|
|
193
|
+
});
|
|
194
|
+
if (balance < BigInt(intent.amount)) {
|
|
195
|
+
return { isValid: false, invalidReason: "Insufficient Balance" };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { isValid: true, payer: signer };
|
|
200
|
+
} catch (e) {
|
|
201
|
+
console.error(e);
|
|
202
|
+
return { isValid: false, invalidReason: (e as Error).message };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async settle(
|
|
207
|
+
payload: PaymentPayload,
|
|
208
|
+
reqs: PaymentRequirements,
|
|
209
|
+
): Promise<SettleResponse> {
|
|
210
|
+
try {
|
|
211
|
+
const verification = await this.verify(payload, reqs);
|
|
212
|
+
if (!verification.isValid) throw new Error(verification.invalidReason);
|
|
213
|
+
|
|
214
|
+
const chainId = parseChainId(reqs.network);
|
|
215
|
+
const { walletClient, publicClient } = getClients(chainId);
|
|
216
|
+
const ethPayment = isEthPayment(reqs);
|
|
217
|
+
const { authorization, signature } = extractPayload<Eip7702PayloadData>(
|
|
218
|
+
payload.payload,
|
|
219
|
+
);
|
|
220
|
+
const payer = verification.payer! as Address;
|
|
221
|
+
|
|
222
|
+
// Encode call data
|
|
223
|
+
let data;
|
|
224
|
+
if (ethPayment) {
|
|
225
|
+
const { intent } = extractPayload<Eip7702EthPayloadData>(
|
|
226
|
+
payload.payload,
|
|
227
|
+
);
|
|
228
|
+
data = encodeFunctionData({
|
|
229
|
+
abi: DELEGATE_ABI,
|
|
230
|
+
functionName: "transferEth",
|
|
231
|
+
args: [
|
|
232
|
+
{
|
|
233
|
+
amount: BigInt(intent.amount),
|
|
234
|
+
to: intent.to,
|
|
235
|
+
nonce: BigInt(intent.nonce),
|
|
236
|
+
deadline: BigInt(intent.deadline),
|
|
237
|
+
},
|
|
238
|
+
signature,
|
|
239
|
+
],
|
|
240
|
+
});
|
|
241
|
+
} else {
|
|
242
|
+
const { intent } = extractPayload<Eip7702PayloadData>(payload.payload);
|
|
243
|
+
data = encodeFunctionData({
|
|
244
|
+
abi: DELEGATE_ABI,
|
|
245
|
+
functionName: "transfer",
|
|
246
|
+
args: [
|
|
247
|
+
{
|
|
248
|
+
token: intent.token,
|
|
249
|
+
amount: BigInt(intent.amount),
|
|
250
|
+
to: intent.to,
|
|
251
|
+
nonce: BigInt(intent.nonce),
|
|
252
|
+
deadline: BigInt(intent.deadline),
|
|
253
|
+
},
|
|
254
|
+
signature,
|
|
255
|
+
],
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Skip authorization list if payer already has delegated code
|
|
260
|
+
const code = await publicClient.getCode({ address: payer });
|
|
261
|
+
const hasCode = code && code !== "0x";
|
|
262
|
+
|
|
263
|
+
const txBase = {
|
|
264
|
+
account: relayerAccount,
|
|
265
|
+
chain: walletClient.chain,
|
|
266
|
+
to: payer,
|
|
267
|
+
data,
|
|
268
|
+
} as const;
|
|
269
|
+
|
|
270
|
+
const hash = hasCode
|
|
271
|
+
? await walletClient.sendTransaction(txBase)
|
|
272
|
+
: await walletClient.sendTransaction({
|
|
273
|
+
...txBase,
|
|
274
|
+
authorizationList: [
|
|
275
|
+
{
|
|
276
|
+
contractAddress: authorization.contractAddress,
|
|
277
|
+
address: authorization.contractAddress,
|
|
278
|
+
chainId: authorization.chainId,
|
|
279
|
+
nonce: authorization.nonce,
|
|
280
|
+
r: authorization.r,
|
|
281
|
+
s: authorization.s,
|
|
282
|
+
yParity: authorization.yParity,
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
await publicClient.waitForTransactionReceipt({ hash });
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
success: true,
|
|
291
|
+
transaction: hash,
|
|
292
|
+
network: reqs.network,
|
|
293
|
+
payer,
|
|
294
|
+
};
|
|
295
|
+
} catch (e) {
|
|
296
|
+
return {
|
|
297
|
+
success: false,
|
|
298
|
+
errorReason: (e as Error).message,
|
|
299
|
+
transaction: "",
|
|
300
|
+
network: reqs.network,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export const mechanism = new Eip7702Mechanism();
|