wolverine-ai 5.2.2 → 5.2.3
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/package.json +1 -1
- package/src/middleware/x402-fastify.js +126 -229
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wolverine-ai",
|
|
3
|
-
"version": "5.2.
|
|
3
|
+
"version": "5.2.3",
|
|
4
4
|
"description": "Self-healing Node.js server framework powered by AI. Catches crashes, diagnoses errors, generates fixes, verifies, and restarts — automatically.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -4,49 +4,21 @@ const path = require("path");
|
|
|
4
4
|
/**
|
|
5
5
|
* x402 Fastify Plugin — add crypto payments to any route with one flag.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* handles the 402 → payment → verification → settlement flow.
|
|
10
|
-
*
|
|
11
|
-
* Settlement is handled by the x402 facilitator (Coinbase CDP),
|
|
12
|
-
* which verifies the payment signature AND executes the on-chain
|
|
13
|
-
* USDC transfer. Compatible with x402 Bazaar for API discovery.
|
|
7
|
+
* Implements the x402 v2 protocol with CDP facilitator for on-chain settlement.
|
|
8
|
+
* Compatible with @x402/fetch, @x402/evm client SDKs and x402 Bazaar.
|
|
14
9
|
*
|
|
15
10
|
* Two modes:
|
|
16
11
|
* Fixed price: { x402: { price: "$0.01" } }
|
|
17
12
|
* Variable price: { x402: { variable: true, min: "$1", max: "$10000" } }
|
|
18
|
-
*
|
|
19
|
-
* Example — fixed price route:
|
|
20
|
-
* fastify.get("/premium-data", { config: { x402: { price: "$0.10" } } }, handler)
|
|
21
|
-
*
|
|
22
|
-
* Example — variable price (credit purchase):
|
|
23
|
-
* fastify.post("/buy-credits", {
|
|
24
|
-
* config: {
|
|
25
|
-
* x402: {
|
|
26
|
-
* variable: true,
|
|
27
|
-
* min: "$1",
|
|
28
|
-
* max: "$10000",
|
|
29
|
-
* priceField: "dollars", // reads amount from request body
|
|
30
|
-
* }
|
|
31
|
-
* }
|
|
32
|
-
* }, async (req, reply) => {
|
|
33
|
-
* // req.x402.paid === true, req.x402.amount === "$5.00"
|
|
34
|
-
* addCredits(req.x402.amount);
|
|
35
|
-
* return { credits: newBalance };
|
|
36
|
-
* })
|
|
37
|
-
*
|
|
38
|
-
* The payment receipt is in req.x402 after verification:
|
|
39
|
-
* { paid: true, amount: "$5.00", receipt: {...}, txHash: "0x..." }
|
|
40
13
|
*/
|
|
41
14
|
|
|
15
|
+
const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
16
|
+
|
|
42
17
|
let _payTo = null;
|
|
43
18
|
let _network = "eip155:8453";
|
|
44
|
-
|
|
45
|
-
let _facilitatorUrl = "https://api.cdp.coinbase.com/platform/v2/x402";
|
|
46
|
-
// Testnet facilitator: "https://x402.org/facilitator"
|
|
19
|
+
let _facilitatorUrl = "https://x402.org/facilitator";
|
|
47
20
|
|
|
48
21
|
async function x402Plugin(fastify, opts) {
|
|
49
|
-
// Config
|
|
50
22
|
_network = opts.network || _network;
|
|
51
23
|
_facilitatorUrl = opts.facilitator || _facilitatorUrl;
|
|
52
24
|
_payTo = opts.payTo || null;
|
|
@@ -73,7 +45,7 @@ async function x402Plugin(fastify, opts) {
|
|
|
73
45
|
const isTestnet = _network.includes("84532") || _network.includes("11155");
|
|
74
46
|
_facilitatorUrl = isTestnet
|
|
75
47
|
? "https://x402.org/facilitator"
|
|
76
|
-
: "https://
|
|
48
|
+
: "https://x402.org/facilitator"; // CDP production requires auth — use x402.org for both
|
|
77
49
|
}
|
|
78
50
|
|
|
79
51
|
if (_payTo) {
|
|
@@ -81,20 +53,18 @@ async function x402Plugin(fastify, opts) {
|
|
|
81
53
|
console.log(` 💰 x402: facilitator ${_facilitatorUrl}`);
|
|
82
54
|
}
|
|
83
55
|
|
|
84
|
-
// ── Route-level x402 hook
|
|
56
|
+
// ── Route-level x402 hook ──
|
|
85
57
|
fastify.addHook("preHandler", async (request, reply) => {
|
|
86
|
-
// Check if this route has x402 config (Fastify v4: context.config, v5: routeOptions.config)
|
|
87
58
|
const routeConfig = request.routeOptions?.config?.x402 || request.routeConfig?.x402 || request.context?.config?.x402;
|
|
88
|
-
if (!routeConfig) return;
|
|
59
|
+
if (!routeConfig) return;
|
|
89
60
|
if (!_payTo) {
|
|
90
61
|
reply.code(500).send({ error: "x402 not configured — no wallet address" });
|
|
91
62
|
return;
|
|
92
63
|
}
|
|
93
64
|
|
|
94
65
|
// Determine the price
|
|
95
|
-
let
|
|
66
|
+
let dollarAmount;
|
|
96
67
|
if (routeConfig.variable) {
|
|
97
|
-
// Variable pricing — read amount from request body or query
|
|
98
68
|
const field = routeConfig.priceField || "dollars";
|
|
99
69
|
const raw = request.body?.[field] || request.query?.[field];
|
|
100
70
|
if (!raw) {
|
|
@@ -104,76 +74,122 @@ async function x402Plugin(fastify, opts) {
|
|
|
104
74
|
});
|
|
105
75
|
return;
|
|
106
76
|
}
|
|
107
|
-
|
|
77
|
+
dollarAmount = parseFloat(String(raw).replace("$", ""));
|
|
108
78
|
const min = parseFloat((routeConfig.min || "$1").replace("$", ""));
|
|
109
79
|
const max = parseFloat((routeConfig.max || "$10000").replace("$", ""));
|
|
110
|
-
if (isNaN(
|
|
80
|
+
if (isNaN(dollarAmount) || dollarAmount < min || dollarAmount > max) {
|
|
111
81
|
reply.code(400).send({ error: `Amount must be $${min}-$${max}` });
|
|
112
82
|
return;
|
|
113
83
|
}
|
|
114
|
-
price = "$" + amount.toFixed(2);
|
|
115
84
|
} else {
|
|
116
|
-
|
|
117
|
-
if (!
|
|
85
|
+
dollarAmount = parseFloat((routeConfig.price || "0").replace("$", ""));
|
|
86
|
+
if (!dollarAmount) { reply.code(500).send({ error: "x402 route missing price config" }); return; }
|
|
118
87
|
}
|
|
119
88
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
89
|
+
// USDC amount in atomic units (6 decimals)
|
|
90
|
+
const usdcAmount = String(Math.round(dollarAmount * 1e6));
|
|
91
|
+
const price = "$" + dollarAmount.toFixed(2);
|
|
92
|
+
|
|
93
|
+
const paymentHeader = request.headers["payment-signature"] || request.headers["x-payment"];
|
|
94
|
+
|
|
95
|
+
// No payment — return 402 with x402 v2 payment requirements
|
|
96
|
+
if (!paymentHeader) {
|
|
97
|
+
const paymentRequirements = {
|
|
98
|
+
scheme: "exact",
|
|
99
|
+
network: _network,
|
|
100
|
+
amount: usdcAmount,
|
|
101
|
+
asset: USDC_BASE,
|
|
102
|
+
payTo: _payTo,
|
|
103
|
+
maxTimeoutSeconds: 300,
|
|
104
|
+
};
|
|
105
|
+
const paymentRequired = {
|
|
106
|
+
x402Version: 2,
|
|
107
|
+
error: "Payment Required",
|
|
108
|
+
resource: {
|
|
109
|
+
url: `${request.method} ${request.url}`,
|
|
110
|
+
description: routeConfig.description || `Payment for ${request.method} ${request.url}`,
|
|
111
|
+
mimeType: "application/json",
|
|
112
|
+
},
|
|
113
|
+
accepts: [paymentRequirements],
|
|
133
114
|
};
|
|
115
|
+
|
|
116
|
+
const encoded = Buffer.from(JSON.stringify(paymentRequired)).toString("base64");
|
|
134
117
|
reply
|
|
135
118
|
.code(402)
|
|
136
|
-
.header("Payment-Required",
|
|
119
|
+
.header("Payment-Required", encoded)
|
|
137
120
|
.send({
|
|
138
121
|
error: "Payment Required",
|
|
139
122
|
price,
|
|
140
123
|
network: _network,
|
|
141
124
|
payTo: _payTo,
|
|
142
125
|
protocol: "x402",
|
|
126
|
+
x402Version: 2,
|
|
143
127
|
...(routeConfig.variable ? { variable: true, min: routeConfig.min, max: routeConfig.max, priceField: routeConfig.priceField || "dollars" } : {}),
|
|
144
128
|
});
|
|
145
129
|
return;
|
|
146
130
|
}
|
|
147
131
|
|
|
148
|
-
// Payment present — verify
|
|
149
|
-
|
|
150
|
-
const routeAccepts = [{
|
|
132
|
+
// Payment present — decode, verify via facilitator, then settle
|
|
133
|
+
const paymentRequirements = {
|
|
151
134
|
scheme: "exact",
|
|
152
|
-
price,
|
|
153
135
|
network: _network,
|
|
136
|
+
amount: usdcAmount,
|
|
137
|
+
asset: USDC_BASE,
|
|
154
138
|
payTo: _payTo,
|
|
155
|
-
|
|
139
|
+
maxTimeoutSeconds: 300,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Decode the payment payload
|
|
143
|
+
let paymentPayload;
|
|
144
|
+
try {
|
|
145
|
+
const decoded = Buffer.from(paymentHeader, "base64").toString();
|
|
146
|
+
paymentPayload = JSON.parse(decoded);
|
|
147
|
+
} catch {
|
|
148
|
+
reply.code(400).send({ error: "Invalid payment encoding" });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Ensure x402Version is set
|
|
153
|
+
if (!paymentPayload.x402Version) {
|
|
154
|
+
paymentPayload.x402Version = 2;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Ensure the accepted requirements are included (x402 v2 spec)
|
|
158
|
+
if (!paymentPayload.accepted) {
|
|
159
|
+
paymentPayload.accepted = paymentRequirements;
|
|
160
|
+
}
|
|
156
161
|
|
|
157
|
-
// Step 1:
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
160
|
-
reply.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
// Step 1: Verify via facilitator
|
|
163
|
+
const verifyResult = await _facilitatorCall("/verify", paymentPayload, paymentRequirements);
|
|
164
|
+
if (!verifyResult.ok) {
|
|
165
|
+
reply.code(402).send({
|
|
166
|
+
error: "Payment verification failed",
|
|
167
|
+
reason: verifyResult.reason,
|
|
168
|
+
price,
|
|
169
|
+
payTo: _payTo,
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
164
172
|
}
|
|
165
173
|
|
|
166
|
-
// Step 2:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
174
|
+
// Step 2: Settle via facilitator (executes on-chain transfer)
|
|
175
|
+
const settleResult = await _facilitatorCall("/settle", paymentPayload, paymentRequirements);
|
|
176
|
+
if (!settleResult.ok) {
|
|
177
|
+
reply.code(402).send({
|
|
178
|
+
error: "Payment settlement failed",
|
|
179
|
+
reason: settleResult.reason,
|
|
180
|
+
price,
|
|
181
|
+
payTo: _payTo,
|
|
182
|
+
});
|
|
173
183
|
return;
|
|
174
184
|
}
|
|
175
185
|
|
|
176
|
-
|
|
186
|
+
// Payment verified AND settled on-chain
|
|
187
|
+
const txHash = settleResult.data?.transaction || settleResult.data?.txHash || null;
|
|
188
|
+
const payer = settleResult.data?.payer || verifyResult.data?.payer || paymentPayload.payload?.authorization?.from || null;
|
|
189
|
+
|
|
190
|
+
reply.header("Payment-Response", JSON.stringify(settleResult.data || {}));
|
|
191
|
+
request.x402 = { paid: true, amount: price, receipt: settleResult.data, txHash, from: payer };
|
|
192
|
+
_logPayment({ route: request.url, method: request.method, amount: price, from: payer, txHash, verified: true, settled: "facilitator", timestamp: Date.now() });
|
|
177
193
|
});
|
|
178
194
|
|
|
179
195
|
// ── Public pricing endpoint ──
|
|
@@ -182,28 +198,28 @@ async function x402Plugin(fastify, opts) {
|
|
|
182
198
|
network: _network,
|
|
183
199
|
facilitator: _facilitatorUrl,
|
|
184
200
|
protocol: "x402",
|
|
201
|
+
x402Version: 2,
|
|
185
202
|
docs: "https://docs.cdp.coinbase.com/x402/welcome",
|
|
186
203
|
}));
|
|
187
204
|
}
|
|
188
205
|
|
|
189
206
|
/**
|
|
190
|
-
*
|
|
191
|
-
* The facilitator:
|
|
192
|
-
* 1. Verifies the EIP-3009 signature
|
|
193
|
-
* 2. Calls transferWithAuthorization() on the USDC contract
|
|
194
|
-
* 3. Returns the transaction hash
|
|
207
|
+
* Call the x402 facilitator — matches the exact format from @x402/core HTTPFacilitatorClient.
|
|
195
208
|
*
|
|
196
|
-
*
|
|
209
|
+
* POST {facilitatorUrl}/verify or /settle
|
|
210
|
+
* Body: { x402Version, paymentPayload, paymentRequirements }
|
|
197
211
|
*/
|
|
198
|
-
async function
|
|
212
|
+
async function _facilitatorCall(endpoint, paymentPayload, paymentRequirements) {
|
|
199
213
|
try {
|
|
200
214
|
const https = require("https");
|
|
201
215
|
const http = require("http");
|
|
202
|
-
const url = new (require("url").URL)(_facilitatorUrl +
|
|
216
|
+
const url = new (require("url").URL)(_facilitatorUrl + endpoint);
|
|
203
217
|
const body = JSON.stringify({
|
|
204
|
-
|
|
205
|
-
|
|
218
|
+
x402Version: paymentPayload.x402Version || 2,
|
|
219
|
+
paymentPayload,
|
|
220
|
+
paymentRequirements,
|
|
206
221
|
});
|
|
222
|
+
|
|
207
223
|
return new Promise((resolve) => {
|
|
208
224
|
const client = url.protocol === "https:" ? https : http;
|
|
209
225
|
const req = client.request({
|
|
@@ -212,161 +228,43 @@ async function _settleViaFacilitator(paymentSig, routeAccepts) {
|
|
|
212
228
|
path: url.pathname,
|
|
213
229
|
method: "POST",
|
|
214
230
|
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
|
|
215
|
-
timeout: 30000,
|
|
231
|
+
timeout: 30000,
|
|
216
232
|
}, (res) => {
|
|
217
233
|
let data = "";
|
|
218
234
|
res.on("data", (c) => data += c);
|
|
219
235
|
res.on("end", () => {
|
|
220
236
|
try {
|
|
221
|
-
const
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
237
|
+
const parsed = JSON.parse(data);
|
|
238
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
239
|
+
// Verify: check isValid. Settle: check success.
|
|
240
|
+
if (endpoint === "/verify" && parsed.isValid === false) {
|
|
241
|
+
resolve({ ok: false, reason: parsed.invalidReason || "verification_rejected", data: parsed });
|
|
242
|
+
} else if (endpoint === "/settle" && parsed.success === false) {
|
|
243
|
+
resolve({ ok: false, reason: parsed.errorReason || "settlement_rejected", data: parsed });
|
|
244
|
+
} else {
|
|
245
|
+
resolve({ ok: true, data: parsed });
|
|
246
|
+
}
|
|
229
247
|
} else {
|
|
230
|
-
|
|
248
|
+
// Error response
|
|
249
|
+
const reason = parsed.invalidReason || parsed.errorReason || parsed.error || `facilitator_${res.statusCode}`;
|
|
250
|
+
console.log(` ⚠️ x402 facilitator ${endpoint} ${res.statusCode}: ${reason}`);
|
|
251
|
+
resolve({ ok: false, reason, data: parsed });
|
|
231
252
|
}
|
|
232
253
|
} catch {
|
|
233
|
-
resolve({
|
|
254
|
+
resolve({ ok: false, reason: `facilitator_parse_error_${res.statusCode}` });
|
|
234
255
|
}
|
|
235
256
|
});
|
|
236
257
|
});
|
|
237
|
-
req.on("error", () =>
|
|
238
|
-
|
|
258
|
+
req.on("error", (err) => {
|
|
259
|
+
console.log(` ⚠️ x402 facilitator ${endpoint} error: ${err.message}`);
|
|
260
|
+
resolve({ ok: false, reason: "facilitator_unavailable: " + err.message });
|
|
261
|
+
});
|
|
262
|
+
req.on("timeout", () => { req.destroy(); resolve({ ok: false, reason: "facilitator_timeout" }); });
|
|
239
263
|
req.write(body);
|
|
240
264
|
req.end();
|
|
241
265
|
});
|
|
242
|
-
} catch {
|
|
243
|
-
return { valid: false, reason: "facilitator_unavailable" };
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Self-settlement fallback — verify the signature AND execute the
|
|
249
|
-
* on-chain transferWithAuthorization() using the server's vault wallet.
|
|
250
|
-
*
|
|
251
|
-
* Only used when the facilitator is unavailable. Requires ethers + RPC.
|
|
252
|
-
* The server pays gas to execute the transfer.
|
|
253
|
-
*/
|
|
254
|
-
async function _selfSettle(paymentSig, price) {
|
|
255
|
-
let payload;
|
|
256
|
-
try {
|
|
257
|
-
payload = JSON.parse(Buffer.from(paymentSig, "base64").toString());
|
|
258
|
-
} catch {
|
|
259
|
-
return { valid: false, reason: "invalid_payment_format" };
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (!payload.payload?.authorization || !payload.payload?.signature) {
|
|
263
|
-
return { valid: false, reason: "missing_authorization" };
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const auth = payload.payload.authorization;
|
|
267
|
-
const sig = payload.payload.signature;
|
|
268
|
-
|
|
269
|
-
// Verify amount
|
|
270
|
-
const expectedUsdc = Math.round(parseFloat(price.replace("$", "")) * 1e6);
|
|
271
|
-
// Value comes as decimal string (e.g. "1000000" for $1) or hex (legacy "0xf4240")
|
|
272
|
-
const valStr = String(auth.value);
|
|
273
|
-
const actualUsdc = valStr.startsWith("0x") ? parseInt(valStr, 16) : parseInt(valStr, 10) || 0;
|
|
274
|
-
if (actualUsdc < expectedUsdc * 0.99) {
|
|
275
|
-
return { valid: false, reason: "amount_mismatch" };
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Verify payTo matches
|
|
279
|
-
if (auth.to?.toLowerCase() !== _payTo?.toLowerCase()) {
|
|
280
|
-
return { valid: false, reason: "wrong_recipient" };
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Verify not expired
|
|
284
|
-
if (auth.validBefore && auth.validBefore < Math.floor(Date.now() / 1000)) {
|
|
285
|
-
return { valid: false, reason: "payment_expired" };
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
try {
|
|
289
|
-
const { ethers } = require("ethers");
|
|
290
|
-
const fromAddr = ethers.getAddress(auth.from.toLowerCase());
|
|
291
|
-
const toAddr = ethers.getAddress(auth.to.toLowerCase());
|
|
292
|
-
|
|
293
|
-
// Step 1: Verify the signature
|
|
294
|
-
const domain = {
|
|
295
|
-
name: "USD Coin",
|
|
296
|
-
version: "2",
|
|
297
|
-
chainId: 8453,
|
|
298
|
-
verifyingContract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
299
|
-
};
|
|
300
|
-
const types = {
|
|
301
|
-
TransferWithAuthorization: [
|
|
302
|
-
{ name: "from", type: "address" },
|
|
303
|
-
{ name: "to", type: "address" },
|
|
304
|
-
{ name: "value", type: "uint256" },
|
|
305
|
-
{ name: "validAfter", type: "uint256" },
|
|
306
|
-
{ name: "validBefore", type: "uint256" },
|
|
307
|
-
{ name: "nonce", type: "bytes32" },
|
|
308
|
-
],
|
|
309
|
-
};
|
|
310
|
-
const message = {
|
|
311
|
-
from: fromAddr,
|
|
312
|
-
to: toAddr,
|
|
313
|
-
value: auth.value,
|
|
314
|
-
validAfter: auth.validAfter,
|
|
315
|
-
validBefore: auth.validBefore,
|
|
316
|
-
nonce: auth.nonce,
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
const recoveredAddress = ethers.verifyTypedData(domain, types, message, sig);
|
|
320
|
-
if (recoveredAddress.toLowerCase() !== fromAddr.toLowerCase()) {
|
|
321
|
-
return { valid: false, reason: "signature_mismatch" };
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Step 2: Execute transferWithAuthorization on-chain
|
|
325
|
-
// Use the vault wallet to submit the transaction (server pays gas)
|
|
326
|
-
const { decryptPrivateKey } = require("../vault/vault-manager");
|
|
327
|
-
let keyBuf = null;
|
|
328
|
-
try {
|
|
329
|
-
keyBuf = decryptPrivateKey();
|
|
330
|
-
const provider = new ethers.JsonRpcProvider("https://mainnet.base.org");
|
|
331
|
-
const wallet = new ethers.Wallet(keyBuf, provider);
|
|
332
|
-
|
|
333
|
-
const usdcContract = new ethers.Contract(
|
|
334
|
-
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
335
|
-
[
|
|
336
|
-
"function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s) external",
|
|
337
|
-
],
|
|
338
|
-
wallet
|
|
339
|
-
);
|
|
340
|
-
|
|
341
|
-
// Parse the signature into v, r, s
|
|
342
|
-
const sigBytes = ethers.getBytes(sig);
|
|
343
|
-
const r = ethers.hexlify(sigBytes.slice(0, 32));
|
|
344
|
-
const s = ethers.hexlify(sigBytes.slice(32, 64));
|
|
345
|
-
const v = sigBytes[64];
|
|
346
|
-
|
|
347
|
-
const tx = await usdcContract.transferWithAuthorization(
|
|
348
|
-
fromAddr, toAddr, auth.value,
|
|
349
|
-
auth.validAfter, auth.validBefore, auth.nonce,
|
|
350
|
-
v, r, s,
|
|
351
|
-
{ gasLimit: 100000 }
|
|
352
|
-
);
|
|
353
|
-
|
|
354
|
-
// Wait for confirmation (1 block)
|
|
355
|
-
const receipt = await tx.wait(1);
|
|
356
|
-
console.log(` 💰 x402 self-settled: ${receipt.hash} (${price} USDC)`);
|
|
357
|
-
|
|
358
|
-
return {
|
|
359
|
-
valid: true,
|
|
360
|
-
from: fromAddr,
|
|
361
|
-
receipt: { authorization: auth, settled: "self", blockNumber: receipt.blockNumber },
|
|
362
|
-
txHash: receipt.hash,
|
|
363
|
-
};
|
|
364
|
-
} finally {
|
|
365
|
-
if (keyBuf) keyBuf.fill(0);
|
|
366
|
-
}
|
|
367
266
|
} catch (err) {
|
|
368
|
-
|
|
369
|
-
return { valid: false, reason: "settlement_failed: " + err.message };
|
|
267
|
+
return { ok: false, reason: "facilitator_error: " + err.message };
|
|
370
268
|
}
|
|
371
269
|
}
|
|
372
270
|
|
|
@@ -376,7 +274,6 @@ function _logPayment(entry) {
|
|
|
376
274
|
let payments = [];
|
|
377
275
|
try { payments = JSON.parse(fs.readFileSync(logPath, "utf-8")); } catch {}
|
|
378
276
|
payments.push(entry);
|
|
379
|
-
// Keep last 1000 payments
|
|
380
277
|
if (payments.length > 1000) payments = payments.slice(-1000);
|
|
381
278
|
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
382
279
|
fs.writeFileSync(logPath, JSON.stringify(payments, null, 2));
|