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.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,38 @@ 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
|
-
async function verifyPayment(params) {
|
|
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
|
+
import { ethers as ethers2 } from "ethers";
|
|
348
|
+
var X402_VERSION2 = 2;
|
|
349
|
+
var PAYMENT_REQUIRED_HEADER2 = "x-payment-required";
|
|
350
|
+
var PAYMENT_HEADER2 = "x-payment";
|
|
385
351
|
var MoltsPayServer = class {
|
|
386
352
|
manifest;
|
|
387
353
|
skills = /* @__PURE__ */ new Map();
|
|
388
|
-
charges = /* @__PURE__ */ new Map();
|
|
389
354
|
options;
|
|
355
|
+
provider = null;
|
|
356
|
+
wallet = null;
|
|
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
362
|
host: options.host || "0.0.0.0",
|
|
396
|
-
|
|
397
|
-
// 5 minutes
|
|
363
|
+
privateKey: options.privateKey || process.env.MOLTSPAY_PRIVATE_KEY
|
|
398
364
|
};
|
|
365
|
+
if (this.options.privateKey) {
|
|
366
|
+
try {
|
|
367
|
+
const chain = getChain(this.manifest.provider.chain);
|
|
368
|
+
this.provider = new ethers2.JsonRpcProvider(chain.rpc);
|
|
369
|
+
this.wallet = new ethers2.Wallet(this.options.privateKey, this.provider);
|
|
370
|
+
console.log(`[MoltsPay] Payment wallet: ${this.wallet.address}`);
|
|
371
|
+
} catch (err) {
|
|
372
|
+
console.warn("[MoltsPay] Warning: Could not initialize wallet for payment claims");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
399
375
|
console.log(`[MoltsPay] Loaded ${this.manifest.services.length} services from ${servicesPath}`);
|
|
400
376
|
console.log(`[MoltsPay] Provider: ${this.manifest.provider.name}`);
|
|
401
|
-
console.log(`[MoltsPay]
|
|
377
|
+
console.log(`[MoltsPay] Receive wallet: ${this.manifest.provider.wallet}`);
|
|
378
|
+
console.log(`[MoltsPay] Protocol: x402 (gasless, pay-for-success)`);
|
|
402
379
|
}
|
|
403
380
|
/**
|
|
404
381
|
* Register a skill handler for a service
|
|
@@ -425,10 +402,8 @@ var MoltsPayServer = class {
|
|
|
425
402
|
server.listen(p, this.options.host, () => {
|
|
426
403
|
console.log(`[MoltsPay] Server listening on http://${this.options.host}:${p}`);
|
|
427
404
|
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`);
|
|
405
|
+
console.log(` GET /services - List available services`);
|
|
406
|
+
console.log(` POST /execute - Execute service (x402 payment)`);
|
|
432
407
|
});
|
|
433
408
|
}
|
|
434
409
|
async handleRequest(req, res) {
|
|
@@ -437,7 +412,8 @@ var MoltsPayServer = class {
|
|
|
437
412
|
const method = req.method || "GET";
|
|
438
413
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
439
414
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
440
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
415
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment");
|
|
416
|
+
res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response");
|
|
441
417
|
if (method === "OPTIONS") {
|
|
442
418
|
res.writeHead(204);
|
|
443
419
|
res.end();
|
|
@@ -447,17 +423,10 @@ var MoltsPayServer = class {
|
|
|
447
423
|
if (method === "GET" && path === "/services") {
|
|
448
424
|
return this.handleGetServices(res);
|
|
449
425
|
}
|
|
450
|
-
if (method === "POST" && path === "/
|
|
426
|
+
if (method === "POST" && path === "/execute") {
|
|
451
427
|
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);
|
|
428
|
+
const paymentHeader = req.headers[PAYMENT_HEADER2];
|
|
429
|
+
return this.handleExecute(body, paymentHeader, res);
|
|
461
430
|
}
|
|
462
431
|
this.sendJson(res, 404, { error: "Not found" });
|
|
463
432
|
} catch (err) {
|
|
@@ -469,6 +438,7 @@ var MoltsPayServer = class {
|
|
|
469
438
|
* GET /services - List available services
|
|
470
439
|
*/
|
|
471
440
|
handleGetServices(res) {
|
|
441
|
+
const chain = getChain(this.manifest.provider.chain);
|
|
472
442
|
const services = this.manifest.services.map((s) => ({
|
|
473
443
|
id: s.id,
|
|
474
444
|
name: s.name,
|
|
@@ -481,14 +451,20 @@ var MoltsPayServer = class {
|
|
|
481
451
|
}));
|
|
482
452
|
this.sendJson(res, 200, {
|
|
483
453
|
provider: this.manifest.provider,
|
|
484
|
-
services
|
|
454
|
+
services,
|
|
455
|
+
x402: {
|
|
456
|
+
version: X402_VERSION2,
|
|
457
|
+
network: `eip155:${chain.chainId}`,
|
|
458
|
+
schemes: ["exact"]
|
|
459
|
+
}
|
|
485
460
|
});
|
|
486
461
|
}
|
|
487
462
|
/**
|
|
488
|
-
* POST /
|
|
463
|
+
* POST /execute - Execute service with x402 payment
|
|
489
464
|
* Body: { service: string, params: object }
|
|
465
|
+
* Header: X-Payment (optional - if missing, returns 402)
|
|
490
466
|
*/
|
|
491
|
-
|
|
467
|
+
async handleExecute(body, paymentHeader, res) {
|
|
492
468
|
const { service, params } = body;
|
|
493
469
|
if (!service) {
|
|
494
470
|
return this.sendJson(res, 400, { error: "Missing service" });
|
|
@@ -502,113 +478,129 @@ var MoltsPayServer = class {
|
|
|
502
478
|
return this.sendJson(res, 400, { error: `Missing required param: ${key}` });
|
|
503
479
|
}
|
|
504
480
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
const charge = {
|
|
508
|
-
id: chargeId,
|
|
509
|
-
service,
|
|
510
|
-
params: params || {},
|
|
511
|
-
amount: skill.config.price,
|
|
512
|
-
currency: skill.config.currency,
|
|
513
|
-
status: "pending",
|
|
514
|
-
createdAt: now,
|
|
515
|
-
expiresAt: now + this.options.chargeExpirySecs * 1e3
|
|
516
|
-
};
|
|
517
|
-
this.charges.set(chargeId, charge);
|
|
518
|
-
const paymentRequest = {
|
|
519
|
-
chargeId,
|
|
520
|
-
service,
|
|
521
|
-
amount: charge.amount,
|
|
522
|
-
currency: charge.currency,
|
|
523
|
-
wallet: this.manifest.provider.wallet,
|
|
524
|
-
chain: this.manifest.provider.chain,
|
|
525
|
-
expiresAt: charge.expiresAt
|
|
526
|
-
};
|
|
527
|
-
this.sendJson(res, 402, {
|
|
528
|
-
message: "Payment required",
|
|
529
|
-
payment: paymentRequest
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
/**
|
|
533
|
-
* POST /verify - Verify payment and execute skill
|
|
534
|
-
* Body: { chargeId: string, txHash: string }
|
|
535
|
-
*/
|
|
536
|
-
async handleVerify(body, res) {
|
|
537
|
-
const { chargeId, txHash } = body;
|
|
538
|
-
if (!chargeId || !txHash) {
|
|
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
|
-
});
|
|
481
|
+
if (!paymentHeader) {
|
|
482
|
+
return this.sendPaymentRequired(skill.config, res);
|
|
554
483
|
}
|
|
484
|
+
let payment;
|
|
555
485
|
try {
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
}
|
|
569
|
-
charge.status = "paid";
|
|
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
|
-
});
|
|
486
|
+
const decoded = Buffer.from(paymentHeader, "base64").toString("utf-8");
|
|
487
|
+
payment = JSON.parse(decoded);
|
|
488
|
+
} catch {
|
|
489
|
+
return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
|
|
490
|
+
}
|
|
491
|
+
const validation = this.validatePayment(payment, skill.config);
|
|
492
|
+
if (!validation.valid) {
|
|
493
|
+
return this.sendJson(res, 402, { error: validation.error });
|
|
494
|
+
}
|
|
495
|
+
console.log(`[MoltsPay] Executing skill: ${service}`);
|
|
496
|
+
let result;
|
|
497
|
+
try {
|
|
498
|
+
result = await skill.handler(params || {});
|
|
584
499
|
} catch (err) {
|
|
585
|
-
console.error("[MoltsPay] Skill execution
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
error: "Skill execution failed",
|
|
500
|
+
console.error("[MoltsPay] Skill execution failed:", err.message);
|
|
501
|
+
return this.sendJson(res, 500, {
|
|
502
|
+
error: "Service execution failed",
|
|
589
503
|
message: err.message
|
|
590
504
|
});
|
|
591
505
|
}
|
|
506
|
+
console.log(`[MoltsPay] Skill succeeded, claiming payment...`);
|
|
507
|
+
let txHash = null;
|
|
508
|
+
try {
|
|
509
|
+
txHash = await this.claimPayment(payment);
|
|
510
|
+
console.log(`[MoltsPay] Payment claimed: ${txHash}`);
|
|
511
|
+
} catch (err) {
|
|
512
|
+
console.error("[MoltsPay] Payment claim failed:", err.message);
|
|
513
|
+
}
|
|
514
|
+
this.sendJson(res, 200, {
|
|
515
|
+
success: true,
|
|
516
|
+
result,
|
|
517
|
+
payment: txHash ? { txHash, status: "claimed" } : { status: "pending" }
|
|
518
|
+
});
|
|
592
519
|
}
|
|
593
520
|
/**
|
|
594
|
-
*
|
|
521
|
+
* Return 402 with x402 payment requirements
|
|
595
522
|
*/
|
|
596
|
-
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
expiresAt: charge.expiresAt
|
|
523
|
+
sendPaymentRequired(config, res) {
|
|
524
|
+
const chain = getChain(this.manifest.provider.chain);
|
|
525
|
+
const amountInUnits = Math.floor(config.price * 1e6).toString();
|
|
526
|
+
const requirements = [{
|
|
527
|
+
scheme: "exact",
|
|
528
|
+
network: `eip155:${chain.chainId}`,
|
|
529
|
+
maxAmountRequired: amountInUnits,
|
|
530
|
+
resource: this.manifest.provider.wallet,
|
|
531
|
+
description: `${config.name} - $${config.price} ${config.currency}`
|
|
532
|
+
}];
|
|
533
|
+
const encoded = Buffer.from(JSON.stringify(requirements)).toString("base64");
|
|
534
|
+
res.writeHead(402, {
|
|
535
|
+
"Content-Type": "application/json",
|
|
536
|
+
[PAYMENT_REQUIRED_HEADER2]: encoded
|
|
611
537
|
});
|
|
538
|
+
res.end(JSON.stringify({
|
|
539
|
+
error: "Payment required",
|
|
540
|
+
message: `Service requires $${config.price} ${config.currency}`,
|
|
541
|
+
x402: requirements[0]
|
|
542
|
+
}, null, 2));
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Validate x402 payment payload
|
|
546
|
+
*/
|
|
547
|
+
validatePayment(payment, config) {
|
|
548
|
+
if (payment.x402Version !== X402_VERSION2) {
|
|
549
|
+
return { valid: false, error: `Unsupported x402 version: ${payment.x402Version}` };
|
|
550
|
+
}
|
|
551
|
+
if (payment.scheme !== "exact") {
|
|
552
|
+
return { valid: false, error: `Unsupported scheme: ${payment.scheme}` };
|
|
553
|
+
}
|
|
554
|
+
const chain = getChain(this.manifest.provider.chain);
|
|
555
|
+
const expectedNetwork = `eip155:${chain.chainId}`;
|
|
556
|
+
if (payment.network !== expectedNetwork) {
|
|
557
|
+
return { valid: false, error: `Network mismatch: expected ${expectedNetwork}` };
|
|
558
|
+
}
|
|
559
|
+
const auth = payment.payload.authorization;
|
|
560
|
+
if (auth.to.toLowerCase() !== this.manifest.provider.wallet.toLowerCase()) {
|
|
561
|
+
return { valid: false, error: "Payment recipient mismatch" };
|
|
562
|
+
}
|
|
563
|
+
const amount = Number(auth.value) / 1e6;
|
|
564
|
+
if (amount < config.price) {
|
|
565
|
+
return { valid: false, error: `Insufficient amount: $${amount} < $${config.price}` };
|
|
566
|
+
}
|
|
567
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
568
|
+
if (Number(auth.validBefore) < now) {
|
|
569
|
+
return { valid: false, error: "Payment authorization expired" };
|
|
570
|
+
}
|
|
571
|
+
if (Number(auth.validAfter) > now) {
|
|
572
|
+
return { valid: false, error: "Payment authorization not yet valid" };
|
|
573
|
+
}
|
|
574
|
+
return { valid: true };
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Claim payment using transferWithAuthorization
|
|
578
|
+
*/
|
|
579
|
+
async claimPayment(payment) {
|
|
580
|
+
if (!this.wallet || !this.provider) {
|
|
581
|
+
throw new Error("Wallet not configured for payment claims");
|
|
582
|
+
}
|
|
583
|
+
const chain = getChain(this.manifest.provider.chain);
|
|
584
|
+
const auth = payment.payload.authorization;
|
|
585
|
+
const sig = payment.payload.signature;
|
|
586
|
+
const { r, s, v } = ethers2.Signature.from(sig);
|
|
587
|
+
const usdcAbi = [
|
|
588
|
+
"function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)"
|
|
589
|
+
];
|
|
590
|
+
const usdc = new ethers2.Contract(chain.usdc, usdcAbi, this.wallet);
|
|
591
|
+
const tx = await usdc.transferWithAuthorization(
|
|
592
|
+
auth.from,
|
|
593
|
+
auth.to,
|
|
594
|
+
auth.value,
|
|
595
|
+
auth.validAfter,
|
|
596
|
+
auth.validBefore,
|
|
597
|
+
auth.nonce,
|
|
598
|
+
v,
|
|
599
|
+
r,
|
|
600
|
+
s
|
|
601
|
+
);
|
|
602
|
+
const receipt = await tx.wait();
|
|
603
|
+
return receipt.hash;
|
|
612
604
|
}
|
|
613
605
|
async readBody(req) {
|
|
614
606
|
return new Promise((resolve2, reject) => {
|
|
@@ -786,7 +778,7 @@ program.command("services <url>").description("List services from a provider").o
|
|
|
786
778
|
console.error("\u274C Error:", err.message);
|
|
787
779
|
}
|
|
788
780
|
});
|
|
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) => {
|
|
781
|
+
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) => {
|
|
790
782
|
const manifestPath = resolve(manifest);
|
|
791
783
|
if (!existsSync2(manifestPath)) {
|
|
792
784
|
console.error(`\u274C Manifest not found: ${manifestPath}`);
|
|
@@ -794,14 +786,16 @@ program.command("start <manifest>").description("Start MoltsPay server from serv
|
|
|
794
786
|
}
|
|
795
787
|
const port = parseInt(options.port, 10);
|
|
796
788
|
const host = options.host;
|
|
789
|
+
const privateKey = options.privateKey || process.env.MOLTSPAY_PRIVATE_KEY;
|
|
797
790
|
console.log(`
|
|
798
|
-
\u{1F680} Starting MoltsPay Server
|
|
791
|
+
\u{1F680} Starting MoltsPay Server (x402 protocol)
|
|
799
792
|
`);
|
|
800
793
|
console.log(` Manifest: ${manifestPath}`);
|
|
801
794
|
console.log(` Port: ${port}`);
|
|
795
|
+
console.log(` Payment claims: ${privateKey ? "enabled" : "disabled (no private key)"}`);
|
|
802
796
|
console.log("");
|
|
803
797
|
try {
|
|
804
|
-
const server = new MoltsPayServer(manifestPath, { port, host });
|
|
798
|
+
const server = new MoltsPayServer(manifestPath, { port, host, privateKey });
|
|
805
799
|
const manifestContent = await import("fs").then(
|
|
806
800
|
(fs) => JSON.parse(fs.readFileSync(manifestPath, "utf-8"))
|
|
807
801
|
);
|
|
@@ -913,7 +907,7 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
|
|
|
913
907
|
process.exit(1);
|
|
914
908
|
}
|
|
915
909
|
});
|
|
916
|
-
program.command("pay <server> <service> [params]").description("Pay for a service and get the result").option("--prompt <text>", "Prompt for the service").option("--image <
|
|
910
|
+
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) => {
|
|
917
911
|
const client = new MoltsPayClient();
|
|
918
912
|
if (!client.isInitialized) {
|
|
919
913
|
console.error("\u274C Wallet not initialized. Run: npx moltspay init");
|
|
@@ -929,11 +923,25 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
929
923
|
}
|
|
930
924
|
}
|
|
931
925
|
if (options.prompt) params.prompt = options.prompt;
|
|
932
|
-
if (options.image)
|
|
926
|
+
if (options.image) {
|
|
927
|
+
const imagePath = options.image;
|
|
928
|
+
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
|
929
|
+
params.image_url = imagePath;
|
|
930
|
+
} else {
|
|
931
|
+
const filePath = resolve(imagePath);
|
|
932
|
+
if (!existsSync2(filePath)) {
|
|
933
|
+
console.error(`\u274C Image file not found: ${filePath}`);
|
|
934
|
+
process.exit(1);
|
|
935
|
+
}
|
|
936
|
+
const imageData = readFileSync3(filePath);
|
|
937
|
+
params.image_base64 = imageData.toString("base64");
|
|
938
|
+
}
|
|
939
|
+
}
|
|
933
940
|
if (!params.prompt) {
|
|
934
941
|
console.error("\u274C Missing prompt. Use --prompt or pass JSON params");
|
|
935
942
|
process.exit(1);
|
|
936
943
|
}
|
|
944
|
+
const imageDisplay = params.image_url || (params.image_base64 ? `[local file: ${options.image}]` : null);
|
|
937
945
|
if (!options.json) {
|
|
938
946
|
console.log(`
|
|
939
947
|
\u{1F4B3} MoltsPay - Paying for service
|
|
@@ -941,7 +949,7 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
941
949
|
console.log(` Server: ${server}`);
|
|
942
950
|
console.log(` Service: ${service}`);
|
|
943
951
|
console.log(` Prompt: ${params.prompt}`);
|
|
944
|
-
if (
|
|
952
|
+
if (imageDisplay) console.log(` Image: ${imageDisplay}`);
|
|
945
953
|
console.log(` Wallet: ${client.address}`);
|
|
946
954
|
console.log("");
|
|
947
955
|
}
|