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
@@ -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
  */
@@ -213,11 +398,26 @@ var MoltsPayClient = class {
213
398
  throw new Error("Client not initialized. Run: npx moltspay init");
214
399
  }
215
400
  console.log(`[MoltsPay] Requesting service: ${service}`);
216
- const requestBody = { service, params };
401
+ let executeUrl = `${serverUrl}/execute`;
402
+ try {
403
+ const services = await this.getServices(serverUrl);
404
+ const svc = services.services?.find((s) => s.id === service);
405
+ if (svc?.endpoint) {
406
+ executeUrl = `${serverUrl}${svc.endpoint}`;
407
+ console.log(`[MoltsPay] Using service endpoint: ${svc.endpoint}`);
408
+ }
409
+ } catch {
410
+ }
411
+ let requestBody;
412
+ if (options.rawData) {
413
+ requestBody = { service, ...params };
414
+ } else {
415
+ requestBody = { service, params };
416
+ }
217
417
  if (options.chain) {
218
418
  requestBody.chain = options.chain;
219
419
  }
220
- const initialRes = await fetch(`${serverUrl}/execute`, {
420
+ const initialRes = await fetch(executeUrl, {
221
421
  method: "POST",
222
422
  headers: { "Content-Type": "application/json" },
223
423
  body: JSON.stringify(requestBody)
@@ -229,9 +429,14 @@ var MoltsPayClient = class {
229
429
  }
230
430
  throw new Error(data.error || "Unexpected response");
231
431
  }
432
+ const wwwAuthHeader = initialRes.headers.get("www-authenticate");
232
433
  const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER);
434
+ if (wwwAuthHeader && wwwAuthHeader.toLowerCase().includes("payment")) {
435
+ console.log("[MoltsPay] Detected MPP protocol, using Tempo flow...");
436
+ return await this.handleMPPPayment(executeUrl, service, params, wwwAuthHeader, options);
437
+ }
233
438
  if (!paymentRequiredHeader) {
234
- throw new Error("Missing x-payment-required header");
439
+ throw new Error("Missing payment header (x-payment-required or www-authenticate)");
235
440
  }
236
441
  let requirements;
237
442
  try {
@@ -248,17 +453,22 @@ var MoltsPayClient = class {
248
453
  throw new Error("Invalid x-payment-required header");
249
454
  }
250
455
  const networkToChainName = (network2) => {
456
+ if (network2 === "solana:mainnet") return "solana";
457
+ if (network2 === "solana:devnet") return "solana_devnet";
251
458
  const match = network2.match(/^eip155:(\d+)$/);
252
459
  if (!match) return null;
253
460
  const chainId = parseInt(match[1]);
254
461
  if (chainId === 8453) return "base";
255
462
  if (chainId === 137) return "polygon";
256
463
  if (chainId === 84532) return "base_sepolia";
464
+ if (chainId === 42431) return "tempo_moderato";
465
+ if (chainId === 56) return "bnb";
466
+ if (chainId === 97) return "bnb_testnet";
257
467
  return null;
258
468
  };
259
469
  const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
260
- let chainName;
261
470
  const userSpecifiedChain = options.chain;
471
+ let selectedChain;
262
472
  if (userSpecifiedChain) {
263
473
  if (!serverChains.includes(userSpecifiedChain)) {
264
474
  throw new Error(
@@ -266,17 +476,27 @@ var MoltsPayClient = class {
266
476
  Server accepts: ${serverChains.join(", ")}`
267
477
  );
268
478
  }
269
- chainName = userSpecifiedChain;
479
+ selectedChain = userSpecifiedChain;
270
480
  } else {
271
481
  if (serverChains.length === 1 && serverChains[0] === "base") {
272
- chainName = "base";
482
+ selectedChain = "base";
273
483
  } else {
274
484
  throw new Error(
275
485
  `Server accepts: ${serverChains.join(", ")}
276
- Please specify: --chain base, --chain polygon, or --chain base_sepolia`
486
+ Please specify: --chain <chain_name>`
277
487
  );
278
488
  }
279
489
  }
490
+ if (selectedChain === "solana" || selectedChain === "solana_devnet") {
491
+ const solanaChain = selectedChain;
492
+ const network2 = solanaChain === "solana" ? "solana:mainnet" : "solana:devnet";
493
+ const req2 = requirements.find((r) => r.network === network2);
494
+ if (!req2) {
495
+ throw new Error(`Failed to find payment requirement for ${selectedChain}`);
496
+ }
497
+ return await this.handleSolanaPayment(executeUrl, service, params, req2, solanaChain, options);
498
+ }
499
+ const chainName = selectedChain;
280
500
  const chain = getChain(chainName);
281
501
  const network = `eip155:${chain.chainId}`;
282
502
  const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
@@ -311,6 +531,25 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
311
531
  } else {
312
532
  console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
313
533
  }
534
+ if (chainName === "bnb" || chainName === "bnb_testnet") {
535
+ console.log(`[MoltsPay] Using BNB intent-based payment flow...`);
536
+ const payTo2 = req.payTo || req.resource;
537
+ if (!payTo2) {
538
+ throw new Error("Missing payTo address in payment requirements");
539
+ }
540
+ const bnbSpender = req.extra?.bnbSpender;
541
+ if (!bnbSpender) {
542
+ throw new Error("Server did not provide bnbSpender address. Server may not support BNB payments.");
543
+ }
544
+ return await this.handleBNBPayment(executeUrl, service, params, {
545
+ to: payTo2,
546
+ amount,
547
+ token,
548
+ chainName,
549
+ chain,
550
+ spender: bnbSpender
551
+ }, options);
552
+ }
314
553
  const payTo = req.payTo || req.resource;
315
554
  if (!payTo) {
316
555
  throw new Error("Missing payTo address in payment requirements");
@@ -340,11 +579,11 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
340
579
  };
341
580
  const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
342
581
  console.log(`[MoltsPay] Sending request with payment...`);
343
- const paidRequestBody = { service, params };
582
+ const paidRequestBody = options.rawData ? { service, ...params } : { service, params };
344
583
  if (options.chain) {
345
584
  paidRequestBody.chain = options.chain;
346
585
  }
347
- const paidRes = await fetch(`${serverUrl}/execute`, {
586
+ const paidRes = await fetch(executeUrl, {
348
587
  method: "POST",
349
588
  headers: {
350
589
  "Content-Type": "application/json",
@@ -358,7 +597,304 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
358
597
  }
359
598
  this.recordSpending(amount);
360
599
  console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
361
- return result.result;
600
+ return result.result || result;
601
+ }
602
+ /**
603
+ * Handle MPP (Machine Payments Protocol) payment flow
604
+ * Called when pay() detects WWW-Authenticate header in 402 response
605
+ */
606
+ async handleMPPPayment(executeUrl, service, params, wwwAuthHeader, options = {}) {
607
+ const { privateKeyToAccount } = await import("viem/accounts");
608
+ const { createWalletClient, createPublicClient, http } = await import("viem");
609
+ const { tempoModerato } = await import("viem/chains");
610
+ const { Actions } = await import("viem/tempo");
611
+ const privateKey = this.walletData.privateKey;
612
+ const account = privateKeyToAccount(privateKey);
613
+ console.log(`[MoltsPay] Using MPP protocol on Tempo`);
614
+ console.log(`[MoltsPay] Account: ${account.address}`);
615
+ const parseAuthParam = (header, key) => {
616
+ const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
617
+ return match ? match[1] : null;
618
+ };
619
+ const challengeId = parseAuthParam(wwwAuthHeader, "id");
620
+ const method = parseAuthParam(wwwAuthHeader, "method");
621
+ const realm = parseAuthParam(wwwAuthHeader, "realm");
622
+ const requestB64 = parseAuthParam(wwwAuthHeader, "request");
623
+ if (method !== "tempo") {
624
+ throw new Error(`Unsupported payment method: ${method}`);
625
+ }
626
+ if (!requestB64) {
627
+ throw new Error("Missing request in WWW-Authenticate");
628
+ }
629
+ const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
630
+ const paymentRequest = JSON.parse(requestJson);
631
+ const { amount, currency, recipient, methodDetails } = paymentRequest;
632
+ const chainId = methodDetails?.chainId || 42431;
633
+ const amountDisplay = Number(amount) / 1e6;
634
+ console.log(`[MoltsPay] Payment: $${amountDisplay} to ${recipient}`);
635
+ this.checkLimits(amountDisplay);
636
+ console.log(`[MoltsPay] Sending transaction on Tempo...`);
637
+ const tempoChain = { ...tempoModerato, feeToken: currency };
638
+ const publicClient = createPublicClient({
639
+ chain: tempoChain,
640
+ transport: http("https://rpc.moderato.tempo.xyz")
641
+ });
642
+ const walletClient = createWalletClient({
643
+ account,
644
+ chain: tempoChain,
645
+ transport: http("https://rpc.moderato.tempo.xyz")
646
+ });
647
+ const txHash = await Actions.token.transfer(walletClient, {
648
+ to: recipient,
649
+ amount: BigInt(amount),
650
+ token: currency
651
+ });
652
+ console.log(`[MoltsPay] Transaction: ${txHash}`);
653
+ await publicClient.waitForTransactionReceipt({ hash: txHash });
654
+ console.log(`[MoltsPay] Confirmed! Retrying with credential...`);
655
+ const credential = {
656
+ challenge: {
657
+ id: challengeId,
658
+ realm,
659
+ method: "tempo",
660
+ intent: "charge",
661
+ request: paymentRequest
662
+ },
663
+ payload: { hash: txHash, type: "hash" },
664
+ source: `did:pkh:eip155:${chainId}:${account.address}`
665
+ };
666
+ const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
667
+ const retryBody = options.rawData ? { service, ...params, chain: "tempo_moderato" } : { service, params, chain: "tempo_moderato" };
668
+ const paidRes = await fetch(executeUrl, {
669
+ method: "POST",
670
+ headers: {
671
+ "Content-Type": "application/json",
672
+ "Authorization": `Payment ${credentialB64}`
673
+ },
674
+ body: JSON.stringify(retryBody)
675
+ });
676
+ const result = await paidRes.json();
677
+ if (!paidRes.ok) {
678
+ throw new Error(result.error || "Payment verification failed");
679
+ }
680
+ this.recordSpending(amountDisplay);
681
+ console.log(`[MoltsPay] Success!`);
682
+ return result.result || result;
683
+ }
684
+ /**
685
+ * Handle BNB Chain payment flow (pre-approval + intent signature)
686
+ *
687
+ * Flow:
688
+ * 1. Check client has approved server wallet (done via `moltspay init`)
689
+ * 2. Sign EIP-712 payment intent (no gas, just signature)
690
+ * 3. Send intent to server
691
+ * 4. Server executes service
692
+ * 5. Server calls transferFrom if successful (pay-for-success)
693
+ */
694
+ async handleBNBPayment(executeUrl, service, params, paymentDetails, options = {}) {
695
+ const { to, amount, token, chainName, chain, spender } = paymentDetails;
696
+ const tokenConfig = chain.tokens[token];
697
+ const provider = new ethers.JsonRpcProvider(chain.rpc);
698
+ const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
699
+ const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
700
+ if (allowance < amountWeiCheck) {
701
+ const nativeBalance = await provider.getBalance(this.wallet.address);
702
+ const minGasBalance = ethers.parseEther("0.0005");
703
+ if (nativeBalance < minGasBalance) {
704
+ const nativeBNB = parseFloat(ethers.formatEther(nativeBalance)).toFixed(4);
705
+ const isTestnet = chainName === "bnb_testnet";
706
+ if (isTestnet) {
707
+ throw new Error(
708
+ `\u274C Insufficient tBNB for approval transaction
709
+
710
+ Current tBNB: ${nativeBNB}
711
+ Required: ~0.001 tBNB
712
+
713
+ Get testnet tokens: npx moltspay faucet --chain bnb_testnet
714
+ (Gives USDC + tBNB for gas)`
715
+ );
716
+ } else {
717
+ throw new Error(
718
+ `\u274C Insufficient BNB for approval transaction
719
+
720
+ Current BNB: ${nativeBNB}
721
+ Required: ~0.001 BNB (~$0.60)
722
+
723
+ To get BNB:
724
+ \u2022 Withdraw from Binance/exchange to your wallet
725
+ \u2022 Most exchanges include BNB dust with withdrawals
726
+
727
+ After funding, run:
728
+ npx moltspay approve --chain ${chainName} --spender ${spender}`
729
+ );
730
+ }
731
+ }
732
+ throw new Error(
733
+ `Insufficient allowance for ${spender.slice(0, 10)}...
734
+ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
735
+ );
736
+ }
737
+ const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
738
+ const intent = {
739
+ from: this.wallet.address,
740
+ to,
741
+ amount: amountWei,
742
+ token: tokenConfig.address,
743
+ service,
744
+ nonce: Date.now(),
745
+ // Use timestamp as nonce for simplicity
746
+ deadline: Date.now() + 36e5
747
+ // 1 hour
748
+ };
749
+ const domain = {
750
+ name: "MoltsPay",
751
+ version: "1",
752
+ chainId: chain.chainId
753
+ };
754
+ const types = {
755
+ PaymentIntent: [
756
+ { name: "from", type: "address" },
757
+ { name: "to", type: "address" },
758
+ { name: "amount", type: "uint256" },
759
+ { name: "token", type: "address" },
760
+ { name: "service", type: "string" },
761
+ { name: "nonce", type: "uint256" },
762
+ { name: "deadline", type: "uint256" }
763
+ ]
764
+ };
765
+ console.log(`[MoltsPay] Signing BNB payment intent...`);
766
+ const signature = await this.wallet.signTypedData(domain, types, intent);
767
+ const network = `eip155:${chain.chainId}`;
768
+ const payload = {
769
+ x402Version: 2,
770
+ scheme: "exact",
771
+ network,
772
+ payload: {
773
+ intent: {
774
+ ...intent,
775
+ signature
776
+ },
777
+ chainId: chain.chainId
778
+ },
779
+ accepted: {
780
+ scheme: "exact",
781
+ network,
782
+ asset: tokenConfig.address,
783
+ amount: amountWei,
784
+ payTo: to,
785
+ maxTimeoutSeconds: 300
786
+ }
787
+ };
788
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
789
+ console.log(`[MoltsPay] Sending BNB payment request...`);
790
+ const bnbRequestBody = options.rawData ? { service, ...params, chain: chainName } : { service, params, chain: chainName };
791
+ const paidRes = await fetch(executeUrl, {
792
+ method: "POST",
793
+ headers: {
794
+ "Content-Type": "application/json",
795
+ "X-Payment": paymentHeader
796
+ },
797
+ body: JSON.stringify(bnbRequestBody)
798
+ });
799
+ const result = await paidRes.json();
800
+ if (!paidRes.ok) {
801
+ throw new Error(result.error || "BNB payment failed");
802
+ }
803
+ this.recordSpending(amount);
804
+ console.log(`[MoltsPay] Success! BNB payment settled.`);
805
+ return result.result || result;
806
+ }
807
+ /**
808
+ * Handle Solana payment flow
809
+ *
810
+ * Solana uses SPL token transfers with pay-for-success model:
811
+ * 1. Client creates and signs a transfer transaction
812
+ * 2. Server submits the transaction after service completes
813
+ */
814
+ async handleSolanaPayment(executeUrl, service, params, requirements, chain, options = {}) {
815
+ const solanaWallet = loadSolanaWallet(this.configDir);
816
+ if (!solanaWallet) {
817
+ throw new Error("No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
818
+ }
819
+ const amount = Number(requirements.amount);
820
+ const amountUSDC = amount / 1e6;
821
+ this.checkLimits(amountUSDC);
822
+ console.log(`[MoltsPay] Creating Solana payment: $${amountUSDC} USDC`);
823
+ if (!requirements.payTo) {
824
+ throw new Error("Missing payTo address in payment requirements");
825
+ }
826
+ const solanaFeePayer = requirements.extra?.solanaFeePayer;
827
+ const feePayerPubkey = solanaFeePayer ? new PublicKey4(solanaFeePayer) : void 0;
828
+ if (feePayerPubkey) {
829
+ console.log(`[MoltsPay] Gasless mode: server pays fees`);
830
+ }
831
+ const recipientPubkey = new PublicKey4(requirements.payTo);
832
+ const transaction = await createSolanaPaymentTransaction(
833
+ solanaWallet.publicKey,
834
+ recipientPubkey,
835
+ BigInt(amount),
836
+ chain,
837
+ feePayerPubkey
838
+ // Optional fee payer for gasless mode
839
+ );
840
+ if (feePayerPubkey) {
841
+ transaction.partialSign(solanaWallet);
842
+ } else {
843
+ transaction.sign(solanaWallet);
844
+ }
845
+ const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
846
+ console.log(`[MoltsPay] Transaction signed, sending to server...`);
847
+ const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
848
+ const payload = {
849
+ x402Version: 2,
850
+ scheme: "exact",
851
+ network,
852
+ payload: {
853
+ signedTransaction: signedTx,
854
+ sender: solanaWallet.publicKey.toBase58(),
855
+ chain
856
+ },
857
+ accepted: {
858
+ scheme: "exact",
859
+ network,
860
+ asset: requirements.asset,
861
+ amount: requirements.amount,
862
+ payTo: requirements.payTo,
863
+ maxTimeoutSeconds: 300
864
+ }
865
+ };
866
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
867
+ const solanaRequestBody = options.rawData ? { service, ...params, chain } : { service, params, chain };
868
+ const paidRes = await fetch(executeUrl, {
869
+ method: "POST",
870
+ headers: {
871
+ "Content-Type": "application/json",
872
+ "X-Payment": paymentHeader
873
+ },
874
+ body: JSON.stringify(solanaRequestBody)
875
+ });
876
+ const result = await paidRes.json();
877
+ if (!paidRes.ok) {
878
+ throw new Error(result.error || "Solana payment failed");
879
+ }
880
+ this.recordSpending(amountUSDC);
881
+ console.log(`[MoltsPay] Success! Solana payment settled.`);
882
+ if (result.payment?.transaction) {
883
+ const explorerUrl = chain === "solana" ? `https://solscan.io/tx/${result.payment.transaction}` : `https://solscan.io/tx/${result.payment.transaction}?cluster=devnet`;
884
+ console.log(`[MoltsPay] Transaction: ${explorerUrl}`);
885
+ }
886
+ return result.result || result;
887
+ }
888
+ /**
889
+ * Check ERC20 allowance for a spender
890
+ */
891
+ async checkAllowance(tokenAddress, spender, provider) {
892
+ const contract = new ethers.Contract(
893
+ tokenAddress,
894
+ ["function allowance(address owner, address spender) view returns (uint256)"],
895
+ provider
896
+ );
897
+ return await contract.allowance(this.wallet.address, spender);
362
898
  }
363
899
  /**
364
900
  * Sign EIP-3009 transferWithAuthorization (GASLESS)
@@ -430,26 +966,26 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
430
966
  }
431
967
  // --- Config & Wallet Management ---
432
968
  loadConfig() {
433
- const configPath = join(this.configDir, "config.json");
434
- if (existsSync(configPath)) {
435
- const content = readFileSync(configPath, "utf-8");
969
+ const configPath = join2(this.configDir, "config.json");
970
+ if (existsSync2(configPath)) {
971
+ const content = readFileSync2(configPath, "utf-8");
436
972
  return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
437
973
  }
438
974
  return { ...DEFAULT_CONFIG };
439
975
  }
440
976
  saveConfig() {
441
- mkdirSync(this.configDir, { recursive: true });
442
- const configPath = join(this.configDir, "config.json");
443
- writeFileSync(configPath, JSON.stringify(this.config, null, 2));
977
+ mkdirSync2(this.configDir, { recursive: true });
978
+ const configPath = join2(this.configDir, "config.json");
979
+ writeFileSync2(configPath, JSON.stringify(this.config, null, 2));
444
980
  }
445
981
  /**
446
982
  * Load spending data from disk
447
983
  */
448
984
  loadSpending() {
449
- const spendingPath = join(this.configDir, "spending.json");
450
- if (existsSync(spendingPath)) {
985
+ const spendingPath = join2(this.configDir, "spending.json");
986
+ if (existsSync2(spendingPath)) {
451
987
  try {
452
- const data = JSON.parse(readFileSync(spendingPath, "utf-8"));
988
+ const data = JSON.parse(readFileSync2(spendingPath, "utf-8"));
453
989
  const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
454
990
  if (data.date && data.date === today) {
455
991
  this.todaySpending = data.amount || 0;
@@ -468,18 +1004,18 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
468
1004
  * Save spending data to disk
469
1005
  */
470
1006
  saveSpending() {
471
- mkdirSync(this.configDir, { recursive: true });
472
- const spendingPath = join(this.configDir, "spending.json");
1007
+ mkdirSync2(this.configDir, { recursive: true });
1008
+ const spendingPath = join2(this.configDir, "spending.json");
473
1009
  const data = {
474
1010
  date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
475
1011
  amount: this.todaySpending,
476
1012
  updatedAt: Date.now()
477
1013
  };
478
- writeFileSync(spendingPath, JSON.stringify(data, null, 2));
1014
+ writeFileSync2(spendingPath, JSON.stringify(data, null, 2));
479
1015
  }
480
1016
  loadWallet() {
481
- const walletPath = join(this.configDir, "wallet.json");
482
- if (existsSync(walletPath)) {
1017
+ const walletPath = join2(this.configDir, "wallet.json");
1018
+ if (existsSync2(walletPath)) {
483
1019
  try {
484
1020
  const stats = statSync(walletPath);
485
1021
  const mode = stats.mode & 511;
@@ -490,7 +1026,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
490
1026
  }
491
1027
  } catch (err) {
492
1028
  }
493
- const content = readFileSync(walletPath, "utf-8");
1029
+ const content = readFileSync2(walletPath, "utf-8");
494
1030
  return JSON.parse(content);
495
1031
  }
496
1032
  return null;
@@ -499,15 +1035,15 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
499
1035
  * Initialize a new wallet (called by CLI)
500
1036
  */
501
1037
  static init(configDir, options) {
502
- mkdirSync(configDir, { recursive: true });
1038
+ mkdirSync2(configDir, { recursive: true });
503
1039
  const wallet = Wallet.createRandom();
504
1040
  const walletData = {
505
1041
  address: wallet.address,
506
1042
  privateKey: wallet.privateKey,
507
1043
  createdAt: Date.now()
508
1044
  };
509
- const walletPath = join(configDir, "wallet.json");
510
- writeFileSync(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
1045
+ const walletPath = join2(configDir, "wallet.json");
1046
+ writeFileSync2(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
511
1047
  const config = {
512
1048
  chain: options.chain,
513
1049
  limits: {
@@ -515,8 +1051,8 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
515
1051
  maxPerDay: options.maxPerDay
516
1052
  }
517
1053
  };
518
- const configPath = join(configDir, "config.json");
519
- writeFileSync(configPath, JSON.stringify(config, null, 2));
1054
+ const configPath = join2(configDir, "config.json");
1055
+ writeFileSync2(configPath, JSON.stringify(config, null, 2));
520
1056
  return { address: wallet.address, configDir };
521
1057
  }
522
1058
  /**
@@ -552,7 +1088,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
552
1088
  if (!this.wallet) {
553
1089
  throw new Error("Client not initialized");
554
1090
  }
555
- const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
1091
+ const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
556
1092
  const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
557
1093
  const results = {};
558
1094
  const tempoTokens = {