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.
Files changed (57) hide show
  1. package/README.md +221 -38
  2. package/dist/cdp/index.d.mts +4 -4
  3. package/dist/cdp/index.d.ts +4 -4
  4. package/dist/cdp/index.js +57 -0
  5. package/dist/cdp/index.js.map +1 -1
  6. package/dist/cdp/index.mjs +57 -0
  7. package/dist/cdp/index.mjs.map +1 -1
  8. package/dist/chains/index.d.mts +9 -8
  9. package/dist/chains/index.d.ts +9 -8
  10. package/dist/chains/index.js +57 -0
  11. package/dist/chains/index.js.map +1 -1
  12. package/dist/chains/index.mjs +57 -0
  13. package/dist/chains/index.mjs.map +1 -1
  14. package/dist/cli/index.js +1975 -273
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/cli/index.mjs +1977 -265
  17. package/dist/cli/index.mjs.map +1 -1
  18. package/dist/client/index.d.mts +36 -3
  19. package/dist/client/index.d.ts +36 -3
  20. package/dist/client/index.js +540 -32
  21. package/dist/client/index.js.map +1 -1
  22. package/dist/client/index.mjs +548 -30
  23. package/dist/client/index.mjs.map +1 -1
  24. package/dist/facilitators/index.d.mts +220 -1
  25. package/dist/facilitators/index.d.ts +220 -1
  26. package/dist/facilitators/index.js +664 -1
  27. package/dist/facilitators/index.js.map +1 -1
  28. package/dist/facilitators/index.mjs +670 -1
  29. package/dist/facilitators/index.mjs.map +1 -1
  30. package/dist/{index-On9ZaGDW.d.mts → index-D_2FkLwV.d.mts} +6 -2
  31. package/dist/{index-On9ZaGDW.d.ts → index-D_2FkLwV.d.ts} +6 -2
  32. package/dist/index.d.mts +2 -1
  33. package/dist/index.d.ts +2 -1
  34. package/dist/index.js +1413 -146
  35. package/dist/index.js.map +1 -1
  36. package/dist/index.mjs +1421 -144
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/server/index.d.mts +13 -3
  39. package/dist/server/index.d.ts +13 -3
  40. package/dist/server/index.js +905 -52
  41. package/dist/server/index.js.map +1 -1
  42. package/dist/server/index.mjs +915 -52
  43. package/dist/server/index.mjs.map +1 -1
  44. package/dist/verify/index.d.mts +1 -1
  45. package/dist/verify/index.d.ts +1 -1
  46. package/dist/verify/index.js +57 -0
  47. package/dist/verify/index.js.map +1 -1
  48. package/dist/verify/index.mjs +57 -0
  49. package/dist/verify/index.mjs.map +1 -1
  50. package/dist/wallet/index.d.mts +3 -3
  51. package/dist/wallet/index.d.ts +3 -3
  52. package/dist/wallet/index.js +57 -0
  53. package/dist/wallet/index.js.map +1 -1
  54. package/dist/wallet/index.mjs +57 -0
  55. package/dist/wallet/index.mjs.map +1 -1
  56. package/package.json +4 -1
  57. package/schemas/moltspay.services.schema.json +27 -132
@@ -1,7 +1,7 @@
1
1
  // src/client/index.ts
2
- import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, chmodSync } from "fs";
3
- import { homedir } from "os";
4
- import { join } from "path";
2
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, statSync, chmodSync } from "fs";
3
+ import { homedir as homedir2 } from "os";
4
+ import { join as join2 } from "path";
5
5
  import { Wallet, ethers } from "ethers";
6
6
 
7
7
  // src/chains/index.ts
