moltspay 1.3.0 → 1.4.1

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 (58) hide show
  1. package/.env.example +14 -0
  2. package/README.md +319 -89
  3. package/dist/cdp/index.d.mts +4 -4
  4. package/dist/cdp/index.d.ts +4 -4
  5. package/dist/cdp/index.js +57 -0
  6. package/dist/cdp/index.js.map +1 -1
  7. package/dist/cdp/index.mjs +57 -0
  8. package/dist/cdp/index.mjs.map +1 -1
  9. package/dist/chains/index.d.mts +9 -8
  10. package/dist/chains/index.d.ts +9 -8
  11. package/dist/chains/index.js +57 -0
  12. package/dist/chains/index.js.map +1 -1
  13. package/dist/chains/index.mjs +57 -0
  14. package/dist/chains/index.mjs.map +1 -1
  15. package/dist/cli/index.js +2021 -285
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/cli/index.mjs +2023 -277
  18. package/dist/cli/index.mjs.map +1 -1
  19. package/dist/client/index.d.mts +39 -3
  20. package/dist/client/index.d.ts +39 -3
  21. package/dist/client/index.js +563 -37
  22. package/dist/client/index.js.map +1 -1
  23. package/dist/client/index.mjs +571 -35
  24. package/dist/client/index.mjs.map +1 -1
  25. package/dist/facilitators/index.d.mts +220 -1
  26. package/dist/facilitators/index.d.ts +220 -1
  27. package/dist/facilitators/index.js +664 -1
  28. package/dist/facilitators/index.js.map +1 -1
  29. package/dist/facilitators/index.mjs +670 -1
  30. package/dist/facilitators/index.mjs.map +1 -1
  31. package/dist/{index-On9ZaGDW.d.mts → index-D_2FkLwV.d.mts} +6 -2
  32. package/dist/{index-On9ZaGDW.d.ts → index-D_2FkLwV.d.ts} +6 -2
  33. package/dist/index.d.mts +2 -1
  34. package/dist/index.d.ts +2 -1
  35. package/dist/index.js +1440 -153
  36. package/dist/index.js.map +1 -1
  37. package/dist/index.mjs +1448 -151
  38. package/dist/index.mjs.map +1 -1
  39. package/dist/server/index.d.mts +13 -3
  40. package/dist/server/index.d.ts +13 -3
  41. package/dist/server/index.js +909 -54
  42. package/dist/server/index.js.map +1 -1
  43. package/dist/server/index.mjs +919 -54
  44. package/dist/server/index.mjs.map +1 -1
  45. package/dist/verify/index.d.mts +1 -1
  46. package/dist/verify/index.d.ts +1 -1
  47. package/dist/verify/index.js +57 -0
  48. package/dist/verify/index.js.map +1 -1
  49. package/dist/verify/index.mjs +57 -0
  50. package/dist/verify/index.mjs.map +1 -1
  51. package/dist/wallet/index.d.mts +3 -3
  52. package/dist/wallet/index.d.ts +3 -3
  53. package/dist/wallet/index.js +57 -0
  54. package/dist/wallet/index.js.map +1 -1
  55. package/dist/wallet/index.mjs +57 -0
  56. package/dist/wallet/index.mjs.map +1 -1
  57. package/package.json +5 -2
  58. 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
  */
@@ -247,11 +422,26 @@ var MoltsPayClient = class {
247
422
  throw new Error("Client not initialized. Run: npx moltspay init");
248
423
  }
249
424
  console.log(`[MoltsPay] Requesting service: ${service}`);
250
- const requestBody = { service, params };
425
+ let executeUrl = `${serverUrl}/execute`;
426
+ try {
427
+ const services = await this.getServices(serverUrl);
428
+ const svc = services.services?.find((s) => s.id === service);
429
+ if (svc?.endpoint) {
430
+ executeUrl = `${serverUrl}${svc.endpoint}`;
431
+ console.log(`[MoltsPay] Using service endpoint: ${svc.endpoint}`);
432
+ }
433
+ } catch {
434
+ }
435
+ let requestBody;
436
+ if (options.rawData) {
437
+ requestBody = { service, ...params };
438
+ } else {
439
+ requestBody = { service, params };
440
+ }
251
441
  if (options.chain) {
252
442
  requestBody.chain = options.chain;
253
443
  }
