moltspay 1.3.0 → 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 +221 -38
- package/dist/cdp/index.d.mts +4 -4
- package/dist/cdp/index.d.ts +4 -4
- package/dist/cdp/index.js +57 -0
- package/dist/cdp/index.js.map +1 -1
- package/dist/cdp/index.mjs +57 -0
- package/dist/cdp/index.mjs.map +1 -1
- package/dist/chains/index.d.mts +9 -8
- package/dist/chains/index.d.ts +9 -8
- package/dist/chains/index.js +57 -0
- package/dist/chains/index.js.map +1 -1
- package/dist/chains/index.mjs +57 -0
- package/dist/chains/index.mjs.map +1 -1
- package/dist/cli/index.js +1975 -273
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1977 -265
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +36 -3
- package/dist/client/index.d.ts +36 -3
- package/dist/client/index.js +540 -32
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +548 -30
- package/dist/client/index.mjs.map +1 -1
- package/dist/facilitators/index.d.mts +220 -1
- package/dist/facilitators/index.d.ts +220 -1
- package/dist/facilitators/index.js +664 -1
- package/dist/facilitators/index.js.map +1 -1
- package/dist/facilitators/index.mjs +670 -1
- package/dist/facilitators/index.mjs.map +1 -1
- package/dist/{index-On9ZaGDW.d.mts → index-D_2FkLwV.d.mts} +6 -2
- package/dist/{index-On9ZaGDW.d.ts → index-D_2FkLwV.d.ts} +6 -2
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1413 -146
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1421 -144
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.d.mts +13 -3
- package/dist/server/index.d.ts +13 -3
- package/dist/server/index.js +905 -52
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +915 -52
- 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 +57 -0
- package/dist/verify/index.js.map +1 -1
- package/dist/verify/index.mjs +57 -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 +57 -0
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/index.mjs +57 -0
- package/dist/wallet/index.mjs.map +1 -1
- package/package.json +4 -1
- package/schemas/moltspay.services.schema.json +27 -132
package/dist/server/index.mjs
CHANGED
|
@@ -336,6 +336,63 @@ var CHAINS = {
|
|
|
336
336
|
explorerTx: "https://explore.testnet.tempo.xyz/tx/",
|
|
337
337
|
avgBlockTime: 0.5
|
|
338
338
|
// ~500ms finality
|
|
339
|
+
},
|
|
340
|
+
// ============ BNB Chain Testnet ============
|
|
341
|
+
bnb_testnet: {
|
|
342
|
+
name: "BNB Testnet",
|
|
343
|
+
chainId: 97,
|
|
344
|
+
rpc: "https://data-seed-prebsc-1-s1.binance.org:8545",
|
|
345
|
+
tokens: {
|
|
346
|
+
// Note: BNB uses 18 decimals for stablecoins (unlike Base/Polygon which use 6)
|
|
347
|
+
// Using official Binance-Peg testnet tokens
|
|
348
|
+
USDC: {
|
|
349
|
+
address: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
350
|
+
// Testnet USDC
|
|
351
|
+
decimals: 18,
|
|
352
|
+
symbol: "USDC",
|
|
353
|
+
eip712Name: "USD Coin"
|
|
354
|
+
},
|
|
355
|
+
USDT: {
|
|
356
|
+
address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
|
|
357
|
+
// Testnet USDT
|
|
358
|
+
decimals: 18,
|
|
359
|
+
symbol: "USDT",
|
|
360
|
+
eip712Name: "Tether USD"
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
usdc: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
364
|
+
explorer: "https://testnet.bscscan.com/address/",
|
|
365
|
+
explorerTx: "https://testnet.bscscan.com/tx/",
|
|
366
|
+
avgBlockTime: 3,
|
|
367
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
368
|
+
requiresApproval: true
|
|
369
|
+
},
|
|
370
|
+
// ============ BNB Chain Mainnet ============
|
|
371
|
+
bnb: {
|
|
372
|
+
name: "BNB Smart Chain",
|
|
373
|
+
chainId: 56,
|
|
374
|
+
rpc: "https://bsc-dataseed.binance.org",
|
|
375
|
+
tokens: {
|
|
376
|
+
// Note: BNB uses 18 decimals for stablecoins
|
|
377
|
+
USDC: {
|
|
378
|
+
address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
379
|
+
decimals: 18,
|
|
380
|
+
symbol: "USDC",
|
|
381
|
+
eip712Name: "USD Coin"
|
|
382
|
+
},
|
|
383
|
+
USDT: {
|
|
384
|
+
address: "0x55d398326f99059fF775485246999027B3197955",
|
|
385
|
+
decimals: 18,
|
|
386
|
+
symbol: "USDT",
|
|
387
|
+
eip712Name: "Tether USD"
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
usdc: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
391
|
+
explorer: "https://bscscan.com/address/",
|
|
392
|
+
explorerTx: "https://bscscan.com/tx/",
|
|
393
|
+
avgBlockTime: 3,
|
|
394
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
395
|
+
requiresApproval: true
|
|
339
396
|
}
|
|
340
397
|
};
|
|
341
398
|
|
|
@@ -459,7 +516,538 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
459
516
|
}
|
|
460
517
|
};
|
|
461
518
|
|
|
519
|
+
// src/facilitators/bnb.ts
|
|
520
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
521
|
+
var TRANSFER_EVENT_TOPIC2 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
522
|
+
var EIP712_DOMAIN = {
|
|
523
|
+
name: "MoltsPay",
|
|
524
|
+
version: "1"
|
|
525
|
+
};
|
|
526
|
+
var INTENT_TYPES = {
|
|
527
|
+
PaymentIntent: [
|
|
528
|
+
{ name: "from", type: "address" },
|
|
529
|
+
{ name: "to", type: "address" },
|
|
530
|
+
{ name: "amount", type: "uint256" },
|
|
531
|
+
{ name: "token", type: "address" },
|
|
532
|
+
{ name: "service", type: "string" },
|
|
533
|
+
{ name: "nonce", type: "uint256" },
|
|
534
|
+
{ name: "deadline", type: "uint256" }
|
|
535
|
+
]
|
|
536
|
+
};
|
|
537
|
+
var BNBFacilitator = class extends BaseFacilitator {
|
|
538
|
+
name = "bnb";
|
|
539
|
+
displayName = "BNB Smart Chain";
|
|
540
|
+
supportedNetworks = ["eip155:56", "eip155:97"];
|
|
541
|
+
// Mainnet + Testnet
|
|
542
|
+
serverPrivateKey;
|
|
543
|
+
spenderAddress = null;
|
|
544
|
+
chainConfigs;
|
|
545
|
+
constructor(serverPrivateKey) {
|
|
546
|
+
super();
|
|
547
|
+
this.serverPrivateKey = serverPrivateKey || process.env.BNB_SERVER_PRIVATE_KEY || "";
|
|
548
|
+
if (this.serverPrivateKey) {
|
|
549
|
+
const key = this.serverPrivateKey.startsWith("0x") ? this.serverPrivateKey : `0x${this.serverPrivateKey}`;
|
|
550
|
+
const account = privateKeyToAccount(key);
|
|
551
|
+
this.spenderAddress = account.address;
|
|
552
|
+
}
|
|
553
|
+
this.chainConfigs = {
|
|
554
|
+
56: { rpc: CHAINS.bnb.rpc, chain: CHAINS.bnb },
|
|
555
|
+
97: { rpc: CHAINS.bnb_testnet.rpc, chain: CHAINS.bnb_testnet }
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
async healthCheck() {
|
|
559
|
+
const start = Date.now();
|
|
560
|
+
try {
|
|
561
|
+
const response = await fetch(this.chainConfigs[56].rpc, {
|
|
562
|
+
method: "POST",
|
|
563
|
+
headers: { "Content-Type": "application/json" },
|
|
564
|
+
body: JSON.stringify({
|
|
565
|
+
jsonrpc: "2.0",
|
|
566
|
+
method: "eth_chainId",
|
|
567
|
+
params: [],
|
|
568
|
+
id: 1
|
|
569
|
+
})
|
|
570
|
+
});
|
|
571
|
+
const data = await response.json();
|
|
572
|
+
const chainId = parseInt(data.result, 16);
|
|
573
|
+
if (chainId !== 56) {
|
|
574
|
+
return { healthy: false, error: `Wrong chainId: ${chainId}` };
|
|
575
|
+
}
|
|
576
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
577
|
+
} catch (error) {
|
|
578
|
+
return { healthy: false, error: String(error) };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Verify a payment intent signature (before service execution)
|
|
583
|
+
*
|
|
584
|
+
* This verifies:
|
|
585
|
+
* 1. Signature is valid for the intent
|
|
586
|
+
* 2. Client has approved server wallet
|
|
587
|
+
* 3. Client has sufficient balance
|
|
588
|
+
* 4. Intent hasn't expired
|
|
589
|
+
*/
|
|
590
|
+
async verify(paymentPayload, requirements) {
|
|
591
|
+
try {
|
|
592
|
+
const bnbPayload = paymentPayload.payload;
|
|
593
|
+
if (!bnbPayload?.intent) {
|
|
594
|
+
return { valid: false, error: "Missing intent in payment payload" };
|
|
595
|
+
}
|
|
596
|
+
const { intent, chainId } = bnbPayload;
|
|
597
|
+
const config = this.chainConfigs[chainId];
|
|
598
|
+
if (!config) {
|
|
599
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
600
|
+
}
|
|
601
|
+
if (intent.deadline < Date.now()) {
|
|
602
|
+
return { valid: false, error: "Intent expired" };
|
|
603
|
+
}
|
|
604
|
+
const recoveredAddress = await this.recoverIntentSigner(intent, chainId);
|
|
605
|
+
if (recoveredAddress.toLowerCase() !== intent.from.toLowerCase()) {
|
|
606
|
+
return { valid: false, error: "Invalid signature" };
|
|
607
|
+
}
|
|
608
|
+
if (intent.to.toLowerCase() !== requirements.payTo.toLowerCase()) {
|
|
609
|
+
return { valid: false, error: `Wrong recipient: ${intent.to}` };
|
|
610
|
+
}
|
|
611
|
+
if (BigInt(intent.amount) < BigInt(requirements.amount)) {
|
|
612
|
+
return { valid: false, error: `Insufficient amount: ${intent.amount}` };
|
|
613
|
+
}
|
|
614
|
+
if (intent.token.toLowerCase() !== requirements.asset.toLowerCase()) {
|
|
615
|
+
return { valid: false, error: `Wrong token: ${intent.token}` };
|
|
616
|
+
}
|
|
617
|
+
const serverAddress = await this.getServerAddress();
|
|
618
|
+
const allowance = await this.getAllowance(intent.from, serverAddress, intent.token, config.rpc);
|
|
619
|
+
if (BigInt(allowance) < BigInt(intent.amount)) {
|
|
620
|
+
return { valid: false, error: "Insufficient allowance. Run: npx moltspay init --chain bnb" };
|
|
621
|
+
}
|
|
622
|
+
const balance = await this.getBalance(intent.from, intent.token, config.rpc);
|
|
623
|
+
if (BigInt(balance) < BigInt(intent.amount)) {
|
|
624
|
+
return { valid: false, error: "Insufficient balance" };
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
valid: true,
|
|
628
|
+
details: {
|
|
629
|
+
from: intent.from,
|
|
630
|
+
to: intent.to,
|
|
631
|
+
amount: intent.amount,
|
|
632
|
+
token: intent.token,
|
|
633
|
+
service: intent.service,
|
|
634
|
+
nonce: intent.nonce,
|
|
635
|
+
deadline: intent.deadline
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
} catch (error) {
|
|
639
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Settle a payment by executing transferFrom
|
|
644
|
+
*
|
|
645
|
+
* This is called AFTER the service has been successfully delivered.
|
|
646
|
+
* Server pays gas, transfers tokens from client to provider.
|
|
647
|
+
*/
|
|
648
|
+
async settle(paymentPayload, requirements) {
|
|
649
|
+
if (!this.serverPrivateKey) {
|
|
650
|
+
return { success: false, error: "Server wallet not configured (BNB_SERVER_PRIVATE_KEY)" };
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
const verifyResult = await this.verify(paymentPayload, requirements);
|
|
654
|
+
if (!verifyResult.valid) {
|
|
655
|
+
return { success: false, error: verifyResult.error };
|
|
656
|
+
}
|
|
657
|
+
const bnbPayload = paymentPayload.payload;
|
|
658
|
+
const { intent, chainId } = bnbPayload;
|
|
659
|
+
const config = this.chainConfigs[chainId];
|
|
660
|
+
const txHash = await this.executeTransferFrom(
|
|
661
|
+
intent.from,
|
|
662
|
+
intent.to,
|
|
663
|
+
intent.amount,
|
|
664
|
+
intent.token,
|
|
665
|
+
config.rpc
|
|
666
|
+
);
|
|
667
|
+
return {
|
|
668
|
+
success: true,
|
|
669
|
+
transaction: txHash,
|
|
670
|
+
status: "settled"
|
|
671
|
+
};
|
|
672
|
+
} catch (error) {
|
|
673
|
+
return { success: false, error: `Settlement failed: ${error}` };
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Check if client has approved the server wallet
|
|
678
|
+
*/
|
|
679
|
+
async checkApproval(clientAddress, token, chainId) {
|
|
680
|
+
const config = this.chainConfigs[chainId];
|
|
681
|
+
if (!config) {
|
|
682
|
+
throw new Error(`Unsupported chainId: ${chainId}`);
|
|
683
|
+
}
|
|
684
|
+
const serverAddress = await this.getServerAddress();
|
|
685
|
+
const allowance = await this.getAllowance(clientAddress, serverAddress, token, config.rpc);
|
|
686
|
+
const minAllowance = BigInt("1000000000000000000000");
|
|
687
|
+
return {
|
|
688
|
+
approved: BigInt(allowance) >= minAllowance,
|
|
689
|
+
allowance
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Verify a completed transaction (for checking past payments)
|
|
694
|
+
*/
|
|
695
|
+
async verifyTransaction(txHash, expected, chainId) {
|
|
696
|
+
const config = this.chainConfigs[chainId];
|
|
697
|
+
if (!config) {
|
|
698
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
const receipt = await this.getTransactionReceipt(txHash, config.rpc);
|
|
702
|
+
if (!receipt) {
|
|
703
|
+
return { valid: false, error: "Transaction not found" };
|
|
704
|
+
}
|
|
705
|
+
if (receipt.status !== "0x1") {
|
|
706
|
+
return { valid: false, error: "Transaction failed" };
|
|
707
|
+
}
|
|
708
|
+
const transferLog = receipt.logs.find(
|
|
709
|
+
(log) => log.topics[0] === TRANSFER_EVENT_TOPIC2 && log.address.toLowerCase() === expected.token.toLowerCase()
|
|
710
|
+
);
|
|
711
|
+
if (!transferLog) {
|
|
712
|
+
return { valid: false, error: "No Transfer event found" };
|
|
713
|
+
}
|
|
714
|
+
const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
|
|
715
|
+
if (toAddress !== expected.to.toLowerCase()) {
|
|
716
|
+
return { valid: false, error: `Wrong recipient: ${toAddress}` };
|
|
717
|
+
}
|
|
718
|
+
const amount = BigInt(transferLog.data);
|
|
719
|
+
if (amount < BigInt(expected.amount)) {
|
|
720
|
+
return { valid: false, error: `Insufficient amount: ${amount}` };
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
valid: true,
|
|
724
|
+
details: {
|
|
725
|
+
txHash,
|
|
726
|
+
from: "0x" + transferLog.topics[1].slice(26),
|
|
727
|
+
to: toAddress,
|
|
728
|
+
amount: amount.toString(),
|
|
729
|
+
token: transferLog.address
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
} catch (error) {
|
|
733
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
// ==================== Private Methods ====================
|
|
737
|
+
/**
|
|
738
|
+
* Get the server's spender address (public, for 402 responses)
|
|
739
|
+
* Returns cached value computed at construction time.
|
|
740
|
+
*/
|
|
741
|
+
getSpenderAddress() {
|
|
742
|
+
return this.spenderAddress;
|
|
743
|
+
}
|
|
744
|
+
async getServerAddress() {
|
|
745
|
+
const { ethers } = await import("ethers");
|
|
746
|
+
const wallet = new ethers.Wallet(this.serverPrivateKey);
|
|
747
|
+
return wallet.address;
|
|
748
|
+
}
|
|
749
|
+
async recoverIntentSigner(intent, chainId) {
|
|
750
|
+
const { ethers } = await import("ethers");
|
|
751
|
+
const domain = {
|
|
752
|
+
...EIP712_DOMAIN,
|
|
753
|
+
chainId
|
|
754
|
+
};
|
|
755
|
+
const message = {
|
|
756
|
+
from: intent.from,
|
|
757
|
+
to: intent.to,
|
|
758
|
+
amount: intent.amount,
|
|
759
|
+
token: intent.token,
|
|
760
|
+
service: intent.service,
|
|
761
|
+
nonce: intent.nonce,
|
|
762
|
+
deadline: intent.deadline
|
|
763
|
+
};
|
|
764
|
+
const recoveredAddress = ethers.verifyTypedData(
|
|
765
|
+
domain,
|
|
766
|
+
INTENT_TYPES,
|
|
767
|
+
message,
|
|
768
|
+
intent.signature
|
|
769
|
+
);
|
|
770
|
+
return recoveredAddress;
|
|
771
|
+
}
|
|
772
|
+
async getAllowance(owner, spender, token, rpcUrl) {
|
|
773
|
+
const selector = "0xdd62ed3e";
|
|
774
|
+
const ownerPadded = owner.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
775
|
+
const spenderPadded = spender.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
776
|
+
const data = selector + ownerPadded + spenderPadded;
|
|
777
|
+
const response = await fetch(rpcUrl, {
|
|
778
|
+
method: "POST",
|
|
779
|
+
headers: { "Content-Type": "application/json" },
|
|
780
|
+
body: JSON.stringify({
|
|
781
|
+
jsonrpc: "2.0",
|
|
782
|
+
method: "eth_call",
|
|
783
|
+
params: [{ to: token, data }, "latest"],
|
|
784
|
+
id: 1
|
|
785
|
+
})
|
|
786
|
+
});
|
|
787
|
+
const result = await response.json();
|
|
788
|
+
return result.result || "0x0";
|
|
789
|
+
}
|
|
790
|
+
async getBalance(account, token, rpcUrl) {
|
|
791
|
+
const selector = "0x70a08231";
|
|
792
|
+
const accountPadded = account.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
793
|
+
const data = selector + accountPadded;
|
|
794
|
+
const response = await fetch(rpcUrl, {
|
|
795
|
+
method: "POST",
|
|
796
|
+
headers: { "Content-Type": "application/json" },
|
|
797
|
+
body: JSON.stringify({
|
|
798
|
+
jsonrpc: "2.0",
|
|
799
|
+
method: "eth_call",
|
|
800
|
+
params: [{ to: token, data }, "latest"],
|
|
801
|
+
id: 1
|
|
802
|
+
})
|
|
803
|
+
});
|
|
804
|
+
const result = await response.json();
|
|
805
|
+
return result.result || "0x0";
|
|
806
|
+
}
|
|
807
|
+
async executeTransferFrom(from, to, amount, token, rpcUrl) {
|
|
808
|
+
const { ethers } = await import("ethers");
|
|
809
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
810
|
+
const wallet = new ethers.Wallet(this.serverPrivateKey, provider);
|
|
811
|
+
const tokenContract = new ethers.Contract(token, [
|
|
812
|
+
"function transferFrom(address from, address to, uint256 amount) returns (bool)"
|
|
813
|
+
], wallet);
|
|
814
|
+
const tx = await tokenContract.transferFrom(from, to, amount);
|
|
815
|
+
const receipt = await tx.wait();
|
|
816
|
+
return receipt.hash;
|
|
817
|
+
}
|
|
818
|
+
async getTransactionReceipt(txHash, rpcUrl) {
|
|
819
|
+
const response = await fetch(rpcUrl, {
|
|
820
|
+
method: "POST",
|
|
821
|
+
headers: { "Content-Type": "application/json" },
|
|
822
|
+
body: JSON.stringify({
|
|
823
|
+
jsonrpc: "2.0",
|
|
824
|
+
method: "eth_getTransactionReceipt",
|
|
825
|
+
params: [txHash],
|
|
826
|
+
id: 1
|
|
827
|
+
})
|
|
828
|
+
});
|
|
829
|
+
const data = await response.json();
|
|
830
|
+
return data.result;
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
// src/facilitators/solana.ts
|
|
835
|
+
import {
|
|
836
|
+
Connection as Connection2,
|
|
837
|
+
PublicKey as PublicKey2,
|
|
838
|
+
Transaction,
|
|
839
|
+
VersionedTransaction
|
|
840
|
+
} from "@solana/web3.js";
|
|
841
|
+
import {
|
|
842
|
+
getAssociatedTokenAddress,
|
|
843
|
+
createTransferCheckedInstruction,
|
|
844
|
+
getAccount,
|
|
845
|
+
createAssociatedTokenAccountInstruction
|
|
846
|
+
} from "@solana/spl-token";
|
|
847
|
+
|
|
848
|
+
// src/chains/solana.ts
|
|
849
|
+
import { Connection, PublicKey } from "@solana/web3.js";
|
|
850
|
+
var SOLANA_CHAINS = {
|
|
851
|
+
solana: {
|
|
852
|
+
name: "Solana Mainnet",
|
|
853
|
+
cluster: "mainnet-beta",
|
|
854
|
+
rpc: "https://api.mainnet-beta.solana.com",
|
|
855
|
+
explorer: "https://solscan.io/account/",
|
|
856
|
+
explorerTx: "https://solscan.io/tx/",
|
|
857
|
+
tokens: {
|
|
858
|
+
USDC: {
|
|
859
|
+
mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
860
|
+
// Circle official USDC
|
|
861
|
+
decimals: 6
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
solana_devnet: {
|
|
866
|
+
name: "Solana Devnet",
|
|
867
|
+
cluster: "devnet",
|
|
868
|
+
rpc: "https://api.devnet.solana.com",
|
|
869
|
+
explorer: "https://solscan.io/account/",
|
|
870
|
+
explorerTx: "https://solscan.io/tx/",
|
|
871
|
+
tokens: {
|
|
872
|
+
USDC: {
|
|
873
|
+
// Circle's devnet USDC (if not available, we'll deploy our own test token)
|
|
874
|
+
mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
875
|
+
decimals: 6
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
// src/facilitators/solana.ts
|
|
882
|
+
var SolanaFacilitator = class extends BaseFacilitator {
|
|
883
|
+
name = "solana";
|
|
884
|
+
displayName = "Solana Direct";
|
|
885
|
+
supportedNetworks = ["solana:mainnet", "solana:devnet"];
|
|
886
|
+
connections = /* @__PURE__ */ new Map();
|
|
887
|
+
feePayerKeypair;
|
|
888
|
+
constructor(config) {
|
|
889
|
+
super();
|
|
890
|
+
this.feePayerKeypair = config?.feePayerKeypair;
|
|
891
|
+
for (const [chain, config2] of Object.entries(SOLANA_CHAINS)) {
|
|
892
|
+
this.connections.set(
|
|
893
|
+
chain,
|
|
894
|
+
new Connection2(config2.rpc, "confirmed")
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
if (this.feePayerKeypair) {
|
|
898
|
+
console.log(`[SolanaFacilitator] Gasless mode enabled. Fee payer: ${this.feePayerKeypair.publicKey.toBase58()}`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Get fee payer public key (for gasless transactions)
|
|
903
|
+
*/
|
|
904
|
+
getFeePayerPubkey() {
|
|
905
|
+
return this.feePayerKeypair?.publicKey.toBase58() || null;
|
|
906
|
+
}
|
|
907
|
+
getConnection(chain) {
|
|
908
|
+
const conn = this.connections.get(chain);
|
|
909
|
+
if (!conn) {
|
|
910
|
+
throw new Error(`No connection for chain: ${chain}`);
|
|
911
|
+
}
|
|
912
|
+
return conn;
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Convert our chain name to network identifier
|
|
916
|
+
*/
|
|
917
|
+
static chainToNetwork(chain) {
|
|
918
|
+
return chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Convert network identifier to chain name
|
|
922
|
+
*/
|
|
923
|
+
static networkToChain(network) {
|
|
924
|
+
if (network === "solana:mainnet") return "solana";
|
|
925
|
+
if (network === "solana:devnet") return "solana_devnet";
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
async healthCheck() {
|
|
929
|
+
const start = Date.now();
|
|
930
|
+
try {
|
|
931
|
+
const conn = this.getConnection("solana_devnet");
|
|
932
|
+
await conn.getSlot();
|
|
933
|
+
return {
|
|
934
|
+
healthy: true,
|
|
935
|
+
latencyMs: Date.now() - start
|
|
936
|
+
};
|
|
937
|
+
} catch (error) {
|
|
938
|
+
return {
|
|
939
|
+
healthy: false,
|
|
940
|
+
error: error.message
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Verify a Solana payment
|
|
946
|
+
*
|
|
947
|
+
* Checks:
|
|
948
|
+
* 1. Transaction is valid and properly signed
|
|
949
|
+
* 2. Transfer instruction matches expected amount and recipient
|
|
950
|
+
*/
|
|
951
|
+
async verify(paymentPayload, requirements) {
|
|
952
|
+
try {
|
|
953
|
+
const solanaPayload = paymentPayload.payload;
|
|
954
|
+
if (!solanaPayload || !solanaPayload.signedTransaction) {
|
|
955
|
+
return { valid: false, error: "Missing signed transaction" };
|
|
956
|
+
}
|
|
957
|
+
const chain = solanaPayload.chain || "solana_devnet";
|
|
958
|
+
const chainConfig = SOLANA_CHAINS[chain];
|
|
959
|
+
if (!chainConfig) {
|
|
960
|
+
return { valid: false, error: `Invalid chain: ${chain}` };
|
|
961
|
+
}
|
|
962
|
+
const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
|
|
963
|
+
let tx;
|
|
964
|
+
try {
|
|
965
|
+
tx = Transaction.from(txBuffer);
|
|
966
|
+
} catch {
|
|
967
|
+
tx = VersionedTransaction.deserialize(txBuffer);
|
|
968
|
+
}
|
|
969
|
+
if (tx instanceof Transaction) {
|
|
970
|
+
const hasAnySignature = tx.signatures.some(
|
|
971
|
+
(sig) => sig.signature && !sig.signature.every((b) => b === 0)
|
|
972
|
+
);
|
|
973
|
+
if (!hasAnySignature) {
|
|
974
|
+
return { valid: false, error: "Transaction not signed" };
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
const expectedAmount = BigInt(requirements.amount);
|
|
978
|
+
const expectedRecipient = new PublicKey2(requirements.payTo);
|
|
979
|
+
return {
|
|
980
|
+
valid: true,
|
|
981
|
+
details: {
|
|
982
|
+
chain,
|
|
983
|
+
sender: solanaPayload.sender,
|
|
984
|
+
recipient: requirements.payTo,
|
|
985
|
+
amount: requirements.amount
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
} catch (error) {
|
|
989
|
+
return { valid: false, error: error.message };
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Settle a Solana payment
|
|
994
|
+
*
|
|
995
|
+
* Submits the signed transaction to the network.
|
|
996
|
+
* In gasless mode, adds fee payer signature before submitting.
|
|
997
|
+
*/
|
|
998
|
+
async settle(paymentPayload, requirements) {
|
|
999
|
+
try {
|
|
1000
|
+
const solanaPayload = paymentPayload.payload;
|
|
1001
|
+
if (!solanaPayload || !solanaPayload.signedTransaction) {
|
|
1002
|
+
return { success: false, error: "Missing signed transaction" };
|
|
1003
|
+
}
|
|
1004
|
+
const chain = solanaPayload.chain || "solana_devnet";
|
|
1005
|
+
const connection = this.getConnection(chain);
|
|
1006
|
+
const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
|
|
1007
|
+
let txToSend;
|
|
1008
|
+
try {
|
|
1009
|
+
const tx = Transaction.from(txBuffer);
|
|
1010
|
+
if (this.feePayerKeypair && tx.feePayer) {
|
|
1011
|
+
const feePayerPubkey = this.feePayerKeypair.publicKey.toBase58();
|
|
1012
|
+
const txFeePayer = tx.feePayer.toBase58();
|
|
1013
|
+
if (txFeePayer === feePayerPubkey) {
|
|
1014
|
+
console.log(`[SolanaFacilitator] Gasless mode: adding fee payer signature`);
|
|
1015
|
+
tx.partialSign(this.feePayerKeypair);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
txToSend = tx.serialize();
|
|
1019
|
+
} catch (e) {
|
|
1020
|
+
txToSend = txBuffer;
|
|
1021
|
+
}
|
|
1022
|
+
const signature = await connection.sendRawTransaction(txToSend, {
|
|
1023
|
+
skipPreflight: false,
|
|
1024
|
+
preflightCommitment: "confirmed"
|
|
1025
|
+
});
|
|
1026
|
+
const confirmation = await connection.confirmTransaction(signature, "confirmed");
|
|
1027
|
+
if (confirmation.value.err) {
|
|
1028
|
+
return {
|
|
1029
|
+
success: false,
|
|
1030
|
+
error: `Transaction failed: ${JSON.stringify(confirmation.value.err)}`,
|
|
1031
|
+
transaction: signature
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
return {
|
|
1035
|
+
success: true,
|
|
1036
|
+
transaction: signature,
|
|
1037
|
+
status: "confirmed"
|
|
1038
|
+
};
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
return { success: false, error: error.message };
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
supportsNetwork(network) {
|
|
1044
|
+
return this.supportedNetworks.includes(network);
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
|
|
462
1048
|
// src/facilitators/registry.ts
|
|
1049
|
+
import { Keypair as Keypair2 } from "@solana/web3.js";
|
|
1050
|
+
import bs58 from "bs58";
|
|
463
1051
|
var FacilitatorRegistry = class {
|
|
464
1052
|
factories = /* @__PURE__ */ new Map();
|
|
465
1053
|
instances = /* @__PURE__ */ new Map();
|
|
@@ -468,7 +1056,20 @@ var FacilitatorRegistry = class {
|
|
|
468
1056
|
constructor(selection) {
|
|
469
1057
|
this.registerFactory("cdp", (config) => new CDPFacilitator(config));
|
|
470
1058
|
this.registerFactory("tempo", () => new TempoFacilitator());
|
|
471
|
-
this.
|
|
1059
|
+
this.registerFactory("bnb", (config) => new BNBFacilitator(config?.serverPrivateKey));
|
|
1060
|
+
this.registerFactory("solana", (config) => {
|
|
1061
|
+
let feePayerKeypair;
|
|
1062
|
+
const feePayerKey = config?.feePayerPrivateKey || process.env.SOLANA_FEE_PAYER_KEY;
|
|
1063
|
+
if (feePayerKey) {
|
|
1064
|
+
try {
|
|
1065
|
+
feePayerKeypair = Keypair2.fromSecretKey(bs58.decode(feePayerKey));
|
|
1066
|
+
} catch (e) {
|
|
1067
|
+
console.warn(`[SolanaFacilitator] Invalid fee payer key: ${e.message}`);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
return new SolanaFacilitator({ feePayerKeypair });
|
|
1071
|
+
});
|
|
1072
|
+
this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
|
|
472
1073
|
}
|
|
473
1074
|
/**
|
|
474
1075
|
* Register a new facilitator factory
|
|
@@ -712,14 +1313,40 @@ var TOKEN_ADDRESSES = {
|
|
|
712
1313
|
// pathUSD
|
|
713
1314
|
USDT: "0x20c0000000000000000000000000000000000001"
|
|
714
1315
|
// alphaUSD
|
|
1316
|
+
},
|
|
1317
|
+
// BNB Smart Chain mainnet
|
|
1318
|
+
"eip155:56": {
|
|
1319
|
+
USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
1320
|
+
USDT: "0x55d398326f99059fF775485246999027B3197955"
|
|
1321
|
+
},
|
|
1322
|
+
// BNB Smart Chain testnet
|
|
1323
|
+
"eip155:97": {
|
|
1324
|
+
USDC: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
1325
|
+
USDT: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd"
|
|
1326
|
+
},
|
|
1327
|
+
// Solana networks use mint addresses (SPL tokens)
|
|
1328
|
+
"solana:mainnet": {
|
|
1329
|
+
USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
|
1330
|
+
// Circle USDC
|
|
1331
|
+
},
|
|
1332
|
+
"solana:devnet": {
|
|
1333
|
+
USDC: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"
|
|
1334
|
+
// Devnet USDC
|
|
715
1335
|
}
|
|
716
1336
|
};
|
|
717
1337
|
var CHAIN_TO_NETWORK = {
|
|
718
1338
|
"base": "eip155:8453",
|
|
719
1339
|
"base_sepolia": "eip155:84532",
|
|
720
1340
|
"polygon": "eip155:137",
|
|
721
|
-
"tempo_moderato": "eip155:42431"
|
|
1341
|
+
"tempo_moderato": "eip155:42431",
|
|
1342
|
+
"bnb": "eip155:56",
|
|
1343
|
+
"bnb_testnet": "eip155:97",
|
|
1344
|
+
"solana": "solana:mainnet",
|
|
1345
|
+
"solana_devnet": "solana:devnet"
|
|
722
1346
|
};
|
|
1347
|
+
function isSolanaNetwork(network) {
|
|
1348
|
+
return network.startsWith("solana:");
|
|
1349
|
+
}
|
|
723
1350
|
var TOKEN_DOMAINS = {
|
|
724
1351
|
// Base mainnet
|
|
725
1352
|
"eip155:8453": {
|
|
@@ -741,6 +1368,16 @@ var TOKEN_DOMAINS = {
|
|
|
741
1368
|
"eip155:42431": {
|
|
742
1369
|
USDC: { name: "pathUSD", version: "1" },
|
|
743
1370
|
USDT: { name: "alphaUSD", version: "1" }
|
|
1371
|
+
},
|
|
1372
|
+
// BNB Smart Chain mainnet
|
|
1373
|
+
"eip155:56": {
|
|
1374
|
+
USDC: { name: "USD Coin", version: "1" },
|
|
1375
|
+
USDT: { name: "Tether USD", version: "1" }
|
|
1376
|
+
},
|
|
1377
|
+
// BNB Smart Chain testnet
|
|
1378
|
+
"eip155:97": {
|
|
1379
|
+
USDC: { name: "USD Coin", version: "1" },
|
|
1380
|
+
USDT: { name: "Tether USD", version: "1" }
|
|
744
1381
|
}
|
|
745
1382
|
};
|
|
746
1383
|
function getTokenDomain(network, token) {
|
|
@@ -798,7 +1435,7 @@ var MoltsPayServer = class {
|
|
|
798
1435
|
};
|
|
799
1436
|
this.useMainnet = process.env.USE_MAINNET?.toLowerCase() === "true";
|
|
800
1437
|
this.networkId = this.useMainnet ? "eip155:8453" : "eip155:84532";
|
|
801
|
-
const defaultFallback = ["tempo"];
|
|
1438
|
+
const defaultFallback = ["tempo", "bnb", "solana"];
|
|
802
1439
|
const envFallback = process.env.FACILITATOR_FALLBACK?.split(",").filter(Boolean);
|
|
803
1440
|
const facilitatorConfig = options.facilitators || {
|
|
804
1441
|
primary: process.env.FACILITATOR_PRIMARY || "cdp",
|
|
@@ -841,12 +1478,20 @@ var MoltsPayServer = class {
|
|
|
841
1478
|
*/
|
|
842
1479
|
getProviderChains() {
|
|
843
1480
|
const provider = this.manifest.provider;
|
|
1481
|
+
const getWalletForChain = (chainName, explicitWallet) => {
|
|
1482
|
+
if (explicitWallet) return explicitWallet;
|
|
1483
|
+
if ((chainName === "solana" || chainName === "solana_devnet") && provider.solana_wallet) {
|
|
1484
|
+
return provider.solana_wallet;
|
|
1485
|
+
}
|
|
1486
|
+
return provider.wallet;
|
|
1487
|
+
};
|
|
844
1488
|
if (provider.chains && provider.chains.length > 0) {
|
|
845
1489
|
return provider.chains.map((c) => {
|
|
846
1490
|
const chainName = typeof c === "string" ? c : c.chain;
|
|
1491
|
+
const explicitWallet = typeof c === "object" ? c.wallet : null;
|
|
847
1492
|
return {
|
|
848
1493
|
network: CHAIN_TO_NETWORK[chainName] || "eip155:8453",
|
|
849
|
-
wallet: (
|
|
1494
|
+
wallet: getWalletForChain(chainName, explicitWallet || void 0),
|
|
850
1495
|
tokens: (typeof c === "object" ? c.tokens : null) || ["USDC"]
|
|
851
1496
|
};
|
|
852
1497
|
});
|
|
@@ -855,7 +1500,7 @@ var MoltsPayServer = class {
|
|
|
855
1500
|
const network = CHAIN_TO_NETWORK[chain] || this.networkId;
|
|
856
1501
|
return [{
|
|
857
1502
|
network,
|
|
858
|
-
wallet:
|
|
1503
|
+
wallet: getWalletForChain(chain),
|
|
859
1504
|
tokens: ["USDC"]
|
|
860
1505
|
}];
|
|
861
1506
|
}
|
|
@@ -926,7 +1571,8 @@ var MoltsPayServer = class {
|
|
|
926
1571
|
}
|
|
927
1572
|
const body = await this.readBody(req);
|
|
928
1573
|
const paymentHeader = req.headers[PAYMENT_HEADER];
|
|
929
|
-
|
|
1574
|
+
const authHeader = req.headers[MPP_AUTH_HEADER];
|
|
1575
|
+
return await this.handleProxy(body, paymentHeader, authHeader, res);
|
|
930
1576
|
}
|
|
931
1577
|
const servicePath = url.pathname.replace(/^\//, "");
|
|
932
1578
|
const skill = this.skills.get(servicePath);
|
|
@@ -963,7 +1609,9 @@ var MoltsPayServer = class {
|
|
|
963
1609
|
name: this.manifest.provider.name,
|
|
964
1610
|
description: this.manifest.provider.description,
|
|
965
1611
|
wallet: this.manifest.provider.wallet,
|
|
966
|
-
chain: this.manifest.provider.chain || "base"
|
|
1612
|
+
chain: this.manifest.provider.chain || "base",
|
|
1613
|
+
solana_wallet: this.manifest.provider.solana_wallet,
|
|
1614
|
+
chains: this.manifest.provider.chains
|
|
967
1615
|
},
|
|
968
1616
|
services,
|
|
969
1617
|
endpoints: {
|
|
@@ -1076,6 +1724,21 @@ var MoltsPayServer = class {
|
|
|
1076
1724
|
});
|
|
1077
1725
|
}
|
|
1078
1726
|
console.log(`[MoltsPay] Verified by ${verifyResult.facilitator}`);
|
|
1727
|
+
const isSolana = isSolanaNetwork(paymentNetwork);
|
|
1728
|
+
let settlement = null;
|
|
1729
|
+
if (isSolana) {
|
|
1730
|
+
console.log(`[MoltsPay] Solana detected - settling payment FIRST (blockhash expiry protection)`);
|
|
1731
|
+
try {
|
|
1732
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
1733
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
1734
|
+
} catch (err) {
|
|
1735
|
+
console.error("[MoltsPay] Solana settlement failed:", err.message);
|
|
1736
|
+
return this.sendJson(res, 402, {
|
|
1737
|
+
error: "Payment settlement failed",
|
|
1738
|
+
message: err.message
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1079
1742
|
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
1080
1743
|
console.log(`[MoltsPay] Executing skill: ${service} (timeout: ${timeoutSeconds}s)`);
|
|
1081
1744
|
let result;
|
|
@@ -1090,16 +1753,19 @@ var MoltsPayServer = class {
|
|
|
1090
1753
|
console.error("[MoltsPay] Skill execution failed:", err.message);
|
|
1091
1754
|
return this.sendJson(res, 500, {
|
|
1092
1755
|
error: "Service execution failed",
|
|
1093
|
-
message: err.message
|
|
1756
|
+
message: err.message,
|
|
1757
|
+
paymentSettled: isSolana ? true : false,
|
|
1758
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
1094
1759
|
});
|
|
1095
1760
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1761
|
+
if (!isSolana) {
|
|
1762
|
+
console.log(`[MoltsPay] Skill succeeded, settling payment...`);
|
|
1763
|
+
try {
|
|
1764
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
1765
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
1766
|
+
} catch (err) {
|
|
1767
|
+
console.error("[MoltsPay] Settlement failed:", err.message);
|
|
1768
|
+
}
|
|
1103
1769
|
}
|
|
1104
1770
|
const responseHeaders = {};
|
|
1105
1771
|
if (settlement?.success) {
|
|
@@ -1375,7 +2041,7 @@ var MoltsPayServer = class {
|
|
|
1375
2041
|
const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
|
|
1376
2042
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
1377
2043
|
const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
|
|
1378
|
-
|
|
2044
|
+
const requirements = {
|
|
1379
2045
|
scheme: "exact",
|
|
1380
2046
|
network: selectedNetwork,
|
|
1381
2047
|
asset: tokenAddress,
|
|
@@ -1384,6 +2050,27 @@ var MoltsPayServer = class {
|
|
|
1384
2050
|
maxTimeoutSeconds: 300,
|
|
1385
2051
|
extra: tokenDomain
|
|
1386
2052
|
};
|
|
2053
|
+
if (selectedNetwork === "solana:mainnet" || selectedNetwork === "solana:devnet") {
|
|
2054
|
+
const solanaFacilitator = this.registry.get("solana");
|
|
2055
|
+
const feePayerPubkey = solanaFacilitator?.getFeePayerPubkey?.();
|
|
2056
|
+
if (feePayerPubkey) {
|
|
2057
|
+
requirements.extra = {
|
|
2058
|
+
...requirements.extra || {},
|
|
2059
|
+
solanaFeePayer: feePayerPubkey
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
if (selectedNetwork === "eip155:56" || selectedNetwork === "eip155:97") {
|
|
2064
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
2065
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
2066
|
+
if (spenderAddress) {
|
|
2067
|
+
requirements.extra = {
|
|
2068
|
+
...requirements.extra || {},
|
|
2069
|
+
bnbSpender: spenderAddress
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
return requirements;
|
|
1387
2074
|
}
|
|
1388
2075
|
/**
|
|
1389
2076
|
* Detect which token is being used in the payment
|
|
@@ -1449,31 +2136,42 @@ var MoltsPayServer = class {
|
|
|
1449
2136
|
/**
|
|
1450
2137
|
* POST /proxy - Handle payment for external services (moltspay-creators)
|
|
1451
2138
|
*
|
|
1452
|
-
* This endpoint allows other services to delegate x402 payment handling.
|
|
2139
|
+
* This endpoint allows other services to delegate x402/MPP payment handling.
|
|
1453
2140
|
* It does NOT execute any skill - just handles payment verification/settlement.
|
|
1454
2141
|
*
|
|
1455
2142
|
* Request body:
|
|
1456
2143
|
* { wallet, amount, currency, chain, memo, serviceId, description }
|
|
1457
2144
|
*
|
|
1458
|
-
*
|
|
1459
|
-
*
|
|
2145
|
+
* For x402 (base, polygon, base_sepolia):
|
|
2146
|
+
* Without X-Payment header: returns 402 with X-Payment-Required
|
|
2147
|
+
* With X-Payment header: verifies payment via CDP
|
|
2148
|
+
*
|
|
2149
|
+
* For MPP (tempo_moderato):
|
|
2150
|
+
* Without Authorization header: returns 402 with WWW-Authenticate
|
|
2151
|
+
* With Authorization: Payment header: verifies tx on Tempo chain
|
|
1460
2152
|
*/
|
|
1461
|
-
async handleProxy(body, paymentHeader, res) {
|
|
2153
|
+
async handleProxy(body, paymentHeader, authHeader, res) {
|
|
1462
2154
|
const { wallet, amount, currency, chain, memo, serviceId, description } = body;
|
|
1463
2155
|
if (!wallet || !amount) {
|
|
1464
2156
|
return this.sendJson(res, 400, { error: "Missing required fields: wallet, amount" });
|
|
1465
2157
|
}
|
|
1466
|
-
|
|
1467
|
-
|
|
2158
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
|
|
2159
|
+
if (chain && !supportedChains.includes(chain)) {
|
|
2160
|
+
return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
|
|
2161
|
+
}
|
|
2162
|
+
const isSolanaChain = chain === "solana" || chain === "solana_devnet";
|
|
2163
|
+
const isValidEvmAddress = /^0x[a-fA-F0-9]{40}$/.test(wallet);
|
|
2164
|
+
const isValidSolanaAddress = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(wallet);
|
|
2165
|
+
if (isSolanaChain && !isValidSolanaAddress) {
|
|
2166
|
+
return this.sendJson(res, 400, { error: "Invalid Solana wallet address format" });
|
|
2167
|
+
}
|
|
2168
|
+
if (!isSolanaChain && !isValidEvmAddress) {
|
|
2169
|
+
return this.sendJson(res, 400, { error: "Invalid EVM wallet address format" });
|
|
1468
2170
|
}
|
|
1469
2171
|
const amountNum = parseFloat(amount);
|
|
1470
2172
|
if (isNaN(amountNum) || amountNum <= 0) {
|
|
1471
2173
|
return this.sendJson(res, 400, { error: "Invalid amount" });
|
|
1472
2174
|
}
|
|
1473
|
-
const supportedChains = ["base", "polygon", "base_sepolia"];
|
|
1474
|
-
if (chain && !supportedChains.includes(chain)) {
|
|
1475
|
-
return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
|
|
1476
|
-
}
|
|
1477
2175
|
const proxyConfig = {
|
|
1478
2176
|
id: serviceId || "proxy",
|
|
1479
2177
|
name: description || "Proxy Payment",
|
|
@@ -1485,6 +2183,9 @@ var MoltsPayServer = class {
|
|
|
1485
2183
|
input: {},
|
|
1486
2184
|
output: {}
|
|
1487
2185
|
};
|
|
2186
|
+
if (chain === "tempo_moderato") {
|
|
2187
|
+
return await this.handleProxyMPP(body, proxyConfig, authHeader, res);
|
|
2188
|
+
}
|
|
1488
2189
|
const requirements = this.buildProxyPaymentRequirements(proxyConfig, wallet, currency, chain);
|
|
1489
2190
|
if (!paymentHeader) {
|
|
1490
2191
|
return this.sendProxyPaymentRequired(proxyConfig, wallet, memo, chain, res);
|
|
@@ -1520,7 +2221,6 @@ var MoltsPayServer = class {
|
|
|
1520
2221
|
console.log(`[MoltsPay] /proxy: Verified by ${verifyResult.facilitator}`);
|
|
1521
2222
|
const { execute, service, params } = body;
|
|
1522
2223
|
if (execute && service) {
|
|
1523
|
-
console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
|
|
1524
2224
|
const skill = this.skills.get(service);
|
|
1525
2225
|
if (!skill) {
|
|
1526
2226
|
console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
|
|
@@ -1530,6 +2230,32 @@ var MoltsPayServer = class {
|
|
|
1530
2230
|
error: `Service not found: ${service}`
|
|
1531
2231
|
});
|
|
1532
2232
|
}
|
|
2233
|
+
const isSolana = isSolanaNetwork(network);
|
|
2234
|
+
let settlement2 = null;
|
|
2235
|
+
if (isSolana) {
|
|
2236
|
+
console.log(`[MoltsPay] /proxy: Solana detected - settling payment FIRST`);
|
|
2237
|
+
try {
|
|
2238
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
2239
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
2240
|
+
if (!settlement2.success) {
|
|
2241
|
+
console.error(`[MoltsPay] /proxy: Solana settlement failed: ${settlement2.error}`);
|
|
2242
|
+
return this.sendJson(res, 402, {
|
|
2243
|
+
success: false,
|
|
2244
|
+
paymentSettled: false,
|
|
2245
|
+
error: `Payment settlement failed: ${settlement2.error || "Unknown error"}`
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
} catch (err) {
|
|
2249
|
+
console.error("[MoltsPay] /proxy: Solana settlement failed:", err.message);
|
|
2250
|
+
return this.sendJson(res, 402, {
|
|
2251
|
+
success: false,
|
|
2252
|
+
paymentSettled: false,
|
|
2253
|
+
error: `Payment settlement failed: ${err.message}`
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
} else {
|
|
2257
|
+
console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
|
|
2258
|
+
}
|
|
1533
2259
|
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
1534
2260
|
let result;
|
|
1535
2261
|
try {
|
|
@@ -1539,34 +2265,36 @@ var MoltsPayServer = class {
|
|
|
1539
2265
|
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
1540
2266
|
)
|
|
1541
2267
|
]);
|
|
1542
|
-
console.log(`[MoltsPay] /proxy: Skill succeeded
|
|
2268
|
+
console.log(`[MoltsPay] /proxy: Skill succeeded`);
|
|
1543
2269
|
} catch (err) {
|
|
1544
|
-
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}
|
|
2270
|
+
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}`);
|
|
1545
2271
|
return this.sendJson(res, 500, {
|
|
1546
2272
|
success: false,
|
|
1547
|
-
paymentSettled: false,
|
|
1548
|
-
error: `Service execution failed: ${err.message}
|
|
2273
|
+
paymentSettled: isSolana ? true : false,
|
|
2274
|
+
error: `Service execution failed: ${err.message}`,
|
|
2275
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
1549
2276
|
});
|
|
1550
2277
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
2278
|
+
if (!isSolana) {
|
|
2279
|
+
console.log(`[MoltsPay] /proxy: Settling payment...`);
|
|
2280
|
+
try {
|
|
2281
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
2282
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
2283
|
+
} catch (err) {
|
|
2284
|
+
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
2285
|
+
return this.sendJson(res, 200, {
|
|
2286
|
+
success: true,
|
|
2287
|
+
verified: true,
|
|
2288
|
+
settled: false,
|
|
2289
|
+
settlementError: err.message,
|
|
2290
|
+
from: payment.payload?.authorization?.from,
|
|
2291
|
+
paidTo: wallet,
|
|
2292
|
+
amount: amountNum,
|
|
2293
|
+
currency: currency || "USDC",
|
|
2294
|
+
memo,
|
|
2295
|
+
result
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
1570
2298
|
}
|
|
1571
2299
|
return this.sendJson(res, 200, {
|
|
1572
2300
|
success: true,
|
|
@@ -1574,7 +2302,6 @@ var MoltsPayServer = class {
|
|
|
1574
2302
|
settled: settlement2?.success || false,
|
|
1575
2303
|
txHash: settlement2?.transaction,
|
|
1576
2304
|
from: payment.payload?.authorization?.from,
|
|
1577
|
-
// Buyer's wallet address
|
|
1578
2305
|
paidTo: wallet,
|
|
1579
2306
|
amount: amountNum,
|
|
1580
2307
|
currency: currency || "USDC",
|
|
@@ -1609,6 +2336,131 @@ var MoltsPayServer = class {
|
|
|
1609
2336
|
memo
|
|
1610
2337
|
});
|
|
1611
2338
|
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Handle MPP payment flow for /proxy endpoint (tempo_moderato chain)
|
|
2341
|
+
*/
|
|
2342
|
+
async handleProxyMPP(body, config, authHeader, res) {
|
|
2343
|
+
const { wallet, amount, memo, serviceId } = body;
|
|
2344
|
+
const amountNum = parseFloat(amount);
|
|
2345
|
+
const amountInUnits = Math.floor(amountNum * 1e6).toString();
|
|
2346
|
+
if (!authHeader || !authHeader.toLowerCase().startsWith("payment ")) {
|
|
2347
|
+
const challengeId = this.generateChallengeId();
|
|
2348
|
+
const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
|
|
2349
|
+
const mppRequest = {
|
|
2350
|
+
amount: amountInUnits,
|
|
2351
|
+
currency: tokenAddress,
|
|
2352
|
+
methodDetails: {
|
|
2353
|
+
chainId: 42431,
|
|
2354
|
+
feePayer: true
|
|
2355
|
+
},
|
|
2356
|
+
recipient: wallet
|
|
2357
|
+
};
|
|
2358
|
+
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
2359
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
2360
|
+
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
2361
|
+
res.writeHead(402, {
|
|
2362
|
+
"Content-Type": "application/problem+json",
|
|
2363
|
+
[MPP_WWW_AUTH_HEADER]: wwwAuth
|
|
2364
|
+
});
|
|
2365
|
+
res.end(JSON.stringify({
|
|
2366
|
+
type: "https://paymentauth.org/problems/payment-required",
|
|
2367
|
+
title: "Payment Required",
|
|
2368
|
+
status: 402,
|
|
2369
|
+
detail: `Payment is required (${config.name}).`,
|
|
2370
|
+
service: serviceId || "proxy",
|
|
2371
|
+
price: amountNum,
|
|
2372
|
+
currency: "USDC"
|
|
2373
|
+
}, null, 2));
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
|
|
2377
|
+
if (!credentialMatch) {
|
|
2378
|
+
return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
|
|
2379
|
+
}
|
|
2380
|
+
let mppCredential;
|
|
2381
|
+
try {
|
|
2382
|
+
const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
2383
|
+
const decoded = Buffer.from(base64, "base64").toString("utf-8");
|
|
2384
|
+
mppCredential = JSON.parse(decoded);
|
|
2385
|
+
} catch (err) {
|
|
2386
|
+
console.error("[MoltsPay] /proxy MPP: Failed to parse credential:", err);
|
|
2387
|
+
return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
|
|
2388
|
+
}
|
|
2389
|
+
let txHash;
|
|
2390
|
+
if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
|
|
2391
|
+
txHash = mppCredential.payload.hash;
|
|
2392
|
+
} else {
|
|
2393
|
+
return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
|
|
2394
|
+
}
|
|
2395
|
+
console.log(`[MoltsPay] /proxy MPP: Verifying tx ${txHash} on Tempo...`);
|
|
2396
|
+
const requirements = this.buildPaymentRequirements(config, "eip155:42431", wallet, "USDC");
|
|
2397
|
+
const paymentPayload = {
|
|
2398
|
+
x402Version: X402_VERSION2,
|
|
2399
|
+
scheme: "exact",
|
|
2400
|
+
network: "eip155:42431",
|
|
2401
|
+
payload: { txHash, chainId: 42431 }
|
|
2402
|
+
};
|
|
2403
|
+
const verification = await this.registry.verify(paymentPayload, requirements);
|
|
2404
|
+
if (!verification.valid) {
|
|
2405
|
+
return this.sendJson(res, 402, {
|
|
2406
|
+
error: `Payment verification failed: ${verification.error}`
|
|
2407
|
+
});
|
|
2408
|
+
}
|
|
2409
|
+
console.log(`[MoltsPay] /proxy MPP: Payment verified by ${verification.facilitator}`);
|
|
2410
|
+
const { execute, service, params } = body;
|
|
2411
|
+
if (execute && service) {
|
|
2412
|
+
console.log(`[MoltsPay] /proxy MPP: Executing skill: ${service}`);
|
|
2413
|
+
const skill = this.skills.get(service);
|
|
2414
|
+
if (!skill) {
|
|
2415
|
+
return this.sendJson(res, 404, {
|
|
2416
|
+
success: false,
|
|
2417
|
+
paymentSettled: true,
|
|
2418
|
+
// Payment already happened on Tempo
|
|
2419
|
+
error: `Service not found: ${service}`
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
2423
|
+
let result;
|
|
2424
|
+
try {
|
|
2425
|
+
result = await Promise.race([
|
|
2426
|
+
skill.handler(params || {}),
|
|
2427
|
+
new Promise(
|
|
2428
|
+
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
2429
|
+
)
|
|
2430
|
+
]);
|
|
2431
|
+
} catch (err) {
|
|
2432
|
+
console.error(`[MoltsPay] /proxy MPP: Skill failed: ${err.message}`);
|
|
2433
|
+
return this.sendJson(res, 500, {
|
|
2434
|
+
success: false,
|
|
2435
|
+
paymentSettled: true,
|
|
2436
|
+
error: `Service execution failed: ${err.message}`
|
|
2437
|
+
});
|
|
2438
|
+
}
|
|
2439
|
+
return this.sendJson(res, 200, {
|
|
2440
|
+
success: true,
|
|
2441
|
+
verified: true,
|
|
2442
|
+
txHash,
|
|
2443
|
+
chain: "tempo_moderato",
|
|
2444
|
+
paidTo: wallet,
|
|
2445
|
+
amount: amountNum,
|
|
2446
|
+
currency: "USDC",
|
|
2447
|
+
facilitator: verification.facilitator,
|
|
2448
|
+
memo,
|
|
2449
|
+
result
|
|
2450
|
+
});
|
|
2451
|
+
}
|
|
2452
|
+
this.sendJson(res, 200, {
|
|
2453
|
+
success: true,
|
|
2454
|
+
verified: true,
|
|
2455
|
+
txHash,
|
|
2456
|
+
chain: "tempo_moderato",
|
|
2457
|
+
paidTo: wallet,
|
|
2458
|
+
amount: amountNum,
|
|
2459
|
+
currency: "USDC",
|
|
2460
|
+
facilitator: verification.facilitator,
|
|
2461
|
+
memo
|
|
2462
|
+
});
|
|
2463
|
+
}
|
|
1612
2464
|
/**
|
|
1613
2465
|
* Build payment requirements for proxy endpoint (uses provided wallet)
|
|
1614
2466
|
*/
|
|
@@ -1620,7 +2472,7 @@ var MoltsPayServer = class {
|
|
|
1620
2472
|
const tokenAddresses = TOKEN_ADDRESSES[networkId] || TOKEN_ADDRESSES[this.networkId] || {};
|
|
1621
2473
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
1622
2474
|
const tokenDomain = getTokenDomain(networkId, selectedToken);
|
|
1623
|
-
|
|
2475
|
+
const requirements = {
|
|
1624
2476
|
scheme: "exact",
|
|
1625
2477
|
network: networkId,
|
|
1626
2478
|
asset: tokenAddress,
|
|
@@ -1630,6 +2482,17 @@ var MoltsPayServer = class {
|
|
|
1630
2482
|
maxTimeoutSeconds: 300,
|
|
1631
2483
|
extra: tokenDomain
|
|
1632
2484
|
};
|
|
2485
|
+
if (networkId === "eip155:56" || networkId === "eip155:97") {
|
|
2486
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
2487
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
2488
|
+
if (spenderAddress) {
|
|
2489
|
+
requirements.extra = {
|
|
2490
|
+
...requirements.extra || {},
|
|
2491
|
+
bnbSpender: spenderAddress
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
return requirements;
|
|
1633
2496
|
}
|
|
1634
2497
|
/**
|
|
1635
2498
|
* Return 402 with x402 payment requirements for proxy endpoint
|