Repository navigation
Expand file tree
/
Copy pathfacilitator.ts
More file actions
261 lines (228 loc) · 8.74 KB
/
Copy pathfacilitator.ts
File metadata and controls
261 lines (228 loc) · 8.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
/**
* Copied from x402 examples/typescript/facilitator/auth-capture/index.ts at 7f2b2f1 (lib/x402).
* Only edit: Arbitrum Sepolia instead of Base Sepolia.
*/
/**
* Auth-capture facilitator example (EVM)
*
* Registers {@link AuthCaptureEvmScheme} on Arbitrum Sepolia. When
* `EVM_RECEIVER_AUTHORIZER_PRIVATE_KEY` is set, advertises `extra.receiverAuthorizer`
* and signs delegated lifecycle payloads with explicit {@link InMemoryAuthCaptureDelegatedAuthStorage}
* and {@link AuthCaptureFacilitatorConfig.onStorageError} (required to opt in).
*/
import { x402Facilitator } from "@x402/core/facilitator";
import {
PaymentPayload,
PaymentRequirements,
SettleResponse,
VerifyResponse,
} from "@x402/core/types";
import { type AuthorizerSigner, toFacilitatorEvmSigner } from "@x402/evm";
import {
AuthCaptureEvmScheme,
InMemoryAuthCaptureDelegatedAuthStorage,
} from "@x402/evm/auth-capture/facilitator";
import dotenv from "dotenv";
import express from "express";
import { createWalletClient, getAddress, http, nonceManager, publicActions } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arbitrumSepolia } from "viem/chains";
import { zeroAddress } from "viem";
dotenv.config();
const PORT = process.env.PORT || "4022";
if (!process.env.EVM_PRIVATE_KEY) {
console.error("EVM_PRIVATE_KEY environment variable is required");
process.exit(1);
}
const evmRpcUrl = process.env.EVM_RPC_URL?.trim() || "https://sepolia-rollup.arbitrum.io/rpc";
const receiverAuthorizerPrivateKey = process.env.EVM_RECEIVER_AUTHORIZER_PRIVATE_KEY?.trim();
const evmAccount = privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`, {
nonceManager,
});
let authorizerSigner: AuthorizerSigner | undefined;
if (receiverAuthorizerPrivateKey) {
const authorizerAccount = privateKeyToAccount(receiverAuthorizerPrivateKey as `0x${string}`);
authorizerSigner = {
address: authorizerAccount.address,
signTypedData: params =>
authorizerAccount.signTypedData(
params as Parameters<typeof authorizerAccount.signTypedData>[0],
),
};
}
const feeRecipient = (process.env.FEE_RECIPIENT?.trim() || zeroAddress) as `0x${string}`;
const minFeeBps = Number(process.env.MIN_FEE_BPS ?? "0");
const maxFeeBps = Number(process.env.MAX_FEE_BPS ?? "0");
// Custom operators are admitted per address. An empty list admits none, leaving
// only "delegated" routes through the relayer.
const customOperators = (process.env.CUSTOM_OPERATOR_ALLOWLIST ?? "")
.split(",")
.map(entry => entry.trim())
.filter(entry => entry.length > 0);
const malformedOperator = customOperators.find(entry => !/^0x[0-9a-fA-F]{40}$/.test(entry));
if (malformedOperator) {
console.error(
`Invalid CUSTOM_OPERATOR_ALLOWLIST entry "${malformedOperator}" ` +
"(comma-separated 20-byte hex addresses, 0x-prefixed)",
);
process.exit(1);
}
console.info(`EVM Facilitator relayer: ${evmAccount.address}`);
if (authorizerSigner) {
console.info(`EVM Receiver authorizer: ${authorizerSigner.address}`);
console.info("Delegated auth bindings: InMemoryAuthCaptureDelegatedAuthStorage (process-local)");
} else {
console.info("EVM Receiver authorizer: not configured (resource servers must self-sign)");
}
console.info(
customOperators.length > 0
? `EVM Custom operators admitted: ${customOperators.join(", ")}`
: "EVM Custom operators: none admitted",
);
const viemClient = createWalletClient({
account: evmAccount,
chain: arbitrumSepolia,
transport: http(evmRpcUrl),
}).extend(publicActions);
const evmSigner = toFacilitatorEvmSigner({
address: evmAccount.address,
getCode: args => viemClient.getCode(args),
readContract: args =>
viemClient.readContract({ ...args, args: args.args ?? [] } as Parameters<
typeof viemClient.readContract
>[0]),
simulateCalls: args =>
viemClient.simulateCalls(args as Parameters<typeof viemClient.simulateCalls>[0]),
verifyTypedData: args =>
viemClient.verifyTypedData(args as Parameters<typeof viemClient.verifyTypedData>[0]),
writeContract: args =>
viemClient.writeContract(args as Parameters<typeof viemClient.writeContract>[0]),
sendTransaction: args =>
viemClient.sendTransaction(args as Parameters<typeof viemClient.sendTransaction>[0]),
waitForTransactionReceipt: args => viemClient.waitForTransactionReceipt(args),
});
const facilitator = new x402Facilitator();
const authCaptureConfig = {
...(authorizerSigner
? {
authorizerSigner,
delegatedAuthStorage: new InMemoryAuthCaptureDelegatedAuthStorage(),
// SDK hook: called during delegated /settle (see DelegatedSettleContext in @x402/evm/auth-capture/facilitator).
resolveCallerIdentity: async () => "example-local-caller",
onStorageError: (error: unknown, network: string, paymentInfoHash: string) => {
console.warn("[delegated-auth-storage]", {
network,
paymentInfoHash,
error: error instanceof Error ? error.message : error,
});
},
}
: {}),
...(minFeeBps > 0 || maxFeeBps > 0 || feeRecipient !== zeroAddress
? { feeTerms: { feeRecipient, minFeeBps, maxFeeBps } }
: {}),
...(customOperators.length > 0
? {
operators: customOperators.map(address => ({
address: getAddress(address),
operatorType: "custom" as const,
})),
}
: {}),
customOperatorAuthorizeGasLimit: 1_000_000n,
refundFunding: false,
};
facilitator.register("eip155:421614", new AuthCaptureEvmScheme(evmSigner, authCaptureConfig));
// Multi-signer rotation: pass an array of toFacilitatorEvmSigner results to one
// scheme. Do not register the scheme twice on the same network — getSupported
// and settle both keep the first kind. Every address in the rotation must stay
// funded (and approved, if refundFunding is true) until payments that used it
// pass refundDeadline.
//
// const evmSignerB = toFacilitatorEvmSigner({ address: accountB.address, ...clientB });
// facilitator.register("eip155:421614", new AuthCaptureEvmScheme([evmSigner, evmSignerB], authCaptureConfig));
const app = express();
app.use(express.json());
/**
* POST /verify
* Verify a payment against requirements.
*/
app.post("/verify", async (req, res) => {
try {
const { paymentPayload, paymentRequirements } = req.body as {
paymentPayload: PaymentPayload;
paymentRequirements: PaymentRequirements;
};
if (!paymentPayload || !paymentRequirements) {
return res.status(400).json({
error: "Missing paymentPayload or paymentRequirements",
});
}
const response: VerifyResponse = await facilitator.verify(paymentPayload, paymentRequirements);
res.json(response);
} catch (error) {
console.error("Verify error:", error);
res.status(500).json({
error: error instanceof Error ? error.message : "Unknown error",
});
}
});
/**
* POST /settle
* Settle a payment onchain.
*/
app.post("/settle", async (req, res) => {
try {
const { paymentPayload, paymentRequirements } = req.body;
if (!paymentPayload || !paymentRequirements) {
return res.status(400).json({
error: "Missing paymentPayload or paymentRequirements",
});
}
const payload = paymentPayload as PaymentPayload;
const requirements = paymentRequirements as PaymentRequirements;
const payloadType = (payload.payload as { type?: string }).type ?? "collect";
const response: SettleResponse = await facilitator.settle(payload, requirements);
console.info(
`[settle] type=${payloadType} requirements.amount=${requirements.amount}`,
JSON.stringify(response, null, 2),
);
res.json(response);
} catch (error) {
console.error("Settle error:", error);
if (error instanceof Error && error.message.includes("Settlement aborted:")) {
return res.json({
success: false,
errorReason: error.message.replace("Settlement aborted: ", ""),
network: req.body?.paymentPayload?.network || "unknown",
} as SettleResponse);
}
res.status(500).json({
error: error instanceof Error ? error.message : "Unknown error",
});
}
});
/**
* GET /supported
* Get supported payment kinds and extensions.
*/
app.get("/supported", async (_req, res) => {
try {
const response = facilitator.getSupported();
res.json(response);
} catch (error) {
console.error("Supported error:", error);
res.status(500).json({
error: error instanceof Error ? error.message : "Unknown error",
});
}
});
app.listen(parseInt(PORT), () => {
console.log(`Auth-capture facilitator listening on http://localhost:${PORT}`);
console.log(` Relayer (extra.captureAuthorizer for delegated): ${evmAccount.address}`);
if (authorizerSigner) {
console.log(
" Delegated settles: resolveCallerIdentity returns example-local-caller (local only)",
);
}
});