@@ -111,6 +111,63 @@ var CHAINS = {
111
111
  explorerTx: "https://explore.testnet.tempo.xyz/tx/",
112
112
  avgBlockTime: 0.5
113
113
  // ~500ms finality
114
+ },
115
+ // ============ BNB Chain Testnet ============
116
+ bnb_testnet: {
117
+ name: "BNB Testnet",
118
+ chainId: 97,
119
+ rpc: "https://data-seed-prebsc-1-s1.binance.org:8545",
120
+ tokens: {
121
+ // Note: BNB uses 18 decimals for stablecoins (unlike Base/Polygon which use 6)
122
+ // Using official Binance-Peg testnet tokens
123
+ USDC: {
124
+ address: "0x64544969ed7EBf5f083679233325356EbE738930",
125
+ // Testnet USDC
126
+ decimals: 18,
127
+ symbol: "USDC",
128
+ eip712Name: "USD Coin"
129
+ },
130
+ USDT: {
131
+ address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
132
+ // Testnet USDT
133
+ decimals: 18,
134
+ symbol: "USDT",
135
+ eip712Name: "Tether USD"
136
+ }
137
+ },
138
+ usdc: "0x64544969ed7EBf5f083679233325356EbE738930",
139
+ explorer: "https://testnet.bscscan.com/address/",
140
+ explorerTx: "https://testnet.bscscan.com/tx/",
141
+ avgBlockTime: 3,
142
+ // BNB-specific: requires approval for pay-for-success flow
143
+ requiresApproval: true
144
+ },
145
+ // ============ BNB Chain Mainnet ============
146
+ bnb: {
147
+ name: "BNB Smart Chain",
148
+ chainId: 56,
149
+ rpc: "https://bsc-dataseed.binance.org",
150
+ tokens: {
151
+ // Note: BNB uses 18 decimals for stablecoins
152
+ USDC: {
153
+ address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
154
+ decimals: 18,
155
+ symbol: "USDC",
156
+ eip712Name: "USD Coin"
157
+ },
158
+ USDT: {
159
+ address: "0x55d398326f99059fF775485246999027B3197955",
160
+ decimals: 18,
161
+ symbol: "USDT",
162
+ eip712Name: "Tether USD"
163
+ }
164
+ },
165
+ usdc: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
166
+ explorer: "https://bscscan.com/address/",
167
+ explorerTx: "https://bscscan.com/tx/",
168
+ avgBlockTime: 3,
169
+ // BNB-specific: requires approval for pay-for-success flow
170
+ requiresApproval: true
114
171
  }
115
172
  };
