moltspay 0.7.1 → 0.8.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/dist/cli/index.js +270 -262
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +272 -264
- 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 +387 -321
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +391 -325
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.d.mts +17 -8
- package/dist/server/index.d.ts +17 -8
- package/dist/server/index.js +145 -194
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +145 -194
- 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,38 @@ 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
|
-
// src/verify/index.ts
|
|
334
370
|
var import_ethers2 = require("ethers");
|
|
335
|
-
var
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
}
|
|
371
|
+
var X402_VERSION2 = 2;
|
|
372
|
+
var PAYMENT_REQUIRED_HEADER2 = "x-payment-required";
|
|
373
|
+
var PAYMENT_HEADER2 = "x-payment";
|
|
408
374
|
var MoltsPayServer = class {
|
|
409
375
|
manifest;
|
|
410
376
|
skills = /* @__PURE__ */ new Map();
|
|
411
|
-
charges = /* @__PURE__ */ new Map();
|
|
412
377
|
options;
|
|
378
|
+
provider = null;
|
|
379
|
+
wallet = null;
|
|
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
385
|
host: options.host || "0.0.0.0",
|
|
419
|
-
|
|
420
|
-
// 5 minutes
|
|
386
|
+
privateKey: options.privateKey || process.env.MOLTSPAY_PRIVATE_KEY
|
|
421
387
|
};
|
|
388
|
+
if (this.options.privateKey) {
|
|
389
|
+
try {
|
|
390
|
+
const chain = getChain(this.manifest.provider.chain);
|
|
391
|
+
this.provider = new import_ethers2.ethers.JsonRpcProvider(chain.rpc);
|
|
392
|
+
this.wallet = new import_ethers2.ethers.Wallet(this.options.privateKey, this.provider);
|
|
393
|
+
console.log(`[MoltsPay] Payment wallet: ${this.wallet.address}`);
|
|
394
|
+
} catch (err) {
|
|
395
|
+
console.warn("[MoltsPay] Warning: Could not initialize wallet for payment claims");
|
|
396
|
+
}
|
|
397
|
+
}
|
|
422
398
|
console.log(`[MoltsPay] Loaded ${this.manifest.services.length} services from ${servicesPath}`);
|
|
423
399
|
console.log(`[MoltsPay] Provider: ${this.manifest.provider.name}`);
|
|
424
|
-
console.log(`[MoltsPay]
|
|
400
|
+
console.log(`[MoltsPay] Receive wallet: ${this.manifest.provider.wallet}`);
|
|
401
|
+
console.log(`[MoltsPay] Protocol: x402 (gasless, pay-for-success)`);
|
|
425
402
|
}
|
|
426
403
|
/**
|
|
427
404
|
* Register a skill handler for a service
|
|
@@ -448,10 +425,8 @@ var MoltsPayServer = class {
|
|
|
448
425
|
server.listen(p, this.options.host, () => {
|
|
449
426
|
console.log(`[MoltsPay] Server listening on http://${this.options.host}:${p}`);
|
|
450
427
|
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`);
|
|
428
|
+
console.log(` GET /services - List available services`);
|
|
429
|
+
console.log(` POST /execute - Execute service (x402 payment)`);
|
|
455
430
|
});
|
|
456
431
|
}
|
|
457
432
|
async handleRequest(req, res) {
|
|
@@ -460,7 +435,8 @@ var MoltsPayServer = class {
|
|
|
460
435
|
const method = req.method || "GET";
|
|
461
436
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
462
437
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
463
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
438
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment");
|
|
439
|
+
res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response");
|
|
464
440
|
if (method === "OPTIONS") {
|
|
465
441
|
res.writeHead(204);
|
|
466
442
|
res.end();
|
|
@@ -470,17 +446,10 @@ var MoltsPayServer = class {
|
|
|
470
446
|
if (method === "GET" && path === "/services") {
|
|
471
447
|
return this.handleGetServices(res);
|
|
472
448
|
}
|
|
473
|
-
if (method === "POST" && path === "/
|
|
474
|
-
const body = await this.readBody(req);
|
|
475
|
-
return this.handlePay(body, res);
|
|
476
|
-
}
|
|
477
|
-
if (method === "POST" && path === "/verify") {
|
|
449
|
+
if (method === "POST" && path === "/execute") {
|
|
478
450
|
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);
|
|
451
|
+
const paymentHeader = req.headers[PAYMENT_HEADER2];
|
|
452
|
+
return this.handleExecute(body, paymentHeader, res);
|
|
484
453
|
}
|
|
485
454
|
this.sendJson(res, 404, { error: "Not found" });
|
|
486
455
|
} catch (err) {
|
|
@@ -492,6 +461,7 @@ var MoltsPayServer = class {
|
|
|
492
461
|
* GET /services - List available services
|
|
493
462
|
*/
|
|
494
463
|
handleGetServices(res) {
|
|
464
|
+
const chain = getChain(this.manifest.provider.chain);
|
|
495
465
|
const services = this.manifest.services.map((s) => ({
|
|
496
466
|
id: s.id,
|
|
497
467
|
name: s.name,
|
|
@@ -504,14 +474,20 @@ var MoltsPayServer = class {
|
|
|
504
474
|
}));
|
|
505
475
|
this.sendJson(res, 200, {
|
|
506
476
|
provider: this.manifest.provider,
|
|
507
|
-
services
|
|
477
|
+
services,
|
|
478
|
+
x402: {
|
|
479
|
+
version: X402_VERSION2,
|
|
480
|
+
network: `eip155:${chain.chainId}`,
|
|
481
|
+
schemes: ["exact"]
|
|
482
|
+
}
|
|
508
483
|
});
|
|
509
484
|
}
|
|
510
485
|
/**
|
|
511
|
-
* POST /
|
|
486
|
+
* POST /execute - Execute service with x402 payment
|
|
512
487
|
* Body: { service: string, params: object }
|
|
488
|
+
* Header: X-Payment (optional - if missing, returns 402)
|
|
513
489
|
*/
|
|
514
|
-
|
|
490
|
+
async handleExecute(body, paymentHeader, res) {
|
|
515
491
|
const { service, params } = body;
|
|
516
492
|
if (!service) {
|
|
517
493
|
return this.sendJson(res, 400, { error: "Missing service" });
|
|
@@ -525,113 +501,129 @@ var MoltsPayServer = class {
|
|
|
525
501
|
return this.sendJson(res, 400, { error: `Missing required param: ${key}` });
|
|
526
502
|
}
|
|
527
503
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const charge = {
|
|
531
|
-
id: chargeId,
|
|
532
|
-
service,
|
|
533
|
-
params: params || {},
|
|
534
|
-
amount: skill.config.price,
|
|
535
|
-
currency: skill.config.currency,
|
|
536
|
-
status: "pending",
|
|
537
|
-
createdAt: now,
|
|
538
|
-
expiresAt: now + this.options.chargeExpirySecs * 1e3
|
|
539
|
-
};
|
|
540
|
-
this.charges.set(chargeId, charge);
|
|
541
|
-
const paymentRequest = {
|
|
542
|
-
chargeId,
|
|
543
|
-
service,
|
|
544
|
-
amount: charge.amount,
|
|
545
|
-
currency: charge.currency,
|
|
546
|
-
wallet: this.manifest.provider.wallet,
|
|
547
|
-
chain: this.manifest.provider.chain,
|
|
548
|
-
expiresAt: charge.expiresAt
|
|
549
|
-
};
|
|
550
|
-
this.sendJson(res, 402, {
|
|
551
|
-
message: "Payment required",
|
|
552
|
-
payment: paymentRequest
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
/**
|
|
556
|
-
* POST /verify - Verify payment and execute skill
|
|
557
|
-
* Body: { chargeId: string, txHash: string }
|
|
558
|
-
*/
|
|
559
|
-
async handleVerify(body, res) {
|
|
560
|
-
const { chargeId, txHash } = body;
|
|
561
|
-
if (!chargeId || !txHash) {
|
|
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
|
-
});
|
|
504
|
+
if (!paymentHeader) {
|
|
505
|
+
return this.sendPaymentRequired(skill.config, res);
|
|
577
506
|
}
|
|
507
|
+
let payment;
|
|
578
508
|
try {
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
592
|
-
charge.status = "paid";
|
|
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
|
-
});
|
|
509
|
+
const decoded = Buffer.from(paymentHeader, "base64").toString("utf-8");
|
|
510
|
+
payment = JSON.parse(decoded);
|
|
511
|
+
} catch {
|
|
512
|
+
return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
|
|
513
|
+
}
|
|
514
|
+
const validation = this.validatePayment(payment, skill.config);
|
|
515
|
+
if (!validation.valid) {
|
|
516
|
+
return this.sendJson(res, 402, { error: validation.error });
|
|
517
|
+
}
|
|
518
|
+
console.log(`[MoltsPay] Executing skill: ${service}`);
|
|
519
|
+
let result;
|
|
520
|
+
try {
|
|
521
|
+
result = await skill.handler(params || {});
|
|
607
522
|
} catch (err) {
|
|
608
|
-
console.error("[MoltsPay] Skill execution
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
error: "Skill execution failed",
|
|
523
|
+
console.error("[MoltsPay] Skill execution failed:", err.message);
|
|
524
|
+
return this.sendJson(res, 500, {
|
|
525
|
+
error: "Service execution failed",
|
|
612
526
|
message: err.message
|
|
613
527
|
});
|
|
614
528
|
}
|
|
529
|
+
console.log(`[MoltsPay] Skill succeeded, claiming payment...`);
|
|
530
|
+
let txHash = null;
|
|
531
|
+
try {
|
|
532
|
+
txHash = await this.claimPayment(payment);
|
|
533
|
+
console.log(`[MoltsPay] Payment claimed: ${txHash}`);
|
|
534
|
+
} catch (err) {
|
|
535
|
+
console.error("[MoltsPay] Payment claim failed:", err.message);
|
|
536
|
+
}
|
|
537
|
+
this.sendJson(res, 200, {
|
|
538
|
+
success: true,
|
|
539
|
+
result,
|
|
540
|
+
payment: txHash ? { txHash, status: "claimed" } : { status: "pending" }
|
|
541
|
+
});
|
|
615
542
|
}
|
|
616
543
|
/**
|
|
617
|
-
*
|
|
544
|
+
* Return 402 with x402 payment requirements
|
|
618
545
|
*/
|
|
619
|
-
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
expiresAt: charge.expiresAt
|
|
546
|
+
sendPaymentRequired(config, res) {
|
|
547
|
+
const chain = getChain(this.manifest.provider.chain);
|
|
548
|
+
const amountInUnits = Math.floor(config.price * 1e6).toString();
|
|
549
|
+
const requirements = [{
|
|
550
|
+
scheme: "exact",
|
|
551
|
+
network: `eip155:${chain.chainId}`,
|
|
552
|
+
maxAmountRequired: amountInUnits,
|
|
553
|
+
resource: this.manifest.provider.wallet,
|
|
554
|
+
description: `${config.name} - $${config.price} ${config.currency}`
|
|
555
|
+
}];
|
|
556
|
+
const encoded = Buffer.from(JSON.stringify(requirements)).toString("base64");
|
|
557
|
+
res.writeHead(402, {
|
|
558
|
+
"Content-Type": "application/json",
|
|
559
|
+
[PAYMENT_REQUIRED_HEADER2]: encoded
|
|
634
560
|
});
|
|
561
|
+
res.end(JSON.stringify({
|
|
562
|
+
error: "Payment required",
|
|
563
|
+
message: `Service requires $${config.price} ${config.currency}`,
|
|
564
|
+
x402: requirements[0]
|
|
565
|
+
}, null, 2));
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Validate x402 payment payload
|
|
569
|
+
*/
|
|
570
|
+
validatePayment(payment, config) {
|
|
571
|
+
if (payment.x402Version !== X402_VERSION2) {
|
|
572
|
+
return { valid: false, error: `Unsupported x402 version: ${payment.x402Version}` };
|
|
573
|
+
}
|
|
574
|
+
if (payment.scheme !== "exact") {
|
|
575
|
+
return { valid: false, error: `Unsupported scheme: ${payment.scheme}` };
|
|
576
|
+
}
|
|
577
|
+
const chain = getChain(this.manifest.provider.chain);
|
|
578
|
+
const expectedNetwork = `eip155:${chain.chainId}`;
|
|
579
|
+
if (payment.network !== expectedNetwork) {
|
|
580
|
+
return { valid: false, error: `Network mismatch: expected ${expectedNetwork}` };
|
|
581
|
+
}
|
|
582
|
+
const auth = payment.payload.authorization;
|
|
583
|
+
if (auth.to.toLowerCase() !== this.manifest.provider.wallet.toLowerCase()) {
|
|
584
|
+
return { valid: false, error: "Payment recipient mismatch" };
|
|
585
|
+
}
|
|
586
|
+
const amount = Number(auth.value) / 1e6;
|
|
587
|
+
if (amount < config.price) {
|
|
588
|
+
return { valid: false, error: `Insufficient amount: $${amount} < $${config.price}` };
|
|
589
|
+
}
|
|
590
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
591
|
+
if (Number(auth.validBefore) < now) {
|
|
592
|
+
return { valid: false, error: "Payment authorization expired" };
|
|
593
|
+
}
|
|
594
|
+
if (Number(auth.validAfter) > now) {
|
|
595
|
+
return { valid: false, error: "Payment authorization not yet valid" };
|
|
596
|
+
}
|
|
597
|
+
return { valid: true };
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Claim payment using transferWithAuthorization
|
|
601
|
+
*/
|
|
602
|
+
async claimPayment(payment) {
|
|
603
|
+
if (!this.wallet || !this.provider) {
|
|
604
|
+
throw new Error("Wallet not configured for payment claims");
|
|
605
|
+
}
|
|
606
|
+
const chain = getChain(this.manifest.provider.chain);
|
|
607
|
+
const auth = payment.payload.authorization;
|
|
608
|
+
const sig = payment.payload.signature;
|
|
609
|
+
const { r, s, v } = import_ethers2.ethers.Signature.from(sig);
|
|
610
|
+
const usdcAbi = [
|
|
611
|
+
"function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)"
|
|
612
|
+
];
|
|
613
|
+
const usdc = new import_ethers2.ethers.Contract(chain.usdc, usdcAbi, this.wallet);
|
|
614
|
+
const tx = await usdc.transferWithAuthorization(
|
|
615
|
+
auth.from,
|
|
616
|
+
auth.to,
|
|
617
|
+
auth.value,
|
|
618
|
+
auth.validAfter,
|
|
619
|
+
auth.validBefore,
|
|
620
|
+
auth.nonce,
|
|
621
|
+
v,
|
|
622
|
+
r,
|
|
623
|
+
s
|
|
624
|
+
);
|
|
625
|
+
const receipt = await tx.wait();
|
|
626
|
+
return receipt.hash;
|
|
635
627
|
}
|
|
636
628
|
async readBody(req) {
|
|
637
629
|
return new Promise((resolve2, reject) => {
|
|
@@ -809,7 +801,7 @@ program.command("services <url>").description("List services from a provider").o
|
|
|
809
801
|
console.error("\u274C Error:", err.message);
|
|
810
802
|
}
|
|
811
803
|
});
|
|
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) => {
|
|
804
|
+
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("--private-key <key>", "Private key for claiming payments (or use MOLTSPAY_PRIVATE_KEY env)").action(async (manifest, options) => {
|
|
813
805
|
const manifestPath = (0, import_path2.resolve)(manifest);
|
|
814
806
|
if (!(0, import_fs3.existsSync)(manifestPath)) {
|
|
815
807
|
console.error(`\u274C Manifest not found: ${manifestPath}`);
|
|
@@ -817,14 +809,16 @@ program.command("start <manifest>").description("Start MoltsPay server from serv
|
|
|
817
809
|
}
|
|
818
810
|
const port = parseInt(options.port, 10);
|
|
819
811
|
const host = options.host;
|
|
812
|
+
const privateKey = options.privateKey || process.env.MOLTSPAY_PRIVATE_KEY;
|
|
820
813
|
console.log(`
|
|
821
|
-
\u{1F680} Starting MoltsPay Server
|
|
814
|
+
\u{1F680} Starting MoltsPay Server (x402 protocol)
|
|
822
815
|
`);
|
|
823
816
|
console.log(` Manifest: ${manifestPath}`);
|
|
824
817
|
console.log(` Port: ${port}`);
|
|
818
|
+
console.log(` Payment claims: ${privateKey ? "enabled" : "disabled (no private key)"}`);
|
|
825
819
|
console.log("");
|
|
826
820
|
try {
|
|
827
|
-
const server = new MoltsPayServer(manifestPath, { port, host });
|
|
821
|
+
const server = new MoltsPayServer(manifestPath, { port, host, privateKey });
|
|
828
822
|
const manifestContent = await import("fs").then(
|
|
829
823
|
(fs) => JSON.parse(fs.readFileSync(manifestPath, "utf-8"))
|
|
830
824
|
);
|
|
@@ -936,7 +930,7 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
|
|
|
936
930
|
process.exit(1);
|
|
937
931
|
}
|
|
938
932
|
});
|
|
939
|
-
program.command("pay <server> <service> [params]").description("Pay for a service and get the result").option("--prompt <text>", "Prompt for the service").option("--image <
|
|
933
|
+
program.command("pay <server> <service> [params]").description("Pay for a service and get the result").option("--prompt <text>", "Prompt for the service").option("--image <path>", "Image URL or local file path").option("--json", "Output raw JSON only").action(async (server, service, paramsJson, options) => {
|
|
940
934
|
const client = new MoltsPayClient();
|
|
941
935
|
if (!client.isInitialized) {
|
|
942
936
|
console.error("\u274C Wallet not initialized. Run: npx moltspay init");
|
|
@@ -952,11 +946,25 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
952
946
|
}
|
|
953
947
|
}
|
|
954
948
|
if (options.prompt) params.prompt = options.prompt;
|
|
955
|
-
if (options.image)
|
|
949
|
+
if (options.image) {
|
|
950
|
+
const imagePath = options.image;
|
|
951
|
+
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
|
952
|
+
params.image_url = imagePath;
|
|
953
|
+
} else {
|
|
954
|
+
const filePath = (0, import_path2.resolve)(imagePath);
|
|
955
|
+
if (!(0, import_fs3.existsSync)(filePath)) {
|
|
956
|
+
console.error(`\u274C Image file not found: ${filePath}`);
|
|
957
|
+
process.exit(1);
|
|
958
|
+
}
|
|
959
|
+
const imageData = (0, import_fs3.readFileSync)(filePath);
|
|
960
|
+
params.image_base64 = imageData.toString("base64");
|
|
961
|
+
}
|
|
962
|
+
}
|
|
956
963
|
if (!params.prompt) {
|
|
957
964
|
console.error("\u274C Missing prompt. Use --prompt or pass JSON params");
|
|
958
965
|
process.exit(1);
|
|
959
966
|
}
|
|
967
|
+
const imageDisplay = params.image_url || (params.image_base64 ? `[local file: ${options.image}]` : null);
|
|
960
968
|
if (!options.json) {
|
|
961
969
|
console.log(`
|
|
962
970
|
\u{1F4B3} MoltsPay - Paying for service
|
|
@@ -964,7 +972,7 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
964
972
|
console.log(` Server: ${server}`);
|
|
965
973
|
console.log(` Service: ${service}`);
|
|
966
974
|
console.log(` Prompt: ${params.prompt}`);
|
|
967
|
-
if (
|
|
975
|
+
if (imageDisplay) console.log(` Image: ${imageDisplay}`);
|
|
968
976
|
console.log(` Wallet: ${client.address}`);
|
|
969
977
|
console.log("");
|
|
970
978
|
}
|