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