moltspay 1.2.1 → 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 (59) hide show
  1. package/README.md +292 -34
  2. package/dist/cdp/index.d.mts +4 -4
  3. package/dist/cdp/index.d.ts +4 -4
  4. package/dist/cdp/index.js +110 -30368
  5. package/dist/cdp/index.js.map +1 -1
  6. package/dist/cdp/index.mjs +94 -30360
  7. package/dist/cdp/index.mjs.map +1 -1
  8. package/dist/cdp-DeohBe1o.d.ts +66 -0
  9. package/dist/cdp-p_eHuQpb.d.mts +66 -0
  10. package/dist/chains/index.d.mts +9 -8
  11. package/dist/chains/index.d.ts +9 -8
  12. package/dist/chains/index.js +86 -0
  13. package/dist/chains/index.js.map +1 -1
  14. package/dist/chains/index.mjs +86 -0
  15. package/dist/chains/index.mjs.map +1 -1
  16. package/dist/cli/index.js +2746 -290
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/cli/index.mjs +2752 -282
  19. package/dist/cli/index.mjs.map +1 -1
  20. package/dist/client/index.d.mts +60 -4
  21. package/dist/client/index.d.ts +60 -4
  22. package/dist/client/index.js +734 -43
  23. package/dist/client/index.js.map +1 -1
  24. package/dist/client/index.mjs +732 -41
  25. package/dist/client/index.mjs.map +1 -1
  26. package/dist/facilitators/index.d.mts +220 -39
  27. package/dist/facilitators/index.d.ts +220 -39
  28. package/dist/facilitators/index.js +897 -1
  29. package/dist/facilitators/index.js.map +1 -1
  30. package/dist/facilitators/index.mjs +902 -1
  31. package/dist/facilitators/index.mjs.map +1 -1
  32. package/dist/{index-DgJPZMBG.d.mts → index-D_2FkLwV.d.mts} +6 -2
  33. package/dist/{index-DgJPZMBG.d.ts → index-D_2FkLwV.d.ts} +6 -2
  34. package/dist/index.d.mts +3 -2
  35. package/dist/index.d.ts +3 -2
  36. package/dist/index.js +2238 -30837
  37. package/dist/index.js.map +1 -1
  38. package/dist/index.mjs +2167 -30766
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/server/index.d.mts +30 -3
  41. package/dist/server/index.d.ts +30 -3
  42. package/dist/server/index.js +1345 -54
  43. package/dist/server/index.js.map +1 -1
  44. package/dist/server/index.mjs +1355 -54
  45. package/dist/server/index.mjs.map +1 -1
  46. package/dist/verify/index.d.mts +1 -1
  47. package/dist/verify/index.d.ts +1 -1
  48. package/dist/verify/index.js +86 -0
  49. package/dist/verify/index.js.map +1 -1
  50. package/dist/verify/index.mjs +86 -0
  51. package/dist/verify/index.mjs.map +1 -1
  52. package/dist/wallet/index.d.mts +3 -3
  53. package/dist/wallet/index.d.ts +3 -3
  54. package/dist/wallet/index.js +86 -0
  55. package/dist/wallet/index.js.map +1 -1
  56. package/dist/wallet/index.mjs +86 -0
  57. package/dist/wallet/index.mjs.map +1 -1
  58. package/package.json +8 -2
  59. 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
@@ -82,6 +82,92 @@ var CHAINS = {
82
82
  explorer: "https://sepolia.basescan.org/address/",
83
83
  explorerTx: "https://sepolia.basescan.org/tx/",
84
84
  avgBlockTime: 2
85
+ },
86
+ // ============ Tempo Testnet (Moderato) ============
87
+ tempo_moderato: {
88
+ name: "Tempo Moderato",
89
+ chainId: 42431,
90
+ rpc: "https://rpc.moderato.tempo.xyz",
91
+ tokens: {
92
+ // TIP-20 stablecoins on Tempo testnet (from mppx SDK)
93
+ // Note: Tempo uses USD as native gas token, not ETH
94
+ USDC: {
95
+ address: "0x20c0000000000000000000000000000000000000",
96
+ // pathUSD - primary testnet stablecoin
97
+ decimals: 6,
98
+ symbol: "USDC",
99
+ eip712Name: "pathUSD"
100
+ },
101
+ USDT: {
102
+ address: "0x20c0000000000000000000000000000000000001",
103
+ // alphaUSD
104
+ decimals: 6,
105
+ symbol: "USDT",
106
+ eip712Name: "alphaUSD"
107
+ }
108
+ },
109
+ usdc: "0x20c0000000000000000000000000000000000000",
110
+ explorer: "https://explore.testnet.tempo.xyz/address/",
111
+ explorerTx: "https://explore.testnet.tempo.xyz/tx/",
112
+ avgBlockTime: 0.5
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
85
171
  }
