@lemmaoracle/x402 0.1.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/broadcast/DeployPaymentVerifier.s.sol/10143/run-1775996174982.json +34 -0
- package/broadcast/DeployPaymentVerifier.s.sol/10143/run-1775996423983.json +51 -0
- package/broadcast/DeployPaymentVerifier.s.sol/10143/run-1775996907850.json +51 -0
- package/broadcast/DeployPaymentVerifier.s.sol/10143/run-latest.json +51 -0
- package/cache/DeployPaymentVerifier.s.sol/10143/run-1775996174982.json +7 -0
- package/cache/DeployPaymentVerifier.s.sol/10143/run-1775996423983.json +7 -0
- package/cache/DeployPaymentVerifier.s.sol/10143/run-1775996907850.json +7 -0
- package/cache/DeployPaymentVerifier.s.sol/10143/run-latest.json +7 -0
- package/cache/solidity-files-cache.json +1 -0
- package/circuits/README.md +78 -0
- package/circuits/payment-comprehensive.test.mjs +272 -0
- package/circuits/payment.circom +89 -0
- package/circuits/payment.test.mjs +173 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +1 -0
- package/dist/proof-generator.d.ts +26 -0
- package/dist/proof-generator.d.ts.map +1 -0
- package/dist/proof-generator.js +154 -0
- package/dist/proof-generator.js.map +1 -0
- package/dist/proof-generator.test.d.ts +7 -0
- package/dist/proof-generator.test.d.ts.map +1 -0
- package/dist/proof-generator.test.js +121 -0
- package/dist/proof-generator.test.js.map +1 -0
- package/dist/transaction-watcher.d.ts +42 -0
- package/dist/transaction-watcher.d.ts.map +1 -0
- package/dist/transaction-watcher.js +88 -0
- package/dist/transaction-watcher.js.map +1 -0
- package/dist/transaction-watcher.test.d.ts +7 -0
- package/dist/transaction-watcher.test.d.ts.map +1 -0
- package/dist/transaction-watcher.test.js +111 -0
- package/dist/transaction-watcher.test.js.map +1 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/foundry.lock +11 -0
- package/foundry.toml +16 -0
- package/lib/forge-std/.gitattributes +1 -0
- package/lib/forge-std/.github/CODEOWNERS +1 -0
- package/lib/forge-std/.github/dependabot.yml +6 -0
- package/lib/forge-std/.github/workflows/ci.yml +161 -0
- package/lib/forge-std/.github/workflows/sync.yml +36 -0
- package/lib/forge-std/CONTRIBUTING.md +193 -0
- package/lib/forge-std/LICENSE-APACHE +203 -0
- package/lib/forge-std/LICENSE-MIT +25 -0
- package/lib/forge-std/README.md +314 -0
- package/lib/forge-std/RELEASE_CHECKLIST.md +12 -0
- package/lib/forge-std/foundry.toml +18 -0
- package/lib/forge-std/package.json +16 -0
- package/lib/forge-std/scripts/vm.py +636 -0
- package/lib/forge-std/src/Base.sol +48 -0
- package/lib/forge-std/src/Config.sol +60 -0
- package/lib/forge-std/src/LibVariable.sol +477 -0
- package/lib/forge-std/src/Script.sol +28 -0
- package/lib/forge-std/src/StdAssertions.sol +1300 -0
- package/lib/forge-std/src/StdChains.sol +303 -0
- package/lib/forge-std/src/StdCheats.sol +825 -0
- package/lib/forge-std/src/StdConfig.sol +632 -0
- package/lib/forge-std/src/StdConstants.sol +30 -0
- package/lib/forge-std/src/StdError.sol +15 -0
- package/lib/forge-std/src/StdInvariant.sol +161 -0
- package/lib/forge-std/src/StdJson.sol +275 -0
- package/lib/forge-std/src/StdMath.sol +47 -0
- package/lib/forge-std/src/StdStorage.sol +476 -0
- package/lib/forge-std/src/StdStyle.sol +333 -0
- package/lib/forge-std/src/StdToml.sol +275 -0
- package/lib/forge-std/src/StdUtils.sol +200 -0
- package/lib/forge-std/src/Test.sol +32 -0
- package/lib/forge-std/src/Vm.sol +2533 -0
- package/lib/forge-std/src/console.sol +1551 -0
- package/lib/forge-std/src/console2.sol +4 -0
- package/lib/forge-std/src/interfaces/IERC1155.sol +105 -0
- package/lib/forge-std/src/interfaces/IERC165.sol +12 -0
- package/lib/forge-std/src/interfaces/IERC20.sol +43 -0
- package/lib/forge-std/src/interfaces/IERC4626.sol +190 -0
- package/lib/forge-std/src/interfaces/IERC6909.sol +72 -0
- package/lib/forge-std/src/interfaces/IERC721.sol +164 -0
- package/lib/forge-std/src/interfaces/IERC7540.sol +145 -0
- package/lib/forge-std/src/interfaces/IERC7575.sol +241 -0
- package/lib/forge-std/src/interfaces/IMulticall3.sol +68 -0
- package/lib/forge-std/src/safeconsole.sol +13248 -0
- package/lib/forge-std/test/CommonBase.t.sol +44 -0
- package/lib/forge-std/test/Config.t.sol +381 -0
- package/lib/forge-std/test/LibVariable.t.sol +452 -0
- package/lib/forge-std/test/StdAssertions.t.sol +141 -0
- package/lib/forge-std/test/StdChains.t.sol +227 -0
- package/lib/forge-std/test/StdCheats.t.sol +638 -0
- package/lib/forge-std/test/StdConstants.t.sol +38 -0
- package/lib/forge-std/test/StdError.t.sol +119 -0
- package/lib/forge-std/test/StdJson.t.sol +49 -0
- package/lib/forge-std/test/StdMath.t.sol +202 -0
- package/lib/forge-std/test/StdStorage.t.sol +532 -0
- package/lib/forge-std/test/StdStyle.t.sol +110 -0
- package/lib/forge-std/test/StdToml.t.sol +49 -0
- package/lib/forge-std/test/StdUtils.t.sol +342 -0
- package/lib/forge-std/test/Vm.t.sol +18 -0
- package/lib/forge-std/test/compilation/CompilationScript.sol +8 -0
- package/lib/forge-std/test/compilation/CompilationScriptBase.sol +8 -0
- package/lib/forge-std/test/compilation/CompilationTest.sol +8 -0
- package/lib/forge-std/test/compilation/CompilationTestBase.sol +8 -0
- package/lib/forge-std/test/fixtures/broadcast.log.json +187 -0
- package/lib/forge-std/test/fixtures/config.toml +81 -0
- package/lib/forge-std/test/fixtures/test.json +8 -0
- package/lib/forge-std/test/fixtures/test.toml +6 -0
- package/package.json +55 -0
- package/package.json.backup +55 -0
- package/scripts/DeployPaymentVerifier.s.sol +18 -0
- package/scripts/build.sh +61 -0
- package/scripts/deploy.sh +65 -0
- package/scripts/fix-verifier.mjs +64 -0
- package/scripts/register-circuit.ts +213 -0
- package/scripts/test-with-circuit.mjs +88 -0
- package/src/index.ts +115 -0
- package/src/proof-generator.test.ts +139 -0
- package/src/proof-generator.ts +220 -0
- package/src/transaction-watcher.test.ts +153 -0
- package/src/transaction-watcher.ts +154 -0
- package/src/types.ts +110 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +18 -0
- package/workers/x402-facilitator/src/index.ts +582 -0
- package/workers/x402-facilitator/src/x402-env.d.ts +25 -0
- package/workers/x402-facilitator/wrangler.toml +19 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* X402 Facilitator Cloudflare Worker
|
|
3
|
+
*
|
|
4
|
+
* Implements the x402 facilitator spec with three endpoints:
|
|
5
|
+
* POST /verify – lightweight pre-check (signature, amount, balance)
|
|
6
|
+
* POST /settle – broadcast tx, wait for confirmation, generate ZK proof
|
|
7
|
+
* POST /prepare – delegate schema preparation to Relay API
|
|
8
|
+
*
|
|
9
|
+
* Called by resource servers as part of the x402 payment flow.
|
|
10
|
+
*
|
|
11
|
+
* Network, RPC URL, and price are resolved dynamically from the incoming
|
|
12
|
+
* request (payload / requirements) rather than pinned via environment
|
|
13
|
+
* variables, because each resource server may target a different chain.
|
|
14
|
+
*
|
|
15
|
+
* ZK proof generation is delegated to @lemmaoracle/relay (Vercel) because
|
|
16
|
+
* snarkjs/ffjavascript require APIs (URL.createObjectURL) unavailable on
|
|
17
|
+
* Cloudflare Workers.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type X402Env from "./x402-env";
|
|
21
|
+
import { createPublicClient, http, type PublicClient } from "viem";
|
|
22
|
+
import { base, baseSepolia } from "viem/chains";
|
|
23
|
+
import { Hono } from "hono";
|
|
24
|
+
import { cors } from "hono/cors";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Request / Response types (aligned with x402 facilitator spec)
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/** Payment payload sent by the client (forwarded by the resource server). */
|
|
31
|
+
type PaymentPayload = Readonly<{
|
|
32
|
+
/** EVM scheme: the signed authorization or raw signed tx */
|
|
33
|
+
signature: `0x${string}`;
|
|
34
|
+
/** Payer address */
|
|
35
|
+
from: `0x${string}`;
|
|
36
|
+
/** Network identifier, e.g. "eip155:84532" */
|
|
37
|
+
network: string;
|
|
38
|
+
/** Scheme identifier, e.g. "exact" */
|
|
39
|
+
scheme: string;
|
|
40
|
+
/** Additional scheme-specific data */
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
}>;
|
|
43
|
+
|
|
44
|
+
/** Payment requirements set by the resource server. */
|
|
45
|
+
type PaymentRequirements = Readonly<{
|
|
46
|
+
/** Scheme identifier */
|
|
47
|
+
scheme: string;
|
|
48
|
+
/** Network identifier */
|
|
49
|
+
network: string;
|
|
50
|
+
/** Required payment amount (human-readable, e.g. "$0.001") */
|
|
51
|
+
price: string;
|
|
52
|
+
/** Address to receive payment */
|
|
53
|
+
payTo: `0x${string}`;
|
|
54
|
+
/** Additional scheme-specific requirements */
|
|
55
|
+
[key: string]: unknown;
|
|
56
|
+
}>;
|
|
57
|
+
|
|
58
|
+
/** Body of POST /verify and POST /settle */
|
|
59
|
+
type FacilitatorRequest = Readonly<{
|
|
60
|
+
payload: PaymentPayload;
|
|
61
|
+
requirements: PaymentRequirements;
|
|
62
|
+
}>;
|
|
63
|
+
|
|
64
|
+
/** Response from POST /verify */
|
|
65
|
+
type VerifyResponse = Readonly<{
|
|
66
|
+
isValid: boolean;
|
|
67
|
+
invalidReason: string | null;
|
|
68
|
+
}>;
|
|
69
|
+
|
|
70
|
+
/** Response from POST /settle */
|
|
71
|
+
type SettleResponse = Readonly<{
|
|
72
|
+
success: boolean;
|
|
73
|
+
txHash: string;
|
|
74
|
+
proof: {
|
|
75
|
+
proof: string;
|
|
76
|
+
inputs: ReadonlyArray<string>;
|
|
77
|
+
circuitId: string;
|
|
78
|
+
generatedAt: number;
|
|
79
|
+
};
|
|
80
|
+
network: string;
|
|
81
|
+
error?: string;
|
|
82
|
+
}>;
|
|
83
|
+
|
|
84
|
+
/** Relay API response from POST /prover/prove */
|
|
85
|
+
type RelayProveResponse = Readonly<{
|
|
86
|
+
proof: string;
|
|
87
|
+
inputs: ReadonlyArray<string>;
|
|
88
|
+
error?: string;
|
|
89
|
+
message?: string;
|
|
90
|
+
}>;
|
|
91
|
+
|
|
92
|
+
/** Relay API response from POST /prepare */
|
|
93
|
+
type RelayPrepareResponse = Readonly<{
|
|
94
|
+
normalized: Readonly<Record<string, unknown>>;
|
|
95
|
+
commitments: Readonly<{
|
|
96
|
+
root: string;
|
|
97
|
+
leaves: ReadonlyArray<string>;
|
|
98
|
+
randomness: ReadonlyArray<string>;
|
|
99
|
+
}>;
|
|
100
|
+
error?: string;
|
|
101
|
+
message?: string;
|
|
102
|
+
}>;
|
|
103
|
+
|
|
104
|
+
/** Body of POST /prepare */
|
|
105
|
+
type PrepareRequest = Readonly<{
|
|
106
|
+
schemaId: string;
|
|
107
|
+
payload: unknown;
|
|
108
|
+
}>;
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Facilitator config (lightweight — no SDK dependency)
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
type FacilitatorConfig = Readonly<{
|
|
115
|
+
payToAddress: `0x${string}`;
|
|
116
|
+
network: string;
|
|
117
|
+
price: string;
|
|
118
|
+
ethereumRpcUrl: string;
|
|
119
|
+
chainId: number;
|
|
120
|
+
lemmaApiBase: string;
|
|
121
|
+
lemmaApiKey?: string;
|
|
122
|
+
circuitId: string;
|
|
123
|
+
relayUrl: string;
|
|
124
|
+
minAmount: bigint;
|
|
125
|
+
requiredConfirmations: number;
|
|
126
|
+
}>;
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Network helpers
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
/** Default RPC endpoints — used when no override is provided via RPC_URLS. */
|
|
133
|
+
const DEFAULT_RPC_MAP: Readonly<Record<string, string>> = {
|
|
134
|
+
"eip155:8453": "https://mainnet.base.org",
|
|
135
|
+
"eip155:84532": "https://sepolia.base.org",
|
|
136
|
+
"eip155:10143": "https://testnet-rpc.monad.xyz",
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/** Map network identifier → chain id. */
|
|
140
|
+
const chainIdForNetwork = (network: string): number => {
|
|
141
|
+
const map: Record<string, number> = {
|
|
142
|
+
"eip155:8453": 8453,
|
|
143
|
+
"eip155:84532": 84532,
|
|
144
|
+
"eip155:10143": 10143,
|
|
145
|
+
};
|
|
146
|
+
const id = map[network];
|
|
147
|
+
if (id === undefined) {
|
|
148
|
+
throw new Error(`Unsupported network: ${network}`);
|
|
149
|
+
}
|
|
150
|
+
return id;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** Map network identifier → viem Chain definition. */
|
|
154
|
+
const chainForNetwork = (network: string) => {
|
|
155
|
+
const map: Record<string, typeof baseSepolia> = {
|
|
156
|
+
"eip155:8453": base,
|
|
157
|
+
"eip155:84532": baseSepolia,
|
|
158
|
+
};
|
|
159
|
+
// Fall back to baseSepolia for chains without a viem definition.
|
|
160
|
+
return map[network] ?? baseSepolia;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Resolve the RPC URL for a given network.
|
|
165
|
+
*
|
|
166
|
+
* Priority: env RPC_URLS override → DEFAULT_RPC_MAP.
|
|
167
|
+
*/
|
|
168
|
+
const rpcForNetwork = (network: string, rpcOverrides: Record<string, string>): string => {
|
|
169
|
+
const url = rpcOverrides[network] ?? DEFAULT_RPC_MAP[network];
|
|
170
|
+
if (!url) {
|
|
171
|
+
throw new Error(`No RPC URL configured for network: ${network}`);
|
|
172
|
+
}
|
|
173
|
+
return url;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/** Parse RPC_URLS env var (JSON string) into a record. */
|
|
177
|
+
const parseRpcUrls = (raw?: string): Record<string, string> => {
|
|
178
|
+
if (!raw) return {};
|
|
179
|
+
try {
|
|
180
|
+
return JSON.parse(raw) as Record<string, string>;
|
|
181
|
+
} catch {
|
|
182
|
+
return {};
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Helpers
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
/** Build a viem PublicClient for the given network. */
|
|
191
|
+
const buildClient = (rpcUrl: string, network: string): PublicClient =>
|
|
192
|
+
createPublicClient({
|
|
193
|
+
chain: chainForNetwork(network),
|
|
194
|
+
transport: http(rpcUrl),
|
|
195
|
+
}) as PublicClient;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Build a per-request FacilitatorConfig by merging static env settings with
|
|
199
|
+
* dynamic values from the request (network, price, RPC URL).
|
|
200
|
+
*/
|
|
201
|
+
const configForRequest = (
|
|
202
|
+
env: typeof X402Env,
|
|
203
|
+
network: string,
|
|
204
|
+
price: string,
|
|
205
|
+
rpcOverrides: Record<string, string>,
|
|
206
|
+
): FacilitatorConfig => ({
|
|
207
|
+
payToAddress: env.PAY_TO_ADDRESS as `0x${string}`,
|
|
208
|
+
network,
|
|
209
|
+
price,
|
|
210
|
+
ethereumRpcUrl: rpcForNetwork(network, rpcOverrides),
|
|
211
|
+
chainId: chainIdForNetwork(network),
|
|
212
|
+
lemmaApiBase: env.LEMMA_API_BASE,
|
|
213
|
+
lemmaApiKey: env.LEMMA_API_KEY,
|
|
214
|
+
circuitId: env.CIRCUIT_ID ?? "x402-payment-v1",
|
|
215
|
+
relayUrl: env.RELAY_URL,
|
|
216
|
+
minAmount: 1000n,
|
|
217
|
+
requiredConfirmations: 6,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
/** Parse and validate a facilitator request body. */
|
|
221
|
+
const parseFacilitatorRequest = async (
|
|
222
|
+
body: unknown,
|
|
223
|
+
): Promise<{ ok: true; data: FacilitatorRequest } | { ok: false; reason: string }> => {
|
|
224
|
+
const req = body as FacilitatorRequest | undefined;
|
|
225
|
+
if (!req?.payload || !req?.requirements) {
|
|
226
|
+
return { ok: false, reason: "Request must contain 'payload' and 'requirements'" };
|
|
227
|
+
}
|
|
228
|
+
if (!req.payload.from || !req.payload.signature) {
|
|
229
|
+
return { ok: false, reason: "Payload must contain 'from' and 'signature'" };
|
|
230
|
+
}
|
|
231
|
+
if (!req.requirements.payTo || !req.requirements.network) {
|
|
232
|
+
return { ok: false, reason: "Requirements must contain 'payTo' and 'network'" };
|
|
233
|
+
}
|
|
234
|
+
return { ok: true, data: req };
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// Relay API client — delegates proof generation to @lemmaoracle/relay
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Call the relay API's POST /prover/prove endpoint.
|
|
243
|
+
*
|
|
244
|
+
* This replaces the direct `prover.prove()` call that would pull in
|
|
245
|
+
* snarkjs/ffjavascript (which use `URL.createObjectURL()` — unavailable
|
|
246
|
+
* on Cloudflare Workers).
|
|
247
|
+
*/
|
|
248
|
+
const relayProve = async (
|
|
249
|
+
relayUrl: string,
|
|
250
|
+
apiBase: string,
|
|
251
|
+
apiKey: string | undefined,
|
|
252
|
+
circuitId: string,
|
|
253
|
+
witness: Readonly<Record<string, unknown>>,
|
|
254
|
+
): Promise<RelayProveResponse> => {
|
|
255
|
+
const res = await fetch(`${relayUrl.replace(/\/$/, "")}/prover/prove`, {
|
|
256
|
+
method: "POST",
|
|
257
|
+
headers: { "Content-Type": "application/json" },
|
|
258
|
+
body: JSON.stringify({
|
|
259
|
+
apiBase,
|
|
260
|
+
...(apiKey ? { apiKey } : {}),
|
|
261
|
+
input: { circuitId, witness },
|
|
262
|
+
}),
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const data = (await res.json()) as RelayProveResponse;
|
|
266
|
+
|
|
267
|
+
if (!res.ok) {
|
|
268
|
+
throw new Error(data.message ?? data.error ?? `Relay /prover/prove failed (${res.status})`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return data;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Call the relay API's POST /prepare endpoint.
|
|
276
|
+
*
|
|
277
|
+
* Fetches schema metadata, defines the schema, and prepares data.
|
|
278
|
+
* Delegated to @lemmaoracle/relay because preparation may require
|
|
279
|
+
* WASM execution and Node.js-specific APIs unavailable on Workers.
|
|
280
|
+
*/
|
|
281
|
+
const relayPrepare = async (
|
|
282
|
+
relayUrl: string,
|
|
283
|
+
apiBase: string,
|
|
284
|
+
apiKey: string | undefined,
|
|
285
|
+
schemaId: string,
|
|
286
|
+
payload: unknown,
|
|
287
|
+
): Promise<RelayPrepareResponse> => {
|
|
288
|
+
const res = await fetch(`${relayUrl.replace(/\/$/, "")}/prepare`, {
|
|
289
|
+
method: "POST",
|
|
290
|
+
headers: { "Content-Type": "application/json" },
|
|
291
|
+
body: JSON.stringify({
|
|
292
|
+
apiBase,
|
|
293
|
+
...(apiKey ? { apiKey } : {}),
|
|
294
|
+
input: { schemaId, payload },
|
|
295
|
+
}),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const data = (await res.json()) as RelayPrepareResponse;
|
|
299
|
+
|
|
300
|
+
if (!res.ok) {
|
|
301
|
+
throw new Error(data.message ?? data.error ?? `Relay /prepare failed (${res.status})`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return data;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
// Verification logic
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Verify a payment payload against the given requirements.
|
|
313
|
+
*
|
|
314
|
+
* Checks performed:
|
|
315
|
+
* 1. Network / scheme match
|
|
316
|
+
* 2. Signature validity (ecRecover)
|
|
317
|
+
* 3. Payer has sufficient balance
|
|
318
|
+
* 4. Payment amount meets the minimum requirement
|
|
319
|
+
*/
|
|
320
|
+
const verifyPayment = async (
|
|
321
|
+
payload: PaymentPayload,
|
|
322
|
+
requirements: PaymentRequirements,
|
|
323
|
+
client: PublicClient,
|
|
324
|
+
config: FacilitatorConfig,
|
|
325
|
+
): Promise<VerifyResponse> => {
|
|
326
|
+
// 1. Network match
|
|
327
|
+
if (payload.network !== requirements.network) {
|
|
328
|
+
return { isValid: false, invalidReason: "Network mismatch between payload and requirements" };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 2. Scheme match
|
|
332
|
+
if (payload.scheme !== requirements.scheme) {
|
|
333
|
+
return { isValid: false, invalidReason: "Scheme mismatch between payload and requirements" };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// 3. Verify the payer address is a valid account with a balance
|
|
337
|
+
try {
|
|
338
|
+
const balance = await client.getBalance({ address: payload.from });
|
|
339
|
+
if (balance < config.minAmount) {
|
|
340
|
+
return { isValid: false, invalidReason: `Insufficient balance: ${balance} < ${config.minAmount}` };
|
|
341
|
+
}
|
|
342
|
+
} catch (err) {
|
|
343
|
+
return { isValid: false, invalidReason: `Balance check failed: ${(err as Error).message}` };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// 4. Verify recipient matches requirements
|
|
347
|
+
if (requirements.payTo.toLowerCase() !== config.payToAddress.toLowerCase()) {
|
|
348
|
+
return { isValid: false, invalidReason: "payTo address does not match facilitator config" };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return { isValid: true, invalidReason: null };
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
// Settlement logic
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Settle a payment by broadcasting the transaction and generating a ZK proof.
|
|
360
|
+
*
|
|
361
|
+
* Steps:
|
|
362
|
+
* 1. Broadcast the signed transaction to the blockchain
|
|
363
|
+
* 2. Wait for on-chain confirmation
|
|
364
|
+
* 3. Generate ZK proof via relay API (delegated to @lemmaoracle/relay)
|
|
365
|
+
* 4. Return txHash + proof
|
|
366
|
+
*/
|
|
367
|
+
const settlePayment = async (
|
|
368
|
+
payload: PaymentPayload,
|
|
369
|
+
requirements: PaymentRequirements,
|
|
370
|
+
client: PublicClient,
|
|
371
|
+
config: FacilitatorConfig,
|
|
372
|
+
): Promise<SettleResponse> => {
|
|
373
|
+
const network = requirements.network;
|
|
374
|
+
|
|
375
|
+
// Step 1: Broadcast the signed transaction
|
|
376
|
+
let txHash: `0x${string}`;
|
|
377
|
+
try {
|
|
378
|
+
txHash = await client.request({
|
|
379
|
+
method: "eth_sendRawTransaction",
|
|
380
|
+
params: [payload.signature],
|
|
381
|
+
}) as `0x${string}`;
|
|
382
|
+
} catch (err) {
|
|
383
|
+
const message = (err as Error).message;
|
|
384
|
+
// If the tx was already submitted (duplicate settlement), extract the hash
|
|
385
|
+
if (message.includes("already known") || message.includes("nonce too low")) {
|
|
386
|
+
return {
|
|
387
|
+
success: false,
|
|
388
|
+
txHash: "",
|
|
389
|
+
proof: { proof: "", inputs: [], circuitId: config.circuitId, generatedAt: Date.now() },
|
|
390
|
+
network,
|
|
391
|
+
error: `Transaction broadcast failed: ${message}`,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
throw err;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Step 2: Wait for on-chain confirmation
|
|
398
|
+
const receipt = await client.waitForTransactionReceipt({
|
|
399
|
+
hash: txHash,
|
|
400
|
+
confirmations: config.requiredConfirmations,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
if (receipt.status !== "success") {
|
|
404
|
+
return {
|
|
405
|
+
success: false,
|
|
406
|
+
txHash,
|
|
407
|
+
proof: { proof: "", inputs: [], circuitId: config.circuitId, generatedAt: Date.now() },
|
|
408
|
+
network,
|
|
409
|
+
error: "Transaction reverted on-chain",
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Step 3: Generate ZK proof via relay API
|
|
414
|
+
const proofResult = await relayProve(
|
|
415
|
+
config.relayUrl,
|
|
416
|
+
config.lemmaApiBase,
|
|
417
|
+
config.lemmaApiKey,
|
|
418
|
+
config.circuitId,
|
|
419
|
+
{
|
|
420
|
+
txHash,
|
|
421
|
+
from: payload.from,
|
|
422
|
+
network,
|
|
423
|
+
},
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
// Step 4: Return settlement result
|
|
427
|
+
return {
|
|
428
|
+
success: true,
|
|
429
|
+
txHash,
|
|
430
|
+
proof: {
|
|
431
|
+
proof: proofResult.proof,
|
|
432
|
+
inputs: [...proofResult.inputs],
|
|
433
|
+
circuitId: config.circuitId,
|
|
434
|
+
generatedAt: Date.now(),
|
|
435
|
+
},
|
|
436
|
+
network,
|
|
437
|
+
};
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
// Hono App
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
const app = new Hono<{ Bindings: typeof X402Env }>();
|
|
445
|
+
|
|
446
|
+
app.use("/*", cors({ origin: "*", credentials: true }));
|
|
447
|
+
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
// POST /verify
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
|
|
452
|
+
app.post("/verify", async (c) => {
|
|
453
|
+
const body = await c.req.json();
|
|
454
|
+
const parsed = await parseFacilitatorRequest(body);
|
|
455
|
+
|
|
456
|
+
if (!parsed.ok) {
|
|
457
|
+
return c.json<VerifyResponse>({ isValid: false, invalidReason: parsed.reason }, 400);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const { payload, requirements } = parsed.data;
|
|
461
|
+
const rpcOverrides = parseRpcUrls(c.env.RPC_URLS);
|
|
462
|
+
|
|
463
|
+
let config: FacilitatorConfig;
|
|
464
|
+
try {
|
|
465
|
+
config = configForRequest(c.env, requirements.network, requirements.price, rpcOverrides);
|
|
466
|
+
} catch (err) {
|
|
467
|
+
return c.json<VerifyResponse>(
|
|
468
|
+
{ isValid: false, invalidReason: (err as Error).message },
|
|
469
|
+
400,
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const rpcUrl = rpcForNetwork(requirements.network, rpcOverrides);
|
|
474
|
+
const client = buildClient(rpcUrl, requirements.network);
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
const result = await verifyPayment(payload, requirements, client, config);
|
|
478
|
+
return c.json<VerifyResponse>(result);
|
|
479
|
+
} catch (err) {
|
|
480
|
+
return c.json<VerifyResponse>(
|
|
481
|
+
{ isValid: false, invalidReason: `Verification error: ${(err as Error).message}` },
|
|
482
|
+
500,
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// ---------------------------------------------------------------------------
|
|
488
|
+
// POST /settle
|
|
489
|
+
// ---------------------------------------------------------------------------
|
|
490
|
+
|
|
491
|
+
app.post("/settle", async (c) => {
|
|
492
|
+
const body = await c.req.json();
|
|
493
|
+
const parsed = await parseFacilitatorRequest(body);
|
|
494
|
+
|
|
495
|
+
if (!parsed.ok) {
|
|
496
|
+
return c.json({ success: false, error: parsed.reason }, 400);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const { payload, requirements } = parsed.data;
|
|
500
|
+
const rpcOverrides = parseRpcUrls(c.env.RPC_URLS);
|
|
501
|
+
|
|
502
|
+
let config: FacilitatorConfig;
|
|
503
|
+
try {
|
|
504
|
+
config = configForRequest(c.env, requirements.network, requirements.price, rpcOverrides);
|
|
505
|
+
} catch (err) {
|
|
506
|
+
return c.json(
|
|
507
|
+
{ success: false, error: (err as Error).message },
|
|
508
|
+
400,
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const rpcUrl = rpcForNetwork(requirements.network, rpcOverrides);
|
|
513
|
+
const client = buildClient(rpcUrl, requirements.network);
|
|
514
|
+
|
|
515
|
+
try {
|
|
516
|
+
const result = await settlePayment(payload, requirements, client, config);
|
|
517
|
+
return c.json<SettleResponse>(result, result.success ? 200 : 500);
|
|
518
|
+
} catch (err) {
|
|
519
|
+
return c.json<SettleResponse>(
|
|
520
|
+
{
|
|
521
|
+
success: false,
|
|
522
|
+
txHash: "",
|
|
523
|
+
proof: { proof: "", inputs: [], circuitId: config.circuitId, generatedAt: Date.now() },
|
|
524
|
+
network: requirements.network,
|
|
525
|
+
error: `Settlement error: ${(err as Error).message}`,
|
|
526
|
+
},
|
|
527
|
+
500,
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// ---------------------------------------------------------------------------
|
|
533
|
+
// POST /prepare — Delegate schema preparation to Relay API
|
|
534
|
+
// ---------------------------------------------------------------------------
|
|
535
|
+
|
|
536
|
+
app.post("/prepare", async (c) => {
|
|
537
|
+
const body = (await c.req.json()) as PrepareRequest | undefined;
|
|
538
|
+
|
|
539
|
+
if (!body?.schemaId || body.payload === undefined) {
|
|
540
|
+
return c.json(
|
|
541
|
+
{ success: false, error: "Request must contain 'schemaId' and 'payload'" },
|
|
542
|
+
400,
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
try {
|
|
547
|
+
const result = await relayPrepare(
|
|
548
|
+
c.env.RELAY_URL,
|
|
549
|
+
c.env.LEMMA_API_BASE,
|
|
550
|
+
c.env.LEMMA_API_KEY,
|
|
551
|
+
body.schemaId,
|
|
552
|
+
body.payload,
|
|
553
|
+
);
|
|
554
|
+
return c.json<RelayPrepareResponse>(result);
|
|
555
|
+
} catch (err) {
|
|
556
|
+
return c.json(
|
|
557
|
+
{ error: `Preparation error: ${(err as Error).message}` },
|
|
558
|
+
502,
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// ---------------------------------------------------------------------------
|
|
564
|
+
// Health Check
|
|
565
|
+
// ---------------------------------------------------------------------------
|
|
566
|
+
|
|
567
|
+
app.get("/", (c) => {
|
|
568
|
+
const rpcOverrides = parseRpcUrls(c.env.RPC_URLS);
|
|
569
|
+
const supportedNetworks = [
|
|
570
|
+
...new Set([...Object.keys(DEFAULT_RPC_MAP), ...Object.keys(rpcOverrides)]),
|
|
571
|
+
];
|
|
572
|
+
|
|
573
|
+
return c.json({
|
|
574
|
+
service: "lemma-x402-facilitator",
|
|
575
|
+
version: "0.4.0",
|
|
576
|
+
circuitId: c.env.CIRCUIT_ID ?? "x402-payment-v1",
|
|
577
|
+
supportedNetworks,
|
|
578
|
+
endpoints: ["/verify", "/settle", "/prepare"],
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
export default app;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
declare const X402Env: {
|
|
2
|
+
/** Address to receive payments. */
|
|
3
|
+
PAY_TO_ADDRESS: string;
|
|
4
|
+
/** Lemma API base URL. */
|
|
5
|
+
LEMMA_API_BASE: string;
|
|
6
|
+
/** Lemma API key (secret). */
|
|
7
|
+
LEMMA_API_KEY?: string;
|
|
8
|
+
/** Circuit ID override. Defaults to "x402-payment-v1". */
|
|
9
|
+
CIRCUIT_ID?: string;
|
|
10
|
+
/**
|
|
11
|
+
* JSON map of network identifier → RPC URL.
|
|
12
|
+
* Example: '{"eip155:84532":"https://sepolia.base.org","eip155:10143":"https://rpc.monad.xyz"}'
|
|
13
|
+
*
|
|
14
|
+
* Extends (and overrides) the built-in default RPC map.
|
|
15
|
+
*/
|
|
16
|
+
RPC_URLS?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Base URL of the @lemmaoracle/relay Vercel deployment.
|
|
19
|
+
* Used to delegate proof generation (snarkjs) which cannot run on Workers.
|
|
20
|
+
* Example: 'https://relay.lemma.finance'
|
|
21
|
+
*/
|
|
22
|
+
RELAY_URL: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default X402Env;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name = "x402-facilitator"
|
|
2
|
+
workers_dev = true
|
|
3
|
+
compatibility_date = "2024-12-30"
|
|
4
|
+
compatibility_flags = ["nodejs_compat"]
|
|
5
|
+
main = "src/index.ts"
|
|
6
|
+
|
|
7
|
+
[vars]
|
|
8
|
+
LEMMA_API_BASE = "https://workers.lemma.workers.dev"
|
|
9
|
+
CIRCUIT_ID = "x402-payment-v1"
|
|
10
|
+
RELAY_URL = "https://p01--lemma-relay-api--svxwx5rc5jzx.code.run/"
|
|
11
|
+
|
|
12
|
+
# Required secrets (set via `wrangler secret put`):
|
|
13
|
+
# PAY_TO_ADDRESS - address to receive payments
|
|
14
|
+
# RELAY_URL - base URL of @lemmaoracle/relay (Vercel)
|
|
15
|
+
# LEMMA_API_KEY - (optional) Lemma API key
|
|
16
|
+
# RPC_URLS - (optional) JSON override for default RPC endpoints
|
|
17
|
+
|
|
18
|
+
[dev]
|
|
19
|
+
port = 8788
|