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
@@ -33,9 +33,9 @@ __export(client_exports, {
33
33
  MoltsPayClient: () => MoltsPayClient
34
34
  });
35
35
  module.exports = __toCommonJS(client_exports);
36
- var import_fs = require("fs");
37
- var import_os = require("os");
38
- var import_path = require("path");
36
+ var import_fs2 = require("fs");
37
+ var import_os2 = require("os");
38
+ var import_path2 = require("path");
39
39
  var import_ethers = require("ethers");
40
40
 
41
41
  // src/chains/index.ts
@@ -145,6 +145,63 @@ var CHAINS = {
145
145
  explorerTx: "https://explore.testnet.tempo.xyz/tx/",
146
146
  avgBlockTime: 0.5
147
147
  // ~500ms finality
148
+ },
149
+ // ============ BNB Chain Testnet ============
150
+ bnb_testnet: {
151
+ name: "BNB Testnet",
152
+ chainId: 97,
153
+ rpc: "https://data-seed-prebsc-1-s1.binance.org:8545",
154
+ tokens: {
155
+ // Note: BNB uses 18 decimals for stablecoins (unlike Base/Polygon which use 6)
156
+ // Using official Binance-Peg testnet tokens
157
+ USDC: {
158
+ address: "0x64544969ed7EBf5f083679233325356EbE738930",
159
+ // Testnet USDC
160
+ decimals: 18,
161
+ symbol: "USDC",
162
+ eip712Name: "USD Coin"
163
+ },
164
+ USDT: {
165
+ address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
166
+ // Testnet USDT
167
+ decimals: 18,
168
+ symbol: "USDT",
169
+ eip712Name: "Tether USD"
170
+ }
171
+ },
172
+ usdc: "0x64544969ed7EBf5f083679233325356EbE738930",
173
+ explorer: "https://testnet.bscscan.com/address/",
174
+ explorerTx: "https://testnet.bscscan.com/tx/",
175
+ avgBlockTime: 3,
176
+ // BNB-specific: requires approval for pay-for-success flow
177
+ requiresApproval: true
178
+ },
179
+ // ============ BNB Chain Mainnet ============
180
+ bnb: {
181
+ name: "BNB Smart Chain",
182
+ chainId: 56,
183
+ rpc: "https://bsc-dataseed.binance.org",
184
+ tokens: {
185
+ // Note: BNB uses 18 decimals for stablecoins
186
+ USDC: {
187
+ address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
188
+ decimals: 18,
189
+ symbol: "USDC",
190
+ eip712Name: "USD Coin"
191
+ },
192
+ USDT: {
193
+ address: "0x55d398326f99059fF775485246999027B3197955",
194
+ decimals: 18,
195
+ symbol: "USDT",
196
+ eip712Name: "Tether USD"
197
+ }
198
+ },
199
+ usdc: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
200
+ explorer: "https://bscscan.com/address/",
201
+ explorerTx: "https://bscscan.com/tx/",
202
+ avgBlockTime: 3,
203
+ // BNB-specific: requires approval for pay-for-success flow
204
+ requiresApproval: true
148
205
  }
149
206
  };
