moltspay 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -14
- package/dist/cdp/index.d.mts +1 -1
- package/dist/cdp/index.d.ts +1 -1
- package/dist/cdp/index.js +53 -30368
- package/dist/cdp/index.js.map +1 -1
- package/dist/cdp/index.mjs +37 -30360
- package/dist/cdp/index.mjs.map +1 -1
- package/dist/cdp-DeohBe1o.d.ts +66 -0
- package/dist/cdp-p_eHuQpb.d.mts +66 -0
- package/dist/chains/index.d.mts +1 -1
- package/dist/chains/index.d.ts +1 -1
- package/dist/chains/index.js +29 -0
- package/dist/chains/index.js.map +1 -1
- package/dist/chains/index.mjs +29 -0
- package/dist/chains/index.mjs.map +1 -1
- package/dist/cli/index.js +863 -109
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +867 -109
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +25 -2
- package/dist/client/index.d.ts +25 -2
- package/dist/client/index.js +195 -12
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +185 -12
- package/dist/client/index.mjs.map +1 -1
- package/dist/facilitators/index.d.mts +15 -53
- package/dist/facilitators/index.d.ts +15 -53
- package/dist/facilitators/index.js +234 -1
- package/dist/facilitators/index.js.map +1 -1
- package/dist/facilitators/index.mjs +233 -1
- package/dist/facilitators/index.mjs.map +1 -1
- package/dist/{index-DgJPZMBG.d.mts → index-On9ZaGDW.d.mts} +1 -1
- package/dist/{index-DgJPZMBG.d.ts → index-On9ZaGDW.d.ts} +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +718 -30584
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +711 -30587
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.d.mts +17 -0
- package/dist/server/index.d.ts +17 -0
- package/dist/server/index.js +443 -5
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +443 -5
- package/dist/server/index.mjs.map +1 -1
- package/dist/verify/index.d.mts +1 -1
- package/dist/verify/index.d.ts +1 -1
- package/dist/verify/index.js +29 -0
- package/dist/verify/index.js.map +1 -1
- package/dist/verify/index.mjs +29 -0
- package/dist/verify/index.mjs.map +1 -1
- package/dist/wallet/index.d.mts +1 -1
- package/dist/wallet/index.d.ts +1 -1
- package/dist/wallet/index.js +29 -0
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/index.mjs +29 -0
- package/dist/wallet/index.mjs.map +1 -1
- package/package.json +5 -2
package/dist/server/index.mjs
CHANGED
|
@@ -229,6 +229,236 @@ var CDPFacilitator = class extends BaseFacilitator {
|
|
|
229
229
|
}
|
|
230
230
|
};
|
|
231
231
|
|
|
232
|
+
// src/chains/index.ts
|
|
233
|
+
var CHAINS = {
|
|
234
|
+
// ============ Mainnet ============
|
|
235
|
+
base: {
|
|
236
|
+
name: "Base",
|
|
237
|
+
chainId: 8453,
|
|
238
|
+
rpc: "https://mainnet.base.org",
|
|
239
|
+
tokens: {
|
|
240
|
+
USDC: {
|
|
241
|
+
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
242
|
+
decimals: 6,
|
|
243
|
+
symbol: "USDC",
|
|
244
|
+
eip712Name: "USD Coin"
|
|
245
|
+
// EIP-712 domain name
|
|
246
|
+
},
|
|
247
|
+
USDT: {
|
|
248
|
+
address: "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2",
|
|
249
|
+
decimals: 6,
|
|
250
|
+
symbol: "USDT",
|
|
251
|
+
eip712Name: "Tether USD"
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
255
|
+
// deprecated, for backward compat
|
|
256
|
+
explorer: "https://basescan.org/address/",
|
|
257
|
+
explorerTx: "https://basescan.org/tx/",
|
|
258
|
+
avgBlockTime: 2
|
|
259
|
+
},
|
|
260
|
+
polygon: {
|
|
261
|
+
name: "Polygon",
|
|
262
|
+
chainId: 137,
|
|
263
|
+
rpc: "https://polygon-bor-rpc.publicnode.com",
|
|
264
|
+
tokens: {
|
|
265
|
+
USDC: {
|
|
266
|
+
address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
267
|
+
decimals: 6,
|
|
268
|
+
symbol: "USDC",
|
|
269
|
+
eip712Name: "USD Coin"
|
|
270
|
+
},
|
|
271
|
+
USDT: {
|
|
272
|
+
address: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
|
|
273
|
+
decimals: 6,
|
|
274
|
+
symbol: "USDT",
|
|
275
|
+
eip712Name: "(PoS) Tether USD"
|
|
276
|
+
// Polygon uses this name
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
usdc: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
280
|
+
explorer: "https://polygonscan.com/address/",
|
|
281
|
+
explorerTx: "https://polygonscan.com/tx/",
|
|
282
|
+
avgBlockTime: 2
|
|
283
|
+
},
|
|
284
|
+
// ============ Testnet ============
|
|
285
|
+
base_sepolia: {
|
|
286
|
+
name: "Base Sepolia",
|
|
287
|
+
chainId: 84532,
|
|
288
|
+
rpc: "https://sepolia.base.org",
|
|
289
|
+
tokens: {
|
|
290
|
+
USDC: {
|
|
291
|
+
address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
292
|
+
decimals: 6,
|
|
293
|
+
symbol: "USDC",
|
|
294
|
+
eip712Name: "USDC"
|
|
295
|
+
// Testnet USDC uses 'USDC' not 'USD Coin'
|
|
296
|
+
},
|
|
297
|
+
USDT: {
|
|
298
|
+
address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
299
|
+
// Same as USDC on testnet (no official USDT)
|
|
300
|
+
decimals: 6,
|
|
301
|
+
symbol: "USDT",
|
|
302
|
+
eip712Name: "USDC"
|
|
303
|
+
// Uses same contract as USDC
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
307
|
+
explorer: "https://sepolia.basescan.org/address/",
|
|
308
|
+
explorerTx: "https://sepolia.basescan.org/tx/",
|
|
309
|
+
avgBlockTime: 2
|
|
310
|
+
},
|
|
311
|
+
// ============ Tempo Testnet (Moderato) ============
|
|
312
|
+
tempo_moderato: {
|
|
313
|
+
name: "Tempo Moderato",
|
|
314
|
+
chainId: 42431,
|
|
315
|
+
rpc: "https://rpc.moderato.tempo.xyz",
|
|
316
|
+
tokens: {
|
|
317
|
+
// TIP-20 stablecoins on Tempo testnet (from mppx SDK)
|
|
318
|
+
// Note: Tempo uses USD as native gas token, not ETH
|
|
319
|
+
USDC: {
|
|
320
|
+
address: "0x20c0000000000000000000000000000000000000",
|
|
321
|
+
// pathUSD - primary testnet stablecoin
|
|
322
|
+
decimals: 6,
|
|
323
|
+
symbol: "USDC",
|
|
324
|
+
eip712Name: "pathUSD"
|
|
325
|
+
},
|
|
326
|
+
USDT: {
|
|
327
|
+
address: "0x20c0000000000000000000000000000000000001",
|
|
328
|
+
// alphaUSD
|
|
329
|
+
decimals: 6,
|
|
330
|
+
symbol: "USDT",
|
|
331
|
+
eip712Name: "alphaUSD"
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
usdc: "0x20c0000000000000000000000000000000000000",
|
|
335
|
+
explorer: "https://explore.testnet.tempo.xyz/address/",
|
|
336
|
+
explorerTx: "https://explore.testnet.tempo.xyz/tx/",
|
|
337
|
+
avgBlockTime: 0.5
|
|
338
|
+
// ~500ms finality
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// src/facilitators/tempo.ts
|
|
343
|
+
var TRANSFER_EVENT_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
344
|
+
var TempoFacilitator = class extends BaseFacilitator {
|
|
345
|
+
name = "tempo";
|
|
346
|
+
displayName = "Tempo Testnet";
|
|
347
|
+
supportedNetworks = ["eip155:42431"];
|
|
348
|
+
// Tempo Moderato
|
|
349
|
+
rpcUrl;
|
|
350
|
+
constructor() {
|
|
351
|
+
super();
|
|
352
|
+
this.rpcUrl = CHAINS.tempo_moderato.rpc;
|
|
353
|
+
}
|
|
354
|
+
async healthCheck() {
|
|
355
|
+
const start = Date.now();
|
|
356
|
+
try {
|
|
357
|
+
const response = await fetch(this.rpcUrl, {
|
|
358
|
+
method: "POST",
|
|
359
|
+
headers: { "Content-Type": "application/json" },
|
|
360
|
+
body: JSON.stringify({
|
|
361
|
+
jsonrpc: "2.0",
|
|
362
|
+
method: "eth_chainId",
|
|
363
|
+
params: [],
|
|
364
|
+
id: 1
|
|
365
|
+
})
|
|
366
|
+
});
|
|
367
|
+
const data = await response.json();
|
|
368
|
+
const chainId = parseInt(data.result, 16);
|
|
369
|
+
if (chainId !== 42431) {
|
|
370
|
+
return { healthy: false, error: `Wrong chainId: ${chainId}` };
|
|
371
|
+
}
|
|
372
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
373
|
+
} catch (error) {
|
|
374
|
+
return { healthy: false, error: String(error) };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async verify(paymentPayload, requirements) {
|
|
378
|
+
try {
|
|
379
|
+
const tempoPayload = paymentPayload.payload;
|
|
380
|
+
if (!tempoPayload?.txHash) {
|
|
381
|
+
return { valid: false, error: "Missing txHash in payment payload" };
|
|
382
|
+
}
|
|
383
|
+
const receipt = await this.getTransactionReceipt(tempoPayload.txHash);
|
|
384
|
+
if (!receipt) {
|
|
385
|
+
return { valid: false, error: "Transaction not found" };
|
|
386
|
+
}
|
|
387
|
+
if (receipt.status !== "0x1") {
|
|
388
|
+
return { valid: false, error: "Transaction failed" };
|
|
389
|
+
}
|
|
390
|
+
const transferLog = receipt.logs.find(
|
|
391
|
+
(log) => log.topics[0] === TRANSFER_EVENT_TOPIC
|
|
392
|
+
);
|
|
393
|
+
if (!transferLog) {
|
|
394
|
+
return { valid: false, error: "No Transfer event found" };
|
|
395
|
+
}
|
|
396
|
+
const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
|
|
397
|
+
const expectedTo = requirements.payTo.toLowerCase();
|
|
398
|
+
if (toAddress !== expectedTo) {
|
|
399
|
+
return {
|
|
400
|
+
valid: false,
|
|
401
|
+
error: `Wrong recipient: ${toAddress}, expected ${expectedTo}`
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
const amount = BigInt(transferLog.data);
|
|
405
|
+
const expectedAmount = BigInt(requirements.amount);
|
|
406
|
+
if (amount < expectedAmount) {
|
|
407
|
+
return {
|
|
408
|
+
valid: false,
|
|
409
|
+
error: `Insufficient amount: ${amount}, expected ${expectedAmount}`
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
const tokenAddress = transferLog.address.toLowerCase();
|
|
413
|
+
const expectedToken = requirements.asset.toLowerCase();
|
|
414
|
+
if (tokenAddress !== expectedToken) {
|
|
415
|
+
return {
|
|
416
|
+
valid: false,
|
|
417
|
+
error: `Wrong token: ${tokenAddress}, expected ${expectedToken}`
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
valid: true,
|
|
422
|
+
details: {
|
|
423
|
+
txHash: tempoPayload.txHash,
|
|
424
|
+
from: "0x" + transferLog.topics[1].slice(26),
|
|
425
|
+
to: toAddress,
|
|
426
|
+
amount: amount.toString(),
|
|
427
|
+
token: tokenAddress
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
} catch (error) {
|
|
431
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async settle(paymentPayload, requirements) {
|
|
435
|
+
const verifyResult = await this.verify(paymentPayload, requirements);
|
|
436
|
+
if (!verifyResult.valid) {
|
|
437
|
+
return { success: false, error: verifyResult.error };
|
|
438
|
+
}
|
|
439
|
+
const tempoPayload = paymentPayload.payload;
|
|
440
|
+
return {
|
|
441
|
+
success: true,
|
|
442
|
+
transaction: tempoPayload.txHash,
|
|
443
|
+
status: "settled"
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
async getTransactionReceipt(txHash) {
|
|
447
|
+
const response = await fetch(this.rpcUrl, {
|
|
448
|
+
method: "POST",
|
|
449
|
+
headers: { "Content-Type": "application/json" },
|
|
450
|
+
body: JSON.stringify({
|
|
451
|
+
jsonrpc: "2.0",
|
|
452
|
+
method: "eth_getTransactionReceipt",
|
|
453
|
+
params: [txHash],
|
|
454
|
+
id: 1
|
|
455
|
+
})
|
|
456
|
+
});
|
|
457
|
+
const data = await response.json();
|
|
458
|
+
return data.result;
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
232
462
|
// src/facilitators/registry.ts
|
|
233
463
|
var FacilitatorRegistry = class {
|
|
234
464
|
factories = /* @__PURE__ */ new Map();
|
|
@@ -237,7 +467,8 @@ var FacilitatorRegistry = class {
|
|
|
237
467
|
roundRobinIndex = 0;
|
|
238
468
|
constructor(selection) {
|
|
239
469
|
this.registerFactory("cdp", (config) => new CDPFacilitator(config));
|
|
240
|
-
this.
|
|
470
|
+
this.registerFactory("tempo", () => new TempoFacilitator());
|
|
471
|
+
this.selection = selection || { primary: "cdp", fallback: ["tempo"], strategy: "failover" };
|
|
241
472
|
}
|
|
242
473
|
/**
|
|
243
474
|
* Register a new facilitator factory
|
|
@@ -458,6 +689,9 @@ var X402_VERSION2 = 2;
|
|
|
458
689
|
var PAYMENT_REQUIRED_HEADER = "x-payment-required";
|
|
459
690
|
var PAYMENT_HEADER = "x-payment";
|
|
460
691
|
var PAYMENT_RESPONSE_HEADER = "x-payment-response";
|
|
692
|
+
var MPP_AUTH_HEADER = "authorization";
|
|
693
|
+
var MPP_WWW_AUTH_HEADER = "www-authenticate";
|
|
694
|
+
var MPP_RECEIPT_HEADER = "payment-receipt";
|
|
461
695
|
var TOKEN_ADDRESSES = {
|
|
462
696
|
"eip155:8453": {
|
|
463
697
|
USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
@@ -471,12 +705,20 @@ var TOKEN_ADDRESSES = {
|
|
|
471
705
|
"eip155:137": {
|
|
472
706
|
USDC: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
473
707
|
USDT: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F"
|
|
708
|
+
},
|
|
709
|
+
"eip155:42431": {
|
|
710
|
+
// Tempo Moderato testnet - TIP-20 stablecoins
|
|
711
|
+
USDC: "0x20c0000000000000000000000000000000000000",
|
|
712
|
+
// pathUSD
|
|
713
|
+
USDT: "0x20c0000000000000000000000000000000000001"
|
|
714
|
+
// alphaUSD
|
|
474
715
|
}
|
|
475
716
|
};
|
|
476
717
|
var CHAIN_TO_NETWORK = {
|
|
477
718
|
"base": "eip155:8453",
|
|
478
719
|
"base_sepolia": "eip155:84532",
|
|
479
|
-
"polygon": "eip155:137"
|
|
720
|
+
"polygon": "eip155:137",
|
|
721
|
+
"tempo_moderato": "eip155:42431"
|
|
480
722
|
};
|
|
481
723
|
var TOKEN_DOMAINS = {
|
|
482
724
|
// Base mainnet
|
|
@@ -494,6 +736,11 @@ var TOKEN_DOMAINS = {
|
|
|
494
736
|
"eip155:137": {
|
|
495
737
|
USDC: { name: "USD Coin", version: "2" },
|
|
496
738
|
USDT: { name: "(PoS) Tether USD", version: "2" }
|
|
739
|
+
},
|
|
740
|
+
// Tempo Moderato testnet - TIP-20 stablecoins
|
|
741
|
+
"eip155:42431": {
|
|
742
|
+
USDC: { name: "pathUSD", version: "1" },
|
|
743
|
+
USDT: { name: "alphaUSD", version: "1" }
|
|
497
744
|
}
|
|
498
745
|
};
|
|
499
746
|
function getTokenDomain(network, token) {
|
|
@@ -551,9 +798,11 @@ var MoltsPayServer = class {
|
|
|
551
798
|
};
|
|
552
799
|
this.useMainnet = process.env.USE_MAINNET?.toLowerCase() === "true";
|
|
553
800
|
this.networkId = this.useMainnet ? "eip155:8453" : "eip155:84532";
|
|
801
|
+
const defaultFallback = ["tempo"];
|
|
802
|
+
const envFallback = process.env.FACILITATOR_FALLBACK?.split(",").filter(Boolean);
|
|
554
803
|
const facilitatorConfig = options.facilitators || {
|
|
555
804
|
primary: process.env.FACILITATOR_PRIMARY || "cdp",
|
|
556
|
-
fallback:
|
|
805
|
+
fallback: envFallback || defaultFallback,
|
|
557
806
|
strategy: process.env.FACILITATOR_STRATEGY || "failover",
|
|
558
807
|
config: {
|
|
559
808
|
cdp: { useMainnet: this.useMainnet }
|
|
@@ -647,8 +896,8 @@ var MoltsPayServer = class {
|
|
|
647
896
|
async handleRequest(req, res) {
|
|
648
897
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
649
898
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
650
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment");
|
|
651
|
-
res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response");
|
|
899
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization");
|
|
900
|
+
res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt");
|
|
652
901
|
if (req.method === "OPTIONS") {
|
|
653
902
|
res.writeHead(204);
|
|
654
903
|
res.end();
|
|
@@ -679,6 +928,14 @@ var MoltsPayServer = class {
|
|
|
679
928
|
const paymentHeader = req.headers[PAYMENT_HEADER];
|
|
680
929
|
return await this.handleProxy(body, paymentHeader, res);
|
|
681
930
|
}
|
|
931
|
+
const servicePath = url.pathname.replace(/^\//, "");
|
|
932
|
+
const skill = this.skills.get(servicePath);
|
|
933
|
+
if (skill && (req.method === "POST" || req.method === "GET")) {
|
|
934
|
+
const body = req.method === "POST" ? await this.readBody(req) : {};
|
|
935
|
+
const authHeader = req.headers[MPP_AUTH_HEADER];
|
|
936
|
+
const x402Header = req.headers[PAYMENT_HEADER];
|
|
937
|
+
return await this.handleMPPRequest(skill, body, authHeader, x402Header, res);
|
|
938
|
+
}
|
|
682
939
|
this.sendJson(res, 404, { error: "Not found" });
|
|
683
940
|
} catch (err) {
|
|
684
941
|
console.error("[MoltsPay] Error:", err);
|
|
@@ -862,6 +1119,187 @@ var MoltsPayServer = class {
|
|
|
862
1119
|
payment: settlement?.success ? { transaction: settlement.transaction, status: "settled", facilitator: settlement.facilitator } : { status: "pending" }
|
|
863
1120
|
}, responseHeaders);
|
|
864
1121
|
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Handle MPP (Machine Payments Protocol) request
|
|
1124
|
+
* Supports both x402 and MPP protocols on service endpoints
|
|
1125
|
+
*/
|
|
1126
|
+
async handleMPPRequest(skill, body, authHeader, x402Header, res) {
|
|
1127
|
+
const config = skill.config;
|
|
1128
|
+
const params = body || {};
|
|
1129
|
+
if (x402Header) {
|
|
1130
|
+
return await this.handleExecute({ service: config.id, params }, x402Header, res);
|
|
1131
|
+
}
|
|
1132
|
+
if (authHeader && authHeader.toLowerCase().startsWith("payment ")) {
|
|
1133
|
+
return await this.handleMPPPayment(skill, params, authHeader, res);
|
|
1134
|
+
}
|
|
1135
|
+
return this.sendMPPPaymentRequired(config, res);
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Handle MPP payment verification and service execution
|
|
1139
|
+
*/
|
|
1140
|
+
async handleMPPPayment(skill, params, authHeader, res) {
|
|
1141
|
+
const config = skill.config;
|
|
1142
|
+
const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
|
|
1143
|
+
if (!credentialMatch) {
|
|
1144
|
+
return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
|
|
1145
|
+
}
|
|
1146
|
+
let mppCredential;
|
|
1147
|
+
try {
|
|
1148
|
+
const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
1149
|
+
const decoded = Buffer.from(base64, "base64").toString("utf-8");
|
|
1150
|
+
mppCredential = JSON.parse(decoded);
|
|
1151
|
+
} catch (err) {
|
|
1152
|
+
console.error("[MoltsPay] Failed to parse MPP credential:", err);
|
|
1153
|
+
return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
|
|
1154
|
+
}
|
|
1155
|
+
let txHash;
|
|
1156
|
+
if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
|
|
1157
|
+
txHash = mppCredential.payload.hash;
|
|
1158
|
+
} else if (mppCredential.payload?.type === "transaction") {
|
|
1159
|
+
return this.sendJson(res, 400, {
|
|
1160
|
+
error: "Transaction type not supported. Please use push mode (hash type)."
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
if (!txHash) {
|
|
1164
|
+
return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
|
|
1165
|
+
}
|
|
1166
|
+
let chainId = mppCredential.challenge?.request?.methodDetails?.chainId;
|
|
1167
|
+
if (!chainId && mppCredential.source) {
|
|
1168
|
+
const chainMatch = mppCredential.source.match(/eip155:(\d+)/);
|
|
1169
|
+
if (chainMatch) chainId = parseInt(chainMatch[1], 10);
|
|
1170
|
+
}
|
|
1171
|
+
chainId = chainId || 42431;
|
|
1172
|
+
const network = `eip155:${chainId}`;
|
|
1173
|
+
if (!this.isNetworkAccepted(network)) {
|
|
1174
|
+
return this.sendJson(res, 402, {
|
|
1175
|
+
error: `Network not accepted: ${network}`
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
const requirements = this.buildPaymentRequirements(
|
|
1179
|
+
config,
|
|
1180
|
+
network,
|
|
1181
|
+
this.getWalletForNetwork(network),
|
|
1182
|
+
"USDC"
|
|
1183
|
+
);
|
|
1184
|
+
const paymentPayload = {
|
|
1185
|
+
x402Version: X402_VERSION2,
|
|
1186
|
+
scheme: "exact",
|
|
1187
|
+
network,
|
|
1188
|
+
payload: {
|
|
1189
|
+
txHash,
|
|
1190
|
+
chainId
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
console.log(`[MoltsPay] Verifying MPP payment: txHash=${txHash}, chainId=${chainId}`);
|
|
1194
|
+
const verification = await this.registry.verify(paymentPayload, requirements);
|
|
1195
|
+
if (!verification.valid) {
|
|
1196
|
+
return this.sendJson(res, 402, {
|
|
1197
|
+
error: `Payment verification failed: ${verification.error}`
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
console.log(`[MoltsPay] Payment verified! Executing service: ${config.id}`);
|
|
1201
|
+
let result;
|
|
1202
|
+
try {
|
|
1203
|
+
result = await skill.handler(params);
|
|
1204
|
+
} catch (err) {
|
|
1205
|
+
console.error(`[MoltsPay] Skill execution error:`, err);
|
|
1206
|
+
return this.sendJson(res, 500, {
|
|
1207
|
+
error: `Service execution failed: ${err.message}`
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
const receipt = {
|
|
1211
|
+
success: true,
|
|
1212
|
+
txHash,
|
|
1213
|
+
network,
|
|
1214
|
+
facilitator: verification.facilitator
|
|
1215
|
+
};
|
|
1216
|
+
const receiptEncoded = Buffer.from(JSON.stringify(receipt)).toString("base64");
|
|
1217
|
+
res.writeHead(200, {
|
|
1218
|
+
"Content-Type": "application/json",
|
|
1219
|
+
[MPP_RECEIPT_HEADER]: receiptEncoded
|
|
1220
|
+
});
|
|
1221
|
+
res.end(JSON.stringify({
|
|
1222
|
+
success: true,
|
|
1223
|
+
result,
|
|
1224
|
+
payment: {
|
|
1225
|
+
txHash,
|
|
1226
|
+
status: "verified",
|
|
1227
|
+
facilitator: verification.facilitator
|
|
1228
|
+
}
|
|
1229
|
+
}, null, 2));
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Return 402 with both x402 and MPP payment requirements
|
|
1233
|
+
*/
|
|
1234
|
+
sendMPPPaymentRequired(config, res) {
|
|
1235
|
+
const acceptedTokens = getAcceptedCurrencies(config);
|
|
1236
|
+
const providerChains = this.getProviderChains();
|
|
1237
|
+
const accepts = [];
|
|
1238
|
+
for (const chainConfig of providerChains) {
|
|
1239
|
+
for (const token of acceptedTokens) {
|
|
1240
|
+
if (chainConfig.tokens.includes(token)) {
|
|
1241
|
+
accepts.push(this.buildPaymentRequirements(config, chainConfig.network, chainConfig.wallet, token));
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
const x402PaymentRequired = {
|
|
1246
|
+
x402Version: X402_VERSION2,
|
|
1247
|
+
accepts,
|
|
1248
|
+
acceptedCurrencies: acceptedTokens,
|
|
1249
|
+
resource: {
|
|
1250
|
+
url: `/${config.id}`,
|
|
1251
|
+
description: `${config.name} - $${config.price} ${config.currency}`
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
const x402Encoded = Buffer.from(JSON.stringify(x402PaymentRequired)).toString("base64");
|
|
1255
|
+
const tempoChain = providerChains.find((c) => c.network === "eip155:42431");
|
|
1256
|
+
let mppWwwAuth = "";
|
|
1257
|
+
if (tempoChain) {
|
|
1258
|
+
const challengeId = this.generateChallengeId();
|
|
1259
|
+
const amountInUnits = Math.floor(config.price * 1e6).toString();
|
|
1260
|
+
const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
|
|
1261
|
+
const mppRequest = {
|
|
1262
|
+
amount: amountInUnits,
|
|
1263
|
+
currency: tokenAddress,
|
|
1264
|
+
methodDetails: {
|
|
1265
|
+
chainId: 42431,
|
|
1266
|
+
feePayer: true
|
|
1267
|
+
},
|
|
1268
|
+
recipient: tempoChain.wallet
|
|
1269
|
+
};
|
|
1270
|
+
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
1271
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
1272
|
+
mppWwwAuth = `Payment id="${challengeId}", realm="${this.manifest.provider.name}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
1273
|
+
}
|
|
1274
|
+
const headers = {
|
|
1275
|
+
"Content-Type": "application/problem+json",
|
|
1276
|
+
[PAYMENT_REQUIRED_HEADER]: x402Encoded
|
|
1277
|
+
};
|
|
1278
|
+
if (mppWwwAuth) {
|
|
1279
|
+
headers[MPP_WWW_AUTH_HEADER] = mppWwwAuth;
|
|
1280
|
+
}
|
|
1281
|
+
res.writeHead(402, headers);
|
|
1282
|
+
res.end(JSON.stringify({
|
|
1283
|
+
type: "https://paymentauth.org/problems/payment-required",
|
|
1284
|
+
title: "Payment Required",
|
|
1285
|
+
status: 402,
|
|
1286
|
+
detail: `Payment is required (${config.name}).`,
|
|
1287
|
+
service: config.id,
|
|
1288
|
+
price: config.price,
|
|
1289
|
+
currency: config.currency,
|
|
1290
|
+
acceptedCurrencies: acceptedTokens
|
|
1291
|
+
}, null, 2));
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Generate a unique challenge ID for MPP
|
|
1295
|
+
*/
|
|
1296
|
+
generateChallengeId() {
|
|
1297
|
+
const bytes = new Uint8Array(24);
|
|
1298
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1299
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
1300
|
+
}
|
|
1301
|
+
return Buffer.from(bytes).toString("base64url");
|
|
1302
|
+
}
|
|
865
1303
|
/**
|
|
866
1304
|
* Return 402 with x402 payment requirements (v2 format)
|
|
867
1305
|
* Includes requirements for all chains and all accepted currencies
|