116
173
  function getChain(name) {
@@ -121,7 +178,129 @@ function getChain(name) {
121
178
  return config;
122
179
  }
123
180
 
181
+ // src/wallet/solana.ts
182
+ import { Keypair, PublicKey as PublicKey2, LAMPORTS_PER_SOL } from "@solana/web3.js";
183
+ import { getAssociatedTokenAddress, getAccount } from "@solana/spl-token";
184
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
185
+ import { join } from "path";
186
+ import { homedir } from "os";
187
+ import bs58 from "bs58";
188
+
189
+ // src/chains/solana.ts
190
+ import { Connection, PublicKey } from "@solana/web3.js";
191
+ var SOLANA_CHAINS = {
192
+ solana: {
193
+ name: "Solana Mainnet",
194
+ cluster: "mainnet-beta",
195
+ rpc: "https://api.mainnet-beta.solana.com",
196
+ explorer: "https://solscan.io/account/",
197
+ explorerTx: "https://solscan.io/tx/",
198
+ tokens: {
199
+ USDC: {
200
+ mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
201
+ // Circle official USDC
202
+ decimals: 6
203
+ }
204
+ }
205
+ },
206
+ solana_devnet: {
207
+ name: "Solana Devnet",
208
+ cluster: "devnet",
209
+ rpc: "https://api.devnet.solana.com",
210
+ explorer: "https://solscan.io/account/",
211
+ explorerTx: "https://solscan.io/tx/",
212
+ tokens: {
213
+ USDC: {
214
+ // Circle's devnet USDC (if not available, we'll deploy our own test token)
215
+ mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
216
+ decimals: 6
217
+ }
218
+ }
219
+ }
220
+ };
221
+
222
+ // src/wallet/solana.ts
223
+ var DEFAULT_CONFIG_DIR = join(homedir(), ".moltspay");
224
+ var SOLANA_WALLET_FILE = "wallet-solana.json";
225
+ function getSolanaWalletPath(configDir = DEFAULT_CONFIG_DIR) {
226
+ return join(configDir, SOLANA_WALLET_FILE);
227
+ }
228
+ function loadSolanaWallet(configDir = DEFAULT_CONFIG_DIR) {
229
+ const walletPath = getSolanaWalletPath(configDir);
230
+ if (!existsSync(walletPath)) {
231
+ return null;
232
+ }
233
+ try {
234
+ const data = JSON.parse(readFileSync(walletPath, "utf-8"));
235
+ const secretKey = bs58.decode(data.secretKey);
236
+ return Keypair.fromSecretKey(secretKey);
237
+ } catch (error) {
238
+ console.error("Failed to load Solana wallet:", error);
239
+ return null;
240
+ }
241
+ }
242
+
243
+ // src/facilitators/solana.ts
244
+ import {
245
+ Connection as Connection3,
246
+ PublicKey as PublicKey3,
247
+ Transaction,
248
+ VersionedTransaction
249
+ } from "@solana/web3.js";
250
+ import {
251
+ getAssociatedTokenAddress as getAssociatedTokenAddress2,
252
+ createTransferCheckedInstruction,
253
+ getAccount as getAccount2,
254
+ createAssociatedTokenAccountInstruction
255
+ } from "@solana/spl-token";
256
+ async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey) {
257
+ const chainConfig = SOLANA_CHAINS[chain];
258
+ const connection = new Connection3(chainConfig.rpc, "confirmed");
259
+ const mint = new PublicKey3(chainConfig.tokens.USDC.mint);
260
+ const actualFeePayer = feePayerPubkey || senderPubkey;
261
+ const senderATA = await getAssociatedTokenAddress2(mint, senderPubkey);
262
+ const recipientATA = await getAssociatedTokenAddress2(mint, recipientPubkey);
263
+ const transaction = new Transaction();
264
+ try {
265
+ await getAccount2(connection, recipientATA);
266
+ } catch {
267
+ transaction.add(
268
+ createAssociatedTokenAccountInstruction(
269
+ actualFeePayer,
270
+ // payer (fee payer in gasless mode)
271
+ recipientATA,
272
+ // ata to create
273
+ recipientPubkey,
274
+ // owner
275
+ mint
276
+ // mint
277
+ )
278
+ );
279
+ }
280
+ transaction.add(
281
+ createTransferCheckedInstruction(
282
+ senderATA,
283
+ // source
284
+ mint,
285
+ // mint
286
+ recipientATA,
287
+ // destination
288
+ senderPubkey,
289
+ // owner (sender still authorizes the transfer)
290
+ amount,
291
+ // amount
292
+ chainConfig.tokens.USDC.decimals
293
+ // decimals
294
+ )
295
+ );
296
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
297
+ transaction.recentBlockhash = blockhash;
298
+ transaction.feePayer = actualFeePayer;
299
+ return transaction;
300
+ }
301
+
124
302
  // src/client/index.ts
303
+ import { PublicKey as PublicKey4 } from "@solana/web3.js";
125
304
  var X402_VERSION = 2;
126
305
  var PAYMENT_REQUIRED_HEADER = "x-payment-required";
127
306
  var PAYMENT_HEADER = "x-payment";
