moltspay 1.2.1 → 1.4.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 +292 -34
- package/dist/cdp/index.d.mts +4 -4
- package/dist/cdp/index.d.ts +4 -4
- package/dist/cdp/index.js +110 -30368
- package/dist/cdp/index.js.map +1 -1
- package/dist/cdp/index.mjs +94 -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 +9 -8
- package/dist/chains/index.d.ts +9 -8
- package/dist/chains/index.js +86 -0
- package/dist/chains/index.js.map +1 -1
- package/dist/chains/index.mjs +86 -0
- package/dist/chains/index.mjs.map +1 -1
- package/dist/cli/index.js +2746 -290
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +2752 -282
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +60 -4
- package/dist/client/index.d.ts +60 -4
- package/dist/client/index.js +734 -43
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +732 -41
- package/dist/client/index.mjs.map +1 -1
- package/dist/facilitators/index.d.mts +220 -39
- package/dist/facilitators/index.d.ts +220 -39
- package/dist/facilitators/index.js +897 -1
- package/dist/facilitators/index.js.map +1 -1
- package/dist/facilitators/index.mjs +902 -1
- package/dist/facilitators/index.mjs.map +1 -1
- package/dist/{index-DgJPZMBG.d.mts → index-D_2FkLwV.d.mts} +6 -2
- package/dist/{index-DgJPZMBG.d.ts → index-D_2FkLwV.d.ts} +6 -2
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2238 -30837
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2167 -30766
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.d.mts +30 -3
- package/dist/server/index.d.ts +30 -3
- package/dist/server/index.js +1345 -54
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +1355 -54
- 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 +86 -0
- package/dist/verify/index.js.map +1 -1
- package/dist/verify/index.mjs +86 -0
- package/dist/verify/index.mjs.map +1 -1
- package/dist/wallet/index.d.mts +3 -3
- package/dist/wallet/index.d.ts +3 -3
- package/dist/wallet/index.js +86 -0
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/index.mjs +86 -0
- package/dist/wallet/index.mjs.map +1 -1
- package/package.json +8 -2
- package/schemas/moltspay.services.schema.json +27 -132
package/dist/server/index.js
CHANGED
|
@@ -263,7 +263,815 @@ var CDPFacilitator = class extends BaseFacilitator {
|
|
|
263
263
|
}
|
|
264
264
|
};
|
|
265
265
|
|
|
266
|
+
// src/chains/index.ts
|
|
267
|
+
var CHAINS = {
|
|
268
|
+
// ============ Mainnet ============
|
|
269
|
+
base: {
|
|
270
|
+
name: "Base",
|
|
271
|
+
chainId: 8453,
|
|
272
|
+
rpc: "https://mainnet.base.org",
|
|
273
|
+
tokens: {
|
|
274
|
+
USDC: {
|
|
275
|
+
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
276
|
+
decimals: 6,
|
|
277
|
+
symbol: "USDC",
|
|
278
|
+
eip712Name: "USD Coin"
|
|
279
|
+
// EIP-712 domain name
|
|
280
|
+
},
|
|
281
|
+
USDT: {
|
|
282
|
+
address: "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2",
|
|
283
|
+
decimals: 6,
|
|
284
|
+
symbol: "USDT",
|
|
285
|
+
eip712Name: "Tether USD"
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
289
|
+
// deprecated, for backward compat
|
|
290
|
+
explorer: "https://basescan.org/address/",
|
|
291
|
+
explorerTx: "https://basescan.org/tx/",
|
|
292
|
+
avgBlockTime: 2
|
|
293
|
+
},
|
|
294
|
+
polygon: {
|
|
295
|
+
name: "Polygon",
|
|
296
|
+
chainId: 137,
|
|
297
|
+
rpc: "https://polygon-bor-rpc.publicnode.com",
|
|
298
|
+
tokens: {
|
|
299
|
+
USDC: {
|
|
300
|
+
address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
301
|
+
decimals: 6,
|
|
302
|
+
symbol: "USDC",
|
|
303
|
+
eip712Name: "USD Coin"
|
|
304
|
+
},
|
|
305
|
+
USDT: {
|
|
306
|
+
address: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
|
|
307
|
+
decimals: 6,
|
|
308
|
+
symbol: "USDT",
|
|
309
|
+
eip712Name: "(PoS) Tether USD"
|
|
310
|
+
// Polygon uses this name
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
usdc: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
314
|
+
explorer: "https://polygonscan.com/address/",
|
|
315
|
+
explorerTx: "https://polygonscan.com/tx/",
|
|
316
|
+
avgBlockTime: 2
|
|
317
|
+
},
|
|
318
|
+
// ============ Testnet ============
|
|
319
|
+
base_sepolia: {
|
|
320
|
+
name: "Base Sepolia",
|
|
321
|
+
chainId: 84532,
|
|
322
|
+
rpc: "https://sepolia.base.org",
|
|
323
|
+
tokens: {
|
|
324
|
+
USDC: {
|
|
325
|
+
address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
326
|
+
decimals: 6,
|
|
327
|
+
symbol: "USDC",
|
|
328
|
+
eip712Name: "USDC"
|
|
329
|
+
// Testnet USDC uses 'USDC' not 'USD Coin'
|
|
330
|
+
},
|
|
331
|
+
USDT: {
|
|
332
|
+
address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
333
|
+
// Same as USDC on testnet (no official USDT)
|
|
334
|
+
decimals: 6,
|
|
335
|
+
symbol: "USDT",
|
|
336
|
+
eip712Name: "USDC"
|
|
337
|
+
// Uses same contract as USDC
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
341
|
+
explorer: "https://sepolia.basescan.org/address/",
|
|
342
|
+
explorerTx: "https://sepolia.basescan.org/tx/",
|
|
343
|
+
avgBlockTime: 2
|
|
344
|
+
},
|
|
345
|
+
// ============ Tempo Testnet (Moderato) ============
|
|
346
|
+
tempo_moderato: {
|
|
347
|
+
name: "Tempo Moderato",
|
|
348
|
+
chainId: 42431,
|
|
349
|
+
rpc: "https://rpc.moderato.tempo.xyz",
|
|
350
|
+
tokens: {
|
|
351
|
+
// TIP-20 stablecoins on Tempo testnet (from mppx SDK)
|
|
352
|
+
// Note: Tempo uses USD as native gas token, not ETH
|
|
353
|
+
USDC: {
|
|
354
|
+
address: "0x20c0000000000000000000000000000000000000",
|
|
355
|
+
// pathUSD - primary testnet stablecoin
|
|
356
|
+
decimals: 6,
|
|
357
|
+
symbol: "USDC",
|
|
358
|
+
eip712Name: "pathUSD"
|
|
359
|
+
},
|
|
360
|
+
USDT: {
|
|
361
|
+
address: "0x20c0000000000000000000000000000000000001",
|
|
362
|
+
// alphaUSD
|
|
363
|
+
decimals: 6,
|
|
364
|
+
symbol: "USDT",
|
|
365
|
+
eip712Name: "alphaUSD"
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
usdc: "0x20c0000000000000000000000000000000000000",
|
|
369
|
+
explorer: "https://explore.testnet.tempo.xyz/address/",
|
|
370
|
+
explorerTx: "https://explore.testnet.tempo.xyz/tx/",
|
|
371
|
+
avgBlockTime: 0.5
|
|
372
|
+
// ~500ms finality
|
|
373
|
+
},
|
|
374
|
+
// ============ BNB Chain Testnet ============
|
|
375
|
+
bnb_testnet: {
|
|
376
|
+
name: "BNB Testnet",
|
|
377
|
+
chainId: 97,
|
|
378
|
+
rpc: "https://data-seed-prebsc-1-s1.binance.org:8545",
|
|
379
|
+
tokens: {
|
|
380
|
+
// Note: BNB uses 18 decimals for stablecoins (unlike Base/Polygon which use 6)
|
|
381
|
+
// Using official Binance-Peg testnet tokens
|
|
382
|
+
USDC: {
|
|
383
|
+
address: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
384
|
+
// Testnet USDC
|
|
385
|
+
decimals: 18,
|
|
386
|
+
symbol: "USDC",
|
|
387
|
+
eip712Name: "USD Coin"
|
|
388
|
+
},
|
|
389
|
+
USDT: {
|
|
390
|
+
address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
|
|
391
|
+
// Testnet USDT
|
|
392
|
+
decimals: 18,
|
|
393
|
+
symbol: "USDT",
|
|
394
|
+
eip712Name: "Tether USD"
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
usdc: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
398
|
+
explorer: "https://testnet.bscscan.com/address/",
|
|
399
|
+
explorerTx: "https://testnet.bscscan.com/tx/",
|
|
400
|
+
avgBlockTime: 3,
|
|
401
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
402
|
+
requiresApproval: true
|
|
403
|
+
},
|
|
404
|
+
// ============ BNB Chain Mainnet ============
|
|
405
|
+
bnb: {
|
|
406
|
+
name: "BNB Smart Chain",
|
|
407
|
+
chainId: 56,
|
|
408
|
+
rpc: "https://bsc-dataseed.binance.org",
|
|
409
|
+
tokens: {
|
|
410
|
+
// Note: BNB uses 18 decimals for stablecoins
|
|
411
|
+
USDC: {
|
|
412
|
+
address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
413
|
+
decimals: 18,
|
|
414
|
+
symbol: "USDC",
|
|
415
|
+
eip712Name: "USD Coin"
|
|
416
|
+
},
|
|
417
|
+
USDT: {
|
|
418
|
+
address: "0x55d398326f99059fF775485246999027B3197955",
|
|
419
|
+
decimals: 18,
|
|
420
|
+
symbol: "USDT",
|
|
421
|
+
eip712Name: "Tether USD"
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
usdc: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
425
|
+
explorer: "https://bscscan.com/address/",
|
|
426
|
+
explorerTx: "https://bscscan.com/tx/",
|
|
427
|
+
avgBlockTime: 3,
|
|
428
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
429
|
+
requiresApproval: true
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// src/facilitators/tempo.ts
|
|
434
|
+
var TRANSFER_EVENT_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
435
|
+
var TempoFacilitator = class extends BaseFacilitator {
|
|
436
|
+
name = "tempo";
|
|
437
|
+
displayName = "Tempo Testnet";
|
|
438
|
+
supportedNetworks = ["eip155:42431"];
|
|
439
|
+
// Tempo Moderato
|
|
440
|
+
rpcUrl;
|
|
441
|
+
constructor() {
|
|
442
|
+
super();
|
|
443
|
+
this.rpcUrl = CHAINS.tempo_moderato.rpc;
|
|
444
|
+
}
|
|
445
|
+
async healthCheck() {
|
|
446
|
+
const start = Date.now();
|
|
447
|
+
try {
|
|
448
|
+
const response = await fetch(this.rpcUrl, {
|
|
449
|
+
method: "POST",
|
|
450
|
+
headers: { "Content-Type": "application/json" },
|
|
451
|
+
body: JSON.stringify({
|
|
452
|
+
jsonrpc: "2.0",
|
|
453
|
+
method: "eth_chainId",
|
|
454
|
+
params: [],
|
|
455
|
+
id: 1
|
|
456
|
+
})
|
|
457
|
+
});
|
|
458
|
+
const data = await response.json();
|
|
459
|
+
const chainId = parseInt(data.result, 16);
|
|
460
|
+
if (chainId !== 42431) {
|
|
461
|
+
return { healthy: false, error: `Wrong chainId: ${chainId}` };
|
|
462
|
+
}
|
|
463
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
464
|
+
} catch (error) {
|
|
465
|
+
return { healthy: false, error: String(error) };
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
async verify(paymentPayload, requirements) {
|
|
469
|
+
try {
|
|
470
|
+
const tempoPayload = paymentPayload.payload;
|
|
471
|
+
if (!tempoPayload?.txHash) {
|
|
472
|
+
return { valid: false, error: "Missing txHash in payment payload" };
|
|
473
|
+
}
|
|
474
|
+
const receipt = await this.getTransactionReceipt(tempoPayload.txHash);
|
|
475
|
+
if (!receipt) {
|
|
476
|
+
return { valid: false, error: "Transaction not found" };
|
|
477
|
+
}
|
|
478
|
+
if (receipt.status !== "0x1") {
|
|
479
|
+
return { valid: false, error: "Transaction failed" };
|
|
480
|
+
}
|
|
481
|
+
const transferLog = receipt.logs.find(
|
|
482
|
+
(log) => log.topics[0] === TRANSFER_EVENT_TOPIC
|
|
483
|
+
);
|
|
484
|
+
if (!transferLog) {
|
|
485
|
+
return { valid: false, error: "No Transfer event found" };
|
|
486
|
+
}
|
|
487
|
+
const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
|
|
488
|
+
const expectedTo = requirements.payTo.toLowerCase();
|
|
489
|
+
if (toAddress !== expectedTo) {
|
|
490
|
+
return {
|
|
491
|
+
valid: false,
|
|
492
|
+
error: `Wrong recipient: ${toAddress}, expected ${expectedTo}`
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
const amount = BigInt(transferLog.data);
|
|
496
|
+
const expectedAmount = BigInt(requirements.amount);
|
|
497
|
+
if (amount < expectedAmount) {
|
|
498
|
+
return {
|
|
499
|
+
valid: false,
|
|
500
|
+
error: `Insufficient amount: ${amount}, expected ${expectedAmount}`
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
const tokenAddress = transferLog.address.toLowerCase();
|
|
504
|
+
const expectedToken = requirements.asset.toLowerCase();
|
|
505
|
+
if (tokenAddress !== expectedToken) {
|
|
506
|
+
return {
|
|
507
|
+
valid: false,
|
|
508
|
+
error: `Wrong token: ${tokenAddress}, expected ${expectedToken}`
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
valid: true,
|
|
513
|
+
details: {
|
|
514
|
+
txHash: tempoPayload.txHash,
|
|
515
|
+
from: "0x" + transferLog.topics[1].slice(26),
|
|
516
|
+
to: toAddress,
|
|
517
|
+
amount: amount.toString(),
|
|
518
|
+
token: tokenAddress
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
} catch (error) {
|
|
522
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async settle(paymentPayload, requirements) {
|
|
526
|
+
const verifyResult = await this.verify(paymentPayload, requirements);
|
|
527
|
+
if (!verifyResult.valid) {
|
|
528
|
+
return { success: false, error: verifyResult.error };
|
|
529
|
+
}
|
|
530
|
+
const tempoPayload = paymentPayload.payload;
|
|
531
|
+
return {
|
|
532
|
+
success: true,
|
|
533
|
+
transaction: tempoPayload.txHash,
|
|
534
|
+
status: "settled"
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
async getTransactionReceipt(txHash) {
|
|
538
|
+
const response = await fetch(this.rpcUrl, {
|
|
539
|
+
method: "POST",
|
|
540
|
+
headers: { "Content-Type": "application/json" },
|
|
541
|
+
body: JSON.stringify({
|
|
542
|
+
jsonrpc: "2.0",
|
|
543
|
+
method: "eth_getTransactionReceipt",
|
|
544
|
+
params: [txHash],
|
|
545
|
+
id: 1
|
|
546
|
+
})
|
|
547
|
+
});
|
|
548
|
+
const data = await response.json();
|
|
549
|
+
return data.result;
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
// src/facilitators/bnb.ts
|
|
554
|
+
var import_accounts = require("viem/accounts");
|
|
555
|
+
var TRANSFER_EVENT_TOPIC2 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
556
|
+
var EIP712_DOMAIN = {
|
|
557
|
+
name: "MoltsPay",
|
|
558
|
+
version: "1"
|
|
559
|
+
};
|
|
560
|
+
var INTENT_TYPES = {
|
|
561
|
+
PaymentIntent: [
|
|
562
|
+
{ name: "from", type: "address" },
|
|
563
|
+
{ name: "to", type: "address" },
|
|
564
|
+
{ name: "amount", type: "uint256" },
|
|
565
|
+
{ name: "token", type: "address" },
|
|
566
|
+
{ name: "service", type: "string" },
|
|
567
|
+
{ name: "nonce", type: "uint256" },
|
|
568
|
+
{ name: "deadline", type: "uint256" }
|
|
569
|
+
]
|
|
570
|
+
};
|
|
571
|
+
var BNBFacilitator = class extends BaseFacilitator {
|
|
572
|
+
name = "bnb";
|
|
573
|
+
displayName = "BNB Smart Chain";
|
|
574
|
+
supportedNetworks = ["eip155:56", "eip155:97"];
|
|
575
|
+
// Mainnet + Testnet
|
|
576
|
+
serverPrivateKey;
|
|
577
|
+
spenderAddress = null;
|
|
578
|
+
chainConfigs;
|
|
579
|
+
constructor(serverPrivateKey) {
|
|
580
|
+
super();
|
|
581
|
+
this.serverPrivateKey = serverPrivateKey || process.env.BNB_SERVER_PRIVATE_KEY || "";
|
|
582
|
+
if (this.serverPrivateKey) {
|
|
583
|
+
const key = this.serverPrivateKey.startsWith("0x") ? this.serverPrivateKey : `0x${this.serverPrivateKey}`;
|
|
584
|
+
const account = (0, import_accounts.privateKeyToAccount)(key);
|
|
585
|
+
this.spenderAddress = account.address;
|
|
586
|
+
}
|
|
587
|
+
this.chainConfigs = {
|
|
588
|
+
56: { rpc: CHAINS.bnb.rpc, chain: CHAINS.bnb },
|
|
589
|
+
97: { rpc: CHAINS.bnb_testnet.rpc, chain: CHAINS.bnb_testnet }
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
async healthCheck() {
|
|
593
|
+
const start = Date.now();
|
|
594
|
+
try {
|
|
595
|
+
const response = await fetch(this.chainConfigs[56].rpc, {
|
|
596
|
+
method: "POST",
|
|
597
|
+
headers: { "Content-Type": "application/json" },
|
|
598
|
+
body: JSON.stringify({
|
|
599
|
+
jsonrpc: "2.0",
|
|
600
|
+
method: "eth_chainId",
|
|
601
|
+
params: [],
|
|
602
|
+
id: 1
|
|
603
|
+
})
|
|
604
|
+
});
|
|
605
|
+
const data = await response.json();
|
|
606
|
+
const chainId = parseInt(data.result, 16);
|
|
607
|
+
if (chainId !== 56) {
|
|
608
|
+
return { healthy: false, error: `Wrong chainId: ${chainId}` };
|
|
609
|
+
}
|
|
610
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
611
|
+
} catch (error) {
|
|
612
|
+
return { healthy: false, error: String(error) };
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Verify a payment intent signature (before service execution)
|
|
617
|
+
*
|
|
618
|
+
* This verifies:
|
|
619
|
+
* 1. Signature is valid for the intent
|
|
620
|
+
* 2. Client has approved server wallet
|
|
621
|
+
* 3. Client has sufficient balance
|
|
622
|
+
* 4. Intent hasn't expired
|
|
623
|
+
*/
|
|
624
|
+
async verify(paymentPayload, requirements) {
|
|
625
|
+
try {
|
|
626
|
+
const bnbPayload = paymentPayload.payload;
|
|
627
|
+
if (!bnbPayload?.intent) {
|
|
628
|
+
return { valid: false, error: "Missing intent in payment payload" };
|
|
629
|
+
}
|
|
630
|
+
const { intent, chainId } = bnbPayload;
|
|
631
|
+
const config = this.chainConfigs[chainId];
|
|
632
|
+
if (!config) {
|
|
633
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
634
|
+
}
|
|
635
|
+
if (intent.deadline < Date.now()) {
|
|
636
|
+
return { valid: false, error: "Intent expired" };
|
|
637
|
+
}
|
|
638
|
+
const recoveredAddress = await this.recoverIntentSigner(intent, chainId);
|
|
639
|
+
if (recoveredAddress.toLowerCase() !== intent.from.toLowerCase()) {
|
|
640
|
+
return { valid: false, error: "Invalid signature" };
|
|
641
|
+
}
|
|
642
|
+
if (intent.to.toLowerCase() !== requirements.payTo.toLowerCase()) {
|
|
643
|
+
return { valid: false, error: `Wrong recipient: ${intent.to}` };
|
|
644
|
+
}
|
|
645
|
+
if (BigInt(intent.amount) < BigInt(requirements.amount)) {
|
|
646
|
+
return { valid: false, error: `Insufficient amount: ${intent.amount}` };
|
|
647
|
+
}
|
|
648
|
+
if (intent.token.toLowerCase() !== requirements.asset.toLowerCase()) {
|
|
649
|
+
return { valid: false, error: `Wrong token: ${intent.token}` };
|
|
650
|
+
}
|
|
651
|
+
const serverAddress = await this.getServerAddress();
|
|
652
|
+
const allowance = await this.getAllowance(intent.from, serverAddress, intent.token, config.rpc);
|
|
653
|
+
if (BigInt(allowance) < BigInt(intent.amount)) {
|
|
654
|
+
return { valid: false, error: "Insufficient allowance. Run: npx moltspay init --chain bnb" };
|
|
655
|
+
}
|
|
656
|
+
const balance = await this.getBalance(intent.from, intent.token, config.rpc);
|
|
657
|
+
if (BigInt(balance) < BigInt(intent.amount)) {
|
|
658
|
+
return { valid: false, error: "Insufficient balance" };
|
|
659
|
+
}
|
|
660
|
+
return {
|
|
661
|
+
valid: true,
|
|
662
|
+
details: {
|
|
663
|
+
from: intent.from,
|
|
664
|
+
to: intent.to,
|
|
665
|
+
amount: intent.amount,
|
|
666
|
+
token: intent.token,
|
|
667
|
+
service: intent.service,
|
|
668
|
+
nonce: intent.nonce,
|
|
669
|
+
deadline: intent.deadline
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
} catch (error) {
|
|
673
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Settle a payment by executing transferFrom
|
|
678
|
+
*
|
|
679
|
+
* This is called AFTER the service has been successfully delivered.
|
|
680
|
+
* Server pays gas, transfers tokens from client to provider.
|
|
681
|
+
*/
|
|
682
|
+
async settle(paymentPayload, requirements) {
|
|
683
|
+
if (!this.serverPrivateKey) {
|
|
684
|
+
return { success: false, error: "Server wallet not configured (BNB_SERVER_PRIVATE_KEY)" };
|
|
685
|
+
}
|
|
686
|
+
try {
|
|
687
|
+
const verifyResult = await this.verify(paymentPayload, requirements);
|
|
688
|
+
if (!verifyResult.valid) {
|
|
689
|
+
return { success: false, error: verifyResult.error };
|
|
690
|
+
}
|
|
691
|
+
const bnbPayload = paymentPayload.payload;
|
|
692
|
+
const { intent, chainId } = bnbPayload;
|
|
693
|
+
const config = this.chainConfigs[chainId];
|
|
694
|
+
const txHash = await this.executeTransferFrom(
|
|
695
|
+
intent.from,
|
|
696
|
+
intent.to,
|
|
697
|
+
intent.amount,
|
|
698
|
+
intent.token,
|
|
699
|
+
config.rpc
|
|
700
|
+
);
|
|
701
|
+
return {
|
|
702
|
+
success: true,
|
|
703
|
+
transaction: txHash,
|
|
704
|
+
status: "settled"
|
|
705
|
+
};
|
|
706
|
+
} catch (error) {
|
|
707
|
+
return { success: false, error: `Settlement failed: ${error}` };
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Check if client has approved the server wallet
|
|
712
|
+
*/
|
|
713
|
+
async checkApproval(clientAddress, token, chainId) {
|
|
714
|
+
const config = this.chainConfigs[chainId];
|
|
715
|
+
if (!config) {
|
|
716
|
+
throw new Error(`Unsupported chainId: ${chainId}`);
|
|
717
|
+
}
|
|
718
|
+
const serverAddress = await this.getServerAddress();
|
|
719
|
+
const allowance = await this.getAllowance(clientAddress, serverAddress, token, config.rpc);
|
|
720
|
+
const minAllowance = BigInt("1000000000000000000000");
|
|
721
|
+
return {
|
|
722
|
+
approved: BigInt(allowance) >= minAllowance,
|
|
723
|
+
allowance
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Verify a completed transaction (for checking past payments)
|
|
728
|
+
*/
|
|
729
|
+
async verifyTransaction(txHash, expected, chainId) {
|
|
730
|
+
const config = this.chainConfigs[chainId];
|
|
731
|
+
if (!config) {
|
|
732
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
733
|
+
}
|
|
734
|
+
try {
|
|
735
|
+
const receipt = await this.getTransactionReceipt(txHash, config.rpc);
|
|
736
|
+
if (!receipt) {
|
|
737
|
+
return { valid: false, error: "Transaction not found" };
|
|
738
|
+
}
|
|
739
|
+
if (receipt.status !== "0x1") {
|
|
740
|
+
return { valid: false, error: "Transaction failed" };
|
|
741
|
+
}
|
|
742
|
+
const transferLog = receipt.logs.find(
|
|
743
|
+
(log) => log.topics[0] === TRANSFER_EVENT_TOPIC2 && log.address.toLowerCase() === expected.token.toLowerCase()
|
|
744
|
+
);
|
|
745
|
+
if (!transferLog) {
|
|
746
|
+
return { valid: false, error: "No Transfer event found" };
|
|
747
|
+
}
|
|
748
|
+
const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
|
|
749
|
+
if (toAddress !== expected.to.toLowerCase()) {
|
|
750
|
+
return { valid: false, error: `Wrong recipient: ${toAddress}` };
|
|
751
|
+
}
|
|
752
|
+
const amount = BigInt(transferLog.data);
|
|
753
|
+
if (amount < BigInt(expected.amount)) {
|
|
754
|
+
return { valid: false, error: `Insufficient amount: ${amount}` };
|
|
755
|
+
}
|
|
756
|
+
return {
|
|
757
|
+
valid: true,
|
|
758
|
+
details: {
|
|
759
|
+
txHash,
|
|
760
|
+
from: "0x" + transferLog.topics[1].slice(26),
|
|
761
|
+
to: toAddress,
|
|
762
|
+
amount: amount.toString(),
|
|
763
|
+
token: transferLog.address
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
} catch (error) {
|
|
767
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
// ==================== Private Methods ====================
|
|
771
|
+
/**
|
|
772
|
+
* Get the server's spender address (public, for 402 responses)
|
|
773
|
+
* Returns cached value computed at construction time.
|
|
774
|
+
*/
|
|
775
|
+
getSpenderAddress() {
|
|
776
|
+
return this.spenderAddress;
|
|
777
|
+
}
|
|
778
|
+
async getServerAddress() {
|
|
779
|
+
const { ethers } = await import("ethers");
|
|
780
|
+
const wallet = new ethers.Wallet(this.serverPrivateKey);
|
|
781
|
+
return wallet.address;
|
|
782
|
+
}
|
|
783
|
+
async recoverIntentSigner(intent, chainId) {
|
|
784
|
+
const { ethers } = await import("ethers");
|
|
785
|
+
const domain = {
|
|
786
|
+
...EIP712_DOMAIN,
|
|
787
|
+
chainId
|
|
788
|
+
};
|
|
789
|
+
const message = {
|
|
790
|
+
from: intent.from,
|
|
791
|
+
to: intent.to,
|
|
792
|
+
amount: intent.amount,
|
|
793
|
+
token: intent.token,
|
|
794
|
+
service: intent.service,
|
|
795
|
+
nonce: intent.nonce,
|
|
796
|
+
deadline: intent.deadline
|
|
797
|
+
};
|
|
798
|
+
const recoveredAddress = ethers.verifyTypedData(
|
|
799
|
+
domain,
|
|
800
|
+
INTENT_TYPES,
|
|
801
|
+
message,
|
|
802
|
+
intent.signature
|
|
803
|
+
);
|
|
804
|
+
return recoveredAddress;
|
|
805
|
+
}
|
|
806
|
+
async getAllowance(owner, spender, token, rpcUrl) {
|
|
807
|
+
const selector = "0xdd62ed3e";
|
|
808
|
+
const ownerPadded = owner.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
809
|
+
const spenderPadded = spender.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
810
|
+
const data = selector + ownerPadded + spenderPadded;
|
|
811
|
+
const response = await fetch(rpcUrl, {
|
|
812
|
+
method: "POST",
|
|
813
|
+
headers: { "Content-Type": "application/json" },
|
|
814
|
+
body: JSON.stringify({
|
|
815
|
+
jsonrpc: "2.0",
|
|
816
|
+
method: "eth_call",
|
|
817
|
+
params: [{ to: token, data }, "latest"],
|
|
818
|
+
id: 1
|
|
819
|
+
})
|
|
820
|
+
});
|
|
821
|
+
const result = await response.json();
|
|
822
|
+
return result.result || "0x0";
|
|
823
|
+
}
|
|
824
|
+
async getBalance(account, token, rpcUrl) {
|
|
825
|
+
const selector = "0x70a08231";
|
|
826
|
+
const accountPadded = account.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
827
|
+
const data = selector + accountPadded;
|
|
828
|
+
const response = await fetch(rpcUrl, {
|
|
829
|
+
method: "POST",
|
|
830
|
+
headers: { "Content-Type": "application/json" },
|
|
831
|
+
body: JSON.stringify({
|
|
832
|
+
jsonrpc: "2.0",
|
|
833
|
+
method: "eth_call",
|
|
834
|
+
params: [{ to: token, data }, "latest"],
|
|
835
|
+
id: 1
|
|
836
|
+
})
|
|
837
|
+
});
|
|
838
|
+
const result = await response.json();
|
|
839
|
+
return result.result || "0x0";
|
|
840
|
+
}
|
|
841
|
+
async executeTransferFrom(from, to, amount, token, rpcUrl) {
|
|
842
|
+
const { ethers } = await import("ethers");
|
|
843
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
844
|
+
const wallet = new ethers.Wallet(this.serverPrivateKey, provider);
|
|
845
|
+
const tokenContract = new ethers.Contract(token, [
|
|
846
|
+
"function transferFrom(address from, address to, uint256 amount) returns (bool)"
|
|
847
|
+
], wallet);
|
|
848
|
+
const tx = await tokenContract.transferFrom(from, to, amount);
|
|
849
|
+
const receipt = await tx.wait();
|
|
850
|
+
return receipt.hash;
|
|
851
|
+
}
|
|
852
|
+
async getTransactionReceipt(txHash, rpcUrl) {
|
|
853
|
+
const response = await fetch(rpcUrl, {
|
|
854
|
+
method: "POST",
|
|
855
|
+
headers: { "Content-Type": "application/json" },
|
|
856
|
+
body: JSON.stringify({
|
|
857
|
+
jsonrpc: "2.0",
|
|
858
|
+
method: "eth_getTransactionReceipt",
|
|
859
|
+
params: [txHash],
|
|
860
|
+
id: 1
|
|
861
|
+
})
|
|
862
|
+
});
|
|
863
|
+
const data = await response.json();
|
|
864
|
+
return data.result;
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
// src/facilitators/solana.ts
|
|
869
|
+
var import_web32 = require("@solana/web3.js");
|
|
870
|
+
var import_spl_token = require("@solana/spl-token");
|
|
871
|
+
|
|
872
|
+
// src/chains/solana.ts
|
|
873
|
+
var import_web3 = require("@solana/web3.js");
|
|
874
|
+
var SOLANA_CHAINS = {
|
|
875
|
+
solana: {
|
|
876
|
+
name: "Solana Mainnet",
|
|
877
|
+
cluster: "mainnet-beta",
|
|
878
|
+
rpc: "https://api.mainnet-beta.solana.com",
|
|
879
|
+
explorer: "https://solscan.io/account/",
|
|
880
|
+
explorerTx: "https://solscan.io/tx/",
|
|
881
|
+
tokens: {
|
|
882
|
+
USDC: {
|
|
883
|
+
mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
884
|
+
// Circle official USDC
|
|
885
|
+
decimals: 6
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
},
|
|
889
|
+
solana_devnet: {
|
|
890
|
+
name: "Solana Devnet",
|
|
891
|
+
cluster: "devnet",
|
|
892
|
+
rpc: "https://api.devnet.solana.com",
|
|
893
|
+
explorer: "https://solscan.io/account/",
|
|
894
|
+
explorerTx: "https://solscan.io/tx/",
|
|
895
|
+
tokens: {
|
|
896
|
+
USDC: {
|
|
897
|
+
// Circle's devnet USDC (if not available, we'll deploy our own test token)
|
|
898
|
+
mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
899
|
+
decimals: 6
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
// src/facilitators/solana.ts
|
|
906
|
+
var SolanaFacilitator = class extends BaseFacilitator {
|
|
907
|
+
name = "solana";
|
|
908
|
+
displayName = "Solana Direct";
|
|
909
|
+
supportedNetworks = ["solana:mainnet", "solana:devnet"];
|
|
910
|
+
connections = /* @__PURE__ */ new Map();
|
|
911
|
+
feePayerKeypair;
|
|
912
|
+
constructor(config) {
|
|
913
|
+
super();
|
|
914
|
+
this.feePayerKeypair = config?.feePayerKeypair;
|
|
915
|
+
for (const [chain, config2] of Object.entries(SOLANA_CHAINS)) {
|
|
916
|
+
this.connections.set(
|
|
917
|
+
chain,
|
|
918
|
+
new import_web32.Connection(config2.rpc, "confirmed")
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
if (this.feePayerKeypair) {
|
|
922
|
+
console.log(`[SolanaFacilitator] Gasless mode enabled. Fee payer: ${this.feePayerKeypair.publicKey.toBase58()}`);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Get fee payer public key (for gasless transactions)
|
|
927
|
+
*/
|
|
928
|
+
getFeePayerPubkey() {
|
|
929
|
+
return this.feePayerKeypair?.publicKey.toBase58() || null;
|
|
930
|
+
}
|
|
931
|
+
getConnection(chain) {
|
|
932
|
+
const conn = this.connections.get(chain);
|
|
933
|
+
if (!conn) {
|
|
934
|
+
throw new Error(`No connection for chain: ${chain}`);
|
|
935
|
+
}
|
|
936
|
+
return conn;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Convert our chain name to network identifier
|
|
940
|
+
*/
|
|
941
|
+
static chainToNetwork(chain) {
|
|
942
|
+
return chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Convert network identifier to chain name
|
|
946
|
+
*/
|
|
947
|
+
static networkToChain(network) {
|
|
948
|
+
if (network === "solana:mainnet") return "solana";
|
|
949
|
+
if (network === "solana:devnet") return "solana_devnet";
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
async healthCheck() {
|
|
953
|
+
const start = Date.now();
|
|
954
|
+
try {
|
|
955
|
+
const conn = this.getConnection("solana_devnet");
|
|
956
|
+
await conn.getSlot();
|
|
957
|
+
return {
|
|
958
|
+
healthy: true,
|
|
959
|
+
latencyMs: Date.now() - start
|
|
960
|
+
};
|
|
961
|
+
} catch (error) {
|
|
962
|
+
return {
|
|
963
|
+
healthy: false,
|
|
964
|
+
error: error.message
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Verify a Solana payment
|
|
970
|
+
*
|
|
971
|
+
* Checks:
|
|
972
|
+
* 1. Transaction is valid and properly signed
|
|
973
|
+
* 2. Transfer instruction matches expected amount and recipient
|
|
974
|
+
*/
|
|
975
|
+
async verify(paymentPayload, requirements) {
|
|
976
|
+
try {
|
|
977
|
+
const solanaPayload = paymentPayload.payload;
|
|
978
|
+
if (!solanaPayload || !solanaPayload.signedTransaction) {
|
|
979
|
+
return { valid: false, error: "Missing signed transaction" };
|
|
980
|
+
}
|
|
981
|
+
const chain = solanaPayload.chain || "solana_devnet";
|
|
982
|
+
const chainConfig = SOLANA_CHAINS[chain];
|
|
983
|
+
if (!chainConfig) {
|
|
984
|
+
return { valid: false, error: `Invalid chain: ${chain}` };
|
|
985
|
+
}
|
|
986
|
+
const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
|
|
987
|
+
let tx;
|
|
988
|
+
try {
|
|
989
|
+
tx = import_web32.Transaction.from(txBuffer);
|
|
990
|
+
} catch {
|
|
991
|
+
tx = import_web32.VersionedTransaction.deserialize(txBuffer);
|
|
992
|
+
}
|
|
993
|
+
if (tx instanceof import_web32.Transaction) {
|
|
994
|
+
const hasAnySignature = tx.signatures.some(
|
|
995
|
+
(sig) => sig.signature && !sig.signature.every((b) => b === 0)
|
|
996
|
+
);
|
|
997
|
+
if (!hasAnySignature) {
|
|
998
|
+
return { valid: false, error: "Transaction not signed" };
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
const expectedAmount = BigInt(requirements.amount);
|
|
1002
|
+
const expectedRecipient = new import_web32.PublicKey(requirements.payTo);
|
|
1003
|
+
return {
|
|
1004
|
+
valid: true,
|
|
1005
|
+
details: {
|
|
1006
|
+
chain,
|
|
1007
|
+
sender: solanaPayload.sender,
|
|
1008
|
+
recipient: requirements.payTo,
|
|
1009
|
+
amount: requirements.amount
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
return { valid: false, error: error.message };
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Settle a Solana payment
|
|
1018
|
+
*
|
|
1019
|
+
* Submits the signed transaction to the network.
|
|
1020
|
+
* In gasless mode, adds fee payer signature before submitting.
|
|
1021
|
+
*/
|
|
1022
|
+
async settle(paymentPayload, requirements) {
|
|
1023
|
+
try {
|
|
1024
|
+
const solanaPayload = paymentPayload.payload;
|
|
1025
|
+
if (!solanaPayload || !solanaPayload.signedTransaction) {
|
|
1026
|
+
return { success: false, error: "Missing signed transaction" };
|
|
1027
|
+
}
|
|
1028
|
+
const chain = solanaPayload.chain || "solana_devnet";
|
|
1029
|
+
const connection = this.getConnection(chain);
|
|
1030
|
+
const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
|
|
1031
|
+
let txToSend;
|
|
1032
|
+
try {
|
|
1033
|
+
const tx = import_web32.Transaction.from(txBuffer);
|
|
1034
|
+
if (this.feePayerKeypair && tx.feePayer) {
|
|
1035
|
+
const feePayerPubkey = this.feePayerKeypair.publicKey.toBase58();
|
|
1036
|
+
const txFeePayer = tx.feePayer.toBase58();
|
|
1037
|
+
if (txFeePayer === feePayerPubkey) {
|
|
1038
|
+
console.log(`[SolanaFacilitator] Gasless mode: adding fee payer signature`);
|
|
1039
|
+
tx.partialSign(this.feePayerKeypair);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
txToSend = tx.serialize();
|
|
1043
|
+
} catch (e) {
|
|
1044
|
+
txToSend = txBuffer;
|
|
1045
|
+
}
|
|
1046
|
+
const signature = await connection.sendRawTransaction(txToSend, {
|
|
1047
|
+
skipPreflight: false,
|
|
1048
|
+
preflightCommitment: "confirmed"
|
|
1049
|
+
});
|
|
1050
|
+
const confirmation = await connection.confirmTransaction(signature, "confirmed");
|
|
1051
|
+
if (confirmation.value.err) {
|
|
1052
|
+
return {
|
|
1053
|
+
success: false,
|
|
1054
|
+
error: `Transaction failed: ${JSON.stringify(confirmation.value.err)}`,
|
|
1055
|
+
transaction: signature
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
return {
|
|
1059
|
+
success: true,
|
|
1060
|
+
transaction: signature,
|
|
1061
|
+
status: "confirmed"
|
|
1062
|
+
};
|
|
1063
|
+
} catch (error) {
|
|
1064
|
+
return { success: false, error: error.message };
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
supportsNetwork(network) {
|
|
1068
|
+
return this.supportedNetworks.includes(network);
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
|
|
266
1072
|
// src/facilitators/registry.ts
|
|
1073
|
+
var import_web33 = require("@solana/web3.js");
|
|
1074
|
+
var import_bs58 = __toESM(require("bs58"));
|
|
267
1075
|
var FacilitatorRegistry = class {
|
|
268
1076
|
factories = /* @__PURE__ */ new Map();
|
|
269
1077
|
instances = /* @__PURE__ */ new Map();
|
|
@@ -271,7 +1079,21 @@ var FacilitatorRegistry = class {
|
|
|
271
1079
|
roundRobinIndex = 0;
|
|
272
1080
|
constructor(selection) {
|
|
273
1081
|
this.registerFactory("cdp", (config) => new CDPFacilitator(config));
|
|
274
|
-
this.
|
|
1082
|
+
this.registerFactory("tempo", () => new TempoFacilitator());
|
|
1083
|
+
this.registerFactory("bnb", (config) => new BNBFacilitator(config?.serverPrivateKey));
|
|
1084
|
+
this.registerFactory("solana", (config) => {
|
|
1085
|
+
let feePayerKeypair;
|
|
1086
|
+
const feePayerKey = config?.feePayerPrivateKey || process.env.SOLANA_FEE_PAYER_KEY;
|
|
1087
|
+
if (feePayerKey) {
|
|
1088
|
+
try {
|
|
1089
|
+
feePayerKeypair = import_web33.Keypair.fromSecretKey(import_bs58.default.decode(feePayerKey));
|
|
1090
|
+
} catch (e) {
|
|
1091
|
+
console.warn(`[SolanaFacilitator] Invalid fee payer key: ${e.message}`);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return new SolanaFacilitator({ feePayerKeypair });
|
|
1095
|
+
});
|
|
1096
|
+
this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
|
|
275
1097
|
}
|
|
276
1098
|
/**
|
|
277
1099
|
* Register a new facilitator factory
|
|
@@ -492,6 +1314,9 @@ var X402_VERSION2 = 2;
|
|
|
492
1314
|
var PAYMENT_REQUIRED_HEADER = "x-payment-required";
|
|
493
1315
|
var PAYMENT_HEADER = "x-payment";
|
|
494
1316
|
var PAYMENT_RESPONSE_HEADER = "x-payment-response";
|
|
1317
|
+
var MPP_AUTH_HEADER = "authorization";
|
|
1318
|
+
var MPP_WWW_AUTH_HEADER = "www-authenticate";
|
|
1319
|
+
var MPP_RECEIPT_HEADER = "payment-receipt";
|
|
495
1320
|
var TOKEN_ADDRESSES = {
|
|
496
1321
|
"eip155:8453": {
|
|
497
1322
|
USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
@@ -505,13 +1330,47 @@ var TOKEN_ADDRESSES = {
|
|
|
505
1330
|
"eip155:137": {
|
|
506
1331
|
USDC: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
507
1332
|
USDT: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F"
|
|
1333
|
+
},
|
|
1334
|
+
"eip155:42431": {
|
|
1335
|
+
// Tempo Moderato testnet - TIP-20 stablecoins
|
|
1336
|
+
USDC: "0x20c0000000000000000000000000000000000000",
|
|
1337
|
+
// pathUSD
|
|
1338
|
+
USDT: "0x20c0000000000000000000000000000000000001"
|
|
1339
|
+
// alphaUSD
|
|
1340
|
+
},
|
|
1341
|
+
// BNB Smart Chain mainnet
|
|
1342
|
+
"eip155:56": {
|
|
1343
|
+
USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
1344
|
+
USDT: "0x55d398326f99059fF775485246999027B3197955"
|
|
1345
|
+
},
|
|
1346
|
+
// BNB Smart Chain testnet
|
|
1347
|
+
"eip155:97": {
|
|
1348
|
+
USDC: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
1349
|
+
USDT: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd"
|
|
1350
|
+
},
|
|
1351
|
+
// Solana networks use mint addresses (SPL tokens)
|
|
1352
|
+
"solana:mainnet": {
|
|
1353
|
+
USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
|
1354
|
+
// Circle USDC
|
|
1355
|
+
},
|
|
1356
|
+
"solana:devnet": {
|
|
1357
|
+
USDC: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"
|
|
1358
|
+
// Devnet USDC
|
|
508
1359
|
}
|
|
509
1360
|
};
|
|
510
1361
|
var CHAIN_TO_NETWORK = {
|
|
511
1362
|
"base": "eip155:8453",
|
|
512
1363
|
"base_sepolia": "eip155:84532",
|
|
513
|
-
"polygon": "eip155:137"
|
|
1364
|
+
"polygon": "eip155:137",
|
|
1365
|
+
"tempo_moderato": "eip155:42431",
|
|
1366
|
+
"bnb": "eip155:56",
|
|
1367
|
+
"bnb_testnet": "eip155:97",
|
|
1368
|
+
"solana": "solana:mainnet",
|
|
1369
|
+
"solana_devnet": "solana:devnet"
|
|
514
1370
|
};
|
|
1371
|
+
function isSolanaNetwork(network) {
|
|
1372
|
+
return network.startsWith("solana:");
|
|
1373
|
+
}
|
|
515
1374
|
var TOKEN_DOMAINS = {
|
|
516
1375
|
// Base mainnet
|
|
517
1376
|
"eip155:8453": {
|
|
@@ -528,6 +1387,21 @@ var TOKEN_DOMAINS = {
|
|
|
528
1387
|
"eip155:137": {
|
|
529
1388
|
USDC: { name: "USD Coin", version: "2" },
|
|
530
1389
|
USDT: { name: "(PoS) Tether USD", version: "2" }
|
|
1390
|
+
},
|
|
1391
|
+
// Tempo Moderato testnet - TIP-20 stablecoins
|
|
1392
|
+
"eip155:42431": {
|
|
1393
|
+
USDC: { name: "pathUSD", version: "1" },
|
|
1394
|
+
USDT: { name: "alphaUSD", version: "1" }
|
|
1395
|
+
},
|
|
1396
|
+
// BNB Smart Chain mainnet
|
|
1397
|
+
"eip155:56": {
|
|
1398
|
+
USDC: { name: "USD Coin", version: "1" },
|
|
1399
|
+
USDT: { name: "Tether USD", version: "1" }
|
|
1400
|
+
},
|
|
1401
|
+
// BNB Smart Chain testnet
|
|
1402
|
+
"eip155:97": {
|
|
1403
|
+
USDC: { name: "USD Coin", version: "1" },
|
|
1404
|
+
USDT: { name: "Tether USD", version: "1" }
|
|
531
1405
|
}
|
|
532
1406
|
};
|
|
533
1407
|
function getTokenDomain(network, token) {
|
|
@@ -585,9 +1459,11 @@ var MoltsPayServer = class {
|
|
|
585
1459
|
};
|
|
586
1460
|
this.useMainnet = process.env.USE_MAINNET?.toLowerCase() === "true";
|
|
587
1461
|
this.networkId = this.useMainnet ? "eip155:8453" : "eip155:84532";
|
|
1462
|
+
const defaultFallback = ["tempo", "bnb", "solana"];
|
|
1463
|
+
const envFallback = process.env.FACILITATOR_FALLBACK?.split(",").filter(Boolean);
|
|
588
1464
|
const facilitatorConfig = options.facilitators || {
|
|
589
1465
|
primary: process.env.FACILITATOR_PRIMARY || "cdp",
|
|
590
|
-
fallback:
|
|
1466
|
+
fallback: envFallback || defaultFallback,
|
|
591
1467
|
strategy: process.env.FACILITATOR_STRATEGY || "failover",
|
|
592
1468
|
config: {
|
|
593
1469
|
cdp: { useMainnet: this.useMainnet }
|
|
@@ -626,12 +1502,20 @@ var MoltsPayServer = class {
|
|
|
626
1502
|
*/
|
|
627
1503
|
getProviderChains() {
|
|
628
1504
|
const provider = this.manifest.provider;
|
|
1505
|
+
const getWalletForChain = (chainName, explicitWallet) => {
|
|
1506
|
+
if (explicitWallet) return explicitWallet;
|
|
1507
|
+
if ((chainName === "solana" || chainName === "solana_devnet") && provider.solana_wallet) {
|
|
1508
|
+
return provider.solana_wallet;
|
|
1509
|
+
}
|
|
1510
|
+
return provider.wallet;
|
|
1511
|
+
};
|
|
629
1512
|
if (provider.chains && provider.chains.length > 0) {
|
|
630
1513
|
return provider.chains.map((c) => {
|
|
631
1514
|
const chainName = typeof c === "string" ? c : c.chain;
|
|
1515
|
+
const explicitWallet = typeof c === "object" ? c.wallet : null;
|
|
632
1516
|
return {
|
|
633
1517
|
network: CHAIN_TO_NETWORK[chainName] || "eip155:8453",
|
|
634
|
-
wallet: (
|
|
1518
|
+
wallet: getWalletForChain(chainName, explicitWallet || void 0),
|
|
635
1519
|
tokens: (typeof c === "object" ? c.tokens : null) || ["USDC"]
|
|
636
1520
|
};
|
|
637
1521
|
});
|
|
@@ -640,7 +1524,7 @@ var MoltsPayServer = class {
|
|
|
640
1524
|
const network = CHAIN_TO_NETWORK[chain] || this.networkId;
|
|
641
1525
|
return [{
|
|
642
1526
|
network,
|
|
643
|
-
wallet:
|
|
1527
|
+
wallet: getWalletForChain(chain),
|
|
644
1528
|
tokens: ["USDC"]
|
|
645
1529
|
}];
|
|
646
1530
|
}
|
|
@@ -681,8 +1565,8 @@ var MoltsPayServer = class {
|
|
|
681
1565
|
async handleRequest(req, res) {
|
|
682
1566
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
683
1567
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
684
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment");
|
|
685
|
-
res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response");
|
|
1568
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization");
|
|
1569
|
+
res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt");
|
|
686
1570
|
if (req.method === "OPTIONS") {
|
|
687
1571
|
res.writeHead(204);
|
|
688
1572
|
res.end();
|
|
@@ -711,7 +1595,16 @@ var MoltsPayServer = class {
|
|
|
711
1595
|
}
|
|
712
1596
|
const body = await this.readBody(req);
|
|
713
1597
|
const paymentHeader = req.headers[PAYMENT_HEADER];
|
|
714
|
-
|
|
1598
|
+
const authHeader = req.headers[MPP_AUTH_HEADER];
|
|
1599
|
+
return await this.handleProxy(body, paymentHeader, authHeader, res);
|
|
1600
|
+
}
|
|
1601
|
+
const servicePath = url.pathname.replace(/^\//, "");
|
|
1602
|
+
const skill = this.skills.get(servicePath);
|
|
1603
|
+
if (skill && (req.method === "POST" || req.method === "GET")) {
|
|
1604
|
+
const body = req.method === "POST" ? await this.readBody(req) : {};
|
|
1605
|
+
const authHeader = req.headers[MPP_AUTH_HEADER];
|
|
1606
|
+
const x402Header = req.headers[PAYMENT_HEADER];
|
|
1607
|
+
return await this.handleMPPRequest(skill, body, authHeader, x402Header, res);
|
|
715
1608
|
}
|
|
716
1609
|
this.sendJson(res, 404, { error: "Not found" });
|
|
717
1610
|
} catch (err) {
|
|
@@ -740,7 +1633,9 @@ var MoltsPayServer = class {
|
|
|
740
1633
|
name: this.manifest.provider.name,
|
|
741
1634
|
description: this.manifest.provider.description,
|
|
742
1635
|
wallet: this.manifest.provider.wallet,
|
|
743
|
-
chain: this.manifest.provider.chain || "base"
|
|
1636
|
+
chain: this.manifest.provider.chain || "base",
|
|
1637
|
+
solana_wallet: this.manifest.provider.solana_wallet,
|
|
1638
|
+
chains: this.manifest.provider.chains
|
|
744
1639
|
},
|
|
745
1640
|
services,
|
|
746
1641
|
endpoints: {
|
|
@@ -853,6 +1748,21 @@ var MoltsPayServer = class {
|
|
|
853
1748
|
});
|
|
854
1749
|
}
|
|
855
1750
|
console.log(`[MoltsPay] Verified by ${verifyResult.facilitator}`);
|
|
1751
|
+
const isSolana = isSolanaNetwork(paymentNetwork);
|
|
1752
|
+
let settlement = null;
|
|
1753
|
+
if (isSolana) {
|
|
1754
|
+
console.log(`[MoltsPay] Solana detected - settling payment FIRST (blockhash expiry protection)`);
|
|
1755
|
+
try {
|
|
1756
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
1757
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
1758
|
+
} catch (err) {
|
|
1759
|
+
console.error("[MoltsPay] Solana settlement failed:", err.message);
|
|
1760
|
+
return this.sendJson(res, 402, {
|
|
1761
|
+
error: "Payment settlement failed",
|
|
1762
|
+
message: err.message
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
856
1766
|
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
857
1767
|
console.log(`[MoltsPay] Executing skill: ${service} (timeout: ${timeoutSeconds}s)`);
|
|
858
1768
|
let result;
|
|
@@ -867,16 +1777,19 @@ var MoltsPayServer = class {
|
|
|
867
1777
|
console.error("[MoltsPay] Skill execution failed:", err.message);
|
|
868
1778
|
return this.sendJson(res, 500, {
|
|
869
1779
|
error: "Service execution failed",
|
|
870
|
-
message: err.message
|
|
1780
|
+
message: err.message,
|
|
1781
|
+
paymentSettled: isSolana ? true : false,
|
|
1782
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
871
1783
|
});
|
|
872
1784
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1785
|
+
if (!isSolana) {
|
|
1786
|
+
console.log(`[MoltsPay] Skill succeeded, settling payment...`);
|
|
1787
|
+
try {
|
|
1788
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
1789
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
1790
|
+
} catch (err) {
|
|
1791
|
+
console.error("[MoltsPay] Settlement failed:", err.message);
|
|
1792
|
+
}
|
|
880
1793
|
}
|
|
881
1794
|
const responseHeaders = {};
|
|
882
1795
|
if (settlement?.success) {
|
|
@@ -896,6 +1809,187 @@ var MoltsPayServer = class {
|
|
|
896
1809
|
payment: settlement?.success ? { transaction: settlement.transaction, status: "settled", facilitator: settlement.facilitator } : { status: "pending" }
|
|
897
1810
|
}, responseHeaders);
|
|
898
1811
|
}
|
|
1812
|
+
/**
|
|
1813
|
+
* Handle MPP (Machine Payments Protocol) request
|
|
1814
|
+
* Supports both x402 and MPP protocols on service endpoints
|
|
1815
|
+
*/
|
|
1816
|
+
async handleMPPRequest(skill, body, authHeader, x402Header, res) {
|
|
1817
|
+
const config = skill.config;
|
|
1818
|
+
const params = body || {};
|
|
1819
|
+
if (x402Header) {
|
|
1820
|
+
return await this.handleExecute({ service: config.id, params }, x402Header, res);
|
|
1821
|
+
}
|
|
1822
|
+
if (authHeader && authHeader.toLowerCase().startsWith("payment ")) {
|
|
1823
|
+
return await this.handleMPPPayment(skill, params, authHeader, res);
|
|
1824
|
+
}
|
|
1825
|
+
return this.sendMPPPaymentRequired(config, res);
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* Handle MPP payment verification and service execution
|
|
1829
|
+
*/
|
|
1830
|
+
async handleMPPPayment(skill, params, authHeader, res) {
|
|
1831
|
+
const config = skill.config;
|
|
1832
|
+
const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
|
|
1833
|
+
if (!credentialMatch) {
|
|
1834
|
+
return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
|
|
1835
|
+
}
|
|
1836
|
+
let mppCredential;
|
|
1837
|
+
try {
|
|
1838
|
+
const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
1839
|
+
const decoded = Buffer.from(base64, "base64").toString("utf-8");
|
|
1840
|
+
mppCredential = JSON.parse(decoded);
|
|
1841
|
+
} catch (err) {
|
|
1842
|
+
console.error("[MoltsPay] Failed to parse MPP credential:", err);
|
|
1843
|
+
return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
|
|
1844
|
+
}
|
|
1845
|
+
let txHash;
|
|
1846
|
+
if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
|
|
1847
|
+
txHash = mppCredential.payload.hash;
|
|
1848
|
+
} else if (mppCredential.payload?.type === "transaction") {
|
|
1849
|
+
return this.sendJson(res, 400, {
|
|
1850
|
+
error: "Transaction type not supported. Please use push mode (hash type)."
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
if (!txHash) {
|
|
1854
|
+
return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
|
|
1855
|
+
}
|
|
1856
|
+
let chainId = mppCredential.challenge?.request?.methodDetails?.chainId;
|
|
1857
|
+
if (!chainId && mppCredential.source) {
|
|
1858
|
+
const chainMatch = mppCredential.source.match(/eip155:(\d+)/);
|
|
1859
|
+
if (chainMatch) chainId = parseInt(chainMatch[1], 10);
|
|
1860
|
+
}
|
|
1861
|
+
chainId = chainId || 42431;
|
|
1862
|
+
const network = `eip155:${chainId}`;
|
|
1863
|
+
if (!this.isNetworkAccepted(network)) {
|
|
1864
|
+
return this.sendJson(res, 402, {
|
|
1865
|
+
error: `Network not accepted: ${network}`
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
const requirements = this.buildPaymentRequirements(
|
|
1869
|
+
config,
|
|
1870
|
+
network,
|
|
1871
|
+
this.getWalletForNetwork(network),
|
|
1872
|
+
"USDC"
|
|
1873
|
+
);
|
|
1874
|
+
const paymentPayload = {
|
|
1875
|
+
x402Version: X402_VERSION2,
|
|
1876
|
+
scheme: "exact",
|
|
1877
|
+
network,
|
|
1878
|
+
payload: {
|
|
1879
|
+
txHash,
|
|
1880
|
+
chainId
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
console.log(`[MoltsPay] Verifying MPP payment: txHash=${txHash}, chainId=${chainId}`);
|
|
1884
|
+
const verification = await this.registry.verify(paymentPayload, requirements);
|
|
1885
|
+
if (!verification.valid) {
|
|
1886
|
+
return this.sendJson(res, 402, {
|
|
1887
|
+
error: `Payment verification failed: ${verification.error}`
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
console.log(`[MoltsPay] Payment verified! Executing service: ${config.id}`);
|
|
1891
|
+
let result;
|
|
1892
|
+
try {
|
|
1893
|
+
result = await skill.handler(params);
|
|
1894
|
+
} catch (err) {
|
|
1895
|
+
console.error(`[MoltsPay] Skill execution error:`, err);
|
|
1896
|
+
return this.sendJson(res, 500, {
|
|
1897
|
+
error: `Service execution failed: ${err.message}`
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
const receipt = {
|
|
1901
|
+
success: true,
|
|
1902
|
+
txHash,
|
|
1903
|
+
network,
|
|
1904
|
+
facilitator: verification.facilitator
|
|
1905
|
+
};
|
|
1906
|
+
const receiptEncoded = Buffer.from(JSON.stringify(receipt)).toString("base64");
|
|
1907
|
+
res.writeHead(200, {
|
|
1908
|
+
"Content-Type": "application/json",
|
|
1909
|
+
[MPP_RECEIPT_HEADER]: receiptEncoded
|
|
1910
|
+
});
|
|
1911
|
+
res.end(JSON.stringify({
|
|
1912
|
+
success: true,
|
|
1913
|
+
result,
|
|
1914
|
+
payment: {
|
|
1915
|
+
txHash,
|
|
1916
|
+
status: "verified",
|
|
1917
|
+
facilitator: verification.facilitator
|
|
1918
|
+
}
|
|
1919
|
+
}, null, 2));
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1922
|
+
* Return 402 with both x402 and MPP payment requirements
|
|
1923
|
+
*/
|
|
1924
|
+
sendMPPPaymentRequired(config, res) {
|
|
1925
|
+
const acceptedTokens = getAcceptedCurrencies(config);
|
|
1926
|
+
const providerChains = this.getProviderChains();
|
|
1927
|
+
const accepts = [];
|
|
1928
|
+
for (const chainConfig of providerChains) {
|
|
1929
|
+
for (const token of acceptedTokens) {
|
|
1930
|
+
if (chainConfig.tokens.includes(token)) {
|
|
1931
|
+
accepts.push(this.buildPaymentRequirements(config, chainConfig.network, chainConfig.wallet, token));
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
const x402PaymentRequired = {
|
|
1936
|
+
x402Version: X402_VERSION2,
|
|
1937
|
+
accepts,
|
|
1938
|
+
acceptedCurrencies: acceptedTokens,
|
|
1939
|
+
resource: {
|
|
1940
|
+
url: `/${config.id}`,
|
|
1941
|
+
description: `${config.name} - $${config.price} ${config.currency}`
|
|
1942
|
+
}
|
|
1943
|
+
};
|
|
1944
|
+
const x402Encoded = Buffer.from(JSON.stringify(x402PaymentRequired)).toString("base64");
|
|
1945
|
+
const tempoChain = providerChains.find((c) => c.network === "eip155:42431");
|
|
1946
|
+
let mppWwwAuth = "";
|
|
1947
|
+
if (tempoChain) {
|
|
1948
|
+
const challengeId = this.generateChallengeId();
|
|
1949
|
+
const amountInUnits = Math.floor(config.price * 1e6).toString();
|
|
1950
|
+
const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
|
|
1951
|
+
const mppRequest = {
|
|
1952
|
+
amount: amountInUnits,
|
|
1953
|
+
currency: tokenAddress,
|
|
1954
|
+
methodDetails: {
|
|
1955
|
+
chainId: 42431,
|
|
1956
|
+
feePayer: true
|
|
1957
|
+
},
|
|
1958
|
+
recipient: tempoChain.wallet
|
|
1959
|
+
};
|
|
1960
|
+
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
1961
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
1962
|
+
mppWwwAuth = `Payment id="${challengeId}", realm="${this.manifest.provider.name}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
1963
|
+
}
|
|
1964
|
+
const headers = {
|
|
1965
|
+
"Content-Type": "application/problem+json",
|
|
1966
|
+
[PAYMENT_REQUIRED_HEADER]: x402Encoded
|
|
1967
|
+
};
|
|
1968
|
+
if (mppWwwAuth) {
|
|
1969
|
+
headers[MPP_WWW_AUTH_HEADER] = mppWwwAuth;
|
|
1970
|
+
}
|
|
1971
|
+
res.writeHead(402, headers);
|
|
1972
|
+
res.end(JSON.stringify({
|
|
1973
|
+
type: "https://paymentauth.org/problems/payment-required",
|
|
1974
|
+
title: "Payment Required",
|
|
1975
|
+
status: 402,
|
|
1976
|
+
detail: `Payment is required (${config.name}).`,
|
|
1977
|
+
service: config.id,
|
|
1978
|
+
price: config.price,
|
|
1979
|
+
currency: config.currency,
|
|
1980
|
+
acceptedCurrencies: acceptedTokens
|
|
1981
|
+
}, null, 2));
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* Generate a unique challenge ID for MPP
|
|
1985
|
+
*/
|
|
1986
|
+
generateChallengeId() {
|
|
1987
|
+
const bytes = new Uint8Array(24);
|
|
1988
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1989
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
1990
|
+
}
|
|
1991
|
+
return Buffer.from(bytes).toString("base64url");
|
|
1992
|
+
}
|
|
899
1993
|
/**
|
|
900
1994
|
* Return 402 with x402 payment requirements (v2 format)
|
|
901
1995
|
* Includes requirements for all chains and all accepted currencies
|
|
@@ -971,7 +2065,7 @@ var MoltsPayServer = class {
|
|
|
971
2065
|
const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
|
|
972
2066
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
973
2067
|
const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
|
|
974
|
-
|
|
2068
|
+
const requirements = {
|
|
975
2069
|
scheme: "exact",
|
|
976
2070
|
network: selectedNetwork,
|
|
977
2071
|
asset: tokenAddress,
|
|
@@ -980,6 +2074,27 @@ var MoltsPayServer = class {
|
|
|
980
2074
|
maxTimeoutSeconds: 300,
|
|
981
2075
|
extra: tokenDomain
|
|
982
2076
|
};
|
|
2077
|
+
if (selectedNetwork === "solana:mainnet" || selectedNetwork === "solana:devnet") {
|
|
2078
|
+
const solanaFacilitator = this.registry.get("solana");
|
|
2079
|
+
const feePayerPubkey = solanaFacilitator?.getFeePayerPubkey?.();
|
|
2080
|
+
if (feePayerPubkey) {
|
|
2081
|
+
requirements.extra = {
|
|
2082
|
+
...requirements.extra || {},
|
|
2083
|
+
solanaFeePayer: feePayerPubkey
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
if (selectedNetwork === "eip155:56" || selectedNetwork === "eip155:97") {
|
|
2088
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
2089
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
2090
|
+
if (spenderAddress) {
|
|
2091
|
+
requirements.extra = {
|
|
2092
|
+
...requirements.extra || {},
|
|
2093
|
+
bnbSpender: spenderAddress
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
return requirements;
|
|
983
2098
|
}
|
|
984
2099
|
/**
|
|
985
2100
|
* Detect which token is being used in the payment
|
|
@@ -1045,31 +2160,42 @@ var MoltsPayServer = class {
|
|
|
1045
2160
|
/**
|
|
1046
2161
|
* POST /proxy - Handle payment for external services (moltspay-creators)
|
|
1047
2162
|
*
|
|
1048
|
-
* This endpoint allows other services to delegate x402 payment handling.
|
|
2163
|
+
* This endpoint allows other services to delegate x402/MPP payment handling.
|
|
1049
2164
|
* It does NOT execute any skill - just handles payment verification/settlement.
|
|
1050
2165
|
*
|
|
1051
2166
|
* Request body:
|
|
1052
2167
|
* { wallet, amount, currency, chain, memo, serviceId, description }
|
|
1053
2168
|
*
|
|
1054
|
-
*
|
|
1055
|
-
*
|
|
2169
|
+
* For x402 (base, polygon, base_sepolia):
|
|
2170
|
+
* Without X-Payment header: returns 402 with X-Payment-Required
|
|
2171
|
+
* With X-Payment header: verifies payment via CDP
|
|
2172
|
+
*
|
|
2173
|
+
* For MPP (tempo_moderato):
|
|
2174
|
+
* Without Authorization header: returns 402 with WWW-Authenticate
|
|
2175
|
+
* With Authorization: Payment header: verifies tx on Tempo chain
|
|
1056
2176
|
*/
|
|
1057
|
-
async handleProxy(body, paymentHeader, res) {
|
|
2177
|
+
async handleProxy(body, paymentHeader, authHeader, res) {
|
|
1058
2178
|
const { wallet, amount, currency, chain, memo, serviceId, description } = body;
|
|
1059
2179
|
if (!wallet || !amount) {
|
|
1060
2180
|
return this.sendJson(res, 400, { error: "Missing required fields: wallet, amount" });
|
|
1061
2181
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
2182
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
|
|
2183
|
+
if (chain && !supportedChains.includes(chain)) {
|
|
2184
|
+
return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
|
|
2185
|
+
}
|
|
2186
|
+
const isSolanaChain = chain === "solana" || chain === "solana_devnet";
|
|
2187
|
+
const isValidEvmAddress = /^0x[a-fA-F0-9]{40}$/.test(wallet);
|
|
2188
|
+
const isValidSolanaAddress = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(wallet);
|
|
2189
|
+
if (isSolanaChain && !isValidSolanaAddress) {
|
|
2190
|
+
return this.sendJson(res, 400, { error: "Invalid Solana wallet address format" });
|
|
2191
|
+
}
|
|
2192
|
+
if (!isSolanaChain && !isValidEvmAddress) {
|
|
2193
|
+
return this.sendJson(res, 400, { error: "Invalid EVM wallet address format" });
|
|
1064
2194
|
}
|
|
1065
2195
|
const amountNum = parseFloat(amount);
|
|
1066
2196
|
if (isNaN(amountNum) || amountNum <= 0) {
|
|
1067
2197
|
return this.sendJson(res, 400, { error: "Invalid amount" });
|
|
1068
2198
|
}
|
|
1069
|
-
const supportedChains = ["base", "polygon", "base_sepolia"];
|
|
1070
|
-
if (chain && !supportedChains.includes(chain)) {
|
|
1071
|
-
return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
|
|
1072
|
-
}
|
|
1073
2199
|
const proxyConfig = {
|
|
1074
2200
|
id: serviceId || "proxy",
|
|
1075
2201
|
name: description || "Proxy Payment",
|
|
@@ -1081,6 +2207,9 @@ var MoltsPayServer = class {
|
|
|
1081
2207
|
input: {},
|
|
1082
2208
|
output: {}
|
|
1083
2209
|
};
|
|
2210
|
+
if (chain === "tempo_moderato") {
|
|
2211
|
+
return await this.handleProxyMPP(body, proxyConfig, authHeader, res);
|
|
2212
|
+
}
|
|
1084
2213
|
const requirements = this.buildProxyPaymentRequirements(proxyConfig, wallet, currency, chain);
|
|
1085
2214
|
if (!paymentHeader) {
|
|
1086
2215
|
return this.sendProxyPaymentRequired(proxyConfig, wallet, memo, chain, res);
|
|
@@ -1116,7 +2245,6 @@ var MoltsPayServer = class {
|
|
|
1116
2245
|
console.log(`[MoltsPay] /proxy: Verified by ${verifyResult.facilitator}`);
|
|
1117
2246
|
const { execute, service, params } = body;
|
|
1118
2247
|
if (execute && service) {
|
|
1119
|
-
console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
|
|
1120
2248
|
const skill = this.skills.get(service);
|
|
1121
2249
|
if (!skill) {
|
|
1122
2250
|
console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
|
|
@@ -1126,6 +2254,32 @@ var MoltsPayServer = class {
|
|
|
1126
2254
|
error: `Service not found: ${service}`
|
|
1127
2255
|
});
|
|
1128
2256
|
}
|
|
2257
|
+
const isSolana = isSolanaNetwork(network);
|
|
2258
|
+
let settlement2 = null;
|
|
2259
|
+
if (isSolana) {
|
|
2260
|
+
console.log(`[MoltsPay] /proxy: Solana detected - settling payment FIRST`);
|
|
2261
|
+
try {
|
|
2262
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
2263
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
2264
|
+
if (!settlement2.success) {
|
|
2265
|
+
console.error(`[MoltsPay] /proxy: Solana settlement failed: ${settlement2.error}`);
|
|
2266
|
+
return this.sendJson(res, 402, {
|
|
2267
|
+
success: false,
|
|
2268
|
+
paymentSettled: false,
|
|
2269
|
+
error: `Payment settlement failed: ${settlement2.error || "Unknown error"}`
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
} catch (err) {
|
|
2273
|
+
console.error("[MoltsPay] /proxy: Solana settlement failed:", err.message);
|
|
2274
|
+
return this.sendJson(res, 402, {
|
|
2275
|
+
success: false,
|
|
2276
|
+
paymentSettled: false,
|
|
2277
|
+
error: `Payment settlement failed: ${err.message}`
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
} else {
|
|
2281
|
+
console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
|
|
2282
|
+
}
|
|
1129
2283
|
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
1130
2284
|
let result;
|
|
1131
2285
|
try {
|
|
@@ -1135,34 +2289,36 @@ var MoltsPayServer = class {
|
|
|
1135
2289
|
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
1136
2290
|
)
|
|
1137
2291
|
]);
|
|
1138
|
-
console.log(`[MoltsPay] /proxy: Skill succeeded
|
|
2292
|
+
console.log(`[MoltsPay] /proxy: Skill succeeded`);
|
|
1139
2293
|
} catch (err) {
|
|
1140
|
-
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}
|
|
2294
|
+
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}`);
|
|
1141
2295
|
return this.sendJson(res, 500, {
|
|
1142
2296
|
success: false,
|
|
1143
|
-
paymentSettled: false,
|
|
1144
|
-
error: `Service execution failed: ${err.message}
|
|
2297
|
+
paymentSettled: isSolana ? true : false,
|
|
2298
|
+
error: `Service execution failed: ${err.message}`,
|
|
2299
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
1145
2300
|
});
|
|
1146
2301
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
2302
|
+
if (!isSolana) {
|
|
2303
|
+
console.log(`[MoltsPay] /proxy: Settling payment...`);
|
|
2304
|
+
try {
|
|
2305
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
2306
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
2307
|
+
} catch (err) {
|
|
2308
|
+
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
2309
|
+
return this.sendJson(res, 200, {
|
|
2310
|
+
success: true,
|
|
2311
|
+
verified: true,
|
|
2312
|
+
settled: false,
|
|
2313
|
+
settlementError: err.message,
|
|
2314
|
+
from: payment.payload?.authorization?.from,
|
|
2315
|
+
paidTo: wallet,
|
|
2316
|
+
amount: amountNum,
|
|
2317
|
+
currency: currency || "USDC",
|
|
2318
|
+
memo,
|
|
2319
|
+
result
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
1166
2322
|
}
|
|
1167
2323
|
return this.sendJson(res, 200, {
|
|
1168
2324
|
success: true,
|
|
@@ -1170,7 +2326,6 @@ var MoltsPayServer = class {
|
|
|
1170
2326
|
settled: settlement2?.success || false,
|
|
1171
2327
|
txHash: settlement2?.transaction,
|
|
1172
2328
|
from: payment.payload?.authorization?.from,
|
|
1173
|
-
// Buyer's wallet address
|
|
1174
2329
|
paidTo: wallet,
|
|
1175
2330
|
amount: amountNum,
|
|
1176
2331
|
currency: currency || "USDC",
|
|
@@ -1205,6 +2360,131 @@ var MoltsPayServer = class {
|
|
|
1205
2360
|
memo
|
|
1206
2361
|
});
|
|
1207
2362
|
}
|
|
2363
|
+
/**
|
|
2364
|
+
* Handle MPP payment flow for /proxy endpoint (tempo_moderato chain)
|
|
2365
|
+
*/
|
|
2366
|
+
async handleProxyMPP(body, config, authHeader, res) {
|
|
2367
|
+
const { wallet, amount, memo, serviceId } = body;
|
|
2368
|
+
const amountNum = parseFloat(amount);
|
|
2369
|
+
const amountInUnits = Math.floor(amountNum * 1e6).toString();
|
|
2370
|
+
if (!authHeader || !authHeader.toLowerCase().startsWith("payment ")) {
|
|
2371
|
+
const challengeId = this.generateChallengeId();
|
|
2372
|
+
const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
|
|
2373
|
+
const mppRequest = {
|
|
2374
|
+
amount: amountInUnits,
|
|
2375
|
+
currency: tokenAddress,
|
|
2376
|
+
methodDetails: {
|
|
2377
|
+
chainId: 42431,
|
|
2378
|
+
feePayer: true
|
|
2379
|
+
},
|
|
2380
|
+
recipient: wallet
|
|
2381
|
+
};
|
|
2382
|
+
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
2383
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
2384
|
+
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
2385
|
+
res.writeHead(402, {
|
|
2386
|
+
"Content-Type": "application/problem+json",
|
|
2387
|
+
[MPP_WWW_AUTH_HEADER]: wwwAuth
|
|
2388
|
+
});
|
|
2389
|
+
res.end(JSON.stringify({
|
|
2390
|
+
type: "https://paymentauth.org/problems/payment-required",
|
|
2391
|
+
title: "Payment Required",
|
|
2392
|
+
status: 402,
|
|
2393
|
+
detail: `Payment is required (${config.name}).`,
|
|
2394
|
+
service: serviceId || "proxy",
|
|
2395
|
+
price: amountNum,
|
|
2396
|
+
currency: "USDC"
|
|
2397
|
+
}, null, 2));
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
|
|
2401
|
+
if (!credentialMatch) {
|
|
2402
|
+
return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
|
|
2403
|
+
}
|
|
2404
|
+
let mppCredential;
|
|
2405
|
+
try {
|
|
2406
|
+
const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
2407
|
+
const decoded = Buffer.from(base64, "base64").toString("utf-8");
|
|
2408
|
+
mppCredential = JSON.parse(decoded);
|
|
2409
|
+
} catch (err) {
|
|
2410
|
+
console.error("[MoltsPay] /proxy MPP: Failed to parse credential:", err);
|
|
2411
|
+
return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
|
|
2412
|
+
}
|
|
2413
|
+
let txHash;
|
|
2414
|
+
if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
|
|
2415
|
+
txHash = mppCredential.payload.hash;
|
|
2416
|
+
} else {
|
|
2417
|
+
return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
|
|
2418
|
+
}
|
|
2419
|
+
console.log(`[MoltsPay] /proxy MPP: Verifying tx ${txHash} on Tempo...`);
|
|
2420
|
+
const requirements = this.buildPaymentRequirements(config, "eip155:42431", wallet, "USDC");
|
|
2421
|
+
const paymentPayload = {
|
|
2422
|
+
x402Version: X402_VERSION2,
|
|
2423
|
+
scheme: "exact",
|
|
2424
|
+
network: "eip155:42431",
|
|
2425
|
+
payload: { txHash, chainId: 42431 }
|
|
2426
|
+
};
|
|
2427
|
+
const verification = await this.registry.verify(paymentPayload, requirements);
|
|
2428
|
+
if (!verification.valid) {
|
|
2429
|
+
return this.sendJson(res, 402, {
|
|
2430
|
+
error: `Payment verification failed: ${verification.error}`
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
console.log(`[MoltsPay] /proxy MPP: Payment verified by ${verification.facilitator}`);
|
|
2434
|
+
const { execute, service, params } = body;
|
|
2435
|
+
if (execute && service) {
|
|
2436
|
+
console.log(`[MoltsPay] /proxy MPP: Executing skill: ${service}`);
|
|
2437
|
+
const skill = this.skills.get(service);
|
|
2438
|
+
if (!skill) {
|
|
2439
|
+
return this.sendJson(res, 404, {
|
|
2440
|
+
success: false,
|
|
2441
|
+
paymentSettled: true,
|
|
2442
|
+
// Payment already happened on Tempo
|
|
2443
|
+
error: `Service not found: ${service}`
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
2447
|
+
let result;
|
|
2448
|
+
try {
|
|
2449
|
+
result = await Promise.race([
|
|
2450
|
+
skill.handler(params || {}),
|
|
2451
|
+
new Promise(
|
|
2452
|
+
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
2453
|
+
)
|
|
2454
|
+
]);
|
|
2455
|
+
} catch (err) {
|
|
2456
|
+
console.error(`[MoltsPay] /proxy MPP: Skill failed: ${err.message}`);
|
|
2457
|
+
return this.sendJson(res, 500, {
|
|
2458
|
+
success: false,
|
|
2459
|
+
paymentSettled: true,
|
|
2460
|
+
error: `Service execution failed: ${err.message}`
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
return this.sendJson(res, 200, {
|
|
2464
|
+
success: true,
|
|
2465
|
+
verified: true,
|
|
2466
|
+
txHash,
|
|
2467
|
+
chain: "tempo_moderato",
|
|
2468
|
+
paidTo: wallet,
|
|
2469
|
+
amount: amountNum,
|
|
2470
|
+
currency: "USDC",
|
|
2471
|
+
facilitator: verification.facilitator,
|
|
2472
|
+
memo,
|
|
2473
|
+
result
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
this.sendJson(res, 200, {
|
|
2477
|
+
success: true,
|
|
2478
|
+
verified: true,
|
|
2479
|
+
txHash,
|
|
2480
|
+
chain: "tempo_moderato",
|
|
2481
|
+
paidTo: wallet,
|
|
2482
|
+
amount: amountNum,
|
|
2483
|
+
currency: "USDC",
|
|
2484
|
+
facilitator: verification.facilitator,
|
|
2485
|
+
memo
|
|
2486
|
+
});
|
|
2487
|
+
}
|
|
1208
2488
|
/**
|
|
1209
2489
|
* Build payment requirements for proxy endpoint (uses provided wallet)
|
|
1210
2490
|
*/
|
|
@@ -1216,7 +2496,7 @@ var MoltsPayServer = class {
|
|
|
1216
2496
|
const tokenAddresses = TOKEN_ADDRESSES[networkId] || TOKEN_ADDRESSES[this.networkId] || {};
|
|
1217
2497
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
1218
2498
|
const tokenDomain = getTokenDomain(networkId, selectedToken);
|
|
1219
|
-
|
|
2499
|
+
const requirements = {
|
|
1220
2500
|
scheme: "exact",
|
|
1221
2501
|
network: networkId,
|
|
1222
2502
|
asset: tokenAddress,
|
|
@@ -1226,6 +2506,17 @@ var MoltsPayServer = class {
|
|
|
1226
2506
|
maxTimeoutSeconds: 300,
|
|
1227
2507
|
extra: tokenDomain
|
|
1228
2508
|
};
|
|
2509
|
+
if (networkId === "eip155:56" || networkId === "eip155:97") {
|
|
2510
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
2511
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
2512
|
+
if (spenderAddress) {
|
|
2513
|
+
requirements.extra = {
|
|
2514
|
+
...requirements.extra || {},
|
|
2515
|
+
bnbSpender: spenderAddress
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
return requirements;
|
|
1229
2520
|
}
|
|
1230
2521
|
/**
|
|
1231
2522
|
* Return 402 with x402 payment requirements for proxy endpoint
|