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