moltspay 0.8.14 → 0.9.0
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 +156 -504
- package/dist/cli/index.js +553 -171
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +555 -173
- package/dist/cli/index.mjs.map +1 -1
- package/dist/facilitators/index.d.mts +61 -0
- package/dist/facilitators/index.d.ts +61 -0
- package/dist/facilitators/index.js +502 -0
- package/dist/facilitators/index.js.map +1 -0
- package/dist/facilitators/index.mjs +461 -0
- package/dist/facilitators/index.mjs.map +1 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +583 -190
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +580 -192
- package/dist/index.mjs.map +1 -1
- package/dist/registry-OsEO2dOu.d.mts +233 -0
- package/dist/registry-OsEO2dOu.d.ts +233 -0
- package/dist/server/index.d.mts +19 -15
- package/dist/server/index.d.ts +19 -15
- package/dist/server/index.js +514 -145
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +514 -145
- package/dist/server/index.mjs.map +1 -1
- package/package.json +7 -2
- package/schemas/moltspay.services.schema.json +1 -1
package/dist/server/index.mjs
CHANGED
|
@@ -1,13 +1,457 @@
|
|
|
1
1
|
// src/server/index.ts
|
|
2
|
-
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
3
3
|
import { createServer } from "http";
|
|
4
|
+
import * as path2 from "path";
|
|
5
|
+
|
|
6
|
+
// src/facilitators/interface.ts
|
|
7
|
+
var BaseFacilitator = class {
|
|
8
|
+
supportsNetwork(network) {
|
|
9
|
+
return this.supportedNetworks.includes(network);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/facilitators/cdp.ts
|
|
14
|
+
import { readFileSync, existsSync } from "fs";
|
|
4
15
|
import * as path from "path";
|
|
5
16
|
var X402_VERSION = 2;
|
|
17
|
+
var CDP_MAINNET_URL = "https://api.cdp.coinbase.com/platform/v2/x402";
|
|
18
|
+
var CDP_TESTNET_URL = "https://www.x402.org/facilitator";
|
|
19
|
+
function loadEnvFile() {
|
|
20
|
+
const envPaths = [
|
|
21
|
+
path.join(process.cwd(), ".env"),
|
|
22
|
+
path.join(process.env.HOME || "", ".moltspay", ".env")
|
|
23
|
+
];
|
|
24
|
+
for (const envPath of envPaths) {
|
|
25
|
+
if (existsSync(envPath)) {
|
|
26
|
+
try {
|
|
27
|
+
const content = readFileSync(envPath, "utf-8");
|
|
28
|
+
for (const line of content.split("\n")) {
|
|
29
|
+
const trimmed = line.trim();
|
|
30
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
31
|
+
const eqIndex = trimmed.indexOf("=");
|
|
32
|
+
if (eqIndex === -1) continue;
|
|
33
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
34
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
35
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
36
|
+
value = value.slice(1, -1);
|
|
37
|
+
}
|
|
38
|
+
if (!process.env[key]) {
|
|
39
|
+
process.env[key] = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
} catch {
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
var CDPFacilitator = class extends BaseFacilitator {
|
|
49
|
+
name = "cdp";
|
|
50
|
+
displayName = "Coinbase CDP";
|
|
51
|
+
supportedNetworks;
|
|
52
|
+
endpoint;
|
|
53
|
+
useMainnet;
|
|
54
|
+
apiKeyId;
|
|
55
|
+
apiKeySecret;
|
|
56
|
+
constructor(config = {}) {
|
|
57
|
+
super();
|
|
58
|
+
loadEnvFile();
|
|
59
|
+
this.useMainnet = config.useMainnet ?? process.env.USE_MAINNET?.toLowerCase() === "true";
|
|
60
|
+
this.apiKeyId = config.apiKeyId || process.env.CDP_API_KEY_ID;
|
|
61
|
+
this.apiKeySecret = config.apiKeySecret || process.env.CDP_API_KEY_SECRET;
|
|
62
|
+
this.endpoint = this.useMainnet ? CDP_MAINNET_URL : CDP_TESTNET_URL;
|
|
63
|
+
this.supportedNetworks = this.useMainnet ? ["eip155:8453"] : ["eip155:8453", "eip155:84532"];
|
|
64
|
+
if (this.useMainnet && (!this.apiKeyId || !this.apiKeySecret)) {
|
|
65
|
+
console.warn("[CDPFacilitator] WARNING: Mainnet mode but missing CDP credentials!");
|
|
66
|
+
console.warn("[CDPFacilitator] Set CDP_API_KEY_ID and CDP_API_KEY_SECRET");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get auth headers for CDP API requests
|
|
71
|
+
*/
|
|
72
|
+
async getAuthHeaders(method, urlPath, body) {
|
|
73
|
+
if (!this.useMainnet) {
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
if (!this.apiKeyId || !this.apiKeySecret) {
|
|
77
|
+
throw new Error("CDP credentials required for mainnet");
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const { getAuthHeaders } = await import("@coinbase/cdp-sdk/auth");
|
|
81
|
+
return await getAuthHeaders({
|
|
82
|
+
apiKeyId: this.apiKeyId,
|
|
83
|
+
apiKeySecret: this.apiKeySecret,
|
|
84
|
+
requestMethod: method,
|
|
85
|
+
requestHost: "api.cdp.coinbase.com",
|
|
86
|
+
requestPath: urlPath,
|
|
87
|
+
requestBody: body
|
|
88
|
+
});
|
|
89
|
+
} catch (err) {
|
|
90
|
+
throw new Error(`Failed to generate CDP auth: ${err.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Health check - verify facilitator is reachable
|
|
95
|
+
*/
|
|
96
|
+
async healthCheck() {
|
|
97
|
+
const start = Date.now();
|
|
98
|
+
try {
|
|
99
|
+
const controller = new AbortController();
|
|
100
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
101
|
+
const response = await fetch(this.endpoint.replace("/x402", ""), {
|
|
102
|
+
method: "HEAD",
|
|
103
|
+
signal: controller.signal
|
|
104
|
+
}).catch(() => null);
|
|
105
|
+
clearTimeout(timeout);
|
|
106
|
+
const latencyMs = Date.now() - start;
|
|
107
|
+
return {
|
|
108
|
+
healthy: response !== null,
|
|
109
|
+
latencyMs
|
|
110
|
+
};
|
|
111
|
+
} catch (err) {
|
|
112
|
+
return {
|
|
113
|
+
healthy: false,
|
|
114
|
+
error: err.message,
|
|
115
|
+
latencyMs: Date.now() - start
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Verify payment signature with facilitator
|
|
121
|
+
*/
|
|
122
|
+
async verify(paymentPayload, requirements) {
|
|
123
|
+
try {
|
|
124
|
+
const requestBody = {
|
|
125
|
+
x402Version: X402_VERSION,
|
|
126
|
+
paymentPayload,
|
|
127
|
+
paymentRequirements: requirements
|
|
128
|
+
};
|
|
129
|
+
const headers = {
|
|
130
|
+
"Content-Type": "application/json"
|
|
131
|
+
};
|
|
132
|
+
if (this.useMainnet) {
|
|
133
|
+
const authHeaders = await this.getAuthHeaders(
|
|
134
|
+
"POST",
|
|
135
|
+
"/platform/v2/x402/verify",
|
|
136
|
+
requestBody
|
|
137
|
+
);
|
|
138
|
+
Object.assign(headers, authHeaders);
|
|
139
|
+
}
|
|
140
|
+
const response = await fetch(`${this.endpoint}/verify`, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers,
|
|
143
|
+
body: JSON.stringify(requestBody)
|
|
144
|
+
});
|
|
145
|
+
const result = await response.json();
|
|
146
|
+
if (!response.ok || !result.isValid) {
|
|
147
|
+
return {
|
|
148
|
+
valid: false,
|
|
149
|
+
error: result.invalidReason || result.error || "Verification failed",
|
|
150
|
+
details: result
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return { valid: true, details: result };
|
|
154
|
+
} catch (err) {
|
|
155
|
+
return {
|
|
156
|
+
valid: false,
|
|
157
|
+
error: `Facilitator error: ${err.message}`
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Settle payment on-chain via facilitator
|
|
163
|
+
*/
|
|
164
|
+
async settle(paymentPayload, requirements) {
|
|
165
|
+
try {
|
|
166
|
+
const requestBody = {
|
|
167
|
+
x402Version: X402_VERSION,
|
|
168
|
+
paymentPayload,
|
|
169
|
+
paymentRequirements: requirements
|
|
170
|
+
};
|
|
171
|
+
const headers = {
|
|
172
|
+
"Content-Type": "application/json"
|
|
173
|
+
};
|
|
174
|
+
if (this.useMainnet) {
|
|
175
|
+
const authHeaders = await this.getAuthHeaders(
|
|
176
|
+
"POST",
|
|
177
|
+
"/platform/v2/x402/settle",
|
|
178
|
+
requestBody
|
|
179
|
+
);
|
|
180
|
+
Object.assign(headers, authHeaders);
|
|
181
|
+
}
|
|
182
|
+
const response = await fetch(`${this.endpoint}/settle`, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
headers,
|
|
185
|
+
body: JSON.stringify(requestBody)
|
|
186
|
+
});
|
|
187
|
+
const result = await response.json();
|
|
188
|
+
if (!response.ok || !result.success) {
|
|
189
|
+
return {
|
|
190
|
+
success: false,
|
|
191
|
+
error: result.error || result.errorReason || "Settlement failed"
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
success: true,
|
|
196
|
+
transaction: result.transaction,
|
|
197
|
+
status: result.status || "settled"
|
|
198
|
+
};
|
|
199
|
+
} catch (err) {
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
error: `Settlement error: ${err.message}`
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Get CDP fee information
|
|
208
|
+
*/
|
|
209
|
+
async getFee() {
|
|
210
|
+
return {
|
|
211
|
+
perTx: 1e-3,
|
|
212
|
+
currency: "USD",
|
|
213
|
+
freeQuota: 1e3
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Get configuration summary (for logging)
|
|
218
|
+
*/
|
|
219
|
+
getConfigSummary() {
|
|
220
|
+
const mode = this.useMainnet ? "mainnet" : "testnet";
|
|
221
|
+
const hasCredentials = !!(this.apiKeyId && this.apiKeySecret);
|
|
222
|
+
return `CDP Facilitator (${mode}, credentials: ${hasCredentials ? "yes" : "no"})`;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// src/facilitators/registry.ts
|
|
227
|
+
var FacilitatorRegistry = class {
|
|
228
|
+
factories = /* @__PURE__ */ new Map();
|
|
229
|
+
instances = /* @__PURE__ */ new Map();
|
|
230
|
+
selection;
|
|
231
|
+
roundRobinIndex = 0;
|
|
232
|
+
constructor(selection) {
|
|
233
|
+
this.registerFactory("cdp", (config) => new CDPFacilitator(config));
|
|
234
|
+
this.selection = selection || { primary: "cdp", strategy: "failover" };
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Register a new facilitator factory
|
|
238
|
+
*/
|
|
239
|
+
registerFactory(name, factory) {
|
|
240
|
+
this.factories.set(name, factory);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Get or create a facilitator instance
|
|
244
|
+
*/
|
|
245
|
+
get(name, config) {
|
|
246
|
+
if (this.instances.has(name)) {
|
|
247
|
+
return this.instances.get(name);
|
|
248
|
+
}
|
|
249
|
+
const factory = this.factories.get(name);
|
|
250
|
+
if (!factory) {
|
|
251
|
+
throw new Error(`Unknown facilitator: ${name}. Available: ${Array.from(this.factories.keys()).join(", ")}`);
|
|
252
|
+
}
|
|
253
|
+
const mergedConfig = {
|
|
254
|
+
...this.selection.config?.[name],
|
|
255
|
+
...config
|
|
256
|
+
};
|
|
257
|
+
const instance = factory(mergedConfig);
|
|
258
|
+
this.instances.set(name, instance);
|
|
259
|
+
return instance;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Get all configured facilitator names
|
|
263
|
+
*/
|
|
264
|
+
getConfiguredNames() {
|
|
265
|
+
const names = [this.selection.primary];
|
|
266
|
+
if (this.selection.fallback) {
|
|
267
|
+
names.push(...this.selection.fallback);
|
|
268
|
+
}
|
|
269
|
+
return names;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Get list of facilitators based on selection strategy
|
|
273
|
+
*/
|
|
274
|
+
async getOrderedFacilitators(network) {
|
|
275
|
+
const names = this.getConfiguredNames();
|
|
276
|
+
const facilitators = [];
|
|
277
|
+
for (const name of names) {
|
|
278
|
+
try {
|
|
279
|
+
const f = this.get(name);
|
|
280
|
+
if (f.supportsNetwork(network)) {
|
|
281
|
+
facilitators.push(f);
|
|
282
|
+
}
|
|
283
|
+
} catch (err) {
|
|
284
|
+
console.warn(`[Registry] Failed to get facilitator ${name}:`, err);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (facilitators.length === 0) {
|
|
288
|
+
throw new Error(`No facilitators available for network: ${network}`);
|
|
289
|
+
}
|
|
290
|
+
switch (this.selection.strategy) {
|
|
291
|
+
case "random":
|
|
292
|
+
return this.shuffle(facilitators);
|
|
293
|
+
case "roundrobin":
|
|
294
|
+
this.roundRobinIndex = (this.roundRobinIndex + 1) % facilitators.length;
|
|
295
|
+
return [
|
|
296
|
+
...facilitators.slice(this.roundRobinIndex),
|
|
297
|
+
...facilitators.slice(0, this.roundRobinIndex)
|
|
298
|
+
];
|
|
299
|
+
case "cheapest":
|
|
300
|
+
return this.sortByCheapest(facilitators);
|
|
301
|
+
case "fastest":
|
|
302
|
+
return this.sortByFastest(facilitators);
|
|
303
|
+
case "failover":
|
|
304
|
+
default:
|
|
305
|
+
return facilitators;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
shuffle(array) {
|
|
309
|
+
const result = [...array];
|
|
310
|
+
for (let i = result.length - 1; i > 0; i--) {
|
|
311
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
312
|
+
[result[i], result[j]] = [result[j], result[i]];
|
|
313
|
+
}
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
async sortByCheapest(facilitators) {
|
|
317
|
+
const withFees = await Promise.all(
|
|
318
|
+
facilitators.map(async (f) => {
|
|
319
|
+
try {
|
|
320
|
+
const fee = await f.getFee?.();
|
|
321
|
+
return { facilitator: f, perTx: fee?.perTx ?? Infinity };
|
|
322
|
+
} catch {
|
|
323
|
+
return { facilitator: f, perTx: Infinity };
|
|
324
|
+
}
|
|
325
|
+
})
|
|
326
|
+
);
|
|
327
|
+
withFees.sort((a, b) => a.perTx - b.perTx);
|
|
328
|
+
return withFees.map((w) => w.facilitator);
|
|
329
|
+
}
|
|
330
|
+
async sortByFastest(facilitators) {
|
|
331
|
+
const withLatency = await Promise.all(
|
|
332
|
+
facilitators.map(async (f) => {
|
|
333
|
+
try {
|
|
334
|
+
const health = await f.healthCheck();
|
|
335
|
+
return { facilitator: f, latency: health.latencyMs ?? Infinity };
|
|
336
|
+
} catch {
|
|
337
|
+
return { facilitator: f, latency: Infinity };
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
);
|
|
341
|
+
withLatency.sort((a, b) => a.latency - b.latency);
|
|
342
|
+
return withLatency.map((w) => w.facilitator);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Verify payment using configured facilitators
|
|
346
|
+
*/
|
|
347
|
+
async verify(paymentPayload, requirements) {
|
|
348
|
+
const network = paymentPayload.accepted?.network || paymentPayload.network || requirements.network;
|
|
349
|
+
const facilitators = await this.getOrderedFacilitators(network);
|
|
350
|
+
let lastError;
|
|
351
|
+
for (const f of facilitators) {
|
|
352
|
+
try {
|
|
353
|
+
console.log(`[Registry] Trying ${f.name} for verify...`);
|
|
354
|
+
const result = await f.verify(paymentPayload, requirements);
|
|
355
|
+
if (result.valid) {
|
|
356
|
+
console.log(`[Registry] ${f.name} verify succeeded`);
|
|
357
|
+
return { ...result, facilitator: f.name };
|
|
358
|
+
}
|
|
359
|
+
lastError = result.error;
|
|
360
|
+
console.log(`[Registry] ${f.name} verify failed: ${result.error}`);
|
|
361
|
+
if (this.selection.strategy === "failover" && !this.isTransientError(result.error)) {
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
} catch (err) {
|
|
365
|
+
lastError = err.message;
|
|
366
|
+
console.error(`[Registry] ${f.name} error:`, err.message);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
valid: false,
|
|
371
|
+
error: lastError || "All facilitators failed",
|
|
372
|
+
facilitator: "none"
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Settle payment using configured facilitators
|
|
377
|
+
*/
|
|
378
|
+
async settle(paymentPayload, requirements) {
|
|
379
|
+
const network = paymentPayload.accepted?.network || paymentPayload.network || requirements.network;
|
|
380
|
+
const facilitators = await this.getOrderedFacilitators(network);
|
|
381
|
+
let lastError;
|
|
382
|
+
for (const f of facilitators) {
|
|
383
|
+
try {
|
|
384
|
+
console.log(`[Registry] Trying ${f.name} for settle...`);
|
|
385
|
+
const result = await f.settle(paymentPayload, requirements);
|
|
386
|
+
if (result.success) {
|
|
387
|
+
console.log(`[Registry] ${f.name} settle succeeded: ${result.transaction}`);
|
|
388
|
+
return { ...result, facilitator: f.name };
|
|
389
|
+
}
|
|
390
|
+
lastError = result.error;
|
|
391
|
+
console.log(`[Registry] ${f.name} settle failed: ${result.error}`);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
lastError = err.message;
|
|
394
|
+
console.error(`[Registry] ${f.name} error:`, err.message);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
success: false,
|
|
399
|
+
error: lastError || "All facilitators failed",
|
|
400
|
+
facilitator: "none"
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Check health of all configured facilitators
|
|
405
|
+
*/
|
|
406
|
+
async healthCheckAll() {
|
|
407
|
+
const results = {};
|
|
408
|
+
for (const name of this.getConfiguredNames()) {
|
|
409
|
+
try {
|
|
410
|
+
const f = this.get(name);
|
|
411
|
+
results[name] = await f.healthCheck();
|
|
412
|
+
} catch (err) {
|
|
413
|
+
results[name] = { healthy: false, error: err.message };
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return results;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Check if an error is transient (network/server issue) vs permanent (bad request)
|
|
420
|
+
*/
|
|
421
|
+
isTransientError(error) {
|
|
422
|
+
if (!error) return true;
|
|
423
|
+
const transientPatterns = [
|
|
424
|
+
/timeout/i,
|
|
425
|
+
/network/i,
|
|
426
|
+
/connection/i,
|
|
427
|
+
/ECONNREFUSED/i,
|
|
428
|
+
/ETIMEDOUT/i,
|
|
429
|
+
/503/,
|
|
430
|
+
/502/,
|
|
431
|
+
/500/
|
|
432
|
+
];
|
|
433
|
+
return transientPatterns.some((p) => p.test(error));
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Update selection configuration
|
|
437
|
+
*/
|
|
438
|
+
setSelection(selection) {
|
|
439
|
+
this.selection = selection;
|
|
440
|
+
this.instances.clear();
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Get current selection configuration
|
|
444
|
+
*/
|
|
445
|
+
getSelection() {
|
|
446
|
+
return { ...this.selection };
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// src/server/index.ts
|
|
451
|
+
var X402_VERSION2 = 2;
|
|
6
452
|
var PAYMENT_REQUIRED_HEADER = "x-payment-required";
|
|
7
453
|
var PAYMENT_HEADER = "x-payment";
|
|
8
454
|
var PAYMENT_RESPONSE_HEADER = "x-payment-response";
|
|
9
|
-
var FACILITATOR_TESTNET = "https://www.x402.org/facilitator";
|
|
10
|
-
var FACILITATOR_MAINNET = "https://api.cdp.coinbase.com/platform/v2/x402";
|
|
11
455
|
var USDC_ADDRESSES = {
|
|
12
456
|
"eip155:8453": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
13
457
|
// Base mainnet
|
|
@@ -18,15 +462,15 @@ var USDC_DOMAIN = {
|
|
|
18
462
|
name: "USD Coin",
|
|
19
463
|
version: "2"
|
|
20
464
|
};
|
|
21
|
-
function
|
|
465
|
+
function loadEnvFile2() {
|
|
22
466
|
const envPaths = [
|
|
23
|
-
|
|
24
|
-
|
|
467
|
+
path2.join(process.cwd(), ".env"),
|
|
468
|
+
path2.join(process.env.HOME || "", ".moltspay", ".env")
|
|
25
469
|
];
|
|
26
470
|
for (const envPath of envPaths) {
|
|
27
|
-
if (
|
|
471
|
+
if (existsSync2(envPath)) {
|
|
28
472
|
try {
|
|
29
|
-
const content =
|
|
473
|
+
const content = readFileSync2(envPath, "utf-8");
|
|
30
474
|
for (const line of content.split("\n")) {
|
|
31
475
|
const trimmed = line.trim();
|
|
32
476
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -43,77 +487,44 @@ function loadEnvFiles() {
|
|
|
43
487
|
}
|
|
44
488
|
console.log(`[MoltsPay] Loaded config from ${envPath}`);
|
|
45
489
|
break;
|
|
46
|
-
} catch
|
|
47
|
-
console.warn(`[MoltsPay] Failed to load ${envPath}:`, err);
|
|
490
|
+
} catch {
|
|
48
491
|
}
|
|
49
492
|
}
|
|
50
493
|
}
|
|
51
494
|
}
|
|
52
|
-
function getCDPConfig() {
|
|
53
|
-
loadEnvFiles();
|
|
54
|
-
return {
|
|
55
|
-
useMainnet: process.env.USE_MAINNET?.toLowerCase() === "true",
|
|
56
|
-
apiKeyId: process.env.CDP_API_KEY_ID,
|
|
57
|
-
apiKeySecret: process.env.CDP_API_KEY_SECRET
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
async function getCDPAuthHeaders(method, urlPath, body) {
|
|
61
|
-
const config = getCDPConfig();
|
|
62
|
-
if (!config.apiKeyId || !config.apiKeySecret) {
|
|
63
|
-
throw new Error("CDP_API_KEY_ID and CDP_API_KEY_SECRET required for mainnet");
|
|
64
|
-
}
|
|
65
|
-
try {
|
|
66
|
-
const { getAuthHeaders } = await import("@coinbase/cdp-sdk/auth");
|
|
67
|
-
const headers = await getAuthHeaders({
|
|
68
|
-
apiKeyId: config.apiKeyId,
|
|
69
|
-
apiKeySecret: config.apiKeySecret,
|
|
70
|
-
requestMethod: method,
|
|
71
|
-
requestHost: "api.cdp.coinbase.com",
|
|
72
|
-
requestPath: urlPath,
|
|
73
|
-
requestBody: body
|
|
74
|
-
});
|
|
75
|
-
return headers;
|
|
76
|
-
} catch (err) {
|
|
77
|
-
console.error("[MoltsPay] Failed to generate CDP auth headers:", err.message);
|
|
78
|
-
throw err;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
495
|
var MoltsPayServer = class {
|
|
82
496
|
manifest;
|
|
83
497
|
skills = /* @__PURE__ */ new Map();
|
|
84
498
|
options;
|
|
85
|
-
|
|
86
|
-
facilitatorUrl;
|
|
499
|
+
registry;
|
|
87
500
|
networkId;
|
|
501
|
+
useMainnet;
|
|
88
502
|
constructor(servicesPath, options = {}) {
|
|
89
|
-
|
|
90
|
-
const content =
|
|
503
|
+
loadEnvFile2();
|
|
504
|
+
const content = readFileSync2(servicesPath, "utf-8");
|
|
91
505
|
this.manifest = JSON.parse(content);
|
|
92
506
|
this.options = {
|
|
93
507
|
port: options.port || 3e3,
|
|
94
|
-
host: options.host || "0.0.0.0"
|
|
508
|
+
host: options.host || "0.0.0.0",
|
|
509
|
+
...options
|
|
95
510
|
};
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
511
|
+
this.useMainnet = process.env.USE_MAINNET?.toLowerCase() === "true";
|
|
512
|
+
this.networkId = this.useMainnet ? "eip155:8453" : "eip155:84532";
|
|
513
|
+
const facilitatorConfig = options.facilitators || {
|
|
514
|
+
primary: "cdp",
|
|
515
|
+
strategy: "failover",
|
|
516
|
+
config: {
|
|
517
|
+
cdp: { useMainnet: this.useMainnet }
|
|
100
518
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
this.networkId = "eip155:84532";
|
|
106
|
-
}
|
|
107
|
-
const networkName = this.cdpConfig.useMainnet ? "Base mainnet" : "Base Sepolia (testnet)";
|
|
108
|
-
const facilitatorName = this.cdpConfig.useMainnet ? "CDP" : "x402.org";
|
|
519
|
+
};
|
|
520
|
+
this.registry = new FacilitatorRegistry(facilitatorConfig);
|
|
521
|
+
const primaryFacilitator = this.registry.get(facilitatorConfig.primary);
|
|
522
|
+
const networkName = this.useMainnet ? "Base mainnet" : "Base Sepolia (testnet)";
|
|
109
523
|
console.log(`[MoltsPay] Loaded ${this.manifest.services.length} services from ${servicesPath}`);
|
|
110
524
|
console.log(`[MoltsPay] Provider: ${this.manifest.provider.name}`);
|
|
111
525
|
console.log(`[MoltsPay] Receive wallet: ${this.manifest.provider.wallet}`);
|
|
112
526
|
console.log(`[MoltsPay] Network: ${this.networkId} (${networkName})`);
|
|
113
|
-
console.log(`[MoltsPay] Facilitator: ${
|
|
114
|
-
if (this.cdpConfig.useMainnet && this.cdpConfig.apiKeyId) {
|
|
115
|
-
console.log(`[MoltsPay] CDP API Key: ${this.cdpConfig.apiKeyId.slice(0, 8)}...`);
|
|
116
|
-
}
|
|
527
|
+
console.log(`[MoltsPay] Facilitator: ${primaryFacilitator.displayName} (${facilitatorConfig.strategy || "failover"})`);
|
|
117
528
|
console.log(`[MoltsPay] Protocol: x402 (gasless for both client AND server)`);
|
|
118
529
|
}
|
|
119
530
|
/**
|
|
@@ -139,6 +550,7 @@ var MoltsPayServer = class {
|
|
|
139
550
|
console.log(`[MoltsPay] Endpoints:`);
|
|
140
551
|
console.log(` GET /services - List available services`);
|
|
141
552
|
console.log(` POST /execute - Execute service (x402 payment)`);
|
|
553
|
+
console.log(` GET /health - Health check (incl. facilitators)`);
|
|
142
554
|
});
|
|
143
555
|
}
|
|
144
556
|
/**
|
|
@@ -159,6 +571,9 @@ var MoltsPayServer = class {
|
|
|
159
571
|
if (url.pathname === "/services" && req.method === "GET") {
|
|
160
572
|
return this.handleGetServices(res);
|
|
161
573
|
}
|
|
574
|
+
if (url.pathname === "/health" && req.method === "GET") {
|
|
575
|
+
return await this.handleHealthCheck(res);
|
|
576
|
+
}
|
|
162
577
|
if (url.pathname === "/execute" && req.method === "POST") {
|
|
163
578
|
const body = await this.readBody(req);
|
|
164
579
|
const paymentHeader = req.headers[PAYMENT_HEADER];
|
|
@@ -184,18 +599,37 @@ var MoltsPayServer = class {
|
|
|
184
599
|
output: s.output,
|
|
185
600
|
available: this.skills.has(s.id)
|
|
186
601
|
}));
|
|
602
|
+
const selection = this.registry.getSelection();
|
|
187
603
|
this.sendJson(res, 200, {
|
|
188
604
|
provider: this.manifest.provider,
|
|
189
605
|
services,
|
|
190
606
|
x402: {
|
|
191
|
-
version:
|
|
607
|
+
version: X402_VERSION2,
|
|
192
608
|
network: this.networkId,
|
|
193
609
|
schemes: ["exact"],
|
|
194
|
-
|
|
195
|
-
|
|
610
|
+
facilitators: {
|
|
611
|
+
primary: selection.primary,
|
|
612
|
+
fallback: selection.fallback,
|
|
613
|
+
strategy: selection.strategy
|
|
614
|
+
},
|
|
615
|
+
mainnet: this.useMainnet
|
|
196
616
|
}
|
|
197
617
|
});
|
|
198
618
|
}
|
|
619
|
+
/**
|
|
620
|
+
* GET /health - Health check endpoint
|
|
621
|
+
*/
|
|
622
|
+
async handleHealthCheck(res) {
|
|
623
|
+
const facilitatorHealth = await this.registry.healthCheckAll();
|
|
624
|
+
const allHealthy = Object.values(facilitatorHealth).every((h) => h.healthy);
|
|
625
|
+
this.sendJson(res, allHealthy ? 200 : 503, {
|
|
626
|
+
status: allHealthy ? "healthy" : "degraded",
|
|
627
|
+
network: this.networkId,
|
|
628
|
+
facilitators: facilitatorHealth,
|
|
629
|
+
services: this.manifest.services.length,
|
|
630
|
+
registered: this.skills.size
|
|
631
|
+
});
|
|
632
|
+
}
|
|
199
633
|
/**
|
|
200
634
|
* POST /execute - Execute service with x402 payment
|
|
201
635
|
*/
|
|
@@ -227,11 +661,16 @@ var MoltsPayServer = class {
|
|
|
227
661
|
if (!validation.valid) {
|
|
228
662
|
return this.sendJson(res, 402, { error: validation.error });
|
|
229
663
|
}
|
|
230
|
-
|
|
231
|
-
|
|
664
|
+
const requirements = this.buildPaymentRequirements(skill.config);
|
|
665
|
+
console.log(`[MoltsPay] Verifying payment...`);
|
|
666
|
+
const verifyResult = await this.registry.verify(payment, requirements);
|
|
232
667
|
if (!verifyResult.valid) {
|
|
233
|
-
return this.sendJson(res, 402, {
|
|
668
|
+
return this.sendJson(res, 402, {
|
|
669
|
+
error: `Payment verification failed: ${verifyResult.error}`,
|
|
670
|
+
facilitator: verifyResult.facilitator
|
|
671
|
+
});
|
|
234
672
|
}
|
|
673
|
+
console.log(`[MoltsPay] Verified by ${verifyResult.facilitator}`);
|
|
235
674
|
console.log(`[MoltsPay] Executing skill: ${service}`);
|
|
236
675
|
let result;
|
|
237
676
|
try {
|
|
@@ -246,17 +685,18 @@ var MoltsPayServer = class {
|
|
|
246
685
|
console.log(`[MoltsPay] Skill succeeded, settling payment...`);
|
|
247
686
|
let settlement = null;
|
|
248
687
|
try {
|
|
249
|
-
settlement = await this.
|
|
250
|
-
console.log(`[MoltsPay] Payment settled: ${settlement.transaction || "pending"}`);
|
|
688
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
689
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
251
690
|
} catch (err) {
|
|
252
691
|
console.error("[MoltsPay] Settlement failed:", err.message);
|
|
253
692
|
}
|
|
254
693
|
const responseHeaders = {};
|
|
255
|
-
if (settlement) {
|
|
694
|
+
if (settlement?.success) {
|
|
256
695
|
const responsePayload = {
|
|
257
696
|
success: true,
|
|
258
697
|
transaction: settlement.transaction,
|
|
259
|
-
network: payment.network
|
|
698
|
+
network: payment.network || payment.accepted?.network,
|
|
699
|
+
facilitator: settlement.facilitator
|
|
260
700
|
};
|
|
261
701
|
responseHeaders[PAYMENT_RESPONSE_HEADER] = Buffer.from(
|
|
262
702
|
JSON.stringify(responsePayload)
|
|
@@ -265,7 +705,7 @@ var MoltsPayServer = class {
|
|
|
265
705
|
this.sendJson(res, 200, {
|
|
266
706
|
success: true,
|
|
267
707
|
result,
|
|
268
|
-
payment: settlement ? { transaction: settlement.transaction, status: "settled" } : { status: "pending" }
|
|
708
|
+
payment: settlement?.success ? { transaction: settlement.transaction, status: "settled", facilitator: settlement.facilitator } : { status: "pending" }
|
|
269
709
|
}, responseHeaders);
|
|
270
710
|
}
|
|
271
711
|
/**
|
|
@@ -274,7 +714,7 @@ var MoltsPayServer = class {
|
|
|
274
714
|
sendPaymentRequired(config, res) {
|
|
275
715
|
const requirements = this.buildPaymentRequirements(config);
|
|
276
716
|
const paymentRequired = {
|
|
277
|
-
x402Version:
|
|
717
|
+
x402Version: X402_VERSION2,
|
|
278
718
|
accepts: [requirements],
|
|
279
719
|
resource: {
|
|
280
720
|
url: `/execute?service=${config.id}`,
|
|
@@ -297,7 +737,7 @@ var MoltsPayServer = class {
|
|
|
297
737
|
* Basic payment validation
|
|
298
738
|
*/
|
|
299
739
|
validatePayment(payment, config) {
|
|
300
|
-
if (payment.x402Version !==
|
|
740
|
+
if (payment.x402Version !== X402_VERSION2) {
|
|
301
741
|
return { valid: false, error: `Unsupported x402 version: ${payment.x402Version}` };
|
|
302
742
|
}
|
|
303
743
|
const scheme = payment.accepted?.scheme || payment.scheme;
|
|
@@ -311,7 +751,7 @@ var MoltsPayServer = class {
|
|
|
311
751
|
return { valid: true };
|
|
312
752
|
}
|
|
313
753
|
/**
|
|
314
|
-
* Build
|
|
754
|
+
* Build payment requirements for facilitator
|
|
315
755
|
*/
|
|
316
756
|
buildPaymentRequirements(config) {
|
|
317
757
|
const amountInUnits = Math.floor(config.price * 1e6).toString();
|
|
@@ -326,77 +766,6 @@ var MoltsPayServer = class {
|
|
|
326
766
|
extra: USDC_DOMAIN
|
|
327
767
|
};
|
|
328
768
|
}
|
|
329
|
-
/**
|
|
330
|
-
* Verify payment with facilitator (testnet or CDP)
|
|
331
|
-
*/
|
|
332
|
-
async verifyWithFacilitator(payment, config) {
|
|
333
|
-
try {
|
|
334
|
-
const requirements = this.buildPaymentRequirements(config);
|
|
335
|
-
const requestBody = {
|
|
336
|
-
x402Version: X402_VERSION,
|
|
337
|
-
// Required at top level for CDP
|
|
338
|
-
paymentPayload: payment,
|
|
339
|
-
paymentRequirements: requirements
|
|
340
|
-
};
|
|
341
|
-
console.log("[MoltsPay] Verify request:", JSON.stringify(requestBody, null, 2));
|
|
342
|
-
let headers = { "Content-Type": "application/json" };
|
|
343
|
-
if (this.cdpConfig.useMainnet) {
|
|
344
|
-
const authHeaders = await getCDPAuthHeaders(
|
|
345
|
-
"POST",
|
|
346
|
-
"/platform/v2/x402/verify",
|
|
347
|
-
requestBody
|
|
348
|
-
);
|
|
349
|
-
headers = { ...headers, ...authHeaders };
|
|
350
|
-
}
|
|
351
|
-
const response = await fetch(`${this.facilitatorUrl}/verify`, {
|
|
352
|
-
method: "POST",
|
|
353
|
-
headers,
|
|
354
|
-
body: JSON.stringify(requestBody)
|
|
355
|
-
});
|
|
356
|
-
const result = await response.json();
|
|
357
|
-
console.log("[MoltsPay] Verify response:", JSON.stringify(result, null, 2));
|
|
358
|
-
if (!response.ok || !result.isValid) {
|
|
359
|
-
return { valid: false, error: result.invalidReason || result.error || "Verification failed" };
|
|
360
|
-
}
|
|
361
|
-
return { valid: true };
|
|
362
|
-
} catch (err) {
|
|
363
|
-
return { valid: false, error: `Facilitator error: ${err.message}` };
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
/**
|
|
367
|
-
* Settle payment with facilitator (execute on-chain transfer)
|
|
368
|
-
*/
|
|
369
|
-
async settleWithFacilitator(payment, config) {
|
|
370
|
-
const requirements = this.buildPaymentRequirements(config);
|
|
371
|
-
const requestBody = {
|
|
372
|
-
x402Version: X402_VERSION,
|
|
373
|
-
// Required at top level for CDP
|
|
374
|
-
paymentPayload: payment,
|
|
375
|
-
paymentRequirements: requirements
|
|
376
|
-
};
|
|
377
|
-
let headers = { "Content-Type": "application/json" };
|
|
378
|
-
if (this.cdpConfig.useMainnet) {
|
|
379
|
-
const authHeaders = await getCDPAuthHeaders(
|
|
380
|
-
"POST",
|
|
381
|
-
"/platform/v2/x402/settle",
|
|
382
|
-
requestBody
|
|
383
|
-
);
|
|
384
|
-
headers = { ...headers, ...authHeaders };
|
|
385
|
-
}
|
|
386
|
-
const response = await fetch(`${this.facilitatorUrl}/settle`, {
|
|
387
|
-
method: "POST",
|
|
388
|
-
headers,
|
|
389
|
-
body: JSON.stringify(requestBody)
|
|
390
|
-
});
|
|
391
|
-
const result = await response.json();
|
|
392
|
-
if (!response.ok || !result.success) {
|
|
393
|
-
throw new Error(result.error || result.errorReason || "Settlement failed");
|
|
394
|
-
}
|
|
395
|
-
return {
|
|
396
|
-
transaction: result.transaction,
|
|
397
|
-
status: result.status || "settled"
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
769
|
async readBody(req) {
|
|
401
770
|
return new Promise((resolve, reject) => {
|
|
402
771
|
let body = "";
|