moltspay 0.7.2 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +293 -274
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +294 -275
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +11 -5
- package/dist/client/index.d.ts +11 -5
- package/dist/client/index.js +99 -68
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +96 -55
- package/dist/client/index.mjs.map +1 -1
- package/dist/index.js +435 -343
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +430 -338
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.d.mts +25 -9
- package/dist/server/index.d.ts +25 -9
- package/dist/server/index.js +187 -210
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +187 -210
- package/dist/server/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -93,11 +93,11 @@ function getChain(name) {
|
|
|
93
93
|
}
|
|
94
94
|
return config;
|
|
95
95
|
}
|
|
96
|
-
function getChainById(chainId) {
|
|
97
|
-
return Object.values(CHAINS).find((c) => c.chainId === chainId);
|
|
98
|
-
}
|
|
99
96
|
|
|
100
97
|
// src/client/index.ts
|
|
98
|
+
var X402_VERSION = 2;
|
|
99
|
+
var PAYMENT_REQUIRED_HEADER = "x-payment-required";
|
|
100
|
+
var PAYMENT_HEADER = "x-payment";
|
|
101
101
|
var DEFAULT_CONFIG = {
|
|
102
102
|
chain: "base",
|
|
103
103
|
limits: {
|
|
@@ -161,43 +161,112 @@ var MoltsPayClient = class {
|
|
|
161
161
|
return res.json();
|
|
162
162
|
}
|
|
163
163
|
/**
|
|
164
|
-
* Pay for a service and get the result
|
|
164
|
+
* Pay for a service and get the result (x402 protocol)
|
|
165
|
+
*
|
|
166
|
+
* This is GASLESS for the client - server pays gas to claim payment.
|
|
167
|
+
* This is PAY-FOR-SUCCESS - payment only claimed if service succeeds.
|
|
165
168
|
*/
|
|
166
169
|
async pay(serverUrl, service, params) {
|
|
167
|
-
if (!this.wallet) {
|
|
170
|
+
if (!this.wallet || !this.walletData) {
|
|
168
171
|
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
169
172
|
}
|
|
170
|
-
|
|
173
|
+
console.log(`[MoltsPay] Requesting service: ${service}`);
|
|
174
|
+
const initialRes = await fetch(`${serverUrl}/execute`, {
|
|
171
175
|
method: "POST",
|
|
172
176
|
headers: { "Content-Type": "application/json" },
|
|
173
177
|
body: JSON.stringify({ service, params })
|
|
174
178
|
});
|
|
175
|
-
if (
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
179
|
+
if (initialRes.status !== 402) {
|
|
180
|
+
const data = await initialRes.json();
|
|
181
|
+
if (initialRes.ok && data.result) {
|
|
182
|
+
return data.result;
|
|
183
|
+
}
|
|
184
|
+
throw new Error(data.error || "Unexpected response");
|
|
185
|
+
}
|
|
186
|
+
const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
187
|
+
if (!paymentRequiredHeader) {
|
|
188
|
+
throw new Error("Missing x-payment-required header");
|
|
189
|
+
}
|
|
190
|
+
let requirements;
|
|
191
|
+
try {
|
|
192
|
+
const decoded = Buffer.from(paymentRequiredHeader, "base64").toString("utf-8");
|
|
193
|
+
requirements = JSON.parse(decoded);
|
|
194
|
+
if (!Array.isArray(requirements)) {
|
|
195
|
+
requirements = [requirements];
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
throw new Error("Invalid x-payment-required header");
|
|
199
|
+
}
|
|
200
|
+
const chain = getChain(this.config.chain);
|
|
201
|
+
const network = `eip155:${chain.chainId}`;
|
|
202
|
+
const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
|
|
203
|
+
if (!req) {
|
|
204
|
+
throw new Error(`No matching payment option for ${network}`);
|
|
205
|
+
}
|
|
206
|
+
const amount = Number(req.maxAmountRequired) / 1e6;
|
|
207
|
+
this.checkLimits(amount);
|
|
208
|
+
console.log(`[MoltsPay] Signing payment: $${amount} USDC (gasless)`);
|
|
209
|
+
const authorization = await this.signEIP3009(req.resource, amount, chain);
|
|
210
|
+
const payload = {
|
|
211
|
+
x402Version: X402_VERSION,
|
|
212
|
+
scheme: "exact",
|
|
213
|
+
network,
|
|
214
|
+
payload: authorization
|
|
215
|
+
};
|
|
216
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
217
|
+
console.log(`[MoltsPay] Sending request with payment...`);
|
|
218
|
+
const paidRes = await fetch(`${serverUrl}/execute`, {
|
|
186
219
|
method: "POST",
|
|
187
|
-
headers: {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
})
|
|
220
|
+
headers: {
|
|
221
|
+
"Content-Type": "application/json",
|
|
222
|
+
[PAYMENT_HEADER]: paymentHeader
|
|
223
|
+
},
|
|
224
|
+
body: JSON.stringify({ service, params })
|
|
192
225
|
});
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
throw new Error(
|
|
226
|
+
const result = await paidRes.json();
|
|
227
|
+
if (!paidRes.ok) {
|
|
228
|
+
throw new Error(result.error || "Service execution failed");
|
|
196
229
|
}
|
|
197
|
-
|
|
198
|
-
|
|
230
|
+
this.recordSpending(amount);
|
|
231
|
+
console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
|
|
199
232
|
return result.result;
|
|
200
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Sign EIP-3009 transferWithAuthorization (GASLESS)
|
|
236
|
+
* This only signs - no on-chain transaction, no gas needed.
|
|
237
|
+
*/
|
|
238
|
+
async signEIP3009(to, amount, chain) {
|
|
239
|
+
const validAfter = 0;
|
|
240
|
+
const validBefore = Math.floor(Date.now() / 1e3) + 3600;
|
|
241
|
+
const nonce = import_ethers.ethers.hexlify(import_ethers.ethers.randomBytes(32));
|
|
242
|
+
const value = BigInt(Math.floor(amount * 1e6)).toString();
|
|
243
|
+
const authorization = {
|
|
244
|
+
from: this.wallet.address,
|
|
245
|
+
to,
|
|
246
|
+
value,
|
|
247
|
+
validAfter: validAfter.toString(),
|
|
248
|
+
validBefore: validBefore.toString(),
|
|
249
|
+
nonce
|
|
250
|
+
};
|
|
251
|
+
const domain = {
|
|
252
|
+
name: "USD Coin",
|
|
253
|
+
version: "2",
|
|
254
|
+
chainId: chain.chainId,
|
|
255
|
+
verifyingContract: chain.usdc
|
|
256
|
+
};
|
|
257
|
+
const types = {
|
|
258
|
+
TransferWithAuthorization: [
|
|
259
|
+
{ name: "from", type: "address" },
|
|
260
|
+
{ name: "to", type: "address" },
|
|
261
|
+
{ name: "value", type: "uint256" },
|
|
262
|
+
{ name: "validAfter", type: "uint256" },
|
|
263
|
+
{ name: "validBefore", type: "uint256" },
|
|
264
|
+
{ name: "nonce", type: "bytes32" }
|
|
265
|
+
]
|
|
266
|
+
};
|
|
267
|
+
const signature = await this.wallet.signTypedData(domain, types, authorization);
|
|
268
|
+
return { authorization, signature };
|
|
269
|
+
}
|
|
201
270
|
/**
|
|
202
271
|
* Check spending limits
|
|
203
272
|
*/
|
|
@@ -224,36 +293,6 @@ var MoltsPayClient = class {
|
|
|
224
293
|
recordSpending(amount) {
|
|
225
294
|
this.todaySpending += amount;
|
|
226
295
|
}
|
|
227
|
-
/**
|
|
228
|
-
* Execute payment on-chain
|
|
229
|
-
*/
|
|
230
|
-
async executePayment(payment) {
|
|
231
|
-
let chain;
|
|
232
|
-
try {
|
|
233
|
-
chain = getChain(payment.chain);
|
|
234
|
-
} catch {
|
|
235
|
-
throw new Error(`Unknown chain: ${payment.chain}`);
|
|
236
|
-
}
|
|
237
|
-
const { ethers: ethers2 } = await import("ethers");
|
|
238
|
-
const provider = new ethers2.JsonRpcProvider(chain.rpc);
|
|
239
|
-
const signer = new ethers2.Wallet(this.walletData.privateKey, provider);
|
|
240
|
-
const usdcAddress = chain.usdc;
|
|
241
|
-
const usdcAbi = [
|
|
242
|
-
"function transfer(address to, uint256 amount) returns (bool)",
|
|
243
|
-
"function balanceOf(address account) view returns (uint256)"
|
|
244
|
-
];
|
|
245
|
-
const usdc = new ethers2.Contract(usdcAddress, usdcAbi, signer);
|
|
246
|
-
const amountInUnits = ethers2.parseUnits(payment.amount.toString(), 6);
|
|
247
|
-
const balance = await usdc.balanceOf(this.wallet.address);
|
|
248
|
-
if (balance < amountInUnits) {
|
|
249
|
-
throw new Error(
|
|
250
|
-
`Insufficient USDC balance: ${ethers2.formatUnits(balance, 6)} < ${payment.amount}`
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
const tx = await usdc.transfer(payment.wallet, amountInUnits);
|
|
254
|
-
const receipt = await tx.wait();
|
|
255
|
-
return receipt.hash;
|
|
256
|
-
}
|
|
257
296
|
// --- Config & Wallet Management ---
|
|
258
297
|
loadConfig() {
|
|
259
298
|
const configPath = (0, import_path.join)(this.configDir, "config.json");
|
|
@@ -313,15 +352,14 @@ var MoltsPayClient = class {
|
|
|
313
352
|
} catch {
|
|
314
353
|
throw new Error(`Unknown chain: ${this.config.chain}`);
|
|
315
354
|
}
|
|
316
|
-
const
|
|
317
|
-
const provider = new ethers2.JsonRpcProvider(chain.rpc);
|
|
355
|
+
const provider = new import_ethers.ethers.JsonRpcProvider(chain.rpc);
|
|
318
356
|
const nativeBalance = await provider.getBalance(this.wallet.address);
|
|
319
357
|
const usdcAbi = ["function balanceOf(address) view returns (uint256)"];
|
|
320
|
-
const usdc = new
|
|
358
|
+
const usdc = new import_ethers.ethers.Contract(chain.usdc, usdcAbi, provider);
|
|
321
359
|
const usdcBalance = await usdc.balanceOf(this.wallet.address);
|
|
322
360
|
return {
|
|
323
|
-
usdc: parseFloat(
|
|
324
|
-
native: parseFloat(
|
|
361
|
+
usdc: parseFloat(import_ethers.ethers.formatUnits(usdcBalance, 6)),
|
|
362
|
+
native: parseFloat(import_ethers.ethers.formatEther(nativeBalance))
|
|
325
363
|
};
|
|
326
364
|
}
|
|
327
365
|
};
|
|
@@ -329,99 +367,29 @@ var MoltsPayClient = class {
|
|
|
329
367
|
// src/server/index.ts
|
|
330
368
|
var import_fs2 = require("fs");
|
|
331
369
|
var import_http = require("http");
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
var
|
|
335
|
-
var
|
|
336
|
-
|
|
337
|
-
const { txHash, expectedAmount, expectedTo } = params;
|
|
338
|
-
let chain;
|
|
339
|
-
try {
|
|
340
|
-
if (typeof params.chain === "number") {
|
|
341
|
-
chain = getChainById(params.chain);
|
|
342
|
-
} else {
|
|
343
|
-
chain = getChain(params.chain || "base");
|
|
344
|
-
}
|
|
345
|
-
if (!chain) {
|
|
346
|
-
return { verified: false, error: `Unsupported chain: ${params.chain}` };
|
|
347
|
-
}
|
|
348
|
-
} catch (e) {
|
|
349
|
-
return { verified: false, error: `Unsupported chain: ${params.chain}` };
|
|
350
|
-
}
|
|
351
|
-
try {
|
|
352
|
-
const provider = new import_ethers2.ethers.JsonRpcProvider(chain.rpc);
|
|
353
|
-
const receipt = await provider.getTransactionReceipt(txHash);
|
|
354
|
-
if (!receipt) {
|
|
355
|
-
return { verified: false, error: "Transaction not found or not confirmed" };
|
|
356
|
-
}
|
|
357
|
-
if (receipt.status !== 1) {
|
|
358
|
-
return { verified: false, error: "Transaction failed" };
|
|
359
|
-
}
|
|
360
|
-
const usdcAddress = chain.usdc?.toLowerCase();
|
|
361
|
-
if (!usdcAddress) {
|
|
362
|
-
return { verified: false, error: `Chain ${chain.name} USDC address not configured` };
|
|
363
|
-
}
|
|
364
|
-
for (const log of receipt.logs) {
|
|
365
|
-
if (log.address.toLowerCase() !== usdcAddress) {
|
|
366
|
-
continue;
|
|
367
|
-
}
|
|
368
|
-
if (log.topics.length < 3 || log.topics[0] !== TRANSFER_EVENT_TOPIC) {
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
const from = "0x" + log.topics[1].slice(-40);
|
|
372
|
-
const to = "0x" + log.topics[2].slice(-40);
|
|
373
|
-
const amountRaw = BigInt(log.data);
|
|
374
|
-
const amount = Number(amountRaw) / 1e6;
|
|
375
|
-
if (expectedTo && to.toLowerCase() !== expectedTo.toLowerCase()) {
|
|
376
|
-
continue;
|
|
377
|
-
}
|
|
378
|
-
if (amount < expectedAmount) {
|
|
379
|
-
return {
|
|
380
|
-
verified: false,
|
|
381
|
-
error: `Insufficient amount: received ${amount} USDC, expected ${expectedAmount} USDC`,
|
|
382
|
-
amount,
|
|
383
|
-
from,
|
|
384
|
-
to,
|
|
385
|
-
txHash,
|
|
386
|
-
blockNumber: receipt.blockNumber
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
return {
|
|
390
|
-
verified: true,
|
|
391
|
-
amount,
|
|
392
|
-
from,
|
|
393
|
-
to,
|
|
394
|
-
txHash,
|
|
395
|
-
blockNumber: receipt.blockNumber
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
return { verified: false, error: "No USDC transfer found" };
|
|
399
|
-
} catch (e) {
|
|
400
|
-
return { verified: false, error: e.message || String(e) };
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// src/server/index.ts
|
|
405
|
-
function generateChargeId() {
|
|
406
|
-
return "ch_" + Math.random().toString(36).substring(2, 15);
|
|
407
|
-
}
|
|
370
|
+
var X402_VERSION2 = 2;
|
|
371
|
+
var PAYMENT_REQUIRED_HEADER2 = "x-payment-required";
|
|
372
|
+
var PAYMENT_HEADER2 = "x-payment";
|
|
373
|
+
var PAYMENT_RESPONSE_HEADER = "x-payment-response";
|
|
374
|
+
var DEFAULT_FACILITATOR_URL = "https://x402.org/facilitator";
|
|
408
375
|
var MoltsPayServer = class {
|
|
409
376
|
manifest;
|
|
410
377
|
skills = /* @__PURE__ */ new Map();
|
|
411
|
-
charges = /* @__PURE__ */ new Map();
|
|
412
378
|
options;
|
|
379
|
+
facilitatorUrl;
|
|
413
380
|
constructor(servicesPath, options = {}) {
|
|
414
381
|
const content = (0, import_fs2.readFileSync)(servicesPath, "utf-8");
|
|
415
382
|
this.manifest = JSON.parse(content);
|
|
416
383
|
this.options = {
|
|
417
384
|
port: options.port || 3e3,
|
|
418
|
-
host: options.host || "0.0.0.0"
|
|
419
|
-
chargeExpirySecs: options.chargeExpirySecs || 300
|
|
420
|
-
// 5 minutes
|
|
385
|
+
host: options.host || "0.0.0.0"
|
|
421
386
|
};
|
|
387
|
+
this.facilitatorUrl = options.facilitatorUrl || DEFAULT_FACILITATOR_URL;
|
|
422
388
|
console.log(`[MoltsPay] Loaded ${this.manifest.services.length} services from ${servicesPath}`);
|
|
423
389
|
console.log(`[MoltsPay] Provider: ${this.manifest.provider.name}`);
|
|
424
|
-
console.log(`[MoltsPay]
|
|
390
|
+
console.log(`[MoltsPay] Receive wallet: ${this.manifest.provider.wallet}`);
|
|
391
|
+
console.log(`[MoltsPay] Facilitator: ${this.facilitatorUrl}`);
|
|
392
|
+
console.log(`[MoltsPay] Protocol: x402 (gasless for both client AND server)`);
|
|
425
393
|
}
|
|
426
394
|
/**
|
|
427
395
|
* Register a skill handler for a service
|
|
@@ -431,56 +399,45 @@ var MoltsPayServer = class {
|
|
|
431
399
|
if (!config) {
|
|
432
400
|
throw new Error(`Service '${serviceId}' not found in manifest`);
|
|
433
401
|
}
|
|
434
|
-
this.skills.set(serviceId, {
|
|
435
|
-
id: serviceId,
|
|
436
|
-
config,
|
|
437
|
-
handler
|
|
438
|
-
});
|
|
439
|
-
console.log(`[MoltsPay] Registered skill: ${serviceId} ($${config.price} ${config.currency})`);
|
|
402
|
+
this.skills.set(serviceId, { id: serviceId, config, handler });
|
|
440
403
|
return this;
|
|
441
404
|
}
|
|
442
405
|
/**
|
|
443
|
-
* Start
|
|
406
|
+
* Start HTTP server
|
|
444
407
|
*/
|
|
445
408
|
listen(port) {
|
|
446
|
-
const p = port || this.options.port;
|
|
409
|
+
const p = port || this.options.port || 3e3;
|
|
410
|
+
const host = this.options.host || "0.0.0.0";
|
|
447
411
|
const server = (0, import_http.createServer)((req, res) => this.handleRequest(req, res));
|
|
448
|
-
server.listen(p,
|
|
449
|
-
console.log(`[MoltsPay] Server listening on http://${
|
|
412
|
+
server.listen(p, host, () => {
|
|
413
|
+
console.log(`[MoltsPay] Server listening on http://${host}:${p}`);
|
|
450
414
|
console.log(`[MoltsPay] Endpoints:`);
|
|
451
|
-
console.log(` GET /services
|
|
452
|
-
console.log(` POST /
|
|
453
|
-
console.log(` POST /verify - Verify payment & get result`);
|
|
454
|
-
console.log(` GET /status/:id - Check charge status`);
|
|
415
|
+
console.log(` GET /services - List available services`);
|
|
416
|
+
console.log(` POST /execute - Execute service (x402 payment)`);
|
|
455
417
|
});
|
|
456
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* Handle incoming request
|
|
421
|
+
*/
|
|
457
422
|
async handleRequest(req, res) {
|
|
458
|
-
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
459
|
-
const path = url.pathname;
|
|
460
|
-
const method = req.method || "GET";
|
|
461
423
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
462
424
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
463
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
464
|
-
|
|
425
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment");
|
|
426
|
+
res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response");
|
|
427
|
+
if (req.method === "OPTIONS") {
|
|
465
428
|
res.writeHead(204);
|
|
466
429
|
res.end();
|
|
467
430
|
return;
|
|
468
431
|
}
|
|
469
432
|
try {
|
|
470
|
-
|
|
433
|
+
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
434
|
+
if (url.pathname === "/services" && req.method === "GET") {
|
|
471
435
|
return this.handleGetServices(res);
|
|
472
436
|
}
|
|
473
|
-
if (
|
|
474
|
-
const body = await this.readBody(req);
|
|
475
|
-
return this.handlePay(body, res);
|
|
476
|
-
}
|
|
477
|
-
if (method === "POST" && path === "/verify") {
|
|
437
|
+
if (url.pathname === "/execute" && req.method === "POST") {
|
|
478
438
|
const body = await this.readBody(req);
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if (method === "GET" && path.startsWith("/status/")) {
|
|
482
|
-
const chargeId = path.replace("/status/", "");
|
|
483
|
-
return this.handleStatus(chargeId, res);
|
|
439
|
+
const paymentHeader = req.headers[PAYMENT_HEADER2];
|
|
440
|
+
return await this.handleExecute(body, paymentHeader, res);
|
|
484
441
|
}
|
|
485
442
|
this.sendJson(res, 404, { error: "Not found" });
|
|
486
443
|
} catch (err) {
|
|
@@ -492,6 +449,7 @@ var MoltsPayServer = class {
|
|
|
492
449
|
* GET /services - List available services
|
|
493
450
|
*/
|
|
494
451
|
handleGetServices(res) {
|
|
452
|
+
const chain = getChain(this.manifest.provider.chain);
|
|
495
453
|
const services = this.manifest.services.map((s) => ({
|
|
496
454
|
id: s.id,
|
|
497
455
|
name: s.name,
|
|
@@ -504,14 +462,21 @@ var MoltsPayServer = class {
|
|
|
504
462
|
}));
|
|
505
463
|
this.sendJson(res, 200, {
|
|
506
464
|
provider: this.manifest.provider,
|
|
507
|
-
services
|
|
465
|
+
services,
|
|
466
|
+
x402: {
|
|
467
|
+
version: X402_VERSION2,
|
|
468
|
+
network: `eip155:${chain.chainId}`,
|
|
469
|
+
schemes: ["exact"],
|
|
470
|
+
facilitator: this.facilitatorUrl
|
|
471
|
+
}
|
|
508
472
|
});
|
|
509
473
|
}
|
|
510
474
|
/**
|
|
511
|
-
* POST /
|
|
475
|
+
* POST /execute - Execute service with x402 payment
|
|
512
476
|
* Body: { service: string, params: object }
|
|
477
|
+
* Header: X-Payment (optional - if missing, returns 402)
|
|
513
478
|
*/
|
|
514
|
-
|
|
479
|
+
async handleExecute(body, paymentHeader, res) {
|
|
515
480
|
const { service, params } = body;
|
|
516
481
|
if (!service) {
|
|
517
482
|
return this.sendJson(res, 400, { error: "Missing service" });
|
|
@@ -525,113 +490,162 @@ var MoltsPayServer = class {
|
|
|
525
490
|
return this.sendJson(res, 400, { error: `Missing required param: ${key}` });
|
|
526
491
|
}
|
|
527
492
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
493
|
+
if (!paymentHeader) {
|
|
494
|
+
return this.sendPaymentRequired(skill.config, res);
|
|
495
|
+
}
|
|
496
|
+
let payment;
|
|
497
|
+
try {
|
|
498
|
+
const decoded = Buffer.from(paymentHeader, "base64").toString("utf-8");
|
|
499
|
+
payment = JSON.parse(decoded);
|
|
500
|
+
} catch {
|
|
501
|
+
return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
|
|
502
|
+
}
|
|
503
|
+
const validation = this.validatePayment(payment, skill.config);
|
|
504
|
+
if (!validation.valid) {
|
|
505
|
+
return this.sendJson(res, 402, { error: validation.error });
|
|
506
|
+
}
|
|
507
|
+
console.log(`[MoltsPay] Verifying payment with facilitator...`);
|
|
508
|
+
const verifyResult = await this.verifyWithFacilitator(payment, skill.config);
|
|
509
|
+
if (!verifyResult.valid) {
|
|
510
|
+
return this.sendJson(res, 402, { error: `Payment verification failed: ${verifyResult.error}` });
|
|
511
|
+
}
|
|
512
|
+
console.log(`[MoltsPay] Executing skill: ${service}`);
|
|
513
|
+
let result;
|
|
514
|
+
try {
|
|
515
|
+
result = await skill.handler(params || {});
|
|
516
|
+
} catch (err) {
|
|
517
|
+
console.error("[MoltsPay] Skill execution failed:", err.message);
|
|
518
|
+
return this.sendJson(res, 500, {
|
|
519
|
+
error: "Service execution failed",
|
|
520
|
+
message: err.message
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
console.log(`[MoltsPay] Skill succeeded, settling payment...`);
|
|
524
|
+
let settlement = null;
|
|
525
|
+
try {
|
|
526
|
+
settlement = await this.settleWithFacilitator(payment, skill.config);
|
|
527
|
+
console.log(`[MoltsPay] Payment settled: ${settlement.transaction || "pending"}`);
|
|
528
|
+
} catch (err) {
|
|
529
|
+
console.error("[MoltsPay] Settlement failed:", err.message);
|
|
530
|
+
}
|
|
531
|
+
const responseHeaders = {};
|
|
532
|
+
if (settlement) {
|
|
533
|
+
const responsePayload = {
|
|
534
|
+
success: true,
|
|
535
|
+
transaction: settlement.transaction,
|
|
536
|
+
network: payment.network
|
|
537
|
+
};
|
|
538
|
+
responseHeaders[PAYMENT_RESPONSE_HEADER] = Buffer.from(
|
|
539
|
+
JSON.stringify(responsePayload)
|
|
540
|
+
).toString("base64");
|
|
541
|
+
}
|
|
542
|
+
this.sendJson(res, 200, {
|
|
543
|
+
success: true,
|
|
544
|
+
result,
|
|
545
|
+
payment: settlement ? { transaction: settlement.transaction, status: "settled" } : { status: "pending" }
|
|
546
|
+
}, responseHeaders);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Return 402 with x402 payment requirements
|
|
550
|
+
*/
|
|
551
|
+
sendPaymentRequired(config, res) {
|
|
552
|
+
const chain = getChain(this.manifest.provider.chain);
|
|
553
|
+
const amountInUnits = Math.floor(config.price * 1e6).toString();
|
|
554
|
+
const requirements = [{
|
|
555
|
+
scheme: "exact",
|
|
556
|
+
network: `eip155:${chain.chainId}`,
|
|
557
|
+
maxAmountRequired: amountInUnits,
|
|
558
|
+
resource: this.manifest.provider.wallet,
|
|
559
|
+
description: `${config.name} - $${config.price} ${config.currency}`,
|
|
560
|
+
// Include facilitator info for client
|
|
561
|
+
extra: JSON.stringify({ facilitator: this.facilitatorUrl })
|
|
562
|
+
}];
|
|
563
|
+
const encoded = Buffer.from(JSON.stringify(requirements)).toString("base64");
|
|
564
|
+
res.writeHead(402, {
|
|
565
|
+
"Content-Type": "application/json",
|
|
566
|
+
[PAYMENT_REQUIRED_HEADER2]: encoded
|
|
553
567
|
});
|
|
568
|
+
res.end(JSON.stringify({
|
|
569
|
+
error: "Payment required",
|
|
570
|
+
message: `Service requires $${config.price} ${config.currency}`,
|
|
571
|
+
x402: requirements[0]
|
|
572
|
+
}, null, 2));
|
|
554
573
|
}
|
|
555
574
|
/**
|
|
556
|
-
*
|
|
557
|
-
* Body: { chargeId: string, txHash: string }
|
|
575
|
+
* Basic payment validation (before calling facilitator)
|
|
558
576
|
*/
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
return this.sendJson(res, 400, { error: "Missing chargeId or txHash" });
|
|
563
|
-
}
|
|
564
|
-
const charge = this.charges.get(chargeId);
|
|
565
|
-
if (!charge) {
|
|
566
|
-
return this.sendJson(res, 404, { error: "Charge not found" });
|
|
567
|
-
}
|
|
568
|
-
if (Date.now() > charge.expiresAt) {
|
|
569
|
-
charge.status = "expired";
|
|
570
|
-
return this.sendJson(res, 400, { error: "Charge expired" });
|
|
571
|
-
}
|
|
572
|
-
if (charge.status === "completed") {
|
|
573
|
-
return this.sendJson(res, 200, {
|
|
574
|
-
status: "completed",
|
|
575
|
-
result: charge.result
|
|
576
|
-
});
|
|
577
|
+
validatePayment(payment, config) {
|
|
578
|
+
if (payment.x402Version !== X402_VERSION2) {
|
|
579
|
+
return { valid: false, error: `Unsupported x402 version: ${payment.x402Version}` };
|
|
577
580
|
}
|
|
581
|
+
if (payment.scheme !== "exact") {
|
|
582
|
+
return { valid: false, error: `Unsupported scheme: ${payment.scheme}` };
|
|
583
|
+
}
|
|
584
|
+
const chain = getChain(this.manifest.provider.chain);
|
|
585
|
+
const expectedNetwork = `eip155:${chain.chainId}`;
|
|
586
|
+
if (payment.network !== expectedNetwork) {
|
|
587
|
+
return { valid: false, error: `Network mismatch: expected ${expectedNetwork}` };
|
|
588
|
+
}
|
|
589
|
+
return { valid: true };
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Verify payment with facilitator
|
|
593
|
+
*/
|
|
594
|
+
async verifyWithFacilitator(payment, config) {
|
|
578
595
|
try {
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
596
|
+
const chain = getChain(this.manifest.provider.chain);
|
|
597
|
+
const amountInUnits = Math.floor(config.price * 1e6).toString();
|
|
598
|
+
const requirements = {
|
|
599
|
+
scheme: "exact",
|
|
600
|
+
network: `eip155:${chain.chainId}`,
|
|
601
|
+
maxAmountRequired: amountInUnits,
|
|
602
|
+
resource: this.manifest.provider.wallet
|
|
603
|
+
};
|
|
604
|
+
const response = await fetch(`${this.facilitatorUrl}/verify`, {
|
|
605
|
+
method: "POST",
|
|
606
|
+
headers: { "Content-Type": "application/json" },
|
|
607
|
+
body: JSON.stringify({
|
|
608
|
+
paymentPayload: payment,
|
|
609
|
+
paymentRequirements: requirements
|
|
610
|
+
})
|
|
584
611
|
});
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
return
|
|
588
|
-
error: "Payment verification failed",
|
|
589
|
-
reason: verification.error
|
|
590
|
-
});
|
|
612
|
+
const result = await response.json();
|
|
613
|
+
if (!response.ok || !result.isValid) {
|
|
614
|
+
return { valid: false, error: result.invalidReason || "Verification failed" };
|
|
591
615
|
}
|
|
592
|
-
|
|
593
|
-
charge.txHash = txHash;
|
|
594
|
-
charge.paidAt = Date.now();
|
|
595
|
-
const skill = this.skills.get(charge.service);
|
|
596
|
-
console.log(`[MoltsPay] Executing skill: ${charge.service}`);
|
|
597
|
-
const result = await skill.handler(charge.params);
|
|
598
|
-
charge.status = "completed";
|
|
599
|
-
charge.result = result;
|
|
600
|
-
charge.completedAt = Date.now();
|
|
601
|
-
this.sendJson(res, 200, {
|
|
602
|
-
status: "completed",
|
|
603
|
-
chargeId,
|
|
604
|
-
txHash,
|
|
605
|
-
result
|
|
606
|
-
});
|
|
616
|
+
return { valid: true };
|
|
607
617
|
} catch (err) {
|
|
608
|
-
|
|
609
|
-
charge.status = "failed";
|
|
610
|
-
this.sendJson(res, 500, {
|
|
611
|
-
error: "Skill execution failed",
|
|
612
|
-
message: err.message
|
|
613
|
-
});
|
|
618
|
+
return { valid: false, error: `Facilitator error: ${err.message}` };
|
|
614
619
|
}
|
|
615
620
|
}
|
|
616
621
|
/**
|
|
617
|
-
*
|
|
622
|
+
* Settle payment with facilitator (execute on-chain transfer)
|
|
618
623
|
*/
|
|
619
|
-
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
624
|
+
async settleWithFacilitator(payment, config) {
|
|
625
|
+
const chain = getChain(this.manifest.provider.chain);
|
|
626
|
+
const amountInUnits = Math.floor(config.price * 1e6).toString();
|
|
627
|
+
const requirements = {
|
|
628
|
+
scheme: "exact",
|
|
629
|
+
network: `eip155:${chain.chainId}`,
|
|
630
|
+
maxAmountRequired: amountInUnits,
|
|
631
|
+
resource: this.manifest.provider.wallet
|
|
632
|
+
};
|
|
633
|
+
const response = await fetch(`${this.facilitatorUrl}/settle`, {
|
|
634
|
+
method: "POST",
|
|
635
|
+
headers: { "Content-Type": "application/json" },
|
|
636
|
+
body: JSON.stringify({
|
|
637
|
+
paymentPayload: payment,
|
|
638
|
+
paymentRequirements: requirements
|
|
639
|
+
})
|
|
634
640
|
});
|
|
641
|
+
const result = await response.json();
|
|
642
|
+
if (!response.ok) {
|
|
643
|
+
throw new Error(result.error || "Settlement failed");
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
transaction: result.transaction,
|
|
647
|
+
status: result.status || "settled"
|
|
648
|
+
};
|
|
635
649
|
}
|
|
636
650
|
async readBody(req) {
|
|
637
651
|
return new Promise((resolve2, reject) => {
|
|
@@ -647,8 +661,12 @@ var MoltsPayServer = class {
|
|
|
647
661
|
req.on("error", reject);
|
|
648
662
|
});
|
|
649
663
|
}
|
|
650
|
-
sendJson(res, status, data) {
|
|
651
|
-
|
|
664
|
+
sendJson(res, status, data, extraHeaders) {
|
|
665
|
+
const headers = { "Content-Type": "application/json" };
|
|
666
|
+
if (extraHeaders) {
|
|
667
|
+
Object.assign(headers, extraHeaders);
|
|
668
|
+
}
|
|
669
|
+
res.writeHead(status, headers);
|
|
652
670
|
res.end(JSON.stringify(data, null, 2));
|
|
653
671
|
}
|
|
654
672
|
};
|
|
@@ -809,7 +827,7 @@ program.command("services <url>").description("List services from a provider").o
|
|
|
809
827
|
console.error("\u274C Error:", err.message);
|
|
810
828
|
}
|
|
811
829
|
});
|
|
812
|
-
program.command("start <manifest>").description("Start MoltsPay server from services manifest").option("-p, --port <port>", "Port to listen on", "3000").option("--host <host>", "Host to bind", "0.0.0.0").action(async (manifest, options) => {
|
|
830
|
+
program.command("start <manifest>").description("Start MoltsPay server from services manifest").option("-p, --port <port>", "Port to listen on", "3000").option("--host <host>", "Host to bind", "0.0.0.0").option("--facilitator <url>", "x402 facilitator URL (default: https://x402.org/facilitator)").action(async (manifest, options) => {
|
|
813
831
|
const manifestPath = (0, import_path2.resolve)(manifest);
|
|
814
832
|
if (!(0, import_fs3.existsSync)(manifestPath)) {
|
|
815
833
|
console.error(`\u274C Manifest not found: ${manifestPath}`);
|
|
@@ -817,14 +835,15 @@ program.command("start <manifest>").description("Start MoltsPay server from serv
|
|
|
817
835
|
}
|
|
818
836
|
const port = parseInt(options.port, 10);
|
|
819
837
|
const host = options.host;
|
|
838
|
+
const facilitatorUrl = options.facilitator;
|
|
820
839
|
console.log(`
|
|
821
|
-
\u{1F680} Starting MoltsPay Server
|
|
840
|
+
\u{1F680} Starting MoltsPay Server (x402 protocol)
|
|
822
841
|
`);
|
|
823
842
|
console.log(` Manifest: ${manifestPath}`);
|
|
824
843
|
console.log(` Port: ${port}`);
|
|
825
844
|
console.log("");
|
|
826
845
|
try {
|
|
827
|
-
const server = new MoltsPayServer(manifestPath, { port, host });
|
|
846
|
+
const server = new MoltsPayServer(manifestPath, { port, host, facilitatorUrl });
|
|
828
847
|
const manifestContent = await import("fs").then(
|
|
829
848
|
(fs) => JSON.parse(fs.readFileSync(manifestPath, "utf-8"))
|
|
830
849
|
);
|