@@ -140,7 +319,7 @@ var MoltsPayClient = class {
140
319
  todaySpending = 0;
141
320
  lastSpendingReset = 0;
142
321
  constructor(options = {}) {
143
- this.configDir = options.configDir || join(homedir(), ".moltspay");
322
+ this.configDir = options.configDir || join2(homedir2(), ".moltspay");
144
323
  this.config = this.loadConfig();
145
324
  this.walletData = this.loadWallet();
146
325
  this.loadSpending();
@@ -160,6 +339,12 @@ var MoltsPayClient = class {
160
339
  get address() {
161
340
  return this.wallet?.address || null;
162
341
  }
342
+ /**
343
+ * Get wallet instance (for direct operations like approvals)
344
+ */
345
+ getWallet() {
346
+ return this.wallet;
347
+ }
163
348
  /**
164
349
  * Get current config
165
350
  */
@@ -229,9 +414,14 @@ var MoltsPayClient = class {
229
414
  }
230
415
  throw new Error(data.error || "Unexpected response");
231
416
  }
417
+ const wwwAuthHeader = initialRes.headers.get("www-authenticate");
232
418
  const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER);
419
+ if (wwwAuthHeader && wwwAuthHeader.toLowerCase().includes("payment")) {
420
+ console.log("[MoltsPay] Detected MPP protocol, using Tempo flow...");
421
+ return await this.handleMPPPayment(serverUrl, service, params, wwwAuthHeader);
422
+ }
233
423
  if (!paymentRequiredHeader) {
234
- throw new Error("Missing x-payment-required header");
424
+ throw new Error("Missing payment header (x-payment-required or www-authenticate)");
235
425
  }
236
426
  let requirements;
237
427
  try {
@@ -248,17 +438,22 @@ var MoltsPayClient = class {
248
438
  throw new Error("Invalid x-payment-required header");
249
439
  }
250
440
  const networkToChainName = (network2) => {
441
+ if (network2 === "solana:mainnet") return "solana";
442
+ if (network2 === "solana:devnet") return "solana_devnet";
251
443
  const match = network2.match(/^eip155:(\d+)$/);
252
444
  if (!match) return null;
253
445
  const chainId = parseInt(match[1]);
254
446
  if (chainId === 8453) return "base";
255
447
  if (chainId === 137) return "polygon";
256
448
  if (chainId === 84532) return "base_sepolia";
449
+ if (chainId === 42431) return "tempo_moderato";
450
+ if (chainId === 56) return "bnb";
451
+ if (chainId === 97) return "bnb_testnet";
257
452
  return null;
258
453
  };
259
454
  const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
260
- let chainName;
261
455
  const userSpecifiedChain = options.chain;
456
+ let selectedChain;
262
457
  if (userSpecifiedChain) {
263
458
  if (!serverChains.includes(userSpecifiedChain)) {
264
459
  throw new Error(
@@ -266,17 +461,27 @@ var MoltsPayClient = class {
266
461
  Server accepts: ${serverChains.join(", ")}`
267
462
  );
268
463
  }
269
- chainName = userSpecifiedChain;
464
+ selectedChain = userSpecifiedChain;
270
465
  } else {
271
466
  if (serverChains.length === 1 && serverChains[0] === "base") {
272
- chainName = "base";
467
+ selectedChain = "base";
273
468
  } else {
274
469
  throw new Error(
275
470
  `Server accepts: ${serverChains.join(", ")}
276
- Please specify: --chain base, --chain polygon, or --chain base_sepolia`
471
+ Please specify: --chain <chain_name>`
277
472
  );
278
473
  }
279
474
  }
475
+ if (selectedChain === "solana" || selectedChain === "solana_devnet") {
476
+ const solanaChain = selectedChain;
477
+ const network2 = solanaChain === "solana" ? "solana:mainnet" : "solana:devnet";
478
+ const req2 = requirements.find((r) => r.network === network2);
479
+ if (!req2) {
480
+ throw new Error(`Failed to find payment requirement for ${selectedChain}`);
481
+ }
482
+ return await this.handleSolanaPayment(serverUrl, service, params, req2, solanaChain);
483
+ }
484
+ const chainName = selectedChain;
280
485
  const chain = getChain(chainName);
281
486
  const network = `eip155:${chain.chainId}`;
282
487
  const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
@@ -311,6 +516,25 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
311
516
  } else {
312
517
  console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
313
518
  }
519
+ if (chainName === "bnb" || chainName === "bnb_testnet") {
520
+ console.log(`[MoltsPay] Using BNB intent-based payment flow...`);
521
+ const payTo2 = req.payTo || req.resource;
522
+ if (!payTo2) {
523
+ throw new Error("Missing payTo address in payment requirements");
524
+ }
525
+ const bnbSpender = req.extra?.bnbSpender;
526
+ if (!bnbSpender) {
527
+ throw new Error("Server did not provide bnbSpender address. Server may not support BNB payments.");
528
+ }
529
+ return await this.handleBNBPayment(serverUrl, service, params, {
530
+ to: payTo2,
531
+ amount,
532
+ token,
533
+ chainName,
534
+ chain,
535
+ spender: bnbSpender
536
+ });
537
+ }
314
538
  const payTo = req.payTo || req.resource;
315
539
  if (!payTo) {
316
540
  throw new Error("Missing payTo address in payment requirements");
@@ -360,6 +584,300 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
360
584
  console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
361
585
  return result.result;
362
586
  }
587
+ /**
588
+ * Handle MPP (Machine Payments Protocol) payment flow
589
+ * Called when pay() detects WWW-Authenticate header in 402 response
590
+ */
591
+ async handleMPPPayment(serverUrl, service, params, wwwAuthHeader) {
592
+ const { privateKeyToAccount } = await import("viem/accounts");
593
+ const { createWalletClient, createPublicClient, http } = await import("viem");
594
+ const { tempoModerato } = await import("viem/chains");
595
+ const { Actions } = await import("viem/tempo");
596
+ const privateKey = this.walletData.privateKey;
597
+ const account = privateKeyToAccount(privateKey);
598
+ console.log(`[MoltsPay] Using MPP protocol on Tempo`);
599
+ console.log(`[MoltsPay] Account: ${account.address}`);
600
+ const parseAuthParam = (header, key) => {
601
+ const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
602
+ return match ? match[1] : null;
603
+ };
604
+ const challengeId = parseAuthParam(wwwAuthHeader, "id");
605
+ const method = parseAuthParam(wwwAuthHeader, "method");
606
+ const realm = parseAuthParam(wwwAuthHeader, "realm");
607
+ const requestB64 = parseAuthParam(wwwAuthHeader, "request");
608
+ if (method !== "tempo") {
609
+ throw new Error(`Unsupported payment method: ${method}`);
610
+ }
611
+ if (!requestB64) {
612
+ throw new Error("Missing request in WWW-Authenticate");
613
+ }
614
+ const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
615
+ const paymentRequest = JSON.parse(requestJson);
616
+ const { amount, currency, recipient, methodDetails } = paymentRequest;
617
+ const chainId = methodDetails?.chainId || 42431;
618
+ const amountDisplay = Number(amount) / 1e6;
619
+ console.log(`[MoltsPay] Payment: $${amountDisplay} to ${recipient}`);
620
+ this.checkLimits(amountDisplay);
621
+ console.log(`[MoltsPay] Sending transaction on Tempo...`);
622
+ const tempoChain = { ...tempoModerato, feeToken: currency };
623
+ const publicClient = createPublicClient({
624
+ chain: tempoChain,
625
+ transport: http("https://rpc.moderato.tempo.xyz")
626
+ });
627
+ const walletClient = createWalletClient({
628
+ account,
629
+ chain: tempoChain,
630
+ transport: http("https://rpc.moderato.tempo.xyz")
631
+ });
632
+ const txHash = await Actions.token.transfer(walletClient, {
633
+ to: recipient,
634
+ amount: BigInt(amount),
635
+ token: currency
636
+ });
637
+ console.log(`[MoltsPay] Transaction: ${txHash}`);
638
+ await publicClient.waitForTransactionReceipt({ hash: txHash });
639
+ console.log(`[MoltsPay] Confirmed! Retrying with credential...`);
640
+ const credential = {
641
+ challenge: {
642
+ id: challengeId,
643
+ realm,
644
+ method: "tempo",
645
+ intent: "charge",
646
+ request: paymentRequest
647
+ },
648
+ payload: { hash: txHash, type: "hash" },
649
+ source: `did:pkh:eip155:${chainId}:${account.address}`
650
+ };
651
+ const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
652
+ const paidRes = await fetch(`${serverUrl}/execute`, {
653
+ method: "POST",
654
+ headers: {
655
+ "Content-Type": "application/json",
656
+ "Authorization": `Payment ${credentialB64}`
657
+ },
658
+ body: JSON.stringify({ service, params, chain: "tempo_moderato" })
659
+ });
660
+ const result = await paidRes.json();
661
+ if (!paidRes.ok) {
662
+ throw new Error(result.error || "Payment verification failed");
663
+ }
664
+ this.recordSpending(amountDisplay);
665
+ console.log(`[MoltsPay] Success!`);
666
+ return result.result || result;
667
+ }
668
+ /**
669
+ * Handle BNB Chain payment flow (pre-approval + intent signature)
670
+ *
671
+ * Flow:
672
+ * 1. Check client has approved server wallet (done via `moltspay init`)
673
+ * 2. Sign EIP-712 payment intent (no gas, just signature)
674
+ * 3. Send intent to server
675
+ * 4. Server executes service
676
+ * 5. Server calls transferFrom if successful (pay-for-success)
677
+ */
678
+ async handleBNBPayment(serverUrl, service, params, paymentDetails) {
679
+ const { to, amount, token, chainName, chain, spender } = paymentDetails;
680
+ const tokenConfig = chain.tokens[token];
681
+ const provider = new ethers.JsonRpcProvider(chain.rpc);
682
+ const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
683
+ const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
684
+ if (allowance < amountWeiCheck) {
685
+ const nativeBalance = await provider.getBalance(this.wallet.address);
686
+ const minGasBalance = ethers.parseEther("0.0005");
687
+ if (nativeBalance < minGasBalance) {
688
+ const nativeBNB = parseFloat(ethers.formatEther(nativeBalance)).toFixed(4);
689
+ const isTestnet = chainName === "bnb_testnet";
690
+ if (isTestnet) {
691
+ throw new Error(
692
+ `\u274C Insufficient tBNB for approval transaction
693
+
694
+ Current tBNB: ${nativeBNB}
695
+ Required: ~0.001 tBNB
696
+
697
+ Get testnet tokens: npx moltspay faucet --chain bnb_testnet
698
+ (Gives USDC + tBNB for gas)`
699
+ );
700
+ } else {
701
+ throw new Error(
702
+ `\u274C Insufficient BNB for approval transaction
703
+
704
+ Current BNB: ${nativeBNB}
705
+ Required: ~0.001 BNB (~$0.60)
706
+
707
+ To get BNB:
708
+ \u2022 Withdraw from Binance/exchange to your wallet
709
+ \u2022 Most exchanges include BNB dust with withdrawals
710
+
711
+ After funding, run:
712
+ npx moltspay approve --chain ${chainName} --spender ${spender}`
713
+ );
714
+ }
715
+ }
716
+ throw new Error(
717
+ `Insufficient allowance for ${spender.slice(0, 10)}...
718
+ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
719
+ );
720
+ }
721
+ const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
722
+ const intent = {
723
+ from: this.wallet.address,
724
+ to,
725
+ amount: amountWei,
726
+ token: tokenConfig.address,
727
+ service,
728
+ nonce: Date.now(),
729
+ // Use timestamp as nonce for simplicity
730
+ deadline: Date.now() + 36e5
731
+ // 1 hour
732
+ };
733
+ const domain = {
734
+ name: "MoltsPay",
735
+ version: "1",
736
+ chainId: chain.chainId
737
+ };
738
+ const types = {
739
+ PaymentIntent: [
740
+ { name: "from", type: "address" },
741
+ { name: "to", type: "address" },
742
+ { name: "amount", type: "uint256" },
743
+ { name: "token", type: "address" },
744
+ { name: "service", type: "string" },
745
+ { name: "nonce", type: "uint256" },
746
+ { name: "deadline", type: "uint256" }
747
+ ]
748
+ };
749
+ console.log(`[MoltsPay] Signing BNB payment intent...`);
750
+ const signature = await this.wallet.signTypedData(domain, types, intent);
751
+ const network = `eip155:${chain.chainId}`;
752
+ const payload = {
753
+ x402Version: 2,
754
+ scheme: "exact",
755
+ network,
756
+ payload: {
757
+ intent: {
758
+ ...intent,
759
+ signature
760
+ },
761
+ chainId: chain.chainId
762
+ },
763
+ accepted: {
764
+ scheme: "exact",
765
+ network,
766
+ asset: tokenConfig.address,
767
+ amount: amountWei,
768
+ payTo: to,
769
+ maxTimeoutSeconds: 300
770
+ }
771
+ };
772
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
773
+ console.log(`[MoltsPay] Sending BNB payment request...`);
774
+ const paidRes = await fetch(`${serverUrl}/execute`, {
775
+ method: "POST",
776
+ headers: {
777
+ "Content-Type": "application/json",
778
+ "X-Payment": paymentHeader
779
+ },
780
+ body: JSON.stringify({ service, params, chain: chainName })
781
+ });
782
+ const result = await paidRes.json();
783
+ if (!paidRes.ok) {
784
+ throw new Error(result.error || "BNB payment failed");
785
+ }
786
+ this.recordSpending(amount);
787
+ console.log(`[MoltsPay] Success! BNB payment settled.`);
788
+ return result.result || result;
789
+ }
790
+ /**
791
+ * Handle Solana payment flow
792
+ *
793
+ * Solana uses SPL token transfers with pay-for-success model:
794
+ * 1. Client creates and signs a transfer transaction
795
+ * 2. Server submits the transaction after service completes
796
+ */
797
+ async handleSolanaPayment(serverUrl, service, params, requirements, chain) {
798
+ const solanaWallet = loadSolanaWallet(this.configDir);
799
+ if (!solanaWallet) {
800
+ throw new Error("No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
801
+ }
802
+ const amount = Number(requirements.amount);
803
+ const amountUSDC = amount / 1e6;
804
+ this.checkLimits(amountUSDC);
805
+ console.log(`[MoltsPay] Creating Solana payment: $${amountUSDC} USDC`);
806
+ if (!requirements.payTo) {
807
+ throw new Error("Missing payTo address in payment requirements");
808
+ }
809
+ const solanaFeePayer = requirements.extra?.solanaFeePayer;
810
+ const feePayerPubkey = solanaFeePayer ? new PublicKey4(solanaFeePayer) : void 0;
811
+ if (feePayerPubkey) {
812
+ console.log(`[MoltsPay] Gasless mode: server pays fees`);
813
+ }
814
+ const recipientPubkey = new PublicKey4(requirements.payTo);
815
+ const transaction = await createSolanaPaymentTransaction(
816
+ solanaWallet.publicKey,
817
+ recipientPubkey,
818
+ BigInt(amount),
819
+ chain,
820
+ feePayerPubkey
821
+ // Optional fee payer for gasless mode
822
+ );
823
+ if (feePayerPubkey) {
824
+ transaction.partialSign(solanaWallet);
825
+ } else {
826
+ transaction.sign(solanaWallet);
827
+ }
828
+ const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
829
+ console.log(`[MoltsPay] Transaction signed, sending to server...`);
830
+ const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
831
+ const payload = {
832
+ x402Version: 2,
833
+ scheme: "exact",
834
+ network,
835
+ payload: {
836
+ signedTransaction: signedTx,
837
+ sender: solanaWallet.publicKey.toBase58(),
838
+ chain
839
+ },
840
+ accepted: {
841
+ scheme: "exact",
842
+ network,
843
+ asset: requirements.asset,
844
+ amount: requirements.amount,
845
+ payTo: requirements.payTo,
846
+ maxTimeoutSeconds: 300
847
+ }
848
+ };
849
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
850
+ const paidRes = await fetch(`${serverUrl}/execute`, {
851
+ method: "POST",
852
+ headers: {
853
+ "Content-Type": "application/json",
854
+ "X-Payment": paymentHeader
855
+ },
856
+ body: JSON.stringify({ service, params, chain })
857
+ });
858
+ const result = await paidRes.json();
859
+ if (!paidRes.ok) {
860
+ throw new Error(result.error || "Solana payment failed");
861
+ }
862
+ this.recordSpending(amountUSDC);
863
+ console.log(`[MoltsPay] Success! Solana payment settled.`);
864
+ if (result.payment?.transaction) {
865
+ const explorerUrl = chain === "solana" ? `https://solscan.io/tx/${result.payment.transaction}` : `https://solscan.io/tx/${result.payment.transaction}?cluster=devnet`;
866
+ console.log(`[MoltsPay] Transaction: ${explorerUrl}`);
867
+ }
868
+ return result.result || result;
869
+ }
870
+ /**
871
+ * Check ERC20 allowance for a spender
872
+ */
873
+ async checkAllowance(tokenAddress, spender, provider) {
874
+ const contract = new ethers.Contract(
875
+ tokenAddress,
876
+ ["function allowance(address owner, address spender) view returns (uint256)"],
877
+ provider
878
+ );
879
+ return await contract.allowance(this.wallet.address, spender);
880
+ }
363
881
  /**
364
882
  * Sign EIP-3009 transferWithAuthorization (GASLESS)
365
883
  * This only signs - no on-chain transaction, no gas needed.
@@ -430,26 +948,26 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
430
948
  }
431
949
  // --- Config & Wallet Management ---
432
950
  loadConfig() {
433
- const configPath = join(this.configDir, "config.json");
434
- if (existsSync(configPath)) {
435
- const content = readFileSync(configPath, "utf-8");
951
+ const configPath = join2(this.configDir, "config.json");
952
+ if (existsSync2(configPath)) {
953
+ const content = readFileSync2(configPath, "utf-8");
436
954
  return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
437
955
  }
438
956
  return { ...DEFAULT_CONFIG };
439
957
  }
440
958
  saveConfig() {
441
- mkdirSync(this.configDir, { recursive: true });
442
- const configPath = join(this.configDir, "config.json");
443
- writeFileSync(configPath, JSON.stringify(this.config, null, 2));
959
+ mkdirSync2(this.configDir, { recursive: true });
960
+ const configPath = join2(this.configDir, "config.json");
961
+ writeFileSync2(configPath, JSON.stringify(this.config, null, 2));
444
962
  }
445
963
  /**
446
964
  * Load spending data from disk
447
965
  */
448
966
  loadSpending() {
449
- const spendingPath = join(this.configDir, "spending.json");
450
- if (existsSync(spendingPath)) {
967
+ const spendingPath = join2(this.configDir, "spending.json");
968
+ if (existsSync2(spendingPath)) {
451
969
  try {
452
- const data = JSON.parse(readFileSync(spendingPath, "utf-8"));
970
+ const data = JSON.parse(readFileSync2(spendingPath, "utf-8"));
453
971
  const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
454
972
  if (data.date && data.date === today) {
455
973
  this.todaySpending = data.amount || 0;
@@ -468,18 +986,18 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
468
986
  * Save spending data to disk
469
987
  */
470
988
  saveSpending() {
471
- mkdirSync(this.configDir, { recursive: true });
472
- const spendingPath = join(this.configDir, "spending.json");
989
+ mkdirSync2(this.configDir, { recursive: true });
990
+ const spendingPath = join2(this.configDir, "spending.json");
473
991
  const data = {
474
992
  date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
475
993
  amount: this.todaySpending,
476
994
  updatedAt: Date.now()
477
995
  };
478
- writeFileSync(spendingPath, JSON.stringify(data, null, 2));
996
+ writeFileSync2(spendingPath, JSON.stringify(data, null, 2));
479
997
  }
480
998
  loadWallet() {
481
- const walletPath = join(this.configDir, "wallet.json");
482
- if (existsSync(walletPath)) {
999
+ const walletPath = join2(this.configDir, "wallet.json");
1000
+ if (existsSync2(walletPath)) {
483
1001
  try {
484
1002
  const stats = statSync(walletPath);
485
1003
  const mode = stats.mode & 511;
@@ -490,7 +1008,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
490
1008
  }
491
1009
  } catch (err) {
492
1010
  }
493
- const content = readFileSync(walletPath, "utf-8");
1011
+ const content = readFileSync2(walletPath, "utf-8");
494
1012
  return JSON.parse(content);
495
1013
  }
496
1014
  return null;
@@ -499,15 +1017,15 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
499
1017
  * Initialize a new wallet (called by CLI)
500
1018
  */
501
1019
  static init(configDir, options) {
502
- mkdirSync(configDir, { recursive: true });
1020
+ mkdirSync2(configDir, { recursive: true });
503
1021
  const wallet = Wallet.createRandom();
504
1022
  const walletData = {
505
1023
  address: wallet.address,
506
1024
  privateKey: wallet.privateKey,
507
1025
  createdAt: Date.now()
508
1026
  };
509
- const walletPath = join(configDir, "wallet.json");
510
- writeFileSync(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
1027
+ const walletPath = join2(configDir, "wallet.json");
1028
+ writeFileSync2(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
511
1029
  const config = {
512
1030
  chain: options.chain,
513
1031
  limits: {
@@ -515,8 +1033,8 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
515
1033
  maxPerDay: options.maxPerDay
516
1034
  }
517
1035
  };
518
- const configPath = join(configDir, "config.json");
519
- writeFileSync(configPath, JSON.stringify(config, null, 2));
1036
+ const configPath = join2(configDir, "config.json");
1037
+ writeFileSync2(configPath, JSON.stringify(config, null, 2));
520
1038
  return { address: wallet.address, configDir };
521
1039
  }
522
1040
  /**
@@ -552,7 +1070,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
552
1070
  if (!this.wallet) {
553
1071
  throw new Error("Client not initialized");
554
1072
  }
555
- const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
1073
+ const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
556
1074
  const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
557
1075
  const results = {};
558
1076
  const tempoTokens = {