86
172
  };
87
173
  function getChain(name) {
@@ -92,7 +178,129 @@ function getChain(name) {
92
178
  return config;
93
179
  }
94
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
+
95
302
  // src/client/index.ts
303
+ import { PublicKey as PublicKey4 } from "@solana/web3.js";
96
304
  var X402_VERSION = 2;
97
305
  var PAYMENT_REQUIRED_HEADER = "x-payment-required";
98
306
  var PAYMENT_HEADER = "x-payment";
@@ -111,7 +319,7 @@ var MoltsPayClient = class {
111
319
  todaySpending = 0;
112
320
  lastSpendingReset = 0;
113
321
  constructor(options = {}) {
114
- this.configDir = options.configDir || join(homedir(), ".moltspay");
322
+ this.configDir = options.configDir || join2(homedir2(), ".moltspay");
115
323
  this.config = this.loadConfig();
116
324
  this.walletData = this.loadWallet();
117
325
  this.loadSpending();
@@ -131,6 +339,12 @@ var MoltsPayClient = class {
131
339
  get address() {
132
340
  return this.wallet?.address || null;
133
341
  }
342
+ /**
343
+ * Get wallet instance (for direct operations like approvals)
344
+ */
345
+ getWallet() {
346
+ return this.wallet;
347
+ }
134
348
  /**
135
349
  * Get current config
136
350
  */
@@ -200,9 +414,14 @@ var MoltsPayClient = class {
200
414
  }
201
415
  throw new Error(data.error || "Unexpected response");
202
416
  }
417
+ const wwwAuthHeader = initialRes.headers.get("www-authenticate");
203
418
  const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER);
419
+ if (wwwAuthHeader && wwwAuthHeader.toLowerCase().includes("payment")) {
420
+ console.log("[MoltsPay] Detected MPP protocol, using Tempo flow...");
421
+ return await this.handleMPPPayment(serverUrl, service, params, wwwAuthHeader);
422
+ }
204
423
  if (!paymentRequiredHeader) {
205
- throw new Error("Missing x-payment-required header");
424
+ throw new Error("Missing payment header (x-payment-required or www-authenticate)");
206
425
  }
207
426
  let requirements;
208
427
  try {
@@ -219,17 +438,22 @@ var MoltsPayClient = class {
219
438
  throw new Error("Invalid x-payment-required header");
220
439
  }
221
440
  const networkToChainName = (network2) => {
441
+ if (network2 === "solana:mainnet") return "solana";
442
+ if (network2 === "solana:devnet") return "solana_devnet";
222
443
  const match = network2.match(/^eip155:(\d+)$/);
223
444
  if (!match) return null;
224
445
  const chainId = parseInt(match[1]);
225
446
  if (chainId === 8453) return "base";
226
447
  if (chainId === 137) return "polygon";
227
448
  if (chainId === 84532) return "base_sepolia";
449
+ if (chainId === 42431) return "tempo_moderato";
450
+ if (chainId === 56) return "bnb";
451
+ if (chainId === 97) return "bnb_testnet";
228
452
  return null;
229
453
  };
230
454
  const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
231
- let chainName;
232
455
  const userSpecifiedChain = options.chain;
456
+ let selectedChain;
233
457
  if (userSpecifiedChain) {
234
458
  if (!serverChains.includes(userSpecifiedChain)) {
235
459
  throw new Error(
@@ -237,17 +461,27 @@ var MoltsPayClient = class {
237
461
  Server accepts: ${serverChains.join(", ")}`
238
462
  );
239
463
  }
240
- chainName = userSpecifiedChain;
464
+ selectedChain = userSpecifiedChain;
241
465
  } else {
242
466
  if (serverChains.length === 1 && serverChains[0] === "base") {
243
- chainName = "base";
467
+ selectedChain = "base";
244
468
  } else {
245
469
  throw new Error(
246
470
  `Server accepts: ${serverChains.join(", ")}
247
- Please specify: --chain base, --chain polygon, or --chain base_sepolia`
471
+ Please specify: --chain <chain_name>`
248
472
  );
249
473
  }
250
474
  }
475
+ if (selectedChain === "solana" || selectedChain === "solana_devnet") {
476
+ const solanaChain = selectedChain;
477
+ const network2 = solanaChain === "solana" ? "solana:mainnet" : "solana:devnet";
478
+ const req2 = requirements.find((r) => r.network === network2);
479
+ if (!req2) {
480
+ throw new Error(`Failed to find payment requirement for ${selectedChain}`);
481
+ }
482
+ return await this.handleSolanaPayment(serverUrl, service, params, req2, solanaChain);
483
+ }
484
+ const chainName = selectedChain;
251
485
  const chain = getChain(chainName);
252
486
  const network = `eip155:${chain.chainId}`;
253
487
  const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
@@ -282,6 +516,25 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
282
516
  } else {
283
517
  console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
284
518
  }
519
+ if (chainName === "bnb" || chainName === "bnb_testnet") {
520
+ console.log(`[MoltsPay] Using BNB intent-based payment flow...`);
521
+ const payTo2 = req.payTo || req.resource;
522
+ if (!payTo2) {
523
+ throw new Error("Missing payTo address in payment requirements");
524
+ }
525
+ const bnbSpender = req.extra?.bnbSpender;
526
+ if (!bnbSpender) {
527
+ throw new Error("Server did not provide bnbSpender address. Server may not support BNB payments.");
528
+ }
529
+ return await this.handleBNBPayment(serverUrl, service, params, {
530
+ to: payTo2,
531
+ amount,
532
+ token,
533
+ chainName,
534
+ chain,
535
+ spender: bnbSpender
536
+ });
537
+ }
285
538
  const payTo = req.payTo || req.resource;
286
539
  if (!payTo) {
287
540
  throw new Error("Missing payTo address in payment requirements");
@@ -331,6 +584,300 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
331
584
  console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
332
585
  return result.result;
333
586
  }
587
+ /**
588
+ * Handle MPP (Machine Payments Protocol) payment flow
589
+ * Called when pay() detects WWW-Authenticate header in 402 response
590
+ */
591
+ async handleMPPPayment(serverUrl, service, params, wwwAuthHeader) {
592
+ const { privateKeyToAccount } = await import("viem/accounts");
593
+ const { createWalletClient, createPublicClient, http } = await import("viem");
594
+ const { tempoModerato } = await import("viem/chains");
595
+ const { Actions } = await import("viem/tempo");
596
+ const privateKey = this.walletData.privateKey;
597
+ const account = privateKeyToAccount(privateKey);
598
+ console.log(`[MoltsPay] Using MPP protocol on Tempo`);
599
+ console.log(`[MoltsPay] Account: ${account.address}`);
600
+ const parseAuthParam = (header, key) => {
601
+ const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
602
+ return match ? match[1] : null;
603
+ };
604
+ const challengeId = parseAuthParam(wwwAuthHeader, "id");
605
+ const method = parseAuthParam(wwwAuthHeader, "method");
606
+ const realm = parseAuthParam(wwwAuthHeader, "realm");
607
+ const requestB64 = parseAuthParam(wwwAuthHeader, "request");
608
+ if (method !== "tempo") {
609
+ throw new Error(`Unsupported payment method: ${method}`);
610
+ }
611
+ if (!requestB64) {
612
+ throw new Error("Missing request in WWW-Authenticate");
613
+ }
614
+ const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
615
+ const paymentRequest = JSON.parse(requestJson);
616
+ const { amount, currency, recipient, methodDetails } = paymentRequest;
617
+ const chainId = methodDetails?.chainId || 42431;
618
+ const amountDisplay = Number(amount) / 1e6;
619
+ console.log(`[MoltsPay] Payment: $${amountDisplay} to ${recipient}`);
620
+ this.checkLimits(amountDisplay);
621
+ console.log(`[MoltsPay] Sending transaction on Tempo...`);
622
+ const tempoChain = { ...tempoModerato, feeToken: currency };
623
+ const publicClient = createPublicClient({
624
+ chain: tempoChain,
625
+ transport: http("https://rpc.moderato.tempo.xyz")
626
+ });
627
+ const walletClient = createWalletClient({
628
+ account,
629
+ chain: tempoChain,
630
+ transport: http("https://rpc.moderato.tempo.xyz")
631
+ });
632
+ const txHash = await Actions.token.transfer(walletClient, {
633
+ to: recipient,
634
+ amount: BigInt(amount),
635
+ token: currency
636
+ });
637
+ console.log(`[MoltsPay] Transaction: ${txHash}`);
638
+ await publicClient.waitForTransactionReceipt({ hash: txHash });
639
+ console.log(`[MoltsPay] Confirmed! Retrying with credential...`);
640
+ const credential = {
641
+ challenge: {
642
+ id: challengeId,
643
+ realm,
644
+ method: "tempo",
645
+ intent: "charge",
646
+ request: paymentRequest
647
+ },
648
+ payload: { hash: txHash, type: "hash" },
649
+ source: `did:pkh:eip155:${chainId}:${account.address}`
650
+ };
651
+ const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
652
+ const paidRes = await fetch(`${serverUrl}/execute`, {
653
+ method: "POST",
654
+ headers: {
655
+ "Content-Type": "application/json",
656
+ "Authorization": `Payment ${credentialB64}`
657
+ },
658
+ body: JSON.stringify({ service, params, chain: "tempo_moderato" })
659
+ });
660
+ const result = await paidRes.json();
661
+ if (!paidRes.ok) {
662
+ throw new Error(result.error || "Payment verification failed");
663
+ }
664
+ this.recordSpending(amountDisplay);
665
+ console.log(`[MoltsPay] Success!`);
666
+ return result.result || result;
667
+ }
668
+ /**
669
+ * Handle BNB Chain payment flow (pre-approval + intent signature)
670
+ *
671
+ * Flow:
672
+ * 1. Check client has approved server wallet (done via `moltspay init`)
673
+ * 2. Sign EIP-712 payment intent (no gas, just signature)
674
+ * 3. Send intent to server
675
+ * 4. Server executes service
676
+ * 5. Server calls transferFrom if successful (pay-for-success)
677
+ */
678
+ async handleBNBPayment(serverUrl, service, params, paymentDetails) {
679
+ const { to, amount, token, chainName, chain, spender } = paymentDetails;
680
+ const tokenConfig = chain.tokens[token];
681
+ const provider = new ethers.JsonRpcProvider(chain.rpc);
682
+ const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
683
+ const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
684
+ if (allowance < amountWeiCheck) {
685
+ const nativeBalance = await provider.getBalance(this.wallet.address);
686
+ const minGasBalance = ethers.parseEther("0.0005");
687
+ if (nativeBalance < minGasBalance) {
688
+ const nativeBNB = parseFloat(ethers.formatEther(nativeBalance)).toFixed(4);
689
+ const isTestnet = chainName === "bnb_testnet";
690
+ if (isTestnet) {
691
+ throw new Error(
692
+ `\u274C Insufficient tBNB for approval transaction
693
+
694
+ Current tBNB: ${nativeBNB}
695
+ Required: ~0.001 tBNB
696
+
697
+ Get testnet tokens: npx moltspay faucet --chain bnb_testnet
698
+ (Gives USDC + tBNB for gas)`
699
+ );
700
+ } else {
701
+ throw new Error(
702
+ `\u274C Insufficient BNB for approval transaction
703
+
704
+ Current BNB: ${nativeBNB}
705
+ Required: ~0.001 BNB (~$0.60)
706
+
707
+ To get BNB:
708
+ \u2022 Withdraw from Binance/exchange to your wallet
709
+ \u2022 Most exchanges include BNB dust with withdrawals
710
+
711
+ After funding, run:
712
+ npx moltspay approve --chain ${chainName} --spender ${spender}`
713
+ );
714
+ }
715
+ }
716
+ throw new Error(
717
+ `Insufficient allowance for ${spender.slice(0, 10)}...
718
+ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
719
+ );
720
+ }
721
+ const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
722
+ const intent = {
723
+ from: this.wallet.address,
724
+ to,
725
+ amount: amountWei,
726
+ token: tokenConfig.address,
727
+ service,
728
+ nonce: Date.now(),
729
+ // Use timestamp as nonce for simplicity
730
+ deadline: Date.now() + 36e5
731
+ // 1 hour
732
+ };
733
+ const domain = {
734
+ name: "MoltsPay",
735
+ version: "1",
736
+ chainId: chain.chainId
737
+ };
738
+ const types = {
739
+ PaymentIntent: [
740
+ { name: "from", type: "address" },
741
+ { name: "to", type: "address" },
742
+ { name: "amount", type: "uint256" },
743
+ { name: "token", type: "address" },
744
+ { name: "service", type: "string" },
745
+ { name: "nonce", type: "uint256" },
746
+ { name: "deadline", type: "uint256" }
747
+ ]
748
+ };
749
+ console.log(`[MoltsPay] Signing BNB payment intent...`);
750
+ const signature = await this.wallet.signTypedData(domain, types, intent);
751
+ const network = `eip155:${chain.chainId}`;
752
+ const payload = {
753
+ x402Version: 2,
754
+ scheme: "exact",
755
+ network,
756
+ payload: {
757
+ intent: {
758
+ ...intent,
759
+ signature
760
+ },
761
+ chainId: chain.chainId
762
+ },
763
+ accepted: {
764
+ scheme: "exact",
765
+ network,
766
+ asset: tokenConfig.address,
767
+ amount: amountWei,
768
+ payTo: to,
769
+ maxTimeoutSeconds: 300
770
+ }
771
+ };
772
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
773
+ console.log(`[MoltsPay] Sending BNB payment request...`);
774
+ const paidRes = await fetch(`${serverUrl}/execute`, {
775
+ method: "POST",
776
+ headers: {
777
+ "Content-Type": "application/json",
778
+ "X-Payment": paymentHeader
779
+ },
780
+ body: JSON.stringify({ service, params, chain: chainName })
781
+ });
782
+ const result = await paidRes.json();
783
+ if (!paidRes.ok) {
784
+ throw new Error(result.error || "BNB payment failed");
785
+ }
786
+ this.recordSpending(amount);
787
+ console.log(`[MoltsPay] Success! BNB payment settled.`);
788
+ return result.result || result;
789
+ }
790
+ /**
791
+ * Handle Solana payment flow
792
+ *
793
+ * Solana uses SPL token transfers with pay-for-success model:
794
+ * 1. Client creates and signs a transfer transaction
795
+ * 2. Server submits the transaction after service completes
796
+ */
797
+ async handleSolanaPayment(serverUrl, service, params, requirements, chain) {
798
+ const solanaWallet = loadSolanaWallet(this.configDir);
799
+ if (!solanaWallet) {
800
+ throw new Error("No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
801
+ }
802
+ const amount = Number(requirements.amount);
803
+ const amountUSDC = amount / 1e6;
804
+ this.checkLimits(amountUSDC);
805
+ console.log(`[MoltsPay] Creating Solana payment: $${amountUSDC} USDC`);
806
+ if (!requirements.payTo) {
807
+ throw new Error("Missing payTo address in payment requirements");
808
+ }
809
+ const solanaFeePayer = requirements.extra?.solanaFeePayer;
810
+ const feePayerPubkey = solanaFeePayer ? new PublicKey4(solanaFeePayer) : void 0;
811
+ if (feePayerPubkey) {
812
+ console.log(`[MoltsPay] Gasless mode: server pays fees`);
813
+ }
814
+ const recipientPubkey = new PublicKey4(requirements.payTo);
815
+ const transaction = await createSolanaPaymentTransaction(
816
+ solanaWallet.publicKey,
817
+ recipientPubkey,
818
+ BigInt(amount),
819
+ chain,
820
+ feePayerPubkey
821
+ // Optional fee payer for gasless mode
822
+ );
823
+ if (feePayerPubkey) {
824
+ transaction.partialSign(solanaWallet);
825
+ } else {
826
+ transaction.sign(solanaWallet);
827
+ }
828
+ const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
829
+ console.log(`[MoltsPay] Transaction signed, sending to server...`);
830
+ const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
831
+ const payload = {
832
+ x402Version: 2,
833
+ scheme: "exact",
834
+ network,
835
+ payload: {
836
+ signedTransaction: signedTx,
837
+ sender: solanaWallet.publicKey.toBase58(),
838
+ chain
839
+ },
840
+ accepted: {
841
+ scheme: "exact",
842
+ network,
843
+ asset: requirements.asset,
844
+ amount: requirements.amount,
845
+ payTo: requirements.payTo,
846
+ maxTimeoutSeconds: 300
847
+ }
848
+ };
849
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
850
+ const paidRes = await fetch(`${serverUrl}/execute`, {
851
+ method: "POST",
852
+ headers: {
853
+ "Content-Type": "application/json",
854
+ "X-Payment": paymentHeader
855
+ },
856
+ body: JSON.stringify({ service, params, chain })
857
+ });
858
+ const result = await paidRes.json();
859
+ if (!paidRes.ok) {
860
+ throw new Error(result.error || "Solana payment failed");
861
+ }
862
+ this.recordSpending(amountUSDC);
863
+ console.log(`[MoltsPay] Success! Solana payment settled.`);
864
+ if (result.payment?.transaction) {
865
+ const explorerUrl = chain === "solana" ? `https://solscan.io/tx/${result.payment.transaction}` : `https://solscan.io/tx/${result.payment.transaction}?cluster=devnet`;
866
+ console.log(`[MoltsPay] Transaction: ${explorerUrl}`);
867
+ }
868
+ return result.result || result;
869
+ }
870
+ /**
871
+ * Check ERC20 allowance for a spender
872
+ */
873
+ async checkAllowance(tokenAddress, spender, provider) {
874
+ const contract = new ethers.Contract(
875
+ tokenAddress,
876
+ ["function allowance(address owner, address spender) view returns (uint256)"],
877
+ provider
878
+ );
879
+ return await contract.allowance(this.wallet.address, spender);
880
+ }
334
881
  /**
335
882
  * Sign EIP-3009 transferWithAuthorization (GASLESS)
336
883
  * This only signs - no on-chain transaction, no gas needed.
@@ -401,26 +948,26 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
401
948
  }
402
949
  // --- Config & Wallet Management ---
403
950
  loadConfig() {
404
- const configPath = join(this.configDir, "config.json");
405
- if (existsSync(configPath)) {
406
- const content = readFileSync(configPath, "utf-8");
951
+ const configPath = join2(this.configDir, "config.json");
952
+ if (existsSync2(configPath)) {
953
+ const content = readFileSync2(configPath, "utf-8");
407
954
  return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
408
955
  }
409
956
  return { ...DEFAULT_CONFIG };
410
957
  }
411
958
  saveConfig() {
412
- mkdirSync(this.configDir, { recursive: true });
413
- const configPath = join(this.configDir, "config.json");
414
- writeFileSync(configPath, JSON.stringify(this.config, null, 2));
959
+ mkdirSync2(this.configDir, { recursive: true });
960
+ const configPath = join2(this.configDir, "config.json");
961
+ writeFileSync2(configPath, JSON.stringify(this.config, null, 2));
415
962
  }
416
963
  /**
417
964
  * Load spending data from disk
418
965
  */
419
966
  loadSpending() {
420
- const spendingPath = join(this.configDir, "spending.json");
421
- if (existsSync(spendingPath)) {
967
+ const spendingPath = join2(this.configDir, "spending.json");
968
+ if (existsSync2(spendingPath)) {
422
969
  try {
423
- const data = JSON.parse(readFileSync(spendingPath, "utf-8"));
970
+ const data = JSON.parse(readFileSync2(spendingPath, "utf-8"));
424
971
  const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
425
972
  if (data.date && data.date === today) {
426
973
  this.todaySpending = data.amount || 0;
@@ -439,18 +986,18 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
439
986
  * Save spending data to disk
440
987
  */
441
988
  saveSpending() {
442
- mkdirSync(this.configDir, { recursive: true });
443
- const spendingPath = join(this.configDir, "spending.json");
989
+ mkdirSync2(this.configDir, { recursive: true });
990
+ const spendingPath = join2(this.configDir, "spending.json");
444
991
  const data = {
445
992
  date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
446
993
  amount: this.todaySpending,
447
994
  updatedAt: Date.now()
448
995
  };
449
- writeFileSync(spendingPath, JSON.stringify(data, null, 2));
996
+ writeFileSync2(spendingPath, JSON.stringify(data, null, 2));
450
997
  }
451
998
  loadWallet() {
452
- const walletPath = join(this.configDir, "wallet.json");
453
- if (existsSync(walletPath)) {
999
+ const walletPath = join2(this.configDir, "wallet.json");
1000
+ if (existsSync2(walletPath)) {
454
1001
  try {
455
1002
  const stats = statSync(walletPath);
456
1003
  const mode = stats.mode & 511;
@@ -461,7 +1008,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
461
1008
  }
462
1009
  } catch (err) {
463
1010
  }
464
- const content = readFileSync(walletPath, "utf-8");
1011
+ const content = readFileSync2(walletPath, "utf-8");
465
1012
  return JSON.parse(content);
466
1013
  }
467
1014
  return null;
@@ -470,15 +1017,15 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
470
1017
  * Initialize a new wallet (called by CLI)
471
1018
  */
472
1019
  static init(configDir, options) {
473
- mkdirSync(configDir, { recursive: true });
1020
+ mkdirSync2(configDir, { recursive: true });
474
1021
  const wallet = Wallet.createRandom();
475
1022
  const walletData = {
476
1023
  address: wallet.address,
477
1024
  privateKey: wallet.privateKey,
478
1025
  createdAt: Date.now()
479
1026
  };
480
- const walletPath = join(configDir, "wallet.json");
481
- writeFileSync(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
1027
+ const walletPath = join2(configDir, "wallet.json");
1028
+ writeFileSync2(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
482
1029
  const config = {
483
1030
  chain: options.chain,
484
1031
  limits: {
@@ -486,8 +1033,8 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
486
1033
  maxPerDay: options.maxPerDay
487
1034
  }
488
1035
  };
489
- const configPath = join(configDir, "config.json");
490
- writeFileSync(configPath, JSON.stringify(config, null, 2));
1036
+ const configPath = join2(configDir, "config.json");
1037
+ writeFileSync2(configPath, JSON.stringify(config, null, 2));
491
1038
  return { address: wallet.address, configDir };
492
1039
  }
493
1040
  /**
@@ -517,30 +1064,59 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
517
1064
  };
518
1065
  }
519
1066
  /**
520
- * Get wallet balances on all supported chains (Base + Polygon)
1067
+ * Get wallet balances on all supported chains (Base + Polygon + Tempo)
521
1068
  */
522
1069
  async getAllBalances() {
523
1070
  if (!this.wallet) {
524
1071
  throw new Error("Client not initialized");
525
1072
  }
526
- const supportedChains = ["base", "polygon", "base_sepolia"];
1073
+ const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
527
1074
  const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
528
1075
  const results = {};
1076
+ const tempoTokens = {
1077
+ pathUSD: "0x20c0000000000000000000000000000000000000",
1078
+ alphaUSD: "0x20c0000000000000000000000000000000000001",
1079
+ betaUSD: "0x20c0000000000000000000000000000000000002",
1080
+ thetaUSD: "0x20c0000000000000000000000000000000000003"
1081
+ };
529
1082
  await Promise.all(
530
1083
  supportedChains.map(async (chainName) => {
531
1084
  try {
532
1085
  const chain = getChain(chainName);
533
1086
  const provider = new ethers.JsonRpcProvider(chain.rpc);
534
- const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
535
- provider.getBalance(this.wallet.address),
536
- new ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
537
- new ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
538
- ]);
539
- results[chainName] = {
540
- usdc: parseFloat(ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
541
- usdt: parseFloat(ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
542
- native: parseFloat(ethers.formatEther(nativeBalance))
543
- };
1087
+ if (chainName === "tempo_moderato") {
1088
+ const [nativeBalance, pathUSD, alphaUSD, betaUSD, thetaUSD] = await Promise.all([
1089
+ provider.getBalance(this.wallet.address),
1090
+ new ethers.Contract(tempoTokens.pathUSD, tokenAbi, provider).balanceOf(this.wallet.address),
1091
+ new ethers.Contract(tempoTokens.alphaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
1092
+ new ethers.Contract(tempoTokens.betaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
1093
+ new ethers.Contract(tempoTokens.thetaUSD, tokenAbi, provider).balanceOf(this.wallet.address)
1094
+ ]);
1095
+ results[chainName] = {
1096
+ usdc: parseFloat(ethers.formatUnits(pathUSD, 6)),
1097
+ // pathUSD as default USDC
1098
+ usdt: parseFloat(ethers.formatUnits(alphaUSD, 6)),
1099
+ // alphaUSD as default USDT
1100
+ native: parseFloat(ethers.formatEther(nativeBalance)),
1101
+ tempo: {
1102
+ pathUSD: parseFloat(ethers.formatUnits(pathUSD, 6)),
1103
+ alphaUSD: parseFloat(ethers.formatUnits(alphaUSD, 6)),
1104
+ betaUSD: parseFloat(ethers.formatUnits(betaUSD, 6)),
1105
+ thetaUSD: parseFloat(ethers.formatUnits(thetaUSD, 6))
1106
+ }
1107
+ };
1108
+ } else {
1109
+ const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
1110
+ provider.getBalance(this.wallet.address),
1111
+ new ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
1112
+ new ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
1113
+ ]);
1114
+ results[chainName] = {
1115
+ usdc: parseFloat(ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
1116
+ usdt: parseFloat(ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
1117
+ native: parseFloat(ethers.formatEther(nativeBalance))
1118
+ };
1119
+ }
544
1120
  } catch (err) {
545
1121
  results[chainName] = { usdc: 0, usdt: 0, native: 0 };
546
1122
  }
@@ -548,6 +1124,121 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
548
1124
  );
549
1125
  return results;
550
1126
  }
1127
+ /**
1128
+ * Pay for a service using MPP (Machine Payments Protocol)
1129
+ *
1130
+ * This implements the MPP flow manually for EOA wallets:
1131
+ * 1. Request service → get 402 with WWW-Authenticate
1132
+ * 2. Parse payment requirements
1133
+ * 3. Execute transfer on Tempo chain
1134
+ * 4. Retry with transaction hash as credential
1135
+ *
1136
+ * @param url - Full URL of the MPP-enabled endpoint
1137
+ * @param options - Request options (body, headers)
1138
+ * @returns Response from the service
1139
+ */
1140
+ async payWithMPP(url, options = {}) {
1141
+ if (!this.wallet || !this.walletData) {
1142
+ throw new Error("Client not initialized. Run: npx moltspay init");
1143
+ }
1144
+ const { privateKeyToAccount } = await import("viem/accounts");
1145
+ const { createWalletClient, createPublicClient, http } = await import("viem");
1146
+ const { tempoModerato } = await import("viem/chains");
1147
+ const { Actions } = await import("viem/tempo");
1148
+ const privateKey = this.walletData.privateKey;
1149
+ const account = privateKeyToAccount(privateKey);
1150
+ console.log(`[MoltsPay] Making MPP request to: ${url}`);
1151
+ console.log(`[MoltsPay] Using account: ${account.address}`);
1152
+ const initResponse = await fetch(url, {
1153
+ method: "POST",
1154
+ headers: {
1155
+ "Content-Type": "application/json",
1156
+ ...options.headers
1157
+ },
1158
+ body: options.body ? JSON.stringify(options.body) : void 0
1159
+ });
1160
+ if (initResponse.status !== 402) {
1161
+ if (initResponse.ok) {
1162
+ return initResponse.json();
1163
+ }
1164
+ const errorText = await initResponse.text();
1165
+ throw new Error(`Request failed (${initResponse.status}): ${errorText}`);
1166
+ }
1167
+ const wwwAuth = initResponse.headers.get("www-authenticate");
1168
+ if (!wwwAuth || !wwwAuth.toLowerCase().includes("payment")) {
1169
+ throw new Error("No WWW-Authenticate Payment challenge in 402 response");
1170
+ }
1171
+ console.log(`[MoltsPay] Got 402, parsing payment challenge...`);
1172
+ const parseAuthParam = (header, key) => {
1173
+ const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
1174
+ return match ? match[1] : null;
1175
+ };
1176
+ const challengeId = parseAuthParam(wwwAuth, "id");
1177
+ const method = parseAuthParam(wwwAuth, "method");
1178
+ const realm = parseAuthParam(wwwAuth, "realm");
1179
+ const requestB64 = parseAuthParam(wwwAuth, "request");
1180
+ if (method !== "tempo") {
1181
+ throw new Error(`Unsupported payment method: ${method}`);
1182
+ }
1183
+ if (!requestB64) {
1184
+ throw new Error("Missing request in WWW-Authenticate");
1185
+ }
1186
+ const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
1187
+ const paymentRequest = JSON.parse(requestJson);
1188
+ console.log(`[MoltsPay] Payment request:`, paymentRequest);
1189
+ const { amount, currency, recipient, methodDetails } = paymentRequest;
1190
+ const chainId = methodDetails?.chainId || 42431;
1191
+ console.log(`[MoltsPay] Executing transfer on Tempo (chainId: ${chainId})...`);
1192
+ console.log(`[MoltsPay] Amount: ${amount}, To: ${recipient}`);
1193
+ const tempoChain = { ...tempoModerato, feeToken: currency };
1194
+ const publicClient = createPublicClient({
1195
+ chain: tempoChain,
1196
+ transport: http("https://rpc.moderato.tempo.xyz")
1197
+ });
1198
+ const walletClient = createWalletClient({
1199
+ account,
1200
+ chain: tempoChain,
1201
+ transport: http("https://rpc.moderato.tempo.xyz")
1202
+ });
1203
+ const txHash = await Actions.token.transfer(walletClient, {
1204
+ to: recipient,
1205
+ amount: BigInt(amount),
1206
+ token: currency
1207
+ });
1208
+ console.log(`[MoltsPay] Transaction sent: ${txHash}`);
1209
+ console.log(`[MoltsPay] Waiting for confirmation...`);
1210
+ await publicClient.waitForTransactionReceipt({ hash: txHash });
1211
+ console.log(`[MoltsPay] Transaction confirmed!`);
1212
+ const challenge = {
1213
+ id: challengeId,
1214
+ realm,
1215
+ method: "tempo",
1216
+ intent: "charge",
1217
+ request: paymentRequest
1218
+ };
1219
+ const credential = {
1220
+ challenge,
1221
+ payload: { hash: txHash, type: "hash" },
1222
+ source: `did:pkh:eip155:${chainId}:${account.address}`
1223
+ };
1224
+ const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1225
+ console.log(`[MoltsPay] Retrying with payment credential...`);
1226
+ const paidResponse = await fetch(url, {
1227
+ method: "POST",
1228
+ headers: {
1229
+ "Content-Type": "application/json",
1230
+ "Authorization": `Payment ${credentialB64}`,
1231
+ ...options.headers
1232
+ },
1233
+ body: options.body ? JSON.stringify(options.body) : void 0
1234
+ });
1235
+ if (!paidResponse.ok) {
1236
+ const errorText = await paidResponse.text();
1237
+ throw new Error(`Payment verification failed (${paidResponse.status}): ${errorText}`);
1238
+ }
1239
+ console.log(`[MoltsPay] Payment verified! Service completed.`);
1240
+ return paidResponse.json();
1241
+ }
551
1242
  };
552
1243
  export {
553
1244
  MoltsPayClient