254
- const initialRes = await fetch(`${serverUrl}/execute`, {
444
+ const initialRes = await fetch(executeUrl, {
255
445
  method: "POST",
256
446
  headers: { "Content-Type": "application/json" },
257
447
  body: JSON.stringify(requestBody)
@@ -263,9 +453,14 @@ var MoltsPayClient = class {
263
453
  }
264
454
  throw new Error(data.error || "Unexpected response");
265
455
  }
456
+ const wwwAuthHeader = initialRes.headers.get("www-authenticate");
266
457
  const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER);
458
+ if (wwwAuthHeader && wwwAuthHeader.toLowerCase().includes("payment")) {
459
+ console.log("[MoltsPay] Detected MPP protocol, using Tempo flow...");
460
+ return await this.handleMPPPayment(executeUrl, service, params, wwwAuthHeader, options);
461
+ }
267
462
  if (!paymentRequiredHeader) {
268
- throw new Error("Missing x-payment-required header");
463
+ throw new Error("Missing payment header (x-payment-required or www-authenticate)");
269
464
  }
270
465
  let requirements;
271
466
  try {
@@ -282,17 +477,22 @@ var MoltsPayClient = class {
282
477
  throw new Error("Invalid x-payment-required header");
283
478
  }
284
479
  const networkToChainName = (network2) => {
480
+ if (network2 === "solana:mainnet") return "solana";
481
+ if (network2 === "solana:devnet") return "solana_devnet";
285
482
  const match = network2.match(/^eip155:(\d+)$/);
286
483
  if (!match) return null;
287
484
  const chainId = parseInt(match[1]);
288
485
  if (chainId === 8453) return "base";
289
486
  if (chainId === 137) return "polygon";
290
487
  if (chainId === 84532) return "base_sepolia";
488
+ if (chainId === 42431) return "tempo_moderato";
489
+ if (chainId === 56) return "bnb";
490
+ if (chainId === 97) return "bnb_testnet";
291
491
  return null;
292
492
  };
293
493
  const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
294
- let chainName;
295
494
  const userSpecifiedChain = options.chain;
495
+ let selectedChain;
296
496
  if (userSpecifiedChain) {
297
497
  if (!serverChains.includes(userSpecifiedChain)) {
298
498
  throw new Error(
@@ -300,17 +500,27 @@ var MoltsPayClient = class {
300
500
  Server accepts: ${serverChains.join(", ")}`
301
501
  );
302
502
  }
303
- chainName = userSpecifiedChain;
503
+ selectedChain = userSpecifiedChain;
304
504
  } else {
305
505
  if (serverChains.length === 1 && serverChains[0] === "base") {
306
- chainName = "base";
506
+ selectedChain = "base";
307
507
  } else {
308
508
  throw new Error(
309
509
  `Server accepts: ${serverChains.join(", ")}
310
- Please specify: --chain base, --chain polygon, or --chain base_sepolia`
510
+ Please specify: --chain <chain_name>`
311
511
  );
312
512
  }
313
513
  }
514
+ if (selectedChain === "solana" || selectedChain === "solana_devnet") {
515
+ const solanaChain = selectedChain;
516
+ const network2 = solanaChain === "solana" ? "solana:mainnet" : "solana:devnet";
517
+ const req2 = requirements.find((r) => r.network === network2);
518
+ if (!req2) {
519
+ throw new Error(`Failed to find payment requirement for ${selectedChain}`);
520
+ }
521
+ return await this.handleSolanaPayment(executeUrl, service, params, req2, solanaChain, options);
522
+ }
523
+ const chainName = selectedChain;
314
524
  const chain = getChain(chainName);
315
525
  const network = `eip155:${chain.chainId}`;
316
526
  const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
@@ -345,6 +555,25 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
345
555
  } else {
346
556
  console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
347
557
  }
558
+ if (chainName === "bnb" || chainName === "bnb_testnet") {
559
+ console.log(`[MoltsPay] Using BNB intent-based payment flow...`);
560
+ const payTo2 = req.payTo || req.resource;
561
+ if (!payTo2) {
562
+ throw new Error("Missing payTo address in payment requirements");
563
+ }
564
+ const bnbSpender = req.extra?.bnbSpender;
565
+ if (!bnbSpender) {
566
+ throw new Error("Server did not provide bnbSpender address. Server may not support BNB payments.");
567
+ }
568
+ return await this.handleBNBPayment(executeUrl, service, params, {
569
+ to: payTo2,
570
+ amount,
571
+ token,
572
+ chainName,
573
+ chain,
574
+ spender: bnbSpender
575
+ }, options);
576
+ }
348
577
  const payTo = req.payTo || req.resource;
349
578
  if (!payTo) {
350
579
  throw new Error("Missing payTo address in payment requirements");
@@ -374,11 +603,11 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
374
603
  };
375
604
  const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
376
605
  console.log(`[MoltsPay] Sending request with payment...`);
377
- const paidRequestBody = { service, params };
606
+ const paidRequestBody = options.rawData ? { service, ...params } : { service, params };
378
607
  if (options.chain) {
379
608
  paidRequestBody.chain = options.chain;
380
609
  }
381
- const paidRes = await fetch(`${serverUrl}/execute`, {
610
+ const paidRes = await fetch(executeUrl, {
382
611
  method: "POST",
383
612
  headers: {
384
613
  "Content-Type": "application/json",
@@ -392,7 +621,304 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
392
621
  }
393
622
  this.recordSpending(amount);
394
623
  console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
395
- return result.result;
624
+ return result.result || result;
625
+ }
626
+ /**
627
+ * Handle MPP (Machine Payments Protocol) payment flow
628
+ * Called when pay() detects WWW-Authenticate header in 402 response
629
+ */
630
+ async handleMPPPayment(executeUrl, service, params, wwwAuthHeader, options = {}) {
631
+ const { privateKeyToAccount } = await import("viem/accounts");
632
+ const { createWalletClient, createPublicClient, http } = await import("viem");
633
+ const { tempoModerato } = await import("viem/chains");
634
+ const { Actions } = await import("viem/tempo");
635
+ const privateKey = this.walletData.privateKey;
636
+ const account = privateKeyToAccount(privateKey);
637
+ console.log(`[MoltsPay] Using MPP protocol on Tempo`);
638
+ console.log(`[MoltsPay] Account: ${account.address}`);
639
+ const parseAuthParam = (header, key) => {
640
+ const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
641
+ return match ? match[1] : null;
642
+ };
643
+ const challengeId = parseAuthParam(wwwAuthHeader, "id");
644
+ const method = parseAuthParam(wwwAuthHeader, "method");
645
+ const realm = parseAuthParam(wwwAuthHeader, "realm");
646
+ const requestB64 = parseAuthParam(wwwAuthHeader, "request");
647
+ if (method !== "tempo") {
648
+ throw new Error(`Unsupported payment method: ${method}`);
649
+ }
650
+ if (!requestB64) {
651
+ throw new Error("Missing request in WWW-Authenticate");
652
+ }
653
+ const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
654
+ const paymentRequest = JSON.parse(requestJson);
655
+ const { amount, currency, recipient, methodDetails } = paymentRequest;
656
+ const chainId = methodDetails?.chainId || 42431;
657
+ const amountDisplay = Number(amount) / 1e6;
658
+ console.log(`[MoltsPay] Payment: $${amountDisplay} to ${recipient}`);
659
+ this.checkLimits(amountDisplay);
660
+ console.log(`[MoltsPay] Sending transaction on Tempo...`);
661
+ const tempoChain = { ...tempoModerato, feeToken: currency };
662
+ const publicClient = createPublicClient({
663
+ chain: tempoChain,
664
+ transport: http("https://rpc.moderato.tempo.xyz")
665
+ });
666
+ const walletClient = createWalletClient({
667
+ account,
668
+ chain: tempoChain,
669
+ transport: http("https://rpc.moderato.tempo.xyz")
670
+ });
671
+ const txHash = await Actions.token.transfer(walletClient, {
672
+ to: recipient,
673
+ amount: BigInt(amount),
674
+ token: currency
675
+ });
676
+ console.log(`[MoltsPay] Transaction: ${txHash}`);
677
+ await publicClient.waitForTransactionReceipt({ hash: txHash });
678
+ console.log(`[MoltsPay] Confirmed! Retrying with credential...`);
679
+ const credential = {
680
+ challenge: {
681
+ id: challengeId,
682
+ realm,
683
+ method: "tempo",
684
+ intent: "charge",
685
+ request: paymentRequest
686
+ },
687
+ payload: { hash: txHash, type: "hash" },
688
+ source: `did:pkh:eip155:${chainId}:${account.address}`
689
+ };
690
+ const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
691
+ const retryBody = options.rawData ? { service, ...params, chain: "tempo_moderato" } : { service, params, chain: "tempo_moderato" };
692
+ const paidRes = await fetch(executeUrl, {
693
+ method: "POST",
694
+ headers: {
695
+ "Content-Type": "application/json",
696
+ "Authorization": `Payment ${credentialB64}`
697
+ },
698
+ body: JSON.stringify(retryBody)
699
+ });
700
+ const result = await paidRes.json();
701
+ if (!paidRes.ok) {
702
+ throw new Error(result.error || "Payment verification failed");
703
+ }
704
+ this.recordSpending(amountDisplay);
705
+ console.log(`[MoltsPay] Success!`);
706
+ return result.result || result;
707
+ }
708
+ /**
709
+ * Handle BNB Chain payment flow (pre-approval + intent signature)
710
+ *
711
+ * Flow:
712
+ * 1. Check client has approved server wallet (done via `moltspay init`)
713
+ * 2. Sign EIP-712 payment intent (no gas, just signature)
714
+ * 3. Send intent to server
715
+ * 4. Server executes service
716
+ * 5. Server calls transferFrom if successful (pay-for-success)
717
+ */
718
+ async handleBNBPayment(executeUrl, service, params, paymentDetails, options = {}) {
719
+ const { to, amount, token, chainName, chain, spender } = paymentDetails;
720
+ const tokenConfig = chain.tokens[token];
721
+ const provider = new import_ethers.ethers.JsonRpcProvider(chain.rpc);
722
+ const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
723
+ const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
724
+ if (allowance < amountWeiCheck) {
725
+ const nativeBalance = await provider.getBalance(this.wallet.address);
726
+ const minGasBalance = import_ethers.ethers.parseEther("0.0005");
727
+ if (nativeBalance < minGasBalance) {
728
+ const nativeBNB = parseFloat(import_ethers.ethers.formatEther(nativeBalance)).toFixed(4);
729
+ const isTestnet = chainName === "bnb_testnet";
730
+ if (isTestnet) {
731
+ throw new Error(
732
+ `\u274C Insufficient tBNB for approval transaction
733
+
734
+ Current tBNB: ${nativeBNB}
735
+ Required: ~0.001 tBNB
736
+
737
+ Get testnet tokens: npx moltspay faucet --chain bnb_testnet
738
+ (Gives USDC + tBNB for gas)`
739
+ );
740
+ } else {
741
+ throw new Error(
742
+ `\u274C Insufficient BNB for approval transaction
743
+
744
+ Current BNB: ${nativeBNB}
745
+ Required: ~0.001 BNB (~$0.60)
746
+
747
+ To get BNB:
748
+ \u2022 Withdraw from Binance/exchange to your wallet
749
+ \u2022 Most exchanges include BNB dust with withdrawals
750
+
751
+ After funding, run:
752
+ npx moltspay approve --chain ${chainName} --spender ${spender}`
753
+ );
754
+ }
755
+ }
756
+ throw new Error(
757
+ `Insufficient allowance for ${spender.slice(0, 10)}...
758
+ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
759
+ );
760
+ }
761
+ const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
762
+ const intent = {
763
+ from: this.wallet.address,
764
+ to,
765
+ amount: amountWei,
766
+ token: tokenConfig.address,
767
+ service,
768
+ nonce: Date.now(),
769
+ // Use timestamp as nonce for simplicity
770
+ deadline: Date.now() + 36e5
771
+ // 1 hour
772
+ };
773
+ const domain = {
774
+ name: "MoltsPay",
775
+ version: "1",
776
+ chainId: chain.chainId
777
+ };
778
+ const types = {
779
+ PaymentIntent: [
780
+ { name: "from", type: "address" },
781
+ { name: "to", type: "address" },
782
+ { name: "amount", type: "uint256" },
783
+ { name: "token", type: "address" },
784
+ { name: "service", type: "string" },
785
+ { name: "nonce", type: "uint256" },
786
+ { name: "deadline", type: "uint256" }
787
+ ]
788
+ };
789
+ console.log(`[MoltsPay] Signing BNB payment intent...`);
790
+ const signature = await this.wallet.signTypedData(domain, types, intent);
791
+ const network = `eip155:${chain.chainId}`;
792
+ const payload = {
793
+ x402Version: 2,
794
+ scheme: "exact",
795
+ network,
796
+ payload: {
797
+ intent: {
798
+ ...intent,
799
+ signature
800
+ },
801
+ chainId: chain.chainId
802
+ },
803
+ accepted: {
804
+ scheme: "exact",
805
+ network,
806
+ asset: tokenConfig.address,
807
+ amount: amountWei,
808
+ payTo: to,
809
+ maxTimeoutSeconds: 300
810
+ }
811
+ };
812
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
813
+ console.log(`[MoltsPay] Sending BNB payment request...`);
814
+ const bnbRequestBody = options.rawData ? { service, ...params, chain: chainName } : { service, params, chain: chainName };
815
+ const paidRes = await fetch(executeUrl, {
816
+ method: "POST",
817
+ headers: {
818
+ "Content-Type": "application/json",
819
+ "X-Payment": paymentHeader
820
+ },
821
+ body: JSON.stringify(bnbRequestBody)
822
+ });
823
+ const result = await paidRes.json();
824
+ if (!paidRes.ok) {
825
+ throw new Error(result.error || "BNB payment failed");
826
+ }
827
+ this.recordSpending(amount);
828
+ console.log(`[MoltsPay] Success! BNB payment settled.`);
829
+ return result.result || result;
830
+ }
831
+ /**
832
+ * Handle Solana payment flow
833
+ *
834
+ * Solana uses SPL token transfers with pay-for-success model:
835
+ * 1. Client creates and signs a transfer transaction
836
+ * 2. Server submits the transaction after service completes
837
+ */
838
+ async handleSolanaPayment(executeUrl, service, params, requirements, chain, options = {}) {
839
+ const solanaWallet = loadSolanaWallet(this.configDir);
840
+ if (!solanaWallet) {
841
+ throw new Error("No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
842
+ }
843
+ const amount = Number(requirements.amount);
844
+ const amountUSDC = amount / 1e6;
845
+ this.checkLimits(amountUSDC);
846
+ console.log(`[MoltsPay] Creating Solana payment: $${amountUSDC} USDC`);
847
+ if (!requirements.payTo) {
848
+ throw new Error("Missing payTo address in payment requirements");
849
+ }
850
+ const solanaFeePayer = requirements.extra?.solanaFeePayer;
851
+ const feePayerPubkey = solanaFeePayer ? new import_web34.PublicKey(solanaFeePayer) : void 0;
852
+ if (feePayerPubkey) {
853
+ console.log(`[MoltsPay] Gasless mode: server pays fees`);
854
+ }
855
+ const recipientPubkey = new import_web34.PublicKey(requirements.payTo);
856
+ const transaction = await createSolanaPaymentTransaction(
857
+ solanaWallet.publicKey,
858
+ recipientPubkey,
859
+ BigInt(amount),
860
+ chain,
861
+ feePayerPubkey
862
+ // Optional fee payer for gasless mode
863
+ );
864
+ if (feePayerPubkey) {
865
+ transaction.partialSign(solanaWallet);
866
+ } else {
867
+ transaction.sign(solanaWallet);
868
+ }
869
+ const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
870
+ console.log(`[MoltsPay] Transaction signed, sending to server...`);
871
+ const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
872
+ const payload = {
873
+ x402Version: 2,
874
+ scheme: "exact",
875
+ network,
876
+ payload: {
877
+ signedTransaction: signedTx,
878
+ sender: solanaWallet.publicKey.toBase58(),
879
+ chain
880
+ },
881
+ accepted: {
882
+ scheme: "exact",
883
+ network,
884
+ asset: requirements.asset,
885
+ amount: requirements.amount,
886
+ payTo: requirements.payTo,
887
+ maxTimeoutSeconds: 300
888
+ }
889
+ };
890
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
891
+ const solanaRequestBody = options.rawData ? { service, ...params, chain } : { service, params, chain };
892
+ const paidRes = await fetch(executeUrl, {
893
+ method: "POST",
894
+ headers: {
895
+ "Content-Type": "application/json",
896
+ "X-Payment": paymentHeader
897
+ },
898
+ body: JSON.stringify(solanaRequestBody)
899
+ });
900
+ const result = await paidRes.json();
901
+ if (!paidRes.ok) {
902
+ throw new Error(result.error || "Solana payment failed");
903
+ }
904
+ this.recordSpending(amountUSDC);
905
+ console.log(`[MoltsPay] Success! Solana payment settled.`);
906
+ if (result.payment?.transaction) {
907
+ const explorerUrl = chain === "solana" ? `https://solscan.io/tx/${result.payment.transaction}` : `https://solscan.io/tx/${result.payment.transaction}?cluster=devnet`;
908
+ console.log(`[MoltsPay] Transaction: ${explorerUrl}`);
909
+ }
910
+ return result.result || result;
911
+ }
912
+ /**
913
+ * Check ERC20 allowance for a spender
914
+ */
915
+ async checkAllowance(tokenAddress, spender, provider) {
916
+ const contract = new import_ethers.ethers.Contract(
917
+ tokenAddress,
918
+ ["function allowance(address owner, address spender) view returns (uint256)"],
919
+ provider
920
+ );
921
+ return await contract.allowance(this.wallet.address, spender);
396
922
  }
397
923
  /**
398
924
  * Sign EIP-3009 transferWithAuthorization (GASLESS)
@@ -464,26 +990,26 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
464
990
  }
465
991
  // --- Config & Wallet Management ---
466
992
  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");
993
+ const configPath = (0, import_path2.join)(this.configDir, "config.json");
994
+ if ((0, import_fs2.existsSync)(configPath)) {
995
+ const content = (0, import_fs2.readFileSync)(configPath, "utf-8");
470
996
  return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
471
997
  }
472
998
  return { ...DEFAULT_CONFIG };
473
999
  }
474
1000
  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));
1001
+ (0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
1002
+ const configPath = (0, import_path2.join)(this.configDir, "config.json");
1003
+ (0, import_fs2.writeFileSync)(configPath, JSON.stringify(this.config, null, 2));
478
1004
  }
479
1005
  /**
480
1006
  * Load spending data from disk
481
1007
  */
482
1008
  loadSpending() {
483
- const spendingPath = (0, import_path.join)(this.configDir, "spending.json");
484
- if ((0, import_fs.existsSync)(spendingPath)) {
1009
+ const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
1010
+ if ((0, import_fs2.existsSync)(spendingPath)) {
485
1011
  try {
486
- const data = JSON.parse((0, import_fs.readFileSync)(spendingPath, "utf-8"));
1012
+ const data = JSON.parse((0, import_fs2.readFileSync)(spendingPath, "utf-8"));
487
1013
  const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
488
1014
  if (data.date && data.date === today) {
489
1015
  this.todaySpending = data.amount || 0;
@@ -502,29 +1028,29 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
502
1028
  * Save spending data to disk
503
1029
  */
504
1030
  saveSpending() {
505
- (0, import_fs.mkdirSync)(this.configDir, { recursive: true });
506
- const spendingPath = (0, import_path.join)(this.configDir, "spending.json");
1031
+ (0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
1032
+ const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
507
1033
  const data = {
508
1034
  date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
509
1035
  amount: this.todaySpending,
510
1036
  updatedAt: Date.now()
511
1037
  };
512
- (0, import_fs.writeFileSync)(spendingPath, JSON.stringify(data, null, 2));
1038
+ (0, import_fs2.writeFileSync)(spendingPath, JSON.stringify(data, null, 2));
513
1039
  }
514
1040
  loadWallet() {
515
- const walletPath = (0, import_path.join)(this.configDir, "wallet.json");
516
- if ((0, import_fs.existsSync)(walletPath)) {
1041
+ const walletPath = (0, import_path2.join)(this.configDir, "wallet.json");
1042
+ if ((0, import_fs2.existsSync)(walletPath)) {
517
1043
  try {
518
- const stats = (0, import_fs.statSync)(walletPath);
1044
+ const stats = (0, import_fs2.statSync)(walletPath);
519
1045
  const mode = stats.mode & 511;
520
1046
  if (mode !== 384) {
521
1047
  console.warn(`[MoltsPay] WARNING: wallet.json has insecure permissions (${mode.toString(8)})`);
522
1048
  console.warn(`[MoltsPay] Fixing permissions to 0600...`);
523
- (0, import_fs.chmodSync)(walletPath, 384);
1049
+ (0, import_fs2.chmodSync)(walletPath, 384);
524
1050
  }
525
1051
  } catch (err) {
526
1052
  }
527
- const content = (0, import_fs.readFileSync)(walletPath, "utf-8");
1053
+ const content = (0, import_fs2.readFileSync)(walletPath, "utf-8");
528
1054
  return JSON.parse(content);
529
1055
  }
530
1056
  return null;
@@ -533,15 +1059,15 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
533
1059
  * Initialize a new wallet (called by CLI)
534
1060
  */
535
1061
  static init(configDir, options) {
536
- (0, import_fs.mkdirSync)(configDir, { recursive: true });
1062
+ (0, import_fs2.mkdirSync)(configDir, { recursive: true });
537
1063
  const wallet = import_ethers.Wallet.createRandom();
538
1064
  const walletData = {
539
1065
  address: wallet.address,
540
1066
  privateKey: wallet.privateKey,
541
1067
  createdAt: Date.now()
542
1068
  };
543
- const walletPath = (0, import_path.join)(configDir, "wallet.json");
544
- (0, import_fs.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
1069
+ const walletPath = (0, import_path2.join)(configDir, "wallet.json");
1070
+ (0, import_fs2.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
545
1071
  const config = {
546
1072
  chain: options.chain,
547
1073
  limits: {
@@ -549,8 +1075,8 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
549
1075
  maxPerDay: options.maxPerDay
550
1076
  }
551
1077
  };
552
- const configPath = (0, import_path.join)(configDir, "config.json");
553
- (0, import_fs.writeFileSync)(configPath, JSON.stringify(config, null, 2));
1078
+ const configPath = (0, import_path2.join)(configDir, "config.json");
1079
+ (0, import_fs2.writeFileSync)(configPath, JSON.stringify(config, null, 2));
554
1080
  return { address: wallet.address, configDir };
555
1081
  }
556
1082
  /**
@@ -586,7 +1112,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
586
1112
  if (!this.wallet) {
587
1113
  throw new Error("Client not initialized");
588
1114
  }
589
- const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
1115
+ const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
590
1116
  const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
591
1117
  const results = {};
592
1118
  const tempoTokens = {