150
207
  function getChain(name) {
@@ -155,7 +212,119 @@ function getChain(name) {
155
212
  return config;
156
213
  }
157
214
 
215
+ // src/wallet/solana.ts
216
+ var import_web32 = require("@solana/web3.js");
217
+ var import_spl_token = require("@solana/spl-token");
218
+ var import_fs = require("fs");
219
+ var import_path = require("path");
220
+ var import_os = require("os");
221
+ var import_bs58 = __toESM(require("bs58"));
222
+
223
+ // src/chains/solana.ts
224
+ var import_web3 = require("@solana/web3.js");
225
+ var SOLANA_CHAINS = {
226
+ solana: {
227
+ name: "Solana Mainnet",
228
+ cluster: "mainnet-beta",
229
+ rpc: "https://api.mainnet-beta.solana.com",
230
+ explorer: "https://solscan.io/account/",
231
+ explorerTx: "https://solscan.io/tx/",
232
+ tokens: {
233
+ USDC: {
234
+ mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
235
+ // Circle official USDC
236
+ decimals: 6
237
+ }
238
+ }
239
+ },
240
+ solana_devnet: {
241
+ name: "Solana Devnet",
242
+ cluster: "devnet",
243
+ rpc: "https://api.devnet.solana.com",
244
+ explorer: "https://solscan.io/account/",
245
+ explorerTx: "https://solscan.io/tx/",
246
+ tokens: {
247
+ USDC: {
248
+ // Circle's devnet USDC (if not available, we'll deploy our own test token)
249
+ mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
250
+ decimals: 6
251
+ }
252
+ }
253
+ }
254
+ };
255
+
256
+ // src/wallet/solana.ts
257
+ var DEFAULT_CONFIG_DIR = (0, import_path.join)((0, import_os.homedir)(), ".moltspay");
258
+ var SOLANA_WALLET_FILE = "wallet-solana.json";
259
+ function getSolanaWalletPath(configDir = DEFAULT_CONFIG_DIR) {
260
+ return (0, import_path.join)(configDir, SOLANA_WALLET_FILE);
261
+ }
262
+ function loadSolanaWallet(configDir = DEFAULT_CONFIG_DIR) {
263
+ const walletPath = getSolanaWalletPath(configDir);
264
+ if (!(0, import_fs.existsSync)(walletPath)) {
265
+ return null;
266
+ }
267
+ try {
268
+ const data = JSON.parse((0, import_fs.readFileSync)(walletPath, "utf-8"));
269
+ const secretKey = import_bs58.default.decode(data.secretKey);
270
+ return import_web32.Keypair.fromSecretKey(secretKey);
271
+ } catch (error) {
272
+ console.error("Failed to load Solana wallet:", error);
273
+ return null;
274
+ }
275
+ }
276
+
277
+ // src/facilitators/solana.ts
278
+ var import_web33 = require("@solana/web3.js");
279
+ var import_spl_token2 = require("@solana/spl-token");
280
+ async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey) {
281
+ const chainConfig = SOLANA_CHAINS[chain];
282
+ const connection = new import_web33.Connection(chainConfig.rpc, "confirmed");
283
+ const mint = new import_web33.PublicKey(chainConfig.tokens.USDC.mint);
284
+ const actualFeePayer = feePayerPubkey || senderPubkey;
285
+ const senderATA = await (0, import_spl_token2.getAssociatedTokenAddress)(mint, senderPubkey);
286
+ const recipientATA = await (0, import_spl_token2.getAssociatedTokenAddress)(mint, recipientPubkey);
287
+ const transaction = new import_web33.Transaction();
288
+ try {
289
+ await (0, import_spl_token2.getAccount)(connection, recipientATA);
290
+ } catch {
291
+ transaction.add(
292
+ (0, import_spl_token2.createAssociatedTokenAccountInstruction)(
293
+ actualFeePayer,
294
+ // payer (fee payer in gasless mode)
295
+ recipientATA,
296
+ // ata to create
297
+ recipientPubkey,
298
+ // owner
299
+ mint
300
+ // mint
301
+ )
302
+ );
303
+ }
304
+ transaction.add(
305
+ (0, import_spl_token2.createTransferCheckedInstruction)(
306
+ senderATA,
307
+ // source
308
+ mint,
309
+ // mint
310
+ recipientATA,
311
+ // destination
312
+ senderPubkey,
313
+ // owner (sender still authorizes the transfer)
314
+ amount,
315
+ // amount
316
+ chainConfig.tokens.USDC.decimals
317
+ // decimals
318
+ )
319
+ );
320
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
321
+ transaction.recentBlockhash = blockhash;
322
+ transaction.feePayer = actualFeePayer;
323
+ return transaction;
324
+ }
325
+
158
326
  // src/client/index.ts
327
+ var import_web34 = require("@solana/web3.js");
159
328
  var X402_VERSION = 2;
160
329
  var PAYMENT_REQUIRED_HEADER = "x-payment-required";
161
330
  var PAYMENT_HEADER = "x-payment";
@@ -174,7 +343,7 @@ var MoltsPayClient = class {
174
343
  todaySpending = 0;
175
344
  lastSpendingReset = 0;
176
345
  constructor(options = {}) {
177
- this.configDir = options.configDir || (0, import_path.join)((0, import_os.homedir)(), ".moltspay");
346
+ this.configDir = options.configDir || (0, import_path2.join)((0, import_os2.homedir)(), ".moltspay");
178
347
  this.config = this.loadConfig();
179
348
  this.walletData = this.loadWallet();
180
349
  this.loadSpending();
@@ -194,6 +363,12 @@ var MoltsPayClient = class {
194
363
  get address() {
195
364
  return this.wallet?.address || null;
196
365
  }
366
+ /**
367
+ * Get wallet instance (for direct operations like approvals)
368
+ */
369
+ getWallet() {
370
+ return this.wallet;
371
+ }
197
372
  /**
198
373
  * Get current config
199
374
  */
@@ -263,9 +438,14 @@ var MoltsPayClient = class {
263
438
  }
264
439
  throw new Error(data.error || "Unexpected response");
265
440
  }
441
+ const wwwAuthHeader = initialRes.headers.get("www-authenticate");
266
442
  const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER);
443
+ if (wwwAuthHeader && wwwAuthHeader.toLowerCase().includes("payment")) {
444
+ console.log("[MoltsPay] Detected MPP protocol, using Tempo flow...");
445
+ return await this.handleMPPPayment(serverUrl, service, params, wwwAuthHeader);
446
+ }
267
447
  if (!paymentRequiredHeader) {
268
- throw new Error("Missing x-payment-required header");
448
+ throw new Error("Missing payment header (x-payment-required or www-authenticate)");
269
449
  }
270
450
  let requirements;
271
451
  try {
@@ -282,17 +462,22 @@ var MoltsPayClient = class {
282
462
  throw new Error("Invalid x-payment-required header");
283
463
  }
284
464
  const networkToChainName = (network2) => {
465
+ if (network2 === "solana:mainnet") return "solana";
466
+ if (network2 === "solana:devnet") return "solana_devnet";
285
467
  const match = network2.match(/^eip155:(\d+)$/);
286
468
  if (!match) return null;
287
469
  const chainId = parseInt(match[1]);
288
470
  if (chainId === 8453) return "base";
289
471
  if (chainId === 137) return "polygon";
290
472
  if (chainId === 84532) return "base_sepolia";
473
+ if (chainId === 42431) return "tempo_moderato";
474
+ if (chainId === 56) return "bnb";
475
+ if (chainId === 97) return "bnb_testnet";
291
476
  return null;
292
477
  };
293
478
  const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
294
- let chainName;
295
479
  const userSpecifiedChain = options.chain;
480
+ let selectedChain;
296
481
  if (userSpecifiedChain) {
297
482
  if (!serverChains.includes(userSpecifiedChain)) {
298
483
  throw new Error(
@@ -300,17 +485,27 @@ var MoltsPayClient = class {
300
485
  Server accepts: ${serverChains.join(", ")}`
301
486
  );
302
487
  }
303
- chainName = userSpecifiedChain;
488
+ selectedChain = userSpecifiedChain;
304
489
  } else {
305
490
  if (serverChains.length === 1 && serverChains[0] === "base") {
306
- chainName = "base";
491
+ selectedChain = "base";
307
492
  } else {
308
493
  throw new Error(
309
494
  `Server accepts: ${serverChains.join(", ")}
310
- Please specify: --chain base, --chain polygon, or --chain base_sepolia`
495
+ Please specify: --chain <chain_name>`
311
496
  );
312
497
  }
313
498
  }
499
+ if (selectedChain === "solana" || selectedChain === "solana_devnet") {
500
+ const solanaChain = selectedChain;
501
+ const network2 = solanaChain === "solana" ? "solana:mainnet" : "solana:devnet";
502
+ const req2 = requirements.find((r) => r.network === network2);
503
+ if (!req2) {
504
+ throw new Error(`Failed to find payment requirement for ${selectedChain}`);
505
+ }
506
+ return await this.handleSolanaPayment(serverUrl, service, params, req2, solanaChain);
507
+ }
508
+ const chainName = selectedChain;
314
509
  const chain = getChain(chainName);
315
510
  const network = `eip155:${chain.chainId}`;
316
511
  const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
@@ -345,6 +540,25 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
345
540
  } else {
346
541
  console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
347
542
  }
543
+ if (chainName === "bnb" || chainName === "bnb_testnet") {
544
+ console.log(`[MoltsPay] Using BNB intent-based payment flow...`);
545
+ const payTo2 = req.payTo || req.resource;
546
+ if (!payTo2) {
547
+ throw new Error("Missing payTo address in payment requirements");
548
+ }
549
+ const bnbSpender = req.extra?.bnbSpender;
550
+ if (!bnbSpender) {
551
+ throw new Error("Server did not provide bnbSpender address. Server may not support BNB payments.");
552
+ }
553
+ return await this.handleBNBPayment(serverUrl, service, params, {
554
+ to: payTo2,
555
+ amount,
556
+ token,
557
+ chainName,
558
+ chain,
559
+ spender: bnbSpender
560
+ });
561
+ }
348
562
  const payTo = req.payTo || req.resource;
349
563
  if (!payTo) {
350
564
  throw new Error("Missing payTo address in payment requirements");
@@ -394,6 +608,300 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
394
608
  console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
395
609
  return result.result;
396
610
  }
611
+ /**
612
+ * Handle MPP (Machine Payments Protocol) payment flow
613
+ * Called when pay() detects WWW-Authenticate header in 402 response
614
+ */
615
+ async handleMPPPayment(serverUrl, service, params, wwwAuthHeader) {
616
+ const { privateKeyToAccount } = await import("viem/accounts");
617
+ const { createWalletClient, createPublicClient, http } = await import("viem");
618
+ const { tempoModerato } = await import("viem/chains");
619
+ const { Actions } = await import("viem/tempo");
620
+ const privateKey = this.walletData.privateKey;
621
+ const account = privateKeyToAccount(privateKey);
622
+ console.log(`[MoltsPay] Using MPP protocol on Tempo`);
623
+ console.log(`[MoltsPay] Account: ${account.address}`);
624
+ const parseAuthParam = (header, key) => {
625
+ const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
626
+ return match ? match[1] : null;
627
+ };
628
+ const challengeId = parseAuthParam(wwwAuthHeader, "id");
629
+ const method = parseAuthParam(wwwAuthHeader, "method");
630
+ const realm = parseAuthParam(wwwAuthHeader, "realm");
631
+ const requestB64 = parseAuthParam(wwwAuthHeader, "request");
632
+ if (method !== "tempo") {
633
+ throw new Error(`Unsupported payment method: ${method}`);
634
+ }
635
+ if (!requestB64) {
636
+ throw new Error("Missing request in WWW-Authenticate");
637
+ }
638
+ const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
639
+ const paymentRequest = JSON.parse(requestJson);
640
+ const { amount, currency, recipient, methodDetails } = paymentRequest;
641
+ const chainId = methodDetails?.chainId || 42431;
642
+ const amountDisplay = Number(amount) / 1e6;
643
+ console.log(`[MoltsPay] Payment: $${amountDisplay} to ${recipient}`);
644
+ this.checkLimits(amountDisplay);
645
+ console.log(`[MoltsPay] Sending transaction on Tempo...`);
646
+ const tempoChain = { ...tempoModerato, feeToken: currency };
647
+ const publicClient = createPublicClient({
648
+ chain: tempoChain,
649
+ transport: http("https://rpc.moderato.tempo.xyz")
650
+ });
651
+ const walletClient = createWalletClient({
652
+ account,
653
+ chain: tempoChain,
654
+ transport: http("https://rpc.moderato.tempo.xyz")
655
+ });
656
+ const txHash = await Actions.token.transfer(walletClient, {
657
+ to: recipient,
658
+ amount: BigInt(amount),
659
+ token: currency
660
+ });
661
+ console.log(`[MoltsPay] Transaction: ${txHash}`);
662
+ await publicClient.waitForTransactionReceipt({ hash: txHash });
663
+ console.log(`[MoltsPay] Confirmed! Retrying with credential...`);
664
+ const credential = {
665
+ challenge: {
666
+ id: challengeId,
667
+ realm,
668
+ method: "tempo",
669
+ intent: "charge",
670
+ request: paymentRequest
671
+ },
672
+ payload: { hash: txHash, type: "hash" },
673
+ source: `did:pkh:eip155:${chainId}:${account.address}`
674
+ };
675
+ const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
676
+ const paidRes = await fetch(`${serverUrl}/execute`, {
677
+ method: "POST",
678
+ headers: {
679
+ "Content-Type": "application/json",
680
+ "Authorization": `Payment ${credentialB64}`
681
+ },
682
+ body: JSON.stringify({ service, params, chain: "tempo_moderato" })
683
+ });
684
+ const result = await paidRes.json();
685
+ if (!paidRes.ok) {
686
+ throw new Error(result.error || "Payment verification failed");
687
+ }
688
+ this.recordSpending(amountDisplay);
689
+ console.log(`[MoltsPay] Success!`);
690
+ return result.result || result;
691
+ }
692
+ /**
693
+ * Handle BNB Chain payment flow (pre-approval + intent signature)
694
+ *
695
+ * Flow:
696
+ * 1. Check client has approved server wallet (done via `moltspay init`)
697
+ * 2. Sign EIP-712 payment intent (no gas, just signature)
698
+ * 3. Send intent to server
699
+ * 4. Server executes service
700
+ * 5. Server calls transferFrom if successful (pay-for-success)
701
+ */
702
+ async handleBNBPayment(serverUrl, service, params, paymentDetails) {
703
+ const { to, amount, token, chainName, chain, spender } = paymentDetails;
704
+ const tokenConfig = chain.tokens[token];
705
+ const provider = new import_ethers.ethers.JsonRpcProvider(chain.rpc);
706
+ const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
707
+ const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
708
+ if (allowance < amountWeiCheck) {
709
+ const nativeBalance = await provider.getBalance(this.wallet.address);
710
+ const minGasBalance = import_ethers.ethers.parseEther("0.0005");
711
+ if (nativeBalance < minGasBalance) {
712
+ const nativeBNB = parseFloat(import_ethers.ethers.formatEther(nativeBalance)).toFixed(4);
713
+ const isTestnet = chainName === "bnb_testnet";
714
+ if (isTestnet) {
715
+ throw new Error(
716
+ `\u274C Insufficient tBNB for approval transaction
717
+
718
+ Current tBNB: ${nativeBNB}
719
+ Required: ~0.001 tBNB
720
+
721
+ Get testnet tokens: npx moltspay faucet --chain bnb_testnet
722
+ (Gives USDC + tBNB for gas)`
723
+ );
724
+ } else {
725
+ throw new Error(
726
+ `\u274C Insufficient BNB for approval transaction
727
+
728
+ Current BNB: ${nativeBNB}
729
+ Required: ~0.001 BNB (~$0.60)
730
+
731
+ To get BNB:
732
+ \u2022 Withdraw from Binance/exchange to your wallet
733
+ \u2022 Most exchanges include BNB dust with withdrawals
734
+
735
+ After funding, run:
736
+ npx moltspay approve --chain ${chainName} --spender ${spender}`
737
+ );
738
+ }
739
+ }
740
+ throw new Error(
741
+ `Insufficient allowance for ${spender.slice(0, 10)}...
742
+ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
743
+ );
744
+ }
745
+ const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
746
+ const intent = {
747
+ from: this.wallet.address,
748
+ to,
749
+ amount: amountWei,
750
+ token: tokenConfig.address,
751
+ service,
752
+ nonce: Date.now(),
753
+ // Use timestamp as nonce for simplicity
754
+ deadline: Date.now() + 36e5
755
+ // 1 hour
756
+ };
757
+ const domain = {
758
+ name: "MoltsPay",
759
+ version: "1",
760
+ chainId: chain.chainId
761
+ };
762
+ const types = {
763
+ PaymentIntent: [
764
+ { name: "from", type: "address" },
765
+ { name: "to", type: "address" },
766
+ { name: "amount", type: "uint256" },
767
+ { name: "token", type: "address" },
768
+ { name: "service", type: "string" },
769
+ { name: "nonce", type: "uint256" },
770
+ { name: "deadline", type: "uint256" }
771
+ ]
772
+ };
773
+ console.log(`[MoltsPay] Signing BNB payment intent...`);
774
+ const signature = await this.wallet.signTypedData(domain, types, intent);
775
+ const network = `eip155:${chain.chainId}`;
776
+ const payload = {
777
+ x402Version: 2,
778
+ scheme: "exact",
779
+ network,
780
+ payload: {
781
+ intent: {
782
+ ...intent,
783
+ signature
784
+ },
785
+ chainId: chain.chainId
786
+ },
787
+ accepted: {
788
+ scheme: "exact",
789
+ network,
790
+ asset: tokenConfig.address,
791
+ amount: amountWei,
792
+ payTo: to,
793
+ maxTimeoutSeconds: 300
794
+ }
795
+ };
796
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
797
+ console.log(`[MoltsPay] Sending BNB payment request...`);
798
+ const paidRes = await fetch(`${serverUrl}/execute`, {
799
+ method: "POST",
800
+ headers: {
801
+ "Content-Type": "application/json",
802
+ "X-Payment": paymentHeader
803
+ },
804
+ body: JSON.stringify({ service, params, chain: chainName })
805
+ });
806
+ const result = await paidRes.json();
807
+ if (!paidRes.ok) {
808
+ throw new Error(result.error || "BNB payment failed");
809
+ }
810
+ this.recordSpending(amount);
811
+ console.log(`[MoltsPay] Success! BNB payment settled.`);
812
+ return result.result || result;
813
+ }
814
+ /**
815
+ * Handle Solana payment flow
816
+ *
817
+ * Solana uses SPL token transfers with pay-for-success model:
818
+ * 1. Client creates and signs a transfer transaction
819
+ * 2. Server submits the transaction after service completes
820
+ */
821
+ async handleSolanaPayment(serverUrl, service, params, requirements, chain) {
822
+ const solanaWallet = loadSolanaWallet(this.configDir);
823
+ if (!solanaWallet) {
824
+ throw new Error("No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
825
+ }
826
+ const amount = Number(requirements.amount);
827
+ const amountUSDC = amount / 1e6;
828
+ this.checkLimits(amountUSDC);
829
+ console.log(`[MoltsPay] Creating Solana payment: $${amountUSDC} USDC`);
830
+ if (!requirements.payTo) {
831
+ throw new Error("Missing payTo address in payment requirements");
832
+ }
833
+ const solanaFeePayer = requirements.extra?.solanaFeePayer;
834
+ const feePayerPubkey = solanaFeePayer ? new import_web34.PublicKey(solanaFeePayer) : void 0;
835
+ if (feePayerPubkey) {
836
+ console.log(`[MoltsPay] Gasless mode: server pays fees`);
837
+ }
838
+ const recipientPubkey = new import_web34.PublicKey(requirements.payTo);
839
+ const transaction = await createSolanaPaymentTransaction(
840
+ solanaWallet.publicKey,
841
+ recipientPubkey,
842
+ BigInt(amount),
843
+ chain,
844
+ feePayerPubkey
845
+ // Optional fee payer for gasless mode
846
+ );
847
+ if (feePayerPubkey) {
848
+ transaction.partialSign(solanaWallet);
849
+ } else {
850
+ transaction.sign(solanaWallet);
851
+ }
852
+ const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
853
+ console.log(`[MoltsPay] Transaction signed, sending to server...`);
854
+ const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
855
+ const payload = {
856
+ x402Version: 2,
857
+ scheme: "exact",
858
+ network,
859
+ payload: {
860
+ signedTransaction: signedTx,
861
+ sender: solanaWallet.publicKey.toBase58(),
862
+ chain
863
+ },
864
+ accepted: {
865
+ scheme: "exact",
866
+ network,
867
+ asset: requirements.asset,
868
+ amount: requirements.amount,
869
+ payTo: requirements.payTo,
870
+ maxTimeoutSeconds: 300
871
+ }
872
+ };
873
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
874
+ const paidRes = await fetch(`${serverUrl}/execute`, {
875
+ method: "POST",
876
+ headers: {
877
+ "Content-Type": "application/json",
878
+ "X-Payment": paymentHeader
879
+ },
880
+ body: JSON.stringify({ service, params, chain })
881
+ });
882
+ const result = await paidRes.json();
883
+ if (!paidRes.ok) {
884
+ throw new Error(result.error || "Solana payment failed");
885
+ }
886
+ this.recordSpending(amountUSDC);
887
+ console.log(`[MoltsPay] Success! Solana payment settled.`);
888
+ if (result.payment?.transaction) {
889
+ const explorerUrl = chain === "solana" ? `https://solscan.io/tx/${result.payment.transaction}` : `https://solscan.io/tx/${result.payment.transaction}?cluster=devnet`;
890
+ console.log(`[MoltsPay] Transaction: ${explorerUrl}`);
891
+ }
892
+ return result.result || result;
893
+ }
894
+ /**
895
+ * Check ERC20 allowance for a spender
896
+ */
897
+ async checkAllowance(tokenAddress, spender, provider) {
898
+ const contract = new import_ethers.ethers.Contract(
899
+ tokenAddress,
900
+ ["function allowance(address owner, address spender) view returns (uint256)"],
901
+ provider
902
+ );
903
+ return await contract.allowance(this.wallet.address, spender);
904
+ }
397
905
  /**
398
906
  * Sign EIP-3009 transferWithAuthorization (GASLESS)
399
907
  * This only signs - no on-chain transaction, no gas needed.
@@ -464,26 +972,26 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
464
972
  }
465
973
  // --- Config & Wallet Management ---
466
974
  loadConfig() {
467
- const configPath = (0, import_path.join)(this.configDir, "config.json");
468
- if ((0, import_fs.existsSync)(configPath)) {
469
- const content = (0, import_fs.readFileSync)(configPath, "utf-8");
975
+ const configPath = (0, import_path2.join)(this.configDir, "config.json");
976
+ if ((0, import_fs2.existsSync)(configPath)) {
977
+ const content = (0, import_fs2.readFileSync)(configPath, "utf-8");
470
978
  return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
471
979
  }
472
980
  return { ...DEFAULT_CONFIG };
473
981
  }
474
982
  saveConfig() {
475
- (0, import_fs.mkdirSync)(this.configDir, { recursive: true });
476
- const configPath = (0, import_path.join)(this.configDir, "config.json");
477
- (0, import_fs.writeFileSync)(configPath, JSON.stringify(this.config, null, 2));
983
+ (0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
984
+ const configPath = (0, import_path2.join)(this.configDir, "config.json");
985
+ (0, import_fs2.writeFileSync)(configPath, JSON.stringify(this.config, null, 2));
478
986
  }
479
987
  /**
480
988
  * Load spending data from disk
481
989
  */
482
990
  loadSpending() {
483
- const spendingPath = (0, import_path.join)(this.configDir, "spending.json");
484
- if ((0, import_fs.existsSync)(spendingPath)) {
991
+ const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
992
+ if ((0, import_fs2.existsSync)(spendingPath)) {
485
993
  try {
486
- const data = JSON.parse((0, import_fs.readFileSync)(spendingPath, "utf-8"));
994
+ const data = JSON.parse((0, import_fs2.readFileSync)(spendingPath, "utf-8"));
487
995
  const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
488
996
  if (data.date && data.date === today) {
489
997
  this.todaySpending = data.amount || 0;
@@ -502,29 +1010,29 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
502
1010
  * Save spending data to disk
503
1011
  */
504
1012
  saveSpending() {
505
- (0, import_fs.mkdirSync)(this.configDir, { recursive: true });
506
- const spendingPath = (0, import_path.join)(this.configDir, "spending.json");
1013
+ (0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
1014
+ const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
507
1015
  const data = {
508
1016
  date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
509
1017
  amount: this.todaySpending,
510
1018
  updatedAt: Date.now()
511
1019
  };
512
- (0, import_fs.writeFileSync)(spendingPath, JSON.stringify(data, null, 2));
1020
+ (0, import_fs2.writeFileSync)(spendingPath, JSON.stringify(data, null, 2));
513
1021
  }
514
1022
  loadWallet() {
515
- const walletPath = (0, import_path.join)(this.configDir, "wallet.json");
516
- if ((0, import_fs.existsSync)(walletPath)) {
1023
+ const walletPath = (0, import_path2.join)(this.configDir, "wallet.json");
1024
+ if ((0, import_fs2.existsSync)(walletPath)) {
517
1025
  try {
518
- const stats = (0, import_fs.statSync)(walletPath);
1026
+ const stats = (0, import_fs2.statSync)(walletPath);
519
1027
  const mode = stats.mode & 511;
520
1028
  if (mode !== 384) {
521
1029
  console.warn(`[MoltsPay] WARNING: wallet.json has insecure permissions (${mode.toString(8)})`);
522
1030
  console.warn(`[MoltsPay] Fixing permissions to 0600...`);
523
- (0, import_fs.chmodSync)(walletPath, 384);
1031
+ (0, import_fs2.chmodSync)(walletPath, 384);
524
1032
  }
525
1033
  } catch (err) {
526
1034
  }
527
- const content = (0, import_fs.readFileSync)(walletPath, "utf-8");
1035
+ const content = (0, import_fs2.readFileSync)(walletPath, "utf-8");
528
1036
  return JSON.parse(content);
529
1037
  }
530
1038
  return null;
@@ -533,15 +1041,15 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
533
1041
  * Initialize a new wallet (called by CLI)
534
1042
  */
535
1043
  static init(configDir, options) {
536
- (0, import_fs.mkdirSync)(configDir, { recursive: true });
1044
+ (0, import_fs2.mkdirSync)(configDir, { recursive: true });
537
1045
  const wallet = import_ethers.Wallet.createRandom();
538
1046
  const walletData = {
539
1047
  address: wallet.address,
540
1048
  privateKey: wallet.privateKey,
541
1049
  createdAt: Date.now()
542
1050
  };
543
- const walletPath = (0, import_path.join)(configDir, "wallet.json");
544
- (0, import_fs.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
1051
+ const walletPath = (0, import_path2.join)(configDir, "wallet.json");
1052
+ (0, import_fs2.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
545
1053
  const config = {
546
1054
  chain: options.chain,
547
1055
  limits: {
@@ -549,8 +1057,8 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
549
1057
  maxPerDay: options.maxPerDay
550
1058
  }
551
1059
  };
552
- const configPath = (0, import_path.join)(configDir, "config.json");
553
- (0, import_fs.writeFileSync)(configPath, JSON.stringify(config, null, 2));
1060
+ const configPath = (0, import_path2.join)(configDir, "config.json");
1061
+ (0, import_fs2.writeFileSync)(configPath, JSON.stringify(config, null, 2));
554
1062
  return { address: wallet.address, configDir };
555
1063
  }
556
1064
  /**
@@ -586,7 +1094,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
586
1094
  if (!this.wallet) {
587
1095
  throw new Error("Client not initialized");
588
1096
  }
589
- const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
1097
+ const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
590
1098
  const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
591
1099
  const results = {};
592
1100
  const tempoTokens = {