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
package/dist/cli/index.js CHANGED
@@ -35,17 +35,19 @@ var init_cjs_shims = __esm({
35
35
 
36
36
  // src/cli/index.ts
37
37
  init_cjs_shims();
38
+ var import_crypto = require("crypto");
38
39
  var import_commander = require("commander");
39
- var import_os2 = require("os");
40
- var import_path2 = require("path");
41
- var import_fs4 = require("fs");
40
+ var import_os3 = require("os");
41
+ var import_path3 = require("path");
42
+ var import_fs5 = require("fs");
42
43
  var import_child_process = require("child_process");
44
+ var import_ethers2 = require("ethers");
43
45
 
44
46
  // src/client/index.ts
45
47
  init_cjs_shims();
46
- var import_fs = require("fs");
47
- var import_os = require("os");
48
- var import_path = require("path");
48
+ var import_fs2 = require("fs");
49
+ var import_os2 = require("os");
50
+ var import_path2 = require("path");
49
51
  var import_ethers = require("ethers");
50
52
 
51
53
  // src/chains/index.ts
@@ -127,6 +129,92 @@ var CHAINS = {
127
129
  explorer: "https://sepolia.basescan.org/address/",
128
130
  explorerTx: "https://sepolia.basescan.org/tx/",
129
131
  avgBlockTime: 2
132
+ },
133
+ // ============ Tempo Testnet (Moderato) ============
134
+ tempo_moderato: {
135
+ name: "Tempo Moderato",
136
+ chainId: 42431,
137
+ rpc: "https://rpc.moderato.tempo.xyz",
138
+ tokens: {
139
+ // TIP-20 stablecoins on Tempo testnet (from mppx SDK)
140
+ // Note: Tempo uses USD as native gas token, not ETH
141
+ USDC: {
142
+ address: "0x20c0000000000000000000000000000000000000",
143
+ // pathUSD - primary testnet stablecoin
144
+ decimals: 6,
145
+ symbol: "USDC",
146
+ eip712Name: "pathUSD"
147
+ },
148
+ USDT: {
149
+ address: "0x20c0000000000000000000000000000000000001",
150
+ // alphaUSD
151
+ decimals: 6,
152
+ symbol: "USDT",
153
+ eip712Name: "alphaUSD"
154
+ }
155
+ },
156
+ usdc: "0x20c0000000000000000000000000000000000000",
157
+ explorer: "https://explore.testnet.tempo.xyz/address/",
158
+ explorerTx: "https://explore.testnet.tempo.xyz/tx/",
159
+ avgBlockTime: 0.5
160
+ // ~500ms finality
161
+ },
162
+ // ============ BNB Chain Testnet ============
163
+ bnb_testnet: {
164
+ name: "BNB Testnet",
165
+ chainId: 97,
166
+ rpc: "https://data-seed-prebsc-1-s1.binance.org:8545",
167
+ tokens: {
168
+ // Note: BNB uses 18 decimals for stablecoins (unlike Base/Polygon which use 6)
169
+ // Using official Binance-Peg testnet tokens
170
+ USDC: {
171
+ address: "0x64544969ed7EBf5f083679233325356EbE738930",
172
+ // Testnet USDC
173
+ decimals: 18,
174
+ symbol: "USDC",
175
+ eip712Name: "USD Coin"
176
+ },
177
+ USDT: {
178
+ address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
179
+ // Testnet USDT
180
+ decimals: 18,
181
+ symbol: "USDT",
182
+ eip712Name: "Tether USD"
183
+ }
184
+ },
185
+ usdc: "0x64544969ed7EBf5f083679233325356EbE738930",
186
+ explorer: "https://testnet.bscscan.com/address/",
187
+ explorerTx: "https://testnet.bscscan.com/tx/",
188
+ avgBlockTime: 3,
189
+ // BNB-specific: requires approval for pay-for-success flow
190
+ requiresApproval: true
191
+ },
192
+ // ============ BNB Chain Mainnet ============
193
+ bnb: {
194
+ name: "BNB Smart Chain",
195
+ chainId: 56,
196
+ rpc: "https://bsc-dataseed.binance.org",
197
+ tokens: {
198
+ // Note: BNB uses 18 decimals for stablecoins
199
+ USDC: {
200
+ address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
201
+ decimals: 18,
202
+ symbol: "USDC",
203
+ eip712Name: "USD Coin"
204
+ },
205
+ USDT: {
206
+ address: "0x55d398326f99059fF775485246999027B3197955",
207
+ decimals: 18,
208
+ symbol: "USDT",
209
+ eip712Name: "Tether USD"
210
+ }
211
+ },
212
+ usdc: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
213
+ explorer: "https://bscscan.com/address/",
214
+ explorerTx: "https://bscscan.com/tx/",
215
+ avgBlockTime: 3,
216
+ // BNB-specific: requires approval for pay-for-success flow
217
+ requiresApproval: true
130
218
  }
131
219
  };
132
220
  function getChain(name) {
@@ -137,6 +225,362 @@ function getChain(name) {
137
225
  return config;
138
226
  }
139
227
 
228
+ // src/wallet/solana.ts
229
+ init_cjs_shims();
230
+ var import_web32 = require("@solana/web3.js");
231
+ var import_spl_token = require("@solana/spl-token");
232
+ var import_fs = require("fs");
233
+ var import_path = require("path");
234
+ var import_os = require("os");
235
+ var import_bs58 = __toESM(require("bs58"));
236
+
237
+ // src/chains/solana.ts
238
+ init_cjs_shims();
239
+ var import_web3 = require("@solana/web3.js");
240
+ var SOLANA_CHAINS = {
241
+ solana: {
242
+ name: "Solana Mainnet",
243
+ cluster: "mainnet-beta",
244
+ rpc: "https://api.mainnet-beta.solana.com",
245
+ explorer: "https://solscan.io/account/",
246
+ explorerTx: "https://solscan.io/tx/",
247
+ tokens: {
248
+ USDC: {
249
+ mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
250
+ // Circle official USDC
251
+ decimals: 6
252
+ }
253
+ }
254
+ },
255
+ solana_devnet: {
256
+ name: "Solana Devnet",
257
+ cluster: "devnet",
258
+ rpc: "https://api.devnet.solana.com",
259
+ explorer: "https://solscan.io/account/",
260
+ explorerTx: "https://solscan.io/tx/",
261
+ tokens: {
262
+ USDC: {
263
+ // Circle's devnet USDC (if not available, we'll deploy our own test token)
264
+ mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
265
+ decimals: 6
266
+ }
267
+ }
268
+ }
269
+ };
270
+ function getSolanaConnection(chain) {
271
+ const config = SOLANA_CHAINS[chain];
272
+ return new import_web3.Connection(config.rpc, "confirmed");
273
+ }
274
+ function getUSDCMint(chain) {
275
+ return new import_web3.PublicKey(SOLANA_CHAINS[chain].tokens.USDC.mint);
276
+ }
277
+
278
+ // src/wallet/solana.ts
279
+ var DEFAULT_CONFIG_DIR = (0, import_path.join)((0, import_os.homedir)(), ".moltspay");
280
+ var SOLANA_WALLET_FILE = "wallet-solana.json";
281
+ function getSolanaWalletPath(configDir = DEFAULT_CONFIG_DIR) {
282
+ return (0, import_path.join)(configDir, SOLANA_WALLET_FILE);
283
+ }
284
+ function solanaWalletExists(configDir = DEFAULT_CONFIG_DIR) {
285
+ return (0, import_fs.existsSync)(getSolanaWalletPath(configDir));
286
+ }
287
+ function loadSolanaWallet(configDir = DEFAULT_CONFIG_DIR) {
288
+ const walletPath = getSolanaWalletPath(configDir);
289
+ if (!(0, import_fs.existsSync)(walletPath)) {
290
+ return null;
291
+ }
292
+ try {
293
+ const data = JSON.parse((0, import_fs.readFileSync)(walletPath, "utf-8"));
294
+ const secretKey = import_bs58.default.decode(data.secretKey);
295
+ return import_web32.Keypair.fromSecretKey(secretKey);
296
+ } catch (error) {
297
+ console.error("Failed to load Solana wallet:", error);
298
+ return null;
299
+ }
300
+ }
301
+ function createSolanaWallet(configDir = DEFAULT_CONFIG_DIR) {
302
+ if (!(0, import_fs.existsSync)(configDir)) {
303
+ (0, import_fs.mkdirSync)(configDir, { recursive: true });
304
+ }
305
+ const keypair = import_web32.Keypair.generate();
306
+ const data = {
307
+ publicKey: keypair.publicKey.toBase58(),
308
+ secretKey: import_bs58.default.encode(keypair.secretKey),
309
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
310
+ };
311
+ const walletPath = getSolanaWalletPath(configDir);
312
+ (0, import_fs.writeFileSync)(walletPath, JSON.stringify(data, null, 2));
313
+ return keypair;
314
+ }
315
+ function getSolanaAddress(configDir = DEFAULT_CONFIG_DIR) {
316
+ const wallet = loadSolanaWallet(configDir);
317
+ return wallet?.publicKey.toBase58() || null;
318
+ }
319
+ async function getSolanaBalance(address, chain) {
320
+ const connection = getSolanaConnection(chain);
321
+ const pubkey = new import_web32.PublicKey(address);
322
+ const balance = await connection.getBalance(pubkey);
323
+ return balance / import_web32.LAMPORTS_PER_SOL;
324
+ }
325
+ async function getSolanaUSDCBalance(address, chain) {
326
+ const connection = getSolanaConnection(chain);
327
+ const owner = new import_web32.PublicKey(address);
328
+ const mint = getUSDCMint(chain);
329
+ try {
330
+ const ata = await (0, import_spl_token.getAssociatedTokenAddress)(mint, owner);
331
+ const account = await (0, import_spl_token.getAccount)(connection, ata);
332
+ return Number(account.amount) / 1e6;
333
+ } catch (error) {
334
+ if (error.name === "TokenAccountNotFoundError" || error.message?.includes("could not find account")) {
335
+ return 0;
336
+ }
337
+ throw error;
338
+ }
339
+ }
340
+ async function getSolanaBalances(address, chain) {
341
+ const [sol, usdc] = await Promise.all([
342
+ getSolanaBalance(address, chain),
343
+ getSolanaUSDCBalance(address, chain)
344
+ ]);
345
+ return { sol, usdc };
346
+ }
347
+ function isValidSolanaAddress(address) {
348
+ try {
349
+ new import_web32.PublicKey(address);
350
+ return true;
351
+ } catch {
352
+ return false;
353
+ }
354
+ }
355
+
356
+ // src/facilitators/solana.ts
357
+ init_cjs_shims();
358
+ var import_web33 = require("@solana/web3.js");
359
+ var import_spl_token2 = require("@solana/spl-token");
360
+
361
+ // src/facilitators/interface.ts
362
+ init_cjs_shims();
363
+ var BaseFacilitator = class {
364
+ supportsNetwork(network) {
365
+ return this.supportedNetworks.includes(network);
366
+ }
367
+ };
368
+
369
+ // src/facilitators/solana.ts
370
+ var SolanaFacilitator = class extends BaseFacilitator {
371
+ name = "solana";
372
+ displayName = "Solana Direct";
373
+ supportedNetworks = ["solana:mainnet", "solana:devnet"];
374
+ connections = /* @__PURE__ */ new Map();
375
+ feePayerKeypair;
376
+ constructor(config) {
377
+ super();
378
+ this.feePayerKeypair = config?.feePayerKeypair;
379
+ for (const [chain, config2] of Object.entries(SOLANA_CHAINS)) {
380
+ this.connections.set(
381
+ chain,
382
+ new import_web33.Connection(config2.rpc, "confirmed")
383
+ );
384
+ }
385
+ if (this.feePayerKeypair) {
386
+ console.log(`[SolanaFacilitator] Gasless mode enabled. Fee payer: ${this.feePayerKeypair.publicKey.toBase58()}`);
387
+ }
388
+ }
389
+ /**
390
+ * Get fee payer public key (for gasless transactions)
391
+ */
392
+ getFeePayerPubkey() {
393
+ return this.feePayerKeypair?.publicKey.toBase58() || null;
394
+ }
395
+ getConnection(chain) {
396
+ const conn = this.connections.get(chain);
397
+ if (!conn) {
398
+ throw new Error(`No connection for chain: ${chain}`);
399
+ }
400
+ return conn;
401
+ }
402
+ /**
403
+ * Convert our chain name to network identifier
404
+ */
405
+ static chainToNetwork(chain) {
406
+ return chain === "solana" ? "solana:mainnet" : "solana:devnet";
407
+ }
408
+ /**
409
+ * Convert network identifier to chain name
410
+ */
411
+ static networkToChain(network) {
412
+ if (network === "solana:mainnet") return "solana";
413
+ if (network === "solana:devnet") return "solana_devnet";
414
+ return null;
415
+ }
416
+ async healthCheck() {
417
+ const start = Date.now();
418
+ try {
419
+ const conn = this.getConnection("solana_devnet");
420
+ await conn.getSlot();
421
+ return {
422
+ healthy: true,
423
+ latencyMs: Date.now() - start
424
+ };
425
+ } catch (error) {
426
+ return {
427
+ healthy: false,
428
+ error: error.message
429
+ };
430
+ }
431
+ }
432
+ /**
433
+ * Verify a Solana payment
434
+ *
435
+ * Checks:
436
+ * 1. Transaction is valid and properly signed
437
+ * 2. Transfer instruction matches expected amount and recipient
438
+ */
439
+ async verify(paymentPayload, requirements) {
440
+ try {
441
+ const solanaPayload = paymentPayload.payload;
442
+ if (!solanaPayload || !solanaPayload.signedTransaction) {
443
+ return { valid: false, error: "Missing signed transaction" };
444
+ }
445
+ const chain = solanaPayload.chain || "solana_devnet";
446
+ const chainConfig = SOLANA_CHAINS[chain];
447
+ if (!chainConfig) {
448
+ return { valid: false, error: `Invalid chain: ${chain}` };
449
+ }
450
+ const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
451
+ let tx;
452
+ try {
453
+ tx = import_web33.Transaction.from(txBuffer);
454
+ } catch {
455
+ tx = import_web33.VersionedTransaction.deserialize(txBuffer);
456
+ }
457
+ if (tx instanceof import_web33.Transaction) {
458
+ const hasAnySignature = tx.signatures.some(
459
+ (sig) => sig.signature && !sig.signature.every((b) => b === 0)
460
+ );
461
+ if (!hasAnySignature) {
462
+ return { valid: false, error: "Transaction not signed" };
463
+ }
464
+ }
465
+ const expectedAmount = BigInt(requirements.amount);
466
+ const expectedRecipient = new import_web33.PublicKey(requirements.payTo);
467
+ return {
468
+ valid: true,
469
+ details: {
470
+ chain,
471
+ sender: solanaPayload.sender,
472
+ recipient: requirements.payTo,
473
+ amount: requirements.amount
474
+ }
475
+ };
476
+ } catch (error) {
477
+ return { valid: false, error: error.message };
478
+ }
479
+ }
480
+ /**
481
+ * Settle a Solana payment
482
+ *
483
+ * Submits the signed transaction to the network.
484
+ * In gasless mode, adds fee payer signature before submitting.
485
+ */
486
+ async settle(paymentPayload, requirements) {
487
+ try {
488
+ const solanaPayload = paymentPayload.payload;
489
+ if (!solanaPayload || !solanaPayload.signedTransaction) {
490
+ return { success: false, error: "Missing signed transaction" };
491
+ }
492
+ const chain = solanaPayload.chain || "solana_devnet";
493
+ const connection = this.getConnection(chain);
494
+ const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
495
+ let txToSend;
496
+ try {
497
+ const tx = import_web33.Transaction.from(txBuffer);
498
+ if (this.feePayerKeypair && tx.feePayer) {
499
+ const feePayerPubkey = this.feePayerKeypair.publicKey.toBase58();
500
+ const txFeePayer = tx.feePayer.toBase58();
501
+ if (txFeePayer === feePayerPubkey) {
502
+ console.log(`[SolanaFacilitator] Gasless mode: adding fee payer signature`);
503
+ tx.partialSign(this.feePayerKeypair);
504
+ }
505
+ }
506
+ txToSend = tx.serialize();
507
+ } catch (e) {
508
+ txToSend = txBuffer;
509
+ }
510
+ const signature = await connection.sendRawTransaction(txToSend, {
511
+ skipPreflight: false,
512
+ preflightCommitment: "confirmed"
513
+ });
514
+ const confirmation = await connection.confirmTransaction(signature, "confirmed");
515
+ if (confirmation.value.err) {
516
+ return {
517
+ success: false,
518
+ error: `Transaction failed: ${JSON.stringify(confirmation.value.err)}`,
519
+ transaction: signature
520
+ };
521
+ }
522
+ return {
523
+ success: true,
524
+ transaction: signature,
525
+ status: "confirmed"
526
+ };
527
+ } catch (error) {
528
+ return { success: false, error: error.message };
529
+ }
530
+ }
531
+ supportsNetwork(network) {
532
+ return this.supportedNetworks.includes(network);
533
+ }
534
+ };
535
+ async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey) {
536
+ const chainConfig = SOLANA_CHAINS[chain];
537
+ const connection = new import_web33.Connection(chainConfig.rpc, "confirmed");
538
+ const mint = new import_web33.PublicKey(chainConfig.tokens.USDC.mint);
539
+ const actualFeePayer = feePayerPubkey || senderPubkey;
540
+ const senderATA = await (0, import_spl_token2.getAssociatedTokenAddress)(mint, senderPubkey);
541
+ const recipientATA = await (0, import_spl_token2.getAssociatedTokenAddress)(mint, recipientPubkey);
542
+ const transaction = new import_web33.Transaction();
543
+ try {
544
+ await (0, import_spl_token2.getAccount)(connection, recipientATA);
545
+ } catch {
546
+ transaction.add(
547
+ (0, import_spl_token2.createAssociatedTokenAccountInstruction)(
548
+ actualFeePayer,
549
+ // payer (fee payer in gasless mode)
550
+ recipientATA,
551
+ // ata to create
552
+ recipientPubkey,
553
+ // owner
554
+ mint
555
+ // mint
556
+ )
557
+ );
558
+ }
559
+ transaction.add(
560
+ (0, import_spl_token2.createTransferCheckedInstruction)(
561
+ senderATA,
562
+ // source
563
+ mint,
564
+ // mint
565
+ recipientATA,
566
+ // destination
567
+ senderPubkey,
568
+ // owner (sender still authorizes the transfer)
569
+ amount,
570
+ // amount
571
+ chainConfig.tokens.USDC.decimals
572
+ // decimals
573
+ )
574
+ );
575
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
576
+ transaction.recentBlockhash = blockhash;
577
+ transaction.feePayer = actualFeePayer;
578
+ return transaction;
579
+ }
580
+
581
+ // src/client/index.ts
582
+ var import_web34 = require("@solana/web3.js");
583
+
140
584
  // src/client/types.ts
141
585
  init_cjs_shims();
142
586
 
@@ -159,7 +603,7 @@ var MoltsPayClient = class {
159
603
  todaySpending = 0;
160
604
  lastSpendingReset = 0;
161
605
  constructor(options = {}) {
162
- this.configDir = options.configDir || (0, import_path.join)((0, import_os.homedir)(), ".moltspay");
606
+ this.configDir = options.configDir || (0, import_path2.join)((0, import_os2.homedir)(), ".moltspay");
163
607
  this.config = this.loadConfig();
164
608
  this.walletData = this.loadWallet();
165
609
  this.loadSpending();
@@ -179,6 +623,12 @@ var MoltsPayClient = class {
179
623
  get address() {
180
624
  return this.wallet?.address || null;
181
625
  }
626
+ /**
627
+ * Get wallet instance (for direct operations like approvals)
628
+ */
629
+ getWallet() {
630
+ return this.wallet;
631
+ }
182
632
  /**
183
633
  * Get current config
184
634
  */
@@ -248,9 +698,14 @@ var MoltsPayClient = class {
248
698
  }
249
699
  throw new Error(data.error || "Unexpected response");
250
700
  }
701
+ const wwwAuthHeader = initialRes.headers.get("www-authenticate");
251
702
  const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER);
703
+ if (wwwAuthHeader && wwwAuthHeader.toLowerCase().includes("payment")) {
704
+ console.log("[MoltsPay] Detected MPP protocol, using Tempo flow...");
705
+ return await this.handleMPPPayment(serverUrl, service, params, wwwAuthHeader);
706
+ }
252
707
  if (!paymentRequiredHeader) {
253
- throw new Error("Missing x-payment-required header");
708
+ throw new Error("Missing payment header (x-payment-required or www-authenticate)");
254
709
  }
255
710
  let requirements;
256
711
  try {
@@ -267,17 +722,22 @@ var MoltsPayClient = class {
267
722
  throw new Error("Invalid x-payment-required header");
268
723
  }
269
724
  const networkToChainName = (network2) => {
725
+ if (network2 === "solana:mainnet") return "solana";
726
+ if (network2 === "solana:devnet") return "solana_devnet";
270
727
  const match = network2.match(/^eip155:(\d+)$/);
271
728
  if (!match) return null;
272
729
  const chainId = parseInt(match[1]);
273
730
  if (chainId === 8453) return "base";
274
731
  if (chainId === 137) return "polygon";
275
732
  if (chainId === 84532) return "base_sepolia";
733
+ if (chainId === 42431) return "tempo_moderato";
734
+ if (chainId === 56) return "bnb";
735
+ if (chainId === 97) return "bnb_testnet";
276
736
  return null;
277
737
  };
278
738
  const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
279
- let chainName;
280
739
  const userSpecifiedChain = options.chain;
740
+ let selectedChain;
281
741
  if (userSpecifiedChain) {
282
742
  if (!serverChains.includes(userSpecifiedChain)) {
283
743
  throw new Error(
@@ -285,17 +745,27 @@ var MoltsPayClient = class {
285
745
  Server accepts: ${serverChains.join(", ")}`
286
746
  );
287
747
  }
288
- chainName = userSpecifiedChain;
748
+ selectedChain = userSpecifiedChain;
289
749
  } else {
290
750
  if (serverChains.length === 1 && serverChains[0] === "base") {
291
- chainName = "base";
751
+ selectedChain = "base";
292
752
  } else {
293
753
  throw new Error(
294
754
  `Server accepts: ${serverChains.join(", ")}
295
- Please specify: --chain base, --chain polygon, or --chain base_sepolia`
755
+ Please specify: --chain <chain_name>`
296
756
  );
297
757
  }
298
758
  }
759
+ if (selectedChain === "solana" || selectedChain === "solana_devnet") {
760
+ const solanaChain = selectedChain;
761
+ const network2 = solanaChain === "solana" ? "solana:mainnet" : "solana:devnet";
762
+ const req2 = requirements.find((r) => r.network === network2);
763
+ if (!req2) {
764
+ throw new Error(`Failed to find payment requirement for ${selectedChain}`);
765
+ }
766
+ return await this.handleSolanaPayment(serverUrl, service, params, req2, solanaChain);
767
+ }
768
+ const chainName = selectedChain;
299
769
  const chain = getChain(chainName);
300
770
  const network = `eip155:${chain.chainId}`;
301
771
  const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
@@ -330,6 +800,25 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
330
800
  } else {
331
801
  console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
332
802
  }
803
+ if (chainName === "bnb" || chainName === "bnb_testnet") {
804
+ console.log(`[MoltsPay] Using BNB intent-based payment flow...`);
805
+ const payTo2 = req.payTo || req.resource;
806
+ if (!payTo2) {
807
+ throw new Error("Missing payTo address in payment requirements");
808
+ }
809
+ const bnbSpender = req.extra?.bnbSpender;
810
+ if (!bnbSpender) {
811
+ throw new Error("Server did not provide bnbSpender address. Server may not support BNB payments.");
812
+ }
813
+ return await this.handleBNBPayment(serverUrl, service, params, {
814
+ to: payTo2,
815
+ amount,
816
+ token,
817
+ chainName,
818
+ chain,
819
+ spender: bnbSpender
820
+ });
821
+ }
333
822
  const payTo = req.payTo || req.resource;
334
823
  if (!payTo) {
335
824
  throw new Error("Missing payTo address in payment requirements");
@@ -379,6 +868,300 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
379
868
  console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
380
869
  return result.result;
381
870
  }
871
+ /**
872
+ * Handle MPP (Machine Payments Protocol) payment flow
873
+ * Called when pay() detects WWW-Authenticate header in 402 response
874
+ */
875
+ async handleMPPPayment(serverUrl, service, params, wwwAuthHeader) {
876
+ const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
877
+ const { createWalletClient, createPublicClient, http } = await import("viem");
878
+ const { tempoModerato } = await import("viem/chains");
879
+ const { Actions } = await import("viem/tempo");
880
+ const privateKey = this.walletData.privateKey;
881
+ const account = privateKeyToAccount2(privateKey);
882
+ console.log(`[MoltsPay] Using MPP protocol on Tempo`);
883
+ console.log(`[MoltsPay] Account: ${account.address}`);
884
+ const parseAuthParam = (header, key) => {
885
+ const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
886
+ return match ? match[1] : null;
887
+ };
888
+ const challengeId = parseAuthParam(wwwAuthHeader, "id");
889
+ const method = parseAuthParam(wwwAuthHeader, "method");
890
+ const realm = parseAuthParam(wwwAuthHeader, "realm");
891
+ const requestB64 = parseAuthParam(wwwAuthHeader, "request");
892
+ if (method !== "tempo") {
893
+ throw new Error(`Unsupported payment method: ${method}`);
894
+ }
895
+ if (!requestB64) {
896
+ throw new Error("Missing request in WWW-Authenticate");
897
+ }
898
+ const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
899
+ const paymentRequest = JSON.parse(requestJson);
900
+ const { amount, currency, recipient, methodDetails } = paymentRequest;
901
+ const chainId = methodDetails?.chainId || 42431;
902
+ const amountDisplay = Number(amount) / 1e6;
903
+ console.log(`[MoltsPay] Payment: $${amountDisplay} to ${recipient}`);
904
+ this.checkLimits(amountDisplay);
905
+ console.log(`[MoltsPay] Sending transaction on Tempo...`);
906
+ const tempoChain = { ...tempoModerato, feeToken: currency };
907
+ const publicClient = createPublicClient({
908
+ chain: tempoChain,
909
+ transport: http("https://rpc.moderato.tempo.xyz")
910
+ });
911
+ const walletClient = createWalletClient({
912
+ account,
913
+ chain: tempoChain,
914
+ transport: http("https://rpc.moderato.tempo.xyz")
915
+ });
916
+ const txHash = await Actions.token.transfer(walletClient, {
917
+ to: recipient,
918
+ amount: BigInt(amount),
919
+ token: currency
920
+ });
921
+ console.log(`[MoltsPay] Transaction: ${txHash}`);
922
+ await publicClient.waitForTransactionReceipt({ hash: txHash });
923
+ console.log(`[MoltsPay] Confirmed! Retrying with credential...`);
924
+ const credential = {
925
+ challenge: {
926
+ id: challengeId,
927
+ realm,
928
+ method: "tempo",
929
+ intent: "charge",
930
+ request: paymentRequest
931
+ },
932
+ payload: { hash: txHash, type: "hash" },
933
+ source: `did:pkh:eip155:${chainId}:${account.address}`
934
+ };
935
+ const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
936
+ const paidRes = await fetch(`${serverUrl}/execute`, {
937
+ method: "POST",
938
+ headers: {
939
+ "Content-Type": "application/json",
940
+ "Authorization": `Payment ${credentialB64}`
941
+ },
942
+ body: JSON.stringify({ service, params, chain: "tempo_moderato" })
943
+ });
944
+ const result = await paidRes.json();
945
+ if (!paidRes.ok) {
946
+ throw new Error(result.error || "Payment verification failed");
947
+ }
948
+ this.recordSpending(amountDisplay);
949
+ console.log(`[MoltsPay] Success!`);
950
+ return result.result || result;
951
+ }
952
+ /**
953
+ * Handle BNB Chain payment flow (pre-approval + intent signature)
954
+ *
955
+ * Flow:
956
+ * 1. Check client has approved server wallet (done via `moltspay init`)
957
+ * 2. Sign EIP-712 payment intent (no gas, just signature)
958
+ * 3. Send intent to server
959
+ * 4. Server executes service
960
+ * 5. Server calls transferFrom if successful (pay-for-success)
961
+ */
962
+ async handleBNBPayment(serverUrl, service, params, paymentDetails) {
963
+ const { to, amount, token, chainName, chain, spender } = paymentDetails;
964
+ const tokenConfig = chain.tokens[token];
965
+ const provider = new import_ethers.ethers.JsonRpcProvider(chain.rpc);
966
+ const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
967
+ const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
968
+ if (allowance < amountWeiCheck) {
969
+ const nativeBalance = await provider.getBalance(this.wallet.address);
970
+ const minGasBalance = import_ethers.ethers.parseEther("0.0005");
971
+ if (nativeBalance < minGasBalance) {
972
+ const nativeBNB = parseFloat(import_ethers.ethers.formatEther(nativeBalance)).toFixed(4);
973
+ const isTestnet = chainName === "bnb_testnet";
974
+ if (isTestnet) {
975
+ throw new Error(
976
+ `\u274C Insufficient tBNB for approval transaction
977
+
978
+ Current tBNB: ${nativeBNB}
979
+ Required: ~0.001 tBNB
980
+
981
+ Get testnet tokens: npx moltspay faucet --chain bnb_testnet
982
+ (Gives USDC + tBNB for gas)`
983
+ );
984
+ } else {
985
+ throw new Error(
986
+ `\u274C Insufficient BNB for approval transaction
987
+
988
+ Current BNB: ${nativeBNB}
989
+ Required: ~0.001 BNB (~$0.60)
990
+
991
+ To get BNB:
992
+ \u2022 Withdraw from Binance/exchange to your wallet
993
+ \u2022 Most exchanges include BNB dust with withdrawals
994
+
995
+ After funding, run:
996
+ npx moltspay approve --chain ${chainName} --spender ${spender}`
997
+ );
998
+ }
999
+ }
1000
+ throw new Error(
1001
+ `Insufficient allowance for ${spender.slice(0, 10)}...
1002
+ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
1003
+ );
1004
+ }
1005
+ const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
1006
+ const intent = {
1007
+ from: this.wallet.address,
1008
+ to,
1009
+ amount: amountWei,
1010
+ token: tokenConfig.address,
1011
+ service,
1012
+ nonce: Date.now(),
1013
+ // Use timestamp as nonce for simplicity
1014
+ deadline: Date.now() + 36e5
1015
+ // 1 hour
1016
+ };
1017
+ const domain = {
1018
+ name: "MoltsPay",
1019
+ version: "1",
1020
+ chainId: chain.chainId
1021
+ };
1022
+ const types = {
1023
+ PaymentIntent: [
1024
+ { name: "from", type: "address" },
1025
+ { name: "to", type: "address" },
1026
+ { name: "amount", type: "uint256" },
1027
+ { name: "token", type: "address" },
1028
+ { name: "service", type: "string" },
1029
+ { name: "nonce", type: "uint256" },
1030
+ { name: "deadline", type: "uint256" }
1031
+ ]
1032
+ };
1033
+ console.log(`[MoltsPay] Signing BNB payment intent...`);
1034
+ const signature = await this.wallet.signTypedData(domain, types, intent);
1035
+ const network = `eip155:${chain.chainId}`;
1036
+ const payload = {
1037
+ x402Version: 2,
1038
+ scheme: "exact",
1039
+ network,
1040
+ payload: {
1041
+ intent: {
1042
+ ...intent,
1043
+ signature
1044
+ },
1045
+ chainId: chain.chainId
1046
+ },
1047
+ accepted: {
1048
+ scheme: "exact",
1049
+ network,
1050
+ asset: tokenConfig.address,
1051
+ amount: amountWei,
1052
+ payTo: to,
1053
+ maxTimeoutSeconds: 300
1054
+ }
1055
+ };
1056
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
1057
+ console.log(`[MoltsPay] Sending BNB payment request...`);
1058
+ const paidRes = await fetch(`${serverUrl}/execute`, {
1059
+ method: "POST",
1060
+ headers: {
1061
+ "Content-Type": "application/json",
1062
+ "X-Payment": paymentHeader
1063
+ },
1064
+ body: JSON.stringify({ service, params, chain: chainName })
1065
+ });
1066
+ const result = await paidRes.json();
1067
+ if (!paidRes.ok) {
1068
+ throw new Error(result.error || "BNB payment failed");
1069
+ }
1070
+ this.recordSpending(amount);
1071
+ console.log(`[MoltsPay] Success! BNB payment settled.`);
1072
+ return result.result || result;
1073
+ }
1074
+ /**
1075
+ * Handle Solana payment flow
1076
+ *
1077
+ * Solana uses SPL token transfers with pay-for-success model:
1078
+ * 1. Client creates and signs a transfer transaction
1079
+ * 2. Server submits the transaction after service completes
1080
+ */
1081
+ async handleSolanaPayment(serverUrl, service, params, requirements, chain) {
1082
+ const solanaWallet = loadSolanaWallet(this.configDir);
1083
+ if (!solanaWallet) {
1084
+ throw new Error("No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
1085
+ }
1086
+ const amount = Number(requirements.amount);
1087
+ const amountUSDC = amount / 1e6;
1088
+ this.checkLimits(amountUSDC);
1089
+ console.log(`[MoltsPay] Creating Solana payment: $${amountUSDC} USDC`);
1090
+ if (!requirements.payTo) {
1091
+ throw new Error("Missing payTo address in payment requirements");
1092
+ }
1093
+ const solanaFeePayer = requirements.extra?.solanaFeePayer;
1094
+ const feePayerPubkey = solanaFeePayer ? new import_web34.PublicKey(solanaFeePayer) : void 0;
1095
+ if (feePayerPubkey) {
1096
+ console.log(`[MoltsPay] Gasless mode: server pays fees`);
1097
+ }
1098
+ const recipientPubkey = new import_web34.PublicKey(requirements.payTo);
1099
+ const transaction = await createSolanaPaymentTransaction(
1100
+ solanaWallet.publicKey,
1101
+ recipientPubkey,
1102
+ BigInt(amount),
1103
+ chain,
1104
+ feePayerPubkey
1105
+ // Optional fee payer for gasless mode
1106
+ );
1107
+ if (feePayerPubkey) {
1108
+ transaction.partialSign(solanaWallet);
1109
+ } else {
1110
+ transaction.sign(solanaWallet);
1111
+ }
1112
+ const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
1113
+ console.log(`[MoltsPay] Transaction signed, sending to server...`);
1114
+ const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
1115
+ const payload = {
1116
+ x402Version: 2,
1117
+ scheme: "exact",
1118
+ network,
1119
+ payload: {
1120
+ signedTransaction: signedTx,
1121
+ sender: solanaWallet.publicKey.toBase58(),
1122
+ chain
1123
+ },
1124
+ accepted: {
1125
+ scheme: "exact",
1126
+ network,
1127
+ asset: requirements.asset,
1128
+ amount: requirements.amount,
1129
+ payTo: requirements.payTo,
1130
+ maxTimeoutSeconds: 300
1131
+ }
1132
+ };
1133
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
1134
+ const paidRes = await fetch(`${serverUrl}/execute`, {
1135
+ method: "POST",
1136
+ headers: {
1137
+ "Content-Type": "application/json",
1138
+ "X-Payment": paymentHeader
1139
+ },
1140
+ body: JSON.stringify({ service, params, chain })
1141
+ });
1142
+ const result = await paidRes.json();
1143
+ if (!paidRes.ok) {
1144
+ throw new Error(result.error || "Solana payment failed");
1145
+ }
1146
+ this.recordSpending(amountUSDC);
1147
+ console.log(`[MoltsPay] Success! Solana payment settled.`);
1148
+ if (result.payment?.transaction) {
1149
+ const explorerUrl = chain === "solana" ? `https://solscan.io/tx/${result.payment.transaction}` : `https://solscan.io/tx/${result.payment.transaction}?cluster=devnet`;
1150
+ console.log(`[MoltsPay] Transaction: ${explorerUrl}`);
1151
+ }
1152
+ return result.result || result;
1153
+ }
1154
+ /**
1155
+ * Check ERC20 allowance for a spender
1156
+ */
1157
+ async checkAllowance(tokenAddress, spender, provider) {
1158
+ const contract = new import_ethers.ethers.Contract(
1159
+ tokenAddress,
1160
+ ["function allowance(address owner, address spender) view returns (uint256)"],
1161
+ provider
1162
+ );
1163
+ return await contract.allowance(this.wallet.address, spender);
1164
+ }
382
1165
  /**
383
1166
  * Sign EIP-3009 transferWithAuthorization (GASLESS)
384
1167
  * This only signs - no on-chain transaction, no gas needed.
@@ -449,26 +1232,26 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
449
1232
  }
450
1233
  // --- Config & Wallet Management ---
451
1234
  loadConfig() {
452
- const configPath = (0, import_path.join)(this.configDir, "config.json");
453
- if ((0, import_fs.existsSync)(configPath)) {
454
- const content = (0, import_fs.readFileSync)(configPath, "utf-8");
1235
+ const configPath = (0, import_path2.join)(this.configDir, "config.json");
1236
+ if ((0, import_fs2.existsSync)(configPath)) {
1237
+ const content = (0, import_fs2.readFileSync)(configPath, "utf-8");
455
1238
  return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
456
1239
  }
457
1240
  return { ...DEFAULT_CONFIG };
458
1241
  }
459
1242
  saveConfig() {
460
- (0, import_fs.mkdirSync)(this.configDir, { recursive: true });
461
- const configPath = (0, import_path.join)(this.configDir, "config.json");
462
- (0, import_fs.writeFileSync)(configPath, JSON.stringify(this.config, null, 2));
1243
+ (0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
1244
+ const configPath = (0, import_path2.join)(this.configDir, "config.json");
1245
+ (0, import_fs2.writeFileSync)(configPath, JSON.stringify(this.config, null, 2));
463
1246
  }
464
1247
  /**
465
1248
  * Load spending data from disk
466
1249
  */
467
1250
  loadSpending() {
468
- const spendingPath = (0, import_path.join)(this.configDir, "spending.json");
469
- if ((0, import_fs.existsSync)(spendingPath)) {
1251
+ const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
1252
+ if ((0, import_fs2.existsSync)(spendingPath)) {
470
1253
  try {
471
- const data = JSON.parse((0, import_fs.readFileSync)(spendingPath, "utf-8"));
1254
+ const data = JSON.parse((0, import_fs2.readFileSync)(spendingPath, "utf-8"));
472
1255
  const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
473
1256
  if (data.date && data.date === today) {
474
1257
  this.todaySpending = data.amount || 0;
@@ -487,29 +1270,29 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
487
1270
  * Save spending data to disk
488
1271
  */
489
1272
  saveSpending() {
490
- (0, import_fs.mkdirSync)(this.configDir, { recursive: true });
491
- const spendingPath = (0, import_path.join)(this.configDir, "spending.json");
1273
+ (0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
1274
+ const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
492
1275
  const data = {
493
1276
  date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
494
1277
  amount: this.todaySpending,
495
1278
  updatedAt: Date.now()
496
1279
  };
497
- (0, import_fs.writeFileSync)(spendingPath, JSON.stringify(data, null, 2));
1280
+ (0, import_fs2.writeFileSync)(spendingPath, JSON.stringify(data, null, 2));
498
1281
  }
499
1282
  loadWallet() {
500
- const walletPath = (0, import_path.join)(this.configDir, "wallet.json");
501
- if ((0, import_fs.existsSync)(walletPath)) {
1283
+ const walletPath = (0, import_path2.join)(this.configDir, "wallet.json");
1284
+ if ((0, import_fs2.existsSync)(walletPath)) {
502
1285
  try {
503
- const stats = (0, import_fs.statSync)(walletPath);
1286
+ const stats = (0, import_fs2.statSync)(walletPath);
504
1287
  const mode = stats.mode & 511;
505
1288
  if (mode !== 384) {
506
1289
  console.warn(`[MoltsPay] WARNING: wallet.json has insecure permissions (${mode.toString(8)})`);
507
1290
  console.warn(`[MoltsPay] Fixing permissions to 0600...`);
508
- (0, import_fs.chmodSync)(walletPath, 384);
1291
+ (0, import_fs2.chmodSync)(walletPath, 384);
509
1292
  }
510
1293
  } catch (err) {
511
1294
  }
512
- const content = (0, import_fs.readFileSync)(walletPath, "utf-8");
1295
+ const content = (0, import_fs2.readFileSync)(walletPath, "utf-8");
513
1296
  return JSON.parse(content);
514
1297
  }
515
1298
  return null;
@@ -518,15 +1301,15 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
518
1301
  * Initialize a new wallet (called by CLI)
519
1302
  */
520
1303
  static init(configDir, options) {
521
- (0, import_fs.mkdirSync)(configDir, { recursive: true });
1304
+ (0, import_fs2.mkdirSync)(configDir, { recursive: true });
522
1305
  const wallet = import_ethers.Wallet.createRandom();
523
1306
  const walletData = {
524
1307
  address: wallet.address,
525
1308
  privateKey: wallet.privateKey,
526
1309
  createdAt: Date.now()
527
1310
  };
528
- const walletPath = (0, import_path.join)(configDir, "wallet.json");
529
- (0, import_fs.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
1311
+ const walletPath = (0, import_path2.join)(configDir, "wallet.json");
1312
+ (0, import_fs2.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
530
1313
  const config = {
531
1314
  chain: options.chain,
532
1315
  limits: {
@@ -534,8 +1317,8 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
534
1317
  maxPerDay: options.maxPerDay
535
1318
  }
536
1319
  };
537
- const configPath = (0, import_path.join)(configDir, "config.json");
538
- (0, import_fs.writeFileSync)(configPath, JSON.stringify(config, null, 2));
1320
+ const configPath = (0, import_path2.join)(configDir, "config.json");
1321
+ (0, import_fs2.writeFileSync)(configPath, JSON.stringify(config, null, 2));
539
1322
  return { address: wallet.address, configDir };
540
1323
  }
541
1324
  /**
@@ -565,30 +1348,59 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
565
1348
  };
566
1349
  }
567
1350
  /**
568
- * Get wallet balances on all supported chains (Base + Polygon)
1351
+ * Get wallet balances on all supported chains (Base + Polygon + Tempo)
569
1352
  */
570
1353
  async getAllBalances() {
571
1354
  if (!this.wallet) {
572
1355
  throw new Error("Client not initialized");
573
1356
  }
574
- const supportedChains = ["base", "polygon", "base_sepolia"];
1357
+ const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
575
1358
  const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
576
1359
  const results = {};
1360
+ const tempoTokens = {
1361
+ pathUSD: "0x20c0000000000000000000000000000000000000",
1362
+ alphaUSD: "0x20c0000000000000000000000000000000000001",
1363
+ betaUSD: "0x20c0000000000000000000000000000000000002",
1364
+ thetaUSD: "0x20c0000000000000000000000000000000000003"
1365
+ };
577
1366
  await Promise.all(
578
1367
  supportedChains.map(async (chainName) => {
579
1368
  try {
580
1369
  const chain = getChain(chainName);
581
1370
  const provider = new import_ethers.ethers.JsonRpcProvider(chain.rpc);
582
- const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
583
- provider.getBalance(this.wallet.address),
584
- new import_ethers.ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
585
- new import_ethers.ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
586
- ]);
587
- results[chainName] = {
588
- usdc: parseFloat(import_ethers.ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
589
- usdt: parseFloat(import_ethers.ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
590
- native: parseFloat(import_ethers.ethers.formatEther(nativeBalance))
591
- };
1371
+ if (chainName === "tempo_moderato") {
1372
+ const [nativeBalance, pathUSD, alphaUSD, betaUSD, thetaUSD] = await Promise.all([
1373
+ provider.getBalance(this.wallet.address),
1374
+ new import_ethers.ethers.Contract(tempoTokens.pathUSD, tokenAbi, provider).balanceOf(this.wallet.address),
1375
+ new import_ethers.ethers.Contract(tempoTokens.alphaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
1376
+ new import_ethers.ethers.Contract(tempoTokens.betaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
1377
+ new import_ethers.ethers.Contract(tempoTokens.thetaUSD, tokenAbi, provider).balanceOf(this.wallet.address)
1378
+ ]);
1379
+ results[chainName] = {
1380
+ usdc: parseFloat(import_ethers.ethers.formatUnits(pathUSD, 6)),
1381
+ // pathUSD as default USDC
1382
+ usdt: parseFloat(import_ethers.ethers.formatUnits(alphaUSD, 6)),
1383
+ // alphaUSD as default USDT
1384
+ native: parseFloat(import_ethers.ethers.formatEther(nativeBalance)),
1385
+ tempo: {
1386
+ pathUSD: parseFloat(import_ethers.ethers.formatUnits(pathUSD, 6)),
1387
+ alphaUSD: parseFloat(import_ethers.ethers.formatUnits(alphaUSD, 6)),
1388
+ betaUSD: parseFloat(import_ethers.ethers.formatUnits(betaUSD, 6)),
1389
+ thetaUSD: parseFloat(import_ethers.ethers.formatUnits(thetaUSD, 6))
1390
+ }
1391
+ };
1392
+ } else {
1393
+ const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
1394
+ provider.getBalance(this.wallet.address),
1395
+ new import_ethers.ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
1396
+ new import_ethers.ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
1397
+ ]);
1398
+ results[chainName] = {
1399
+ usdc: parseFloat(import_ethers.ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
1400
+ usdt: parseFloat(import_ethers.ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
1401
+ native: parseFloat(import_ethers.ethers.formatEther(nativeBalance))
1402
+ };
1403
+ }
592
1404
  } catch (err) {
593
1405
  results[chainName] = { usdc: 0, usdt: 0, native: 0 };
594
1406
  }
@@ -596,28 +1408,135 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
596
1408
  );
597
1409
  return results;
598
1410
  }
1411
+ /**
1412
+ * Pay for a service using MPP (Machine Payments Protocol)
1413
+ *
1414
+ * This implements the MPP flow manually for EOA wallets:
1415
+ * 1. Request service → get 402 with WWW-Authenticate
1416
+ * 2. Parse payment requirements
1417
+ * 3. Execute transfer on Tempo chain
1418
+ * 4. Retry with transaction hash as credential
1419
+ *
1420
+ * @param url - Full URL of the MPP-enabled endpoint
1421
+ * @param options - Request options (body, headers)
1422
+ * @returns Response from the service
1423
+ */
1424
+ async payWithMPP(url, options = {}) {
1425
+ if (!this.wallet || !this.walletData) {
1426
+ throw new Error("Client not initialized. Run: npx moltspay init");
1427
+ }
1428
+ const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
1429
+ const { createWalletClient, createPublicClient, http } = await import("viem");
1430
+ const { tempoModerato } = await import("viem/chains");
1431
+ const { Actions } = await import("viem/tempo");
1432
+ const privateKey = this.walletData.privateKey;
1433
+ const account = privateKeyToAccount2(privateKey);
1434
+ console.log(`[MoltsPay] Making MPP request to: ${url}`);
1435
+ console.log(`[MoltsPay] Using account: ${account.address}`);
1436
+ const initResponse = await fetch(url, {
1437
+ method: "POST",
1438
+ headers: {
1439
+ "Content-Type": "application/json",
1440
+ ...options.headers
1441
+ },
1442
+ body: options.body ? JSON.stringify(options.body) : void 0
1443
+ });
1444
+ if (initResponse.status !== 402) {
1445
+ if (initResponse.ok) {
1446
+ return initResponse.json();
1447
+ }
1448
+ const errorText = await initResponse.text();
1449
+ throw new Error(`Request failed (${initResponse.status}): ${errorText}`);
1450
+ }
1451
+ const wwwAuth = initResponse.headers.get("www-authenticate");
1452
+ if (!wwwAuth || !wwwAuth.toLowerCase().includes("payment")) {
1453
+ throw new Error("No WWW-Authenticate Payment challenge in 402 response");
1454
+ }
1455
+ console.log(`[MoltsPay] Got 402, parsing payment challenge...`);
1456
+ const parseAuthParam = (header, key) => {
1457
+ const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
1458
+ return match ? match[1] : null;
1459
+ };
1460
+ const challengeId = parseAuthParam(wwwAuth, "id");
1461
+ const method = parseAuthParam(wwwAuth, "method");
1462
+ const realm = parseAuthParam(wwwAuth, "realm");
1463
+ const requestB64 = parseAuthParam(wwwAuth, "request");
1464
+ if (method !== "tempo") {
1465
+ throw new Error(`Unsupported payment method: ${method}`);
1466
+ }
1467
+ if (!requestB64) {
1468
+ throw new Error("Missing request in WWW-Authenticate");
1469
+ }
1470
+ const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
1471
+ const paymentRequest = JSON.parse(requestJson);
1472
+ console.log(`[MoltsPay] Payment request:`, paymentRequest);
1473
+ const { amount, currency, recipient, methodDetails } = paymentRequest;
1474
+ const chainId = methodDetails?.chainId || 42431;
1475
+ console.log(`[MoltsPay] Executing transfer on Tempo (chainId: ${chainId})...`);
1476
+ console.log(`[MoltsPay] Amount: ${amount}, To: ${recipient}`);
1477
+ const tempoChain = { ...tempoModerato, feeToken: currency };
1478
+ const publicClient = createPublicClient({
1479
+ chain: tempoChain,
1480
+ transport: http("https://rpc.moderato.tempo.xyz")
1481
+ });
1482
+ const walletClient = createWalletClient({
1483
+ account,
1484
+ chain: tempoChain,
1485
+ transport: http("https://rpc.moderato.tempo.xyz")
1486
+ });
1487
+ const txHash = await Actions.token.transfer(walletClient, {
1488
+ to: recipient,
1489
+ amount: BigInt(amount),
1490
+ token: currency
1491
+ });
1492
+ console.log(`[MoltsPay] Transaction sent: ${txHash}`);
1493
+ console.log(`[MoltsPay] Waiting for confirmation...`);
1494
+ await publicClient.waitForTransactionReceipt({ hash: txHash });
1495
+ console.log(`[MoltsPay] Transaction confirmed!`);
1496
+ const challenge = {
1497
+ id: challengeId,
1498
+ realm,
1499
+ method: "tempo",
1500
+ intent: "charge",
1501
+ request: paymentRequest
1502
+ };
1503
+ const credential = {
1504
+ challenge,
1505
+ payload: { hash: txHash, type: "hash" },
1506
+ source: `did:pkh:eip155:${chainId}:${account.address}`
1507
+ };
1508
+ const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1509
+ console.log(`[MoltsPay] Retrying with payment credential...`);
1510
+ const paidResponse = await fetch(url, {
1511
+ method: "POST",
1512
+ headers: {
1513
+ "Content-Type": "application/json",
1514
+ "Authorization": `Payment ${credentialB64}`,
1515
+ ...options.headers
1516
+ },
1517
+ body: options.body ? JSON.stringify(options.body) : void 0
1518
+ });
1519
+ if (!paidResponse.ok) {
1520
+ const errorText = await paidResponse.text();
1521
+ throw new Error(`Payment verification failed (${paidResponse.status}): ${errorText}`);
1522
+ }
1523
+ console.log(`[MoltsPay] Payment verified! Service completed.`);
1524
+ return paidResponse.json();
1525
+ }
599
1526
  };
600
1527
 
601
1528
  // src/server/index.ts
602
1529
  init_cjs_shims();
603
- var import_fs3 = require("fs");
1530
+ var import_fs4 = require("fs");
604
1531
  var import_http = require("http");
605
1532
  var path2 = __toESM(require("path"));
606
1533
 
607
1534
  // src/facilitators/index.ts
608
1535
  init_cjs_shims();
609
1536
 
610
- // src/facilitators/interface.ts
611
- init_cjs_shims();
612
- var BaseFacilitator = class {
613
- supportsNetwork(network) {
614
- return this.supportedNetworks.includes(network);
615
- }
616
- };
617
-
618
1537
  // src/facilitators/cdp.ts
619
1538
  init_cjs_shims();
620
- var import_fs2 = require("fs");
1539
+ var import_fs3 = require("fs");
621
1540
  var path = __toESM(require("path"));
622
1541
  var X402_VERSION2 = 2;
623
1542
  var CDP_URL = "https://api.cdp.coinbase.com/platform/v2/x402";
@@ -628,9 +1547,9 @@ function loadEnvFile() {
628
1547
  path.join(process.env.HOME || "", ".moltspay", ".env")
629
1548
  ];
630
1549
  for (const envPath of envPaths) {
631
- if ((0, import_fs2.existsSync)(envPath)) {
1550
+ if ((0, import_fs3.existsSync)(envPath)) {
632
1551
  try {
633
- const content = (0, import_fs2.readFileSync)(envPath, "utf-8");
1552
+ const content = (0, import_fs3.readFileSync)(envPath, "utf-8");
634
1553
  for (const line of content.split("\n")) {
635
1554
  const trimmed = line.trim();
636
1555
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -820,23 +1739,462 @@ var CDPFacilitator = class extends BaseFacilitator {
820
1739
  };
821
1740
  }
822
1741
  /**
823
- * Check if a chain ID is testnet
1742
+ * Check if a chain ID is testnet
1743
+ */
1744
+ static isTestnet(chainId) {
1745
+ return TESTNET_CHAIN_IDS.includes(chainId);
1746
+ }
1747
+ /**
1748
+ * Get configuration summary (for logging)
1749
+ */
1750
+ getConfigSummary() {
1751
+ const hasCredentials = !!(this.apiKeyId && this.apiKeySecret);
1752
+ const networks = this.supportedNetworks.join(", ");
1753
+ return `CDP Facilitator (networks: ${networks}, credentials: ${hasCredentials ? "yes" : "no"})`;
1754
+ }
1755
+ };
1756
+
1757
+ // src/facilitators/tempo.ts
1758
+ init_cjs_shims();
1759
+ var TRANSFER_EVENT_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
1760
+ var TempoFacilitator = class extends BaseFacilitator {
1761
+ name = "tempo";
1762
+ displayName = "Tempo Testnet";
1763
+ supportedNetworks = ["eip155:42431"];
1764
+ // Tempo Moderato
1765
+ rpcUrl;
1766
+ constructor() {
1767
+ super();
1768
+ this.rpcUrl = CHAINS.tempo_moderato.rpc;
1769
+ }
1770
+ async healthCheck() {
1771
+ const start = Date.now();
1772
+ try {
1773
+ const response = await fetch(this.rpcUrl, {
1774
+ method: "POST",
1775
+ headers: { "Content-Type": "application/json" },
1776
+ body: JSON.stringify({
1777
+ jsonrpc: "2.0",
1778
+ method: "eth_chainId",
1779
+ params: [],
1780
+ id: 1
1781
+ })
1782
+ });
1783
+ const data = await response.json();
1784
+ const chainId = parseInt(data.result, 16);
1785
+ if (chainId !== 42431) {
1786
+ return { healthy: false, error: `Wrong chainId: ${chainId}` };
1787
+ }
1788
+ return { healthy: true, latencyMs: Date.now() - start };
1789
+ } catch (error) {
1790
+ return { healthy: false, error: String(error) };
1791
+ }
1792
+ }
1793
+ async verify(paymentPayload, requirements) {
1794
+ try {
1795
+ const tempoPayload = paymentPayload.payload;
1796
+ if (!tempoPayload?.txHash) {
1797
+ return { valid: false, error: "Missing txHash in payment payload" };
1798
+ }
1799
+ const receipt = await this.getTransactionReceipt(tempoPayload.txHash);
1800
+ if (!receipt) {
1801
+ return { valid: false, error: "Transaction not found" };
1802
+ }
1803
+ if (receipt.status !== "0x1") {
1804
+ return { valid: false, error: "Transaction failed" };
1805
+ }
1806
+ const transferLog = receipt.logs.find(
1807
+ (log) => log.topics[0] === TRANSFER_EVENT_TOPIC
1808
+ );
1809
+ if (!transferLog) {
1810
+ return { valid: false, error: "No Transfer event found" };
1811
+ }
1812
+ const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
1813
+ const expectedTo = requirements.payTo.toLowerCase();
1814
+ if (toAddress !== expectedTo) {
1815
+ return {
1816
+ valid: false,
1817
+ error: `Wrong recipient: ${toAddress}, expected ${expectedTo}`
1818
+ };
1819
+ }
1820
+ const amount = BigInt(transferLog.data);
1821
+ const expectedAmount = BigInt(requirements.amount);
1822
+ if (amount < expectedAmount) {
1823
+ return {
1824
+ valid: false,
1825
+ error: `Insufficient amount: ${amount}, expected ${expectedAmount}`
1826
+ };
1827
+ }
1828
+ const tokenAddress = transferLog.address.toLowerCase();
1829
+ const expectedToken = requirements.asset.toLowerCase();
1830
+ if (tokenAddress !== expectedToken) {
1831
+ return {
1832
+ valid: false,
1833
+ error: `Wrong token: ${tokenAddress}, expected ${expectedToken}`
1834
+ };
1835
+ }
1836
+ return {
1837
+ valid: true,
1838
+ details: {
1839
+ txHash: tempoPayload.txHash,
1840
+ from: "0x" + transferLog.topics[1].slice(26),
1841
+ to: toAddress,
1842
+ amount: amount.toString(),
1843
+ token: tokenAddress
1844
+ }
1845
+ };
1846
+ } catch (error) {
1847
+ return { valid: false, error: `Verification failed: ${error}` };
1848
+ }
1849
+ }
1850
+ async settle(paymentPayload, requirements) {
1851
+ const verifyResult = await this.verify(paymentPayload, requirements);
1852
+ if (!verifyResult.valid) {
1853
+ return { success: false, error: verifyResult.error };
1854
+ }
1855
+ const tempoPayload = paymentPayload.payload;
1856
+ return {
1857
+ success: true,
1858
+ transaction: tempoPayload.txHash,
1859
+ status: "settled"
1860
+ };
1861
+ }
1862
+ async getTransactionReceipt(txHash) {
1863
+ const response = await fetch(this.rpcUrl, {
1864
+ method: "POST",
1865
+ headers: { "Content-Type": "application/json" },
1866
+ body: JSON.stringify({
1867
+ jsonrpc: "2.0",
1868
+ method: "eth_getTransactionReceipt",
1869
+ params: [txHash],
1870
+ id: 1
1871
+ })
1872
+ });
1873
+ const data = await response.json();
1874
+ return data.result;
1875
+ }
1876
+ };
1877
+
1878
+ // src/facilitators/bnb.ts
1879
+ init_cjs_shims();
1880
+ var import_accounts = require("viem/accounts");
1881
+ var TRANSFER_EVENT_TOPIC2 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
1882
+ var EIP712_DOMAIN = {
1883
+ name: "MoltsPay",
1884
+ version: "1"
1885
+ };
1886
+ var INTENT_TYPES = {
1887
+ PaymentIntent: [
1888
+ { name: "from", type: "address" },
1889
+ { name: "to", type: "address" },
1890
+ { name: "amount", type: "uint256" },
1891
+ { name: "token", type: "address" },
1892
+ { name: "service", type: "string" },
1893
+ { name: "nonce", type: "uint256" },
1894
+ { name: "deadline", type: "uint256" }
1895
+ ]
1896
+ };
1897
+ var BNBFacilitator = class extends BaseFacilitator {
1898
+ name = "bnb";
1899
+ displayName = "BNB Smart Chain";
1900
+ supportedNetworks = ["eip155:56", "eip155:97"];
1901
+ // Mainnet + Testnet
1902
+ serverPrivateKey;
1903
+ spenderAddress = null;
1904
+ chainConfigs;
1905
+ constructor(serverPrivateKey) {
1906
+ super();
1907
+ this.serverPrivateKey = serverPrivateKey || process.env.BNB_SERVER_PRIVATE_KEY || "";
1908
+ if (this.serverPrivateKey) {
1909
+ const key = this.serverPrivateKey.startsWith("0x") ? this.serverPrivateKey : `0x${this.serverPrivateKey}`;
1910
+ const account = (0, import_accounts.privateKeyToAccount)(key);
1911
+ this.spenderAddress = account.address;
1912
+ }
1913
+ this.chainConfigs = {
1914
+ 56: { rpc: CHAINS.bnb.rpc, chain: CHAINS.bnb },
1915
+ 97: { rpc: CHAINS.bnb_testnet.rpc, chain: CHAINS.bnb_testnet }
1916
+ };
1917
+ }
1918
+ async healthCheck() {
1919
+ const start = Date.now();
1920
+ try {
1921
+ const response = await fetch(this.chainConfigs[56].rpc, {
1922
+ method: "POST",
1923
+ headers: { "Content-Type": "application/json" },
1924
+ body: JSON.stringify({
1925
+ jsonrpc: "2.0",
1926
+ method: "eth_chainId",
1927
+ params: [],
1928
+ id: 1
1929
+ })
1930
+ });
1931
+ const data = await response.json();
1932
+ const chainId = parseInt(data.result, 16);
1933
+ if (chainId !== 56) {
1934
+ return { healthy: false, error: `Wrong chainId: ${chainId}` };
1935
+ }
1936
+ return { healthy: true, latencyMs: Date.now() - start };
1937
+ } catch (error) {
1938
+ return { healthy: false, error: String(error) };
1939
+ }
1940
+ }
1941
+ /**
1942
+ * Verify a payment intent signature (before service execution)
1943
+ *
1944
+ * This verifies:
1945
+ * 1. Signature is valid for the intent
1946
+ * 2. Client has approved server wallet
1947
+ * 3. Client has sufficient balance
1948
+ * 4. Intent hasn't expired
1949
+ */
1950
+ async verify(paymentPayload, requirements) {
1951
+ try {
1952
+ const bnbPayload = paymentPayload.payload;
1953
+ if (!bnbPayload?.intent) {
1954
+ return { valid: false, error: "Missing intent in payment payload" };
1955
+ }
1956
+ const { intent, chainId } = bnbPayload;
1957
+ const config = this.chainConfigs[chainId];
1958
+ if (!config) {
1959
+ return { valid: false, error: `Unsupported chainId: ${chainId}` };
1960
+ }
1961
+ if (intent.deadline < Date.now()) {
1962
+ return { valid: false, error: "Intent expired" };
1963
+ }
1964
+ const recoveredAddress = await this.recoverIntentSigner(intent, chainId);
1965
+ if (recoveredAddress.toLowerCase() !== intent.from.toLowerCase()) {
1966
+ return { valid: false, error: "Invalid signature" };
1967
+ }
1968
+ if (intent.to.toLowerCase() !== requirements.payTo.toLowerCase()) {
1969
+ return { valid: false, error: `Wrong recipient: ${intent.to}` };
1970
+ }
1971
+ if (BigInt(intent.amount) < BigInt(requirements.amount)) {
1972
+ return { valid: false, error: `Insufficient amount: ${intent.amount}` };
1973
+ }
1974
+ if (intent.token.toLowerCase() !== requirements.asset.toLowerCase()) {
1975
+ return { valid: false, error: `Wrong token: ${intent.token}` };
1976
+ }
1977
+ const serverAddress = await this.getServerAddress();
1978
+ const allowance = await this.getAllowance(intent.from, serverAddress, intent.token, config.rpc);
1979
+ if (BigInt(allowance) < BigInt(intent.amount)) {
1980
+ return { valid: false, error: "Insufficient allowance. Run: npx moltspay init --chain bnb" };
1981
+ }
1982
+ const balance = await this.getBalance(intent.from, intent.token, config.rpc);
1983
+ if (BigInt(balance) < BigInt(intent.amount)) {
1984
+ return { valid: false, error: "Insufficient balance" };
1985
+ }
1986
+ return {
1987
+ valid: true,
1988
+ details: {
1989
+ from: intent.from,
1990
+ to: intent.to,
1991
+ amount: intent.amount,
1992
+ token: intent.token,
1993
+ service: intent.service,
1994
+ nonce: intent.nonce,
1995
+ deadline: intent.deadline
1996
+ }
1997
+ };
1998
+ } catch (error) {
1999
+ return { valid: false, error: `Verification failed: ${error}` };
2000
+ }
2001
+ }
2002
+ /**
2003
+ * Settle a payment by executing transferFrom
2004
+ *
2005
+ * This is called AFTER the service has been successfully delivered.
2006
+ * Server pays gas, transfers tokens from client to provider.
2007
+ */
2008
+ async settle(paymentPayload, requirements) {
2009
+ if (!this.serverPrivateKey) {
2010
+ return { success: false, error: "Server wallet not configured (BNB_SERVER_PRIVATE_KEY)" };
2011
+ }
2012
+ try {
2013
+ const verifyResult = await this.verify(paymentPayload, requirements);
2014
+ if (!verifyResult.valid) {
2015
+ return { success: false, error: verifyResult.error };
2016
+ }
2017
+ const bnbPayload = paymentPayload.payload;
2018
+ const { intent, chainId } = bnbPayload;
2019
+ const config = this.chainConfigs[chainId];
2020
+ const txHash = await this.executeTransferFrom(
2021
+ intent.from,
2022
+ intent.to,
2023
+ intent.amount,
2024
+ intent.token,
2025
+ config.rpc
2026
+ );
2027
+ return {
2028
+ success: true,
2029
+ transaction: txHash,
2030
+ status: "settled"
2031
+ };
2032
+ } catch (error) {
2033
+ return { success: false, error: `Settlement failed: ${error}` };
2034
+ }
2035
+ }
2036
+ /**
2037
+ * Check if client has approved the server wallet
2038
+ */
2039
+ async checkApproval(clientAddress, token, chainId) {
2040
+ const config = this.chainConfigs[chainId];
2041
+ if (!config) {
2042
+ throw new Error(`Unsupported chainId: ${chainId}`);
2043
+ }
2044
+ const serverAddress = await this.getServerAddress();
2045
+ const allowance = await this.getAllowance(clientAddress, serverAddress, token, config.rpc);
2046
+ const minAllowance = BigInt("1000000000000000000000");
2047
+ return {
2048
+ approved: BigInt(allowance) >= minAllowance,
2049
+ allowance
2050
+ };
2051
+ }
2052
+ /**
2053
+ * Verify a completed transaction (for checking past payments)
2054
+ */
2055
+ async verifyTransaction(txHash, expected, chainId) {
2056
+ const config = this.chainConfigs[chainId];
2057
+ if (!config) {
2058
+ return { valid: false, error: `Unsupported chainId: ${chainId}` };
2059
+ }
2060
+ try {
2061
+ const receipt = await this.getTransactionReceipt(txHash, config.rpc);
2062
+ if (!receipt) {
2063
+ return { valid: false, error: "Transaction not found" };
2064
+ }
2065
+ if (receipt.status !== "0x1") {
2066
+ return { valid: false, error: "Transaction failed" };
2067
+ }
2068
+ const transferLog = receipt.logs.find(
2069
+ (log) => log.topics[0] === TRANSFER_EVENT_TOPIC2 && log.address.toLowerCase() === expected.token.toLowerCase()
2070
+ );
2071
+ if (!transferLog) {
2072
+ return { valid: false, error: "No Transfer event found" };
2073
+ }
2074
+ const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
2075
+ if (toAddress !== expected.to.toLowerCase()) {
2076
+ return { valid: false, error: `Wrong recipient: ${toAddress}` };
2077
+ }
2078
+ const amount = BigInt(transferLog.data);
2079
+ if (amount < BigInt(expected.amount)) {
2080
+ return { valid: false, error: `Insufficient amount: ${amount}` };
2081
+ }
2082
+ return {
2083
+ valid: true,
2084
+ details: {
2085
+ txHash,
2086
+ from: "0x" + transferLog.topics[1].slice(26),
2087
+ to: toAddress,
2088
+ amount: amount.toString(),
2089
+ token: transferLog.address
2090
+ }
2091
+ };
2092
+ } catch (error) {
2093
+ return { valid: false, error: `Verification failed: ${error}` };
2094
+ }
2095
+ }
2096
+ // ==================== Private Methods ====================
2097
+ /**
2098
+ * Get the server's spender address (public, for 402 responses)
2099
+ * Returns cached value computed at construction time.
824
2100
  */
825
- static isTestnet(chainId) {
826
- return TESTNET_CHAIN_IDS.includes(chainId);
2101
+ getSpenderAddress() {
2102
+ return this.spenderAddress;
827
2103
  }
828
- /**
829
- * Get configuration summary (for logging)
830
- */
831
- getConfigSummary() {
832
- const hasCredentials = !!(this.apiKeyId && this.apiKeySecret);
833
- const networks = this.supportedNetworks.join(", ");
834
- return `CDP Facilitator (networks: ${networks}, credentials: ${hasCredentials ? "yes" : "no"})`;
2104
+ async getServerAddress() {
2105
+ const { ethers: ethers3 } = await import("ethers");
2106
+ const wallet = new ethers3.Wallet(this.serverPrivateKey);
2107
+ return wallet.address;
2108
+ }
2109
+ async recoverIntentSigner(intent, chainId) {
2110
+ const { ethers: ethers3 } = await import("ethers");
2111
+ const domain = {
2112
+ ...EIP712_DOMAIN,
2113
+ chainId
2114
+ };
2115
+ const message = {
2116
+ from: intent.from,
2117
+ to: intent.to,
2118
+ amount: intent.amount,
2119
+ token: intent.token,
2120
+ service: intent.service,
2121
+ nonce: intent.nonce,
2122
+ deadline: intent.deadline
2123
+ };
2124
+ const recoveredAddress = ethers3.verifyTypedData(
2125
+ domain,
2126
+ INTENT_TYPES,
2127
+ message,
2128
+ intent.signature
2129
+ );
2130
+ return recoveredAddress;
2131
+ }
2132
+ async getAllowance(owner, spender, token, rpcUrl) {
2133
+ const selector = "0xdd62ed3e";
2134
+ const ownerPadded = owner.toLowerCase().replace("0x", "").padStart(64, "0");
2135
+ const spenderPadded = spender.toLowerCase().replace("0x", "").padStart(64, "0");
2136
+ const data = selector + ownerPadded + spenderPadded;
2137
+ const response = await fetch(rpcUrl, {
2138
+ method: "POST",
2139
+ headers: { "Content-Type": "application/json" },
2140
+ body: JSON.stringify({
2141
+ jsonrpc: "2.0",
2142
+ method: "eth_call",
2143
+ params: [{ to: token, data }, "latest"],
2144
+ id: 1
2145
+ })
2146
+ });
2147
+ const result = await response.json();
2148
+ return result.result || "0x0";
2149
+ }
2150
+ async getBalance(account, token, rpcUrl) {
2151
+ const selector = "0x70a08231";
2152
+ const accountPadded = account.toLowerCase().replace("0x", "").padStart(64, "0");
2153
+ const data = selector + accountPadded;
2154
+ const response = await fetch(rpcUrl, {
2155
+ method: "POST",
2156
+ headers: { "Content-Type": "application/json" },
2157
+ body: JSON.stringify({
2158
+ jsonrpc: "2.0",
2159
+ method: "eth_call",
2160
+ params: [{ to: token, data }, "latest"],
2161
+ id: 1
2162
+ })
2163
+ });
2164
+ const result = await response.json();
2165
+ return result.result || "0x0";
2166
+ }
2167
+ async executeTransferFrom(from, to, amount, token, rpcUrl) {
2168
+ const { ethers: ethers3 } = await import("ethers");
2169
+ const provider = new ethers3.JsonRpcProvider(rpcUrl);
2170
+ const wallet = new ethers3.Wallet(this.serverPrivateKey, provider);
2171
+ const tokenContract = new ethers3.Contract(token, [
2172
+ "function transferFrom(address from, address to, uint256 amount) returns (bool)"
2173
+ ], wallet);
2174
+ const tx = await tokenContract.transferFrom(from, to, amount);
2175
+ const receipt = await tx.wait();
2176
+ return receipt.hash;
2177
+ }
2178
+ async getTransactionReceipt(txHash, rpcUrl) {
2179
+ const response = await fetch(rpcUrl, {
2180
+ method: "POST",
2181
+ headers: { "Content-Type": "application/json" },
2182
+ body: JSON.stringify({
2183
+ jsonrpc: "2.0",
2184
+ method: "eth_getTransactionReceipt",
2185
+ params: [txHash],
2186
+ id: 1
2187
+ })
2188
+ });
2189
+ const data = await response.json();
2190
+ return data.result;
835
2191
  }
836
2192
  };
837
2193
 
838
2194
  // src/facilitators/registry.ts
839
2195
  init_cjs_shims();
2196
+ var import_web35 = require("@solana/web3.js");
2197
+ var import_bs582 = __toESM(require("bs58"));
840
2198
  var FacilitatorRegistry = class {
841
2199
  factories = /* @__PURE__ */ new Map();
842
2200
  instances = /* @__PURE__ */ new Map();
@@ -844,7 +2202,21 @@ var FacilitatorRegistry = class {
844
2202
  roundRobinIndex = 0;
845
2203
  constructor(selection) {
846
2204
  this.registerFactory("cdp", (config) => new CDPFacilitator(config));
847
- this.selection = selection || { primary: "cdp", strategy: "failover" };
2205
+ this.registerFactory("tempo", () => new TempoFacilitator());
2206
+ this.registerFactory("bnb", (config) => new BNBFacilitator(config?.serverPrivateKey));
2207
+ this.registerFactory("solana", (config) => {
2208
+ let feePayerKeypair;
2209
+ const feePayerKey = config?.feePayerPrivateKey || process.env.SOLANA_FEE_PAYER_KEY;
2210
+ if (feePayerKey) {
2211
+ try {
2212
+ feePayerKeypair = import_web35.Keypair.fromSecretKey(import_bs582.default.decode(feePayerKey));
2213
+ } catch (e) {
2214
+ console.warn(`[SolanaFacilitator] Invalid fee payer key: ${e.message}`);
2215
+ }
2216
+ }
2217
+ return new SolanaFacilitator({ feePayerKeypair });
2218
+ });
2219
+ this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
848
2220
  }
849
2221
  /**
850
2222
  * Register a new facilitator factory
@@ -1068,6 +2440,9 @@ var X402_VERSION3 = 2;
1068
2440
  var PAYMENT_REQUIRED_HEADER2 = "x-payment-required";
1069
2441
  var PAYMENT_HEADER2 = "x-payment";
1070
2442
  var PAYMENT_RESPONSE_HEADER = "x-payment-response";
2443
+ var MPP_AUTH_HEADER = "authorization";
2444
+ var MPP_WWW_AUTH_HEADER = "www-authenticate";
2445
+ var MPP_RECEIPT_HEADER = "payment-receipt";
1071
2446
  var TOKEN_ADDRESSES = {
1072
2447
  "eip155:8453": {
1073
2448
  USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
@@ -1081,13 +2456,47 @@ var TOKEN_ADDRESSES = {
1081
2456
  "eip155:137": {
1082
2457
  USDC: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
1083
2458
  USDT: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F"
2459
+ },
2460
+ "eip155:42431": {
2461
+ // Tempo Moderato testnet - TIP-20 stablecoins
2462
+ USDC: "0x20c0000000000000000000000000000000000000",
2463
+ // pathUSD
2464
+ USDT: "0x20c0000000000000000000000000000000000001"
2465
+ // alphaUSD
2466
+ },
2467
+ // BNB Smart Chain mainnet
2468
+ "eip155:56": {
2469
+ USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
2470
+ USDT: "0x55d398326f99059fF775485246999027B3197955"
2471
+ },
2472
+ // BNB Smart Chain testnet
2473
+ "eip155:97": {
2474
+ USDC: "0x64544969ed7EBf5f083679233325356EbE738930",
2475
+ USDT: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd"
2476
+ },
2477
+ // Solana networks use mint addresses (SPL tokens)
2478
+ "solana:mainnet": {
2479
+ USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
2480
+ // Circle USDC
2481
+ },
2482
+ "solana:devnet": {
2483
+ USDC: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"
2484
+ // Devnet USDC
1084
2485
  }
1085
2486
  };
1086
2487
  var CHAIN_TO_NETWORK = {
1087
2488
  "base": "eip155:8453",
1088
2489
  "base_sepolia": "eip155:84532",
1089
- "polygon": "eip155:137"
2490
+ "polygon": "eip155:137",
2491
+ "tempo_moderato": "eip155:42431",
2492
+ "bnb": "eip155:56",
2493
+ "bnb_testnet": "eip155:97",
2494
+ "solana": "solana:mainnet",
2495
+ "solana_devnet": "solana:devnet"
1090
2496
  };
2497
+ function isSolanaNetwork(network) {
2498
+ return network.startsWith("solana:");
2499
+ }
1091
2500
  var TOKEN_DOMAINS = {
1092
2501
  // Base mainnet
1093
2502
  "eip155:8453": {
@@ -1104,6 +2513,21 @@ var TOKEN_DOMAINS = {
1104
2513
  "eip155:137": {
1105
2514
  USDC: { name: "USD Coin", version: "2" },
1106
2515
  USDT: { name: "(PoS) Tether USD", version: "2" }
2516
+ },
2517
+ // Tempo Moderato testnet - TIP-20 stablecoins
2518
+ "eip155:42431": {
2519
+ USDC: { name: "pathUSD", version: "1" },
2520
+ USDT: { name: "alphaUSD", version: "1" }
2521
+ },
2522
+ // BNB Smart Chain mainnet
2523
+ "eip155:56": {
2524
+ USDC: { name: "USD Coin", version: "1" },
2525
+ USDT: { name: "Tether USD", version: "1" }
2526
+ },
2527
+ // BNB Smart Chain testnet
2528
+ "eip155:97": {
2529
+ USDC: { name: "USD Coin", version: "1" },
2530
+ USDT: { name: "Tether USD", version: "1" }
1107
2531
  }
1108
2532
  };
1109
2533
  function getTokenDomain(network, token) {
@@ -1119,9 +2543,9 @@ function loadEnvFile2() {
1119
2543
  path2.join(process.env.HOME || "", ".moltspay", ".env")
1120
2544
  ];
1121
2545
  for (const envPath of envPaths) {
1122
- if ((0, import_fs3.existsSync)(envPath)) {
2546
+ if ((0, import_fs4.existsSync)(envPath)) {
1123
2547
  try {
1124
- const content = (0, import_fs3.readFileSync)(envPath, "utf-8");
2548
+ const content = (0, import_fs4.readFileSync)(envPath, "utf-8");
1125
2549
  for (const line of content.split("\n")) {
1126
2550
  const trimmed = line.trim();
1127
2551
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -1152,7 +2576,7 @@ var MoltsPayServer = class {
1152
2576
  useMainnet;
1153
2577
  constructor(servicesPath, options = {}) {
1154
2578
  loadEnvFile2();
1155
- const content = (0, import_fs3.readFileSync)(servicesPath, "utf-8");
2579
+ const content = (0, import_fs4.readFileSync)(servicesPath, "utf-8");
1156
2580
  this.manifest = JSON.parse(content);
1157
2581
  this.options = {
1158
2582
  port: options.port || 3e3,
@@ -1161,9 +2585,11 @@ var MoltsPayServer = class {
1161
2585
  };
1162
2586
  this.useMainnet = process.env.USE_MAINNET?.toLowerCase() === "true";
1163
2587
  this.networkId = this.useMainnet ? "eip155:8453" : "eip155:84532";
2588
+ const defaultFallback = ["tempo", "bnb", "solana"];
2589
+ const envFallback = process.env.FACILITATOR_FALLBACK?.split(",").filter(Boolean);
1164
2590
  const facilitatorConfig = options.facilitators || {
1165
2591
  primary: process.env.FACILITATOR_PRIMARY || "cdp",
1166
- fallback: process.env.FACILITATOR_FALLBACK?.split(",").filter(Boolean),
2592
+ fallback: envFallback || defaultFallback,
1167
2593
  strategy: process.env.FACILITATOR_STRATEGY || "failover",
1168
2594
  config: {
1169
2595
  cdp: { useMainnet: this.useMainnet }
@@ -1202,12 +2628,20 @@ var MoltsPayServer = class {
1202
2628
  */
1203
2629
  getProviderChains() {
1204
2630
  const provider = this.manifest.provider;
2631
+ const getWalletForChain = (chainName, explicitWallet) => {
2632
+ if (explicitWallet) return explicitWallet;
2633
+ if ((chainName === "solana" || chainName === "solana_devnet") && provider.solana_wallet) {
2634
+ return provider.solana_wallet;
2635
+ }
2636
+ return provider.wallet;
2637
+ };
1205
2638
  if (provider.chains && provider.chains.length > 0) {
1206
2639
  return provider.chains.map((c) => {
1207
2640
  const chainName = typeof c === "string" ? c : c.chain;
2641
+ const explicitWallet = typeof c === "object" ? c.wallet : null;
1208
2642
  return {
1209
2643
  network: CHAIN_TO_NETWORK[chainName] || "eip155:8453",
1210
- wallet: (typeof c === "object" ? c.wallet : null) || provider.wallet,
2644
+ wallet: getWalletForChain(chainName, explicitWallet || void 0),
1211
2645
  tokens: (typeof c === "object" ? c.tokens : null) || ["USDC"]
1212
2646
  };
1213
2647
  });
@@ -1216,7 +2650,7 @@ var MoltsPayServer = class {
1216
2650
  const network = CHAIN_TO_NETWORK[chain] || this.networkId;
1217
2651
  return [{
1218
2652
  network,
1219
- wallet: provider.wallet,
2653
+ wallet: getWalletForChain(chain),
1220
2654
  tokens: ["USDC"]
1221
2655
  }];
1222
2656
  }
@@ -1257,8 +2691,8 @@ var MoltsPayServer = class {
1257
2691
  async handleRequest(req, res) {
1258
2692
  res.setHeader("Access-Control-Allow-Origin", "*");
1259
2693
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1260
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment");
1261
- res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response");
2694
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization");
2695
+ res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt");
1262
2696
  if (req.method === "OPTIONS") {
1263
2697
  res.writeHead(204);
1264
2698
  res.end();
@@ -1287,7 +2721,16 @@ var MoltsPayServer = class {
1287
2721
  }
1288
2722
  const body = await this.readBody(req);
1289
2723
  const paymentHeader = req.headers[PAYMENT_HEADER2];
1290
- return await this.handleProxy(body, paymentHeader, res);
2724
+ const authHeader = req.headers[MPP_AUTH_HEADER];
2725
+ return await this.handleProxy(body, paymentHeader, authHeader, res);
2726
+ }
2727
+ const servicePath = url.pathname.replace(/^\//, "");
2728
+ const skill = this.skills.get(servicePath);
2729
+ if (skill && (req.method === "POST" || req.method === "GET")) {
2730
+ const body = req.method === "POST" ? await this.readBody(req) : {};
2731
+ const authHeader = req.headers[MPP_AUTH_HEADER];
2732
+ const x402Header = req.headers[PAYMENT_HEADER2];
2733
+ return await this.handleMPPRequest(skill, body, authHeader, x402Header, res);
1291
2734
  }
1292
2735
  this.sendJson(res, 404, { error: "Not found" });
1293
2736
  } catch (err) {
@@ -1316,7 +2759,9 @@ var MoltsPayServer = class {
1316
2759
  name: this.manifest.provider.name,
1317
2760
  description: this.manifest.provider.description,
1318
2761
  wallet: this.manifest.provider.wallet,
1319
- chain: this.manifest.provider.chain || "base"
2762
+ chain: this.manifest.provider.chain || "base",
2763
+ solana_wallet: this.manifest.provider.solana_wallet,
2764
+ chains: this.manifest.provider.chains
1320
2765
  },
1321
2766
  services,
1322
2767
  endpoints: {
@@ -1429,6 +2874,21 @@ var MoltsPayServer = class {
1429
2874
  });
1430
2875
  }
1431
2876
  console.log(`[MoltsPay] Verified by ${verifyResult.facilitator}`);
2877
+ const isSolana = isSolanaNetwork(paymentNetwork);
2878
+ let settlement = null;
2879
+ if (isSolana) {
2880
+ console.log(`[MoltsPay] Solana detected - settling payment FIRST (blockhash expiry protection)`);
2881
+ try {
2882
+ settlement = await this.registry.settle(payment, requirements);
2883
+ console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
2884
+ } catch (err) {
2885
+ console.error("[MoltsPay] Solana settlement failed:", err.message);
2886
+ return this.sendJson(res, 402, {
2887
+ error: "Payment settlement failed",
2888
+ message: err.message
2889
+ });
2890
+ }
2891
+ }
1432
2892
  const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
1433
2893
  console.log(`[MoltsPay] Executing skill: ${service} (timeout: ${timeoutSeconds}s)`);
1434
2894
  let result;
@@ -1443,16 +2903,19 @@ var MoltsPayServer = class {
1443
2903
  console.error("[MoltsPay] Skill execution failed:", err.message);
1444
2904
  return this.sendJson(res, 500, {
1445
2905
  error: "Service execution failed",
1446
- message: err.message
2906
+ message: err.message,
2907
+ paymentSettled: isSolana ? true : false,
2908
+ note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
1447
2909
  });
1448
2910
  }
1449
- console.log(`[MoltsPay] Skill succeeded, settling payment...`);
1450
- let settlement = null;
1451
- try {
1452
- settlement = await this.registry.settle(payment, requirements);
1453
- console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
1454
- } catch (err) {
1455
- console.error("[MoltsPay] Settlement failed:", err.message);
2911
+ if (!isSolana) {
2912
+ console.log(`[MoltsPay] Skill succeeded, settling payment...`);
2913
+ try {
2914
+ settlement = await this.registry.settle(payment, requirements);
2915
+ console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
2916
+ } catch (err) {
2917
+ console.error("[MoltsPay] Settlement failed:", err.message);
2918
+ }
1456
2919
  }
1457
2920
  const responseHeaders = {};
1458
2921
  if (settlement?.success) {
@@ -1472,6 +2935,187 @@ var MoltsPayServer = class {
1472
2935
  payment: settlement?.success ? { transaction: settlement.transaction, status: "settled", facilitator: settlement.facilitator } : { status: "pending" }
1473
2936
  }, responseHeaders);
1474
2937
  }
2938
+ /**
2939
+ * Handle MPP (Machine Payments Protocol) request
2940
+ * Supports both x402 and MPP protocols on service endpoints
2941
+ */
2942
+ async handleMPPRequest(skill, body, authHeader, x402Header, res) {
2943
+ const config = skill.config;
2944
+ const params = body || {};
2945
+ if (x402Header) {
2946
+ return await this.handleExecute({ service: config.id, params }, x402Header, res);
2947
+ }
2948
+ if (authHeader && authHeader.toLowerCase().startsWith("payment ")) {
2949
+ return await this.handleMPPPayment(skill, params, authHeader, res);
2950
+ }
2951
+ return this.sendMPPPaymentRequired(config, res);
2952
+ }
2953
+ /**
2954
+ * Handle MPP payment verification and service execution
2955
+ */
2956
+ async handleMPPPayment(skill, params, authHeader, res) {
2957
+ const config = skill.config;
2958
+ const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
2959
+ if (!credentialMatch) {
2960
+ return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
2961
+ }
2962
+ let mppCredential;
2963
+ try {
2964
+ const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
2965
+ const decoded = Buffer.from(base64, "base64").toString("utf-8");
2966
+ mppCredential = JSON.parse(decoded);
2967
+ } catch (err) {
2968
+ console.error("[MoltsPay] Failed to parse MPP credential:", err);
2969
+ return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
2970
+ }
2971
+ let txHash;
2972
+ if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
2973
+ txHash = mppCredential.payload.hash;
2974
+ } else if (mppCredential.payload?.type === "transaction") {
2975
+ return this.sendJson(res, 400, {
2976
+ error: "Transaction type not supported. Please use push mode (hash type)."
2977
+ });
2978
+ }
2979
+ if (!txHash) {
2980
+ return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
2981
+ }
2982
+ let chainId = mppCredential.challenge?.request?.methodDetails?.chainId;
2983
+ if (!chainId && mppCredential.source) {
2984
+ const chainMatch = mppCredential.source.match(/eip155:(\d+)/);
2985
+ if (chainMatch) chainId = parseInt(chainMatch[1], 10);
2986
+ }
2987
+ chainId = chainId || 42431;
2988
+ const network = `eip155:${chainId}`;
2989
+ if (!this.isNetworkAccepted(network)) {
2990
+ return this.sendJson(res, 402, {
2991
+ error: `Network not accepted: ${network}`
2992
+ });
2993
+ }
2994
+ const requirements = this.buildPaymentRequirements(
2995
+ config,
2996
+ network,
2997
+ this.getWalletForNetwork(network),
2998
+ "USDC"
2999
+ );
3000
+ const paymentPayload = {
3001
+ x402Version: X402_VERSION3,
3002
+ scheme: "exact",
3003
+ network,
3004
+ payload: {
3005
+ txHash,
3006
+ chainId
3007
+ }
3008
+ };
3009
+ console.log(`[MoltsPay] Verifying MPP payment: txHash=${txHash}, chainId=${chainId}`);
3010
+ const verification = await this.registry.verify(paymentPayload, requirements);
3011
+ if (!verification.valid) {
3012
+ return this.sendJson(res, 402, {
3013
+ error: `Payment verification failed: ${verification.error}`
3014
+ });
3015
+ }
3016
+ console.log(`[MoltsPay] Payment verified! Executing service: ${config.id}`);
3017
+ let result;
3018
+ try {
3019
+ result = await skill.handler(params);
3020
+ } catch (err) {
3021
+ console.error(`[MoltsPay] Skill execution error:`, err);
3022
+ return this.sendJson(res, 500, {
3023
+ error: `Service execution failed: ${err.message}`
3024
+ });
3025
+ }
3026
+ const receipt = {
3027
+ success: true,
3028
+ txHash,
3029
+ network,
3030
+ facilitator: verification.facilitator
3031
+ };
3032
+ const receiptEncoded = Buffer.from(JSON.stringify(receipt)).toString("base64");
3033
+ res.writeHead(200, {
3034
+ "Content-Type": "application/json",
3035
+ [MPP_RECEIPT_HEADER]: receiptEncoded
3036
+ });
3037
+ res.end(JSON.stringify({
3038
+ success: true,
3039
+ result,
3040
+ payment: {
3041
+ txHash,
3042
+ status: "verified",
3043
+ facilitator: verification.facilitator
3044
+ }
3045
+ }, null, 2));
3046
+ }
3047
+ /**
3048
+ * Return 402 with both x402 and MPP payment requirements
3049
+ */
3050
+ sendMPPPaymentRequired(config, res) {
3051
+ const acceptedTokens = getAcceptedCurrencies(config);
3052
+ const providerChains = this.getProviderChains();
3053
+ const accepts = [];
3054
+ for (const chainConfig of providerChains) {
3055
+ for (const token of acceptedTokens) {
3056
+ if (chainConfig.tokens.includes(token)) {
3057
+ accepts.push(this.buildPaymentRequirements(config, chainConfig.network, chainConfig.wallet, token));
3058
+ }
3059
+ }
3060
+ }
3061
+ const x402PaymentRequired = {
3062
+ x402Version: X402_VERSION3,
3063
+ accepts,
3064
+ acceptedCurrencies: acceptedTokens,
3065
+ resource: {
3066
+ url: `/${config.id}`,
3067
+ description: `${config.name} - $${config.price} ${config.currency}`
3068
+ }
3069
+ };
3070
+ const x402Encoded = Buffer.from(JSON.stringify(x402PaymentRequired)).toString("base64");
3071
+ const tempoChain = providerChains.find((c) => c.network === "eip155:42431");
3072
+ let mppWwwAuth = "";
3073
+ if (tempoChain) {
3074
+ const challengeId = this.generateChallengeId();
3075
+ const amountInUnits = Math.floor(config.price * 1e6).toString();
3076
+ const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
3077
+ const mppRequest = {
3078
+ amount: amountInUnits,
3079
+ currency: tokenAddress,
3080
+ methodDetails: {
3081
+ chainId: 42431,
3082
+ feePayer: true
3083
+ },
3084
+ recipient: tempoChain.wallet
3085
+ };
3086
+ const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
3087
+ const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
3088
+ mppWwwAuth = `Payment id="${challengeId}", realm="${this.manifest.provider.name}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
3089
+ }
3090
+ const headers = {
3091
+ "Content-Type": "application/problem+json",
3092
+ [PAYMENT_REQUIRED_HEADER2]: x402Encoded
3093
+ };
3094
+ if (mppWwwAuth) {
3095
+ headers[MPP_WWW_AUTH_HEADER] = mppWwwAuth;
3096
+ }
3097
+ res.writeHead(402, headers);
3098
+ res.end(JSON.stringify({
3099
+ type: "https://paymentauth.org/problems/payment-required",
3100
+ title: "Payment Required",
3101
+ status: 402,
3102
+ detail: `Payment is required (${config.name}).`,
3103
+ service: config.id,
3104
+ price: config.price,
3105
+ currency: config.currency,
3106
+ acceptedCurrencies: acceptedTokens
3107
+ }, null, 2));
3108
+ }
3109
+ /**
3110
+ * Generate a unique challenge ID for MPP
3111
+ */
3112
+ generateChallengeId() {
3113
+ const bytes = new Uint8Array(24);
3114
+ for (let i = 0; i < bytes.length; i++) {
3115
+ bytes[i] = Math.floor(Math.random() * 256);
3116
+ }
3117
+ return Buffer.from(bytes).toString("base64url");
3118
+ }
1475
3119
  /**
1476
3120
  * Return 402 with x402 payment requirements (v2 format)
1477
3121
  * Includes requirements for all chains and all accepted currencies
@@ -1547,7 +3191,7 @@ var MoltsPayServer = class {
1547
3191
  const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
1548
3192
  const tokenAddress = tokenAddresses[selectedToken];
1549
3193
  const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
1550
- return {
3194
+ const requirements = {
1551
3195
  scheme: "exact",
1552
3196
  network: selectedNetwork,
1553
3197
  asset: tokenAddress,
@@ -1556,6 +3200,27 @@ var MoltsPayServer = class {
1556
3200
  maxTimeoutSeconds: 300,
1557
3201
  extra: tokenDomain
1558
3202
  };
3203
+ if (selectedNetwork === "solana:mainnet" || selectedNetwork === "solana:devnet") {
3204
+ const solanaFacilitator = this.registry.get("solana");
3205
+ const feePayerPubkey = solanaFacilitator?.getFeePayerPubkey?.();
3206
+ if (feePayerPubkey) {
3207
+ requirements.extra = {
3208
+ ...requirements.extra || {},
3209
+ solanaFeePayer: feePayerPubkey
3210
+ };
3211
+ }
3212
+ }
3213
+ if (selectedNetwork === "eip155:56" || selectedNetwork === "eip155:97") {
3214
+ const bnbFacilitator = this.registry.get("bnb");
3215
+ const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
3216
+ if (spenderAddress) {
3217
+ requirements.extra = {
3218
+ ...requirements.extra || {},
3219
+ bnbSpender: spenderAddress
3220
+ };
3221
+ }
3222
+ }
3223
+ return requirements;
1559
3224
  }
1560
3225
  /**
1561
3226
  * Detect which token is being used in the payment
@@ -1621,31 +3286,42 @@ var MoltsPayServer = class {
1621
3286
  /**
1622
3287
  * POST /proxy - Handle payment for external services (moltspay-creators)
1623
3288
  *
1624
- * This endpoint allows other services to delegate x402 payment handling.
3289
+ * This endpoint allows other services to delegate x402/MPP payment handling.
1625
3290
  * It does NOT execute any skill - just handles payment verification/settlement.
1626
3291
  *
1627
3292
  * Request body:
1628
3293
  * { wallet, amount, currency, chain, memo, serviceId, description }
1629
3294
  *
1630
- * Without X-Payment header: returns 402 with payment requirements
1631
- * With X-Payment header: verifies payment and returns result
3295
+ * For x402 (base, polygon, base_sepolia):
3296
+ * Without X-Payment header: returns 402 with X-Payment-Required
3297
+ * With X-Payment header: verifies payment via CDP
3298
+ *
3299
+ * For MPP (tempo_moderato):
3300
+ * Without Authorization header: returns 402 with WWW-Authenticate
3301
+ * With Authorization: Payment header: verifies tx on Tempo chain
1632
3302
  */
1633
- async handleProxy(body, paymentHeader, res) {
3303
+ async handleProxy(body, paymentHeader, authHeader, res) {
1634
3304
  const { wallet, amount, currency, chain, memo, serviceId, description } = body;
1635
3305
  if (!wallet || !amount) {
1636
3306
  return this.sendJson(res, 400, { error: "Missing required fields: wallet, amount" });
1637
3307
  }
1638
- if (!/^0x[a-fA-F0-9]{40}$/.test(wallet)) {
1639
- return this.sendJson(res, 400, { error: "Invalid wallet address format" });
3308
+ const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
3309
+ if (chain && !supportedChains.includes(chain)) {
3310
+ return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
3311
+ }
3312
+ const isSolanaChain = chain === "solana" || chain === "solana_devnet";
3313
+ const isValidEvmAddress = /^0x[a-fA-F0-9]{40}$/.test(wallet);
3314
+ const isValidSolanaAddress2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(wallet);
3315
+ if (isSolanaChain && !isValidSolanaAddress2) {
3316
+ return this.sendJson(res, 400, { error: "Invalid Solana wallet address format" });
3317
+ }
3318
+ if (!isSolanaChain && !isValidEvmAddress) {
3319
+ return this.sendJson(res, 400, { error: "Invalid EVM wallet address format" });
1640
3320
  }
1641
3321
  const amountNum = parseFloat(amount);
1642
3322
  if (isNaN(amountNum) || amountNum <= 0) {
1643
3323
  return this.sendJson(res, 400, { error: "Invalid amount" });
1644
3324
  }
1645
- const supportedChains = ["base", "polygon", "base_sepolia"];
1646
- if (chain && !supportedChains.includes(chain)) {
1647
- return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
1648
- }
1649
3325
  const proxyConfig = {
1650
3326
  id: serviceId || "proxy",
1651
3327
  name: description || "Proxy Payment",
@@ -1657,6 +3333,9 @@ var MoltsPayServer = class {
1657
3333
  input: {},
1658
3334
  output: {}
1659
3335
  };
3336
+ if (chain === "tempo_moderato") {
3337
+ return await this.handleProxyMPP(body, proxyConfig, authHeader, res);
3338
+ }
1660
3339
  const requirements = this.buildProxyPaymentRequirements(proxyConfig, wallet, currency, chain);
1661
3340
  if (!paymentHeader) {
1662
3341
  return this.sendProxyPaymentRequired(proxyConfig, wallet, memo, chain, res);
@@ -1692,7 +3371,6 @@ var MoltsPayServer = class {
1692
3371
  console.log(`[MoltsPay] /proxy: Verified by ${verifyResult.facilitator}`);
1693
3372
  const { execute, service, params } = body;
1694
3373
  if (execute && service) {
1695
- console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
1696
3374
  const skill = this.skills.get(service);
1697
3375
  if (!skill) {
1698
3376
  console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
@@ -1702,6 +3380,32 @@ var MoltsPayServer = class {
1702
3380
  error: `Service not found: ${service}`
1703
3381
  });
1704
3382
  }
3383
+ const isSolana = isSolanaNetwork(network);
3384
+ let settlement2 = null;
3385
+ if (isSolana) {
3386
+ console.log(`[MoltsPay] /proxy: Solana detected - settling payment FIRST`);
3387
+ try {
3388
+ settlement2 = await this.registry.settle(payment, requirements);
3389
+ console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
3390
+ if (!settlement2.success) {
3391
+ console.error(`[MoltsPay] /proxy: Solana settlement failed: ${settlement2.error}`);
3392
+ return this.sendJson(res, 402, {
3393
+ success: false,
3394
+ paymentSettled: false,
3395
+ error: `Payment settlement failed: ${settlement2.error || "Unknown error"}`
3396
+ });
3397
+ }
3398
+ } catch (err) {
3399
+ console.error("[MoltsPay] /proxy: Solana settlement failed:", err.message);
3400
+ return this.sendJson(res, 402, {
3401
+ success: false,
3402
+ paymentSettled: false,
3403
+ error: `Payment settlement failed: ${err.message}`
3404
+ });
3405
+ }
3406
+ } else {
3407
+ console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
3408
+ }
1705
3409
  const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
1706
3410
  let result;
1707
3411
  try {
@@ -1711,73 +3415,199 @@ var MoltsPayServer = class {
1711
3415
  (_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
1712
3416
  )
1713
3417
  ]);
1714
- console.log(`[MoltsPay] /proxy: Skill succeeded, now settling payment...`);
3418
+ console.log(`[MoltsPay] /proxy: Skill succeeded`);
1715
3419
  } catch (err) {
1716
- console.error(`[MoltsPay] /proxy: Skill failed: ${err.message} - NOT settling`);
3420
+ console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}`);
1717
3421
  return this.sendJson(res, 500, {
1718
3422
  success: false,
1719
- paymentSettled: false,
1720
- error: `Service execution failed: ${err.message}`
3423
+ paymentSettled: isSolana ? true : false,
3424
+ error: `Service execution failed: ${err.message}`,
3425
+ note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
1721
3426
  });
1722
3427
  }
1723
- let settlement2 = null;
3428
+ if (!isSolana) {
3429
+ console.log(`[MoltsPay] /proxy: Settling payment...`);
3430
+ try {
3431
+ settlement2 = await this.registry.settle(payment, requirements);
3432
+ console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
3433
+ } catch (err) {
3434
+ console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
3435
+ return this.sendJson(res, 200, {
3436
+ success: true,
3437
+ verified: true,
3438
+ settled: false,
3439
+ settlementError: err.message,
3440
+ from: payment.payload?.authorization?.from,
3441
+ paidTo: wallet,
3442
+ amount: amountNum,
3443
+ currency: currency || "USDC",
3444
+ memo,
3445
+ result
3446
+ });
3447
+ }
3448
+ }
3449
+ return this.sendJson(res, 200, {
3450
+ success: true,
3451
+ verified: true,
3452
+ settled: settlement2?.success || false,
3453
+ txHash: settlement2?.transaction,
3454
+ from: payment.payload?.authorization?.from,
3455
+ paidTo: wallet,
3456
+ amount: amountNum,
3457
+ currency: currency || "USDC",
3458
+ facilitator: settlement2?.facilitator,
3459
+ memo,
3460
+ result
3461
+ });
3462
+ }
3463
+ console.log(`[MoltsPay] /proxy: Settling payment (no execution)...`);
3464
+ let settlement = null;
3465
+ try {
3466
+ settlement = await this.registry.settle(payment, requirements);
3467
+ console.log(`[MoltsPay] /proxy: Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
3468
+ } catch (err) {
3469
+ console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
3470
+ return this.sendJson(res, 500, {
3471
+ success: false,
3472
+ error: `Settlement failed: ${err.message}`
3473
+ });
3474
+ }
3475
+ this.sendJson(res, 200, {
3476
+ success: true,
3477
+ verified: true,
3478
+ settled: settlement?.success || false,
3479
+ txHash: settlement?.transaction,
3480
+ from: payment.payload?.authorization?.from,
3481
+ // Buyer's wallet address
3482
+ paidTo: wallet,
3483
+ amount: amountNum,
3484
+ currency: currency || "USDC",
3485
+ facilitator: settlement?.facilitator,
3486
+ memo
3487
+ });
3488
+ }
3489
+ /**
3490
+ * Handle MPP payment flow for /proxy endpoint (tempo_moderato chain)
3491
+ */
3492
+ async handleProxyMPP(body, config, authHeader, res) {
3493
+ const { wallet, amount, memo, serviceId } = body;
3494
+ const amountNum = parseFloat(amount);
3495
+ const amountInUnits = Math.floor(amountNum * 1e6).toString();
3496
+ if (!authHeader || !authHeader.toLowerCase().startsWith("payment ")) {
3497
+ const challengeId = this.generateChallengeId();
3498
+ const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
3499
+ const mppRequest = {
3500
+ amount: amountInUnits,
3501
+ currency: tokenAddress,
3502
+ methodDetails: {
3503
+ chainId: 42431,
3504
+ feePayer: true
3505
+ },
3506
+ recipient: wallet
3507
+ };
3508
+ const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
3509
+ const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
3510
+ const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
3511
+ res.writeHead(402, {
3512
+ "Content-Type": "application/problem+json",
3513
+ [MPP_WWW_AUTH_HEADER]: wwwAuth
3514
+ });
3515
+ res.end(JSON.stringify({
3516
+ type: "https://paymentauth.org/problems/payment-required",
3517
+ title: "Payment Required",
3518
+ status: 402,
3519
+ detail: `Payment is required (${config.name}).`,
3520
+ service: serviceId || "proxy",
3521
+ price: amountNum,
3522
+ currency: "USDC"
3523
+ }, null, 2));
3524
+ return;
3525
+ }
3526
+ const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
3527
+ if (!credentialMatch) {
3528
+ return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
3529
+ }
3530
+ let mppCredential;
3531
+ try {
3532
+ const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
3533
+ const decoded = Buffer.from(base64, "base64").toString("utf-8");
3534
+ mppCredential = JSON.parse(decoded);
3535
+ } catch (err) {
3536
+ console.error("[MoltsPay] /proxy MPP: Failed to parse credential:", err);
3537
+ return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
3538
+ }
3539
+ let txHash;
3540
+ if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
3541
+ txHash = mppCredential.payload.hash;
3542
+ } else {
3543
+ return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
3544
+ }
3545
+ console.log(`[MoltsPay] /proxy MPP: Verifying tx ${txHash} on Tempo...`);
3546
+ const requirements = this.buildPaymentRequirements(config, "eip155:42431", wallet, "USDC");
3547
+ const paymentPayload = {
3548
+ x402Version: X402_VERSION3,
3549
+ scheme: "exact",
3550
+ network: "eip155:42431",
3551
+ payload: { txHash, chainId: 42431 }
3552
+ };
3553
+ const verification = await this.registry.verify(paymentPayload, requirements);
3554
+ if (!verification.valid) {
3555
+ return this.sendJson(res, 402, {
3556
+ error: `Payment verification failed: ${verification.error}`
3557
+ });
3558
+ }
3559
+ console.log(`[MoltsPay] /proxy MPP: Payment verified by ${verification.facilitator}`);
3560
+ const { execute, service, params } = body;
3561
+ if (execute && service) {
3562
+ console.log(`[MoltsPay] /proxy MPP: Executing skill: ${service}`);
3563
+ const skill = this.skills.get(service);
3564
+ if (!skill) {
3565
+ return this.sendJson(res, 404, {
3566
+ success: false,
3567
+ paymentSettled: true,
3568
+ // Payment already happened on Tempo
3569
+ error: `Service not found: ${service}`
3570
+ });
3571
+ }
3572
+ const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
3573
+ let result;
1724
3574
  try {
1725
- settlement2 = await this.registry.settle(payment, requirements);
1726
- console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
3575
+ result = await Promise.race([
3576
+ skill.handler(params || {}),
3577
+ new Promise(
3578
+ (_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
3579
+ )
3580
+ ]);
1727
3581
  } catch (err) {
1728
- console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
1729
- return this.sendJson(res, 200, {
1730
- success: true,
1731
- verified: true,
1732
- settled: false,
1733
- settlementError: err.message,
1734
- from: payment.payload?.authorization?.from,
1735
- // Buyer's wallet address
1736
- paidTo: wallet,
1737
- amount: amountNum,
1738
- currency: currency || "USDC",
1739
- memo,
1740
- result
3582
+ console.error(`[MoltsPay] /proxy MPP: Skill failed: ${err.message}`);
3583
+ return this.sendJson(res, 500, {
3584
+ success: false,
3585
+ paymentSettled: true,
3586
+ error: `Service execution failed: ${err.message}`
1741
3587
  });
1742
3588
  }
1743
3589
  return this.sendJson(res, 200, {
1744
3590
  success: true,
1745
3591
  verified: true,
1746
- settled: settlement2?.success || false,
1747
- txHash: settlement2?.transaction,
1748
- from: payment.payload?.authorization?.from,
1749
- // Buyer's wallet address
3592
+ txHash,
3593
+ chain: "tempo_moderato",
1750
3594
  paidTo: wallet,
1751
3595
  amount: amountNum,
1752
- currency: currency || "USDC",
1753
- facilitator: settlement2?.facilitator,
3596
+ currency: "USDC",
3597
+ facilitator: verification.facilitator,
1754
3598
  memo,
1755
3599
  result
1756
3600
  });
1757
3601
  }
1758
- console.log(`[MoltsPay] /proxy: Settling payment (no execution)...`);
1759
- let settlement = null;
1760
- try {
1761
- settlement = await this.registry.settle(payment, requirements);
1762
- console.log(`[MoltsPay] /proxy: Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
1763
- } catch (err) {
1764
- console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
1765
- return this.sendJson(res, 500, {
1766
- success: false,
1767
- error: `Settlement failed: ${err.message}`
1768
- });
1769
- }
1770
3602
  this.sendJson(res, 200, {
1771
3603
  success: true,
1772
3604
  verified: true,
1773
- settled: settlement?.success || false,
1774
- txHash: settlement?.transaction,
1775
- from: payment.payload?.authorization?.from,
1776
- // Buyer's wallet address
3605
+ txHash,
3606
+ chain: "tempo_moderato",
1777
3607
  paidTo: wallet,
1778
3608
  amount: amountNum,
1779
- currency: currency || "USDC",
1780
- facilitator: settlement?.facilitator,
3609
+ currency: "USDC",
3610
+ facilitator: verification.facilitator,
1781
3611
  memo
1782
3612
  });
1783
3613
  }
@@ -1792,7 +3622,7 @@ var MoltsPayServer = class {
1792
3622
  const tokenAddresses = TOKEN_ADDRESSES[networkId] || TOKEN_ADDRESSES[this.networkId] || {};
1793
3623
  const tokenAddress = tokenAddresses[selectedToken];
1794
3624
  const tokenDomain = getTokenDomain(networkId, selectedToken);
1795
- return {
3625
+ const requirements = {
1796
3626
  scheme: "exact",
1797
3627
  network: networkId,
1798
3628
  asset: tokenAddress,
@@ -1802,6 +3632,17 @@ var MoltsPayServer = class {
1802
3632
  maxTimeoutSeconds: 300,
1803
3633
  extra: tokenDomain
1804
3634
  };
3635
+ if (networkId === "eip155:56" || networkId === "eip155:97") {
3636
+ const bnbFacilitator = this.registry.get("bnb");
3637
+ const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
3638
+ if (spenderAddress) {
3639
+ requirements.extra = {
3640
+ ...requirements.extra || {},
3641
+ bnbSpender: spenderAddress
3642
+ };
3643
+ }
3644
+ }
3645
+ return requirements;
1805
3646
  }
1806
3647
  /**
1807
3648
  * Return 402 with x402 payment requirements for proxy endpoint
@@ -1846,11 +3687,114 @@ async function printQRCode(url) {
1846
3687
 
1847
3688
  // src/cli/index.ts
1848
3689
  var readline = __toESM(require("readline"));
3690
+ if (!globalThis.crypto) {
3691
+ globalThis.crypto = import_crypto.webcrypto;
3692
+ }
3693
+ function getVersion() {
3694
+ const locations = [
3695
+ (0, import_path3.join)(__dirname, "../../package.json"),
3696
+ (0, import_path3.join)(__dirname, "../package.json"),
3697
+ (0, import_path3.join)(process.cwd(), "node_modules/moltspay/package.json")
3698
+ ];
3699
+ for (const loc of locations) {
3700
+ try {
3701
+ if ((0, import_fs5.existsSync)(loc)) {
3702
+ const pkg = JSON.parse((0, import_fs5.readFileSync)(loc, "utf-8"));
3703
+ if (pkg.name === "moltspay") return pkg.version;
3704
+ }
3705
+ } catch {
3706
+ }
3707
+ }
3708
+ return "0.0.0";
3709
+ }
3710
+ var BNB_SPONSOR_KEY = process.env.MOLTSPAY_BNB_SPONSOR_KEY;
3711
+ var BNB_SPENDER_ADDRESS = process.env.MOLTSPAY_BNB_SPENDER || "0xEBB45208D806A0c73F9673E0c5713FF720DD6b79";
3712
+ var ERC20_APPROVE_ABI = [
3713
+ "function approve(address spender, uint256 amount) returns (bool)",
3714
+ "function allowance(address owner, address spender) view returns (uint256)"
3715
+ ];
3716
+ async function setupBNBApprovals(client, chain, spenderAddress, sponsorGas = false) {
3717
+ const chainConfig = CHAINS[chain];
3718
+ const provider = new import_ethers2.ethers.JsonRpcProvider(chainConfig.rpc);
3719
+ const wallet = client.getWallet();
3720
+ if (!wallet) {
3721
+ console.log(" \u274C No wallet found");
3722
+ return;
3723
+ }
3724
+ const signer = wallet.connect(provider);
3725
+ console.log(` Spender: ${spenderAddress}`);
3726
+ let bnbBalance = await provider.getBalance(wallet.address);
3727
+ const minGasRequired = import_ethers2.ethers.parseEther("0.0005");
3728
+ if (bnbBalance < minGasRequired) {
3729
+ if (sponsorGas && BNB_SPONSOR_KEY) {
3730
+ console.log(" \u23F3 Sponsoring BNB gas for approvals...");
3731
+ try {
3732
+ const sponsorWallet = new import_ethers2.ethers.Wallet(BNB_SPONSOR_KEY, provider);
3733
+ const tx = await sponsorWallet.sendTransaction({
3734
+ to: wallet.address,
3735
+ value: import_ethers2.ethers.parseEther("0.001")
3736
+ });
3737
+ await tx.wait();
3738
+ console.log(` \u2705 Sponsored 0.001 BNB (tx: ${tx.hash.slice(0, 10)}...)`);
3739
+ bnbBalance = await provider.getBalance(wallet.address);
3740
+ } catch (err) {
3741
+ console.log(` \u26A0\uFE0F Gas sponsorship failed: ${err.message}`);
3742
+ console.log(` \u{1F4A1} Get testnet BNB: https://testnet.bnbchain.org/faucet-smart`);
3743
+ return;
3744
+ }
3745
+ } else {
3746
+ console.log(` \u26A0\uFE0F Need BNB for gas (~0.0005 BNB)`);
3747
+ console.log(` \u{1F4A1} Run: npx moltspay faucet --chain bnb_testnet`);
3748
+ console.log(` Then run: npx moltspay approve --chain ${chain} --spender ${spenderAddress}`);
3749
+ return;
3750
+ }
3751
+ }
3752
+ for (const tokenSymbol of ["USDT", "USDC"]) {
3753
+ const tokenConfig = chainConfig.tokens[tokenSymbol];
3754
+ const tokenContract = new import_ethers2.ethers.Contract(tokenConfig.address, ERC20_APPROVE_ABI, signer);
3755
+ const allowance = await tokenContract.allowance(wallet.address, spenderAddress);
3756
+ if (allowance > 0n) {
3757
+ console.log(` \u2705 ${tokenSymbol}: already approved for ${spenderAddress.slice(0, 10)}...`);
3758
+ continue;
3759
+ }
3760
+ console.log(` \u23F3 Approving ${tokenSymbol}...`);
3761
+ try {
3762
+ const tx = await tokenContract.approve(spenderAddress, import_ethers2.ethers.MaxUint256);
3763
+ await tx.wait();
3764
+ console.log(` \u2705 ${tokenSymbol}: approved (tx: ${tx.hash.slice(0, 10)}...)`);
3765
+ } catch (err) {
3766
+ console.log(` \u274C ${tokenSymbol}: approval failed - ${err.message}`);
3767
+ }
3768
+ }
3769
+ console.log("");
3770
+ }
3771
+ async function checkBNBApprovals(address, chain, configDir = DEFAULT_CONFIG_DIR2) {
3772
+ const chainConfig = CHAINS[chain];
3773
+ const provider = new import_ethers2.ethers.JsonRpcProvider(chainConfig.rpc);
3774
+ let spenderAddress = null;
3775
+ try {
3776
+ const walletPath = (0, import_path3.join)(configDir, "wallet.json");
3777
+ const walletData = JSON.parse((0, import_fs5.readFileSync)(walletPath, "utf-8"));
3778
+ spenderAddress = walletData.approvals?.[chain] || null;
3779
+ } catch {
3780
+ }
3781
+ const result = { usdt: false, usdc: false, spender: spenderAddress };
3782
+ if (!spenderAddress) {
3783
+ return result;
3784
+ }
3785
+ for (const tokenSymbol of ["USDT", "USDC"]) {
3786
+ const tokenConfig = chainConfig.tokens[tokenSymbol];
3787
+ const tokenContract = new import_ethers2.ethers.Contract(tokenConfig.address, ERC20_APPROVE_ABI, provider);
3788
+ const allowance = await tokenContract.allowance(address, spenderAddress);
3789
+ result[tokenSymbol.toLowerCase()] = allowance > 0n;
3790
+ }
3791
+ return result;
3792
+ }
1849
3793
  var program = new import_commander.Command();
1850
- var DEFAULT_CONFIG_DIR = (0, import_path2.join)((0, import_os2.homedir)(), ".moltspay");
1851
- var PID_FILE = (0, import_path2.join)(DEFAULT_CONFIG_DIR, "server.pid");
1852
- if (!(0, import_fs4.existsSync)(DEFAULT_CONFIG_DIR)) {
1853
- (0, import_fs4.mkdirSync)(DEFAULT_CONFIG_DIR, { recursive: true });
3794
+ var DEFAULT_CONFIG_DIR2 = (0, import_path3.join)((0, import_os3.homedir)(), ".moltspay");
3795
+ var PID_FILE = (0, import_path3.join)(DEFAULT_CONFIG_DIR2, "server.pid");
3796
+ if (!(0, import_fs5.existsSync)(DEFAULT_CONFIG_DIR2)) {
3797
+ (0, import_fs5.mkdirSync)(DEFAULT_CONFIG_DIR2, { recursive: true });
1854
3798
  }
1855
3799
  function prompt(question) {
1856
3800
  const rl = readline.createInterface({
@@ -1864,20 +3808,50 @@ function prompt(question) {
1864
3808
  });
1865
3809
  });
1866
3810
  }
1867
- program.name("moltspay").description("MoltsPay - Payment infrastructure for AI Agents").version("1.0.0");
1868
- program.command("init").description("Initialize MoltsPay client (create wallet, set limits)").option("--chain <chain>", "Blockchain to use", "base").option("--max-per-tx <amount>", "Max amount per transaction").option("--max-per-day <amount>", "Max amount per day").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR).action(async (options) => {
1869
- console.log("\n\u{1F510} MoltsPay Client Setup\n");
1870
- if ((0, import_fs4.existsSync)((0, import_path2.join)(options.configDir, "wallet.json"))) {
1871
- console.log('\u26A0\uFE0F Already initialized. Use "moltspay config" to update settings.');
1872
- console.log(` Config dir: ${options.configDir}`);
1873
- return;
1874
- }
3811
+ program.name("moltspay").description("MoltsPay - Payment infrastructure for AI Agents").version(getVersion());
3812
+ program.command("init").description("Initialize MoltsPay client (create wallet, set limits)").option("--chain <chain>", "Blockchain to use", "base").option("--max-per-tx <amount>", "Max amount per transaction").option("--max-per-day <amount>", "Max amount per day").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (options) => {
1875
3813
  let chain = options.chain;
1876
- const supportedChains = ["base", "polygon", "base_sepolia"];
3814
+ const supportedEVMChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
3815
+ const supportedSolanaChains = ["solana", "solana_devnet"];
3816
+ const supportedChains = [...supportedEVMChains, ...supportedSolanaChains];
1877
3817
  if (!supportedChains.includes(chain)) {
1878
3818
  console.error(`\u274C Unknown chain: ${chain}. Supported: ${supportedChains.join(", ")}`);
1879
3819
  process.exit(1);
1880
3820
  }
3821
+ if (supportedSolanaChains.includes(chain)) {
3822
+ console.log("\n\u{1F7E3} Solana Wallet Setup\n");
3823
+ if (solanaWalletExists(options.configDir)) {
3824
+ const existingAddress = getSolanaAddress(options.configDir);
3825
+ console.log(`\u26A0\uFE0F Solana wallet already exists: ${existingAddress}`);
3826
+ console.log(` Config dir: ${options.configDir}`);
3827
+ return;
3828
+ }
3829
+ console.log("Creating Solana wallet...");
3830
+ const keypair = createSolanaWallet(options.configDir);
3831
+ const address = keypair.publicKey.toBase58();
3832
+ console.log(`
3833
+ \u2705 Solana wallet created: ${address}`);
3834
+ console.log(`
3835
+ \u{1F4C1} Config saved to: ${(0, import_path3.join)(options.configDir, "wallet-solana.json")}`);
3836
+ console.log(`
3837
+ \u26A0\uFE0F IMPORTANT: Back up your wallet file!`);
3838
+ console.log(` This file contains your private key!
3839
+ `);
3840
+ if (chain === "solana_devnet") {
3841
+ console.log("\u{1F4A1} Get testnet tokens:");
3842
+ console.log(" npx moltspay faucet --chain solana_devnet\n");
3843
+ } else {
3844
+ console.log(`\u{1F4B0} Fund your wallet with USDC on Solana to start (gasless - no SOL needed).
3845
+ `);
3846
+ }
3847
+ return;
3848
+ }
3849
+ console.log("\n\u{1F510} MoltsPay Client Setup\n");
3850
+ if ((0, import_fs5.existsSync)((0, import_path3.join)(options.configDir, "wallet.json"))) {
3851
+ console.log('\u26A0\uFE0F EVM wallet already initialized. Use "moltspay config" to update settings.');
3852
+ console.log(` Config dir: ${options.configDir}`);
3853
+ return;
3854
+ }
1881
3855
  let maxPerTx = options.maxPerTx ? parseFloat(options.maxPerTx) : null;
1882
3856
  let maxPerDay = options.maxPerDay ? parseFloat(options.maxPerDay) : null;
1883
3857
  if (!maxPerTx) {
@@ -1899,13 +3873,21 @@ program.command("init").description("Initialize MoltsPay client (create wallet,
1899
3873
  console.log(`
1900
3874
  \u{1F4C1} Config saved to: ${result.configDir}`);
1901
3875
  console.log(`
1902
- \u26A0\uFE0F IMPORTANT: Back up ${(0, import_path2.join)(result.configDir, "wallet.json")}`);
3876
+ \u26A0\uFE0F IMPORTANT: Back up ${(0, import_path3.join)(result.configDir, "wallet.json")}`);
1903
3877
  console.log(` This file contains your private key!
1904
3878
  `);
3879
+ if (chain === "bnb" || chain === "bnb_testnet") {
3880
+ console.log("\u{1F4CB} Setting up BNB chain approvals...\n");
3881
+ console.log(" \u2139\uFE0F Using default spender. For other services, run:");
3882
+ console.log(` npx moltspay approve --chain ${chain} --spender <address>
3883
+ `);
3884
+ const client = new MoltsPayClient({ configDir: options.configDir });
3885
+ await setupBNBApprovals(client, chain, BNB_SPENDER_ADDRESS, true);
3886
+ }
1905
3887
  console.log(`\u{1F4B0} Fund your wallet with USDC on ${chain} to start using services.
1906
3888
  `);
1907
3889
  });
1908
- program.command("config").description("Update MoltsPay settings").option("--max-per-tx <amount>", "Max amount per transaction").option("--max-per-day <amount>", "Max amount per day").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR).action(async (options) => {
3890
+ program.command("config").description("Update MoltsPay settings").option("--max-per-tx <amount>", "Max amount per transaction").option("--max-per-day <amount>", "Max amount per day").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (options) => {
1909
3891
  const client = new MoltsPayClient({ configDir: options.configDir });
1910
3892
  if (!client.isInitialized) {
1911
3893
  console.log("\u274C Not initialized. Run: npx moltspay init");
@@ -1940,37 +3922,77 @@ program.command("config").description("Update MoltsPay settings").option("--max-
1940
3922
  }
1941
3923
  }
1942
3924
  });
1943
- program.command("fund <amount>").description("Fund wallet with USDC via Coinbase (US debit card / Apple Pay)").option("--chain <chain>", "Chain to fund (base, polygon, or base_sepolia)", "base").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR).action(async (amountStr, options) => {
3925
+ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase (US debit card / Apple Pay)").option("--chain <chain>", "Chain to fund (base, polygon, solana, base_sepolia, bnb, or bnb_testnet)", "base").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (amountStr, options) => {
1944
3926
  const client = new MoltsPayClient({ configDir: options.configDir });
1945
- if (!client.isInitialized) {
1946
- console.log("\u274C Not initialized. Run: npx moltspay init");
1947
- return;
1948
- }
1949
3927
  const amount = parseFloat(amountStr);
1950
3928
  if (isNaN(amount) || amount < 5) {
1951
3929
  console.log("\u274C Minimum $5.");
1952
3930
  return;
1953
3931
  }
1954
3932
  const chain = options.chain?.toLowerCase() || "base";
1955
- if (!["base", "polygon", "base_sepolia"].includes(chain)) {
1956
- console.log("\u274C Invalid chain. Use: base, polygon, or base_sepolia");
3933
+ if (!["base", "polygon", "base_sepolia", "solana", "bnb", "bnb_testnet"].includes(chain)) {
3934
+ console.log("\u274C Invalid chain. Use: base, polygon, solana, base_sepolia, bnb, or bnb_testnet");
1957
3935
  return;
1958
3936
  }
3937
+ let walletAddress;
3938
+ if (chain === "solana") {
3939
+ const solanaWallet = loadSolanaWallet(options.configDir || DEFAULT_CONFIG_DIR2);
3940
+ if (!solanaWallet) {
3941
+ console.log("\u274C No Solana wallet found. Run: npx moltspay init --chain solana");
3942
+ return;
3943
+ }
3944
+ walletAddress = getSolanaAddress(options.configDir || DEFAULT_CONFIG_DIR2) || "";
3945
+ if (!walletAddress) {
3946
+ console.log("\u274C Could not get Solana wallet address.");
3947
+ return;
3948
+ }
3949
+ } else {
3950
+ if (!client.isInitialized) {
3951
+ console.log("\u274C Not initialized. Run: npx moltspay init");
3952
+ return;
3953
+ }
3954
+ walletAddress = client.address;
3955
+ }
1959
3956
  if (chain === "base_sepolia") {
1960
3957
  console.log("\n\u{1F9EA} Testnet Funding\n");
1961
- console.log(` Wallet: ${client.address}`);
3958
+ console.log(` Wallet: ${walletAddress}`);
1962
3959
  console.log(` Chain: Base Sepolia (testnet)
1963
3960
  `);
1964
- console.log("\u{1F4DD} Get testnet USDC from these faucets:");
1965
- console.log(" \u2022 Circle Faucet: https://faucet.circle.com/");
1966
- console.log(" \u2022 Base Sepolia: https://www.coinbase.com/faucets/base-ethereum-sepolia-faucet\n");
1967
- console.log(`\u{1F4A1} Send USDC to: ${client.address}
3961
+ console.log("\u{1F4A1} Use the MoltsPay faucet to get free testnet USDC:\n");
3962
+ console.log(" npx moltspay faucet\n");
3963
+ console.log(" Or get from Circle Faucet: https://faucet.circle.com/\n");
3964
+ return;
3965
+ }
3966
+ if (chain === "bnb_testnet") {
3967
+ console.log("\n\u{1F9EA} BNB Testnet Funding\n");
3968
+ console.log(` Wallet: ${walletAddress}`);
3969
+ console.log(` Chain: BNB Testnet
3970
+ `);
3971
+ console.log("\u{1F4A1} Use the MoltsPay faucet to get testnet USDC + tBNB:\n");
3972
+ console.log(" npx moltspay faucet --chain bnb_testnet\n");
3973
+ console.log(" This gives you:\n");
3974
+ console.log(" \u2022 1 USDC (testnet) for payments");
3975
+ console.log(" \u2022 0.001 tBNB for gas (first approval tx)\n");
3976
+ return;
3977
+ }
3978
+ if (chain === "bnb") {
3979
+ console.log("\n\u{1F4CB} BNB Chain Funding\n");
3980
+ console.log(` Wallet: ${walletAddress}
1968
3981
  `);
3982
+ console.log(" To use MoltsPay on BNB Chain, you need:\n");
3983
+ console.log(" 1. USDC for payments");
3984
+ console.log(" \u2192 Withdraw from Binance/exchange to your wallet address\n");
3985
+ console.log(" 2. Small amount of BNB for gas (~0.001 BNB / ~$0.60)");
3986
+ console.log(" \u2192 First approval transaction requires gas");
3987
+ console.log(" \u2192 After approval, all payments are gasless\n");
3988
+ console.log(" \u{1F4A1} Tip: Most exchanges include BNB dust when you withdraw to BNB Chain\n");
3989
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
3990
+ console.log(" After funding, check status: npx moltspay status\n");
1969
3991
  return;
1970
3992
  }
1971
3993
  console.log("\n\u{1F4B3} Fund your agent wallet\n");
1972
- console.log(` Wallet: ${client.address}`);
1973
- console.log(` Chain: ${chain}`);
3994
+ console.log(` Wallet: ${walletAddress}`);
3995
+ console.log(` Chain: ${chain === "solana" ? "Solana" : chain}`);
1974
3996
  console.log(` Amount: $${amount.toFixed(2)}
1975
3997
  `);
1976
3998
  try {
@@ -1979,7 +4001,7 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
1979
4001
  method: "POST",
1980
4002
  headers: { "Content-Type": "application/json" },
1981
4003
  body: JSON.stringify({
1982
- address: client.address,
4004
+ address: walletAddress,
1983
4005
  amount,
1984
4006
  chain
1985
4007
  })
@@ -1997,8 +4019,94 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
1997
4019
  console.log(`\u274C ${error.message}`);
1998
4020
  }
1999
4021
  });
2000
- program.command("faucet").description("Request testnet USDC from MoltsPay faucet (Base Sepolia)").option("--address <address>", "Wallet address (defaults to your wallet)").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR).action(async (options) => {
4022
+ program.command("approve").description("Approve a spender address for BNB chain payments").requiredOption("--spender <address>", "Spender address to approve (from server 402 response)").option("--chain <chain>", "BNB chain (bnb or bnb_testnet)", "bnb_testnet").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (options) => {
4023
+ const chain = options.chain;
4024
+ if (chain !== "bnb" && chain !== "bnb_testnet") {
4025
+ console.log("\u274C approve command is only for BNB chains (bnb or bnb_testnet)");
4026
+ return;
4027
+ }
4028
+ if (!options.spender.match(/^0x[a-fA-F0-9]{40}$/)) {
4029
+ console.log("\u274C Invalid spender address format");
4030
+ return;
4031
+ }
4032
+ const client = new MoltsPayClient({ configDir: options.configDir });
4033
+ if (!client.isInitialized) {
4034
+ console.log("\u274C Wallet not initialized. Run: npx moltspay init --chain " + chain);
4035
+ return;
4036
+ }
4037
+ console.log(`
4038
+ \u{1F510} Approving spender for ${chain}...
4039
+ `);
4040
+ await setupBNBApprovals(client, chain, options.spender, false);
4041
+ const walletPath = (0, import_path3.join)(options.configDir || DEFAULT_CONFIG_DIR2, "wallet.json");
4042
+ try {
4043
+ const walletData = JSON.parse((0, import_fs5.readFileSync)(walletPath, "utf-8"));
4044
+ walletData.approvals = walletData.approvals || {};
4045
+ walletData.approvals[chain] = options.spender;
4046
+ (0, import_fs5.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2));
4047
+ console.log(`\u2705 Approval complete! Spender saved for ${chain}.
4048
+ `);
4049
+ } catch (err) {
4050
+ console.log("\u2705 Approval complete!\n");
4051
+ console.log("\u26A0\uFE0F Could not save spender to wallet config");
4052
+ }
4053
+ });
4054
+ program.command("faucet").description("Request testnet tokens from faucet (Base Sepolia, Tempo Moderato, BNB Testnet, or Solana Devnet)").option("--chain <chain>", "Chain to get tokens on (base_sepolia, tempo_moderato, bnb_testnet, or solana_devnet)", "base_sepolia").option("--address <address>", "Wallet address (defaults to your wallet)").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (options) => {
2001
4055
  let address = options.address;
4056
+ const chain = options.chain?.toLowerCase() || "base_sepolia";
4057
+ if (!["base_sepolia", "tempo_moderato", "bnb_testnet", "solana_devnet"].includes(chain)) {
4058
+ console.log("\u274C Invalid chain. Use: base_sepolia, tempo_moderato, bnb_testnet, or solana_devnet");
4059
+ return;
4060
+ }
4061
+ if (chain === "solana_devnet") {
4062
+ if (!address) {
4063
+ address = getSolanaAddress(options.configDir);
4064
+ if (!address) {
4065
+ console.log("\u274C No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
4066
+ return;
4067
+ }
4068
+ }
4069
+ if (!isValidSolanaAddress(address)) {
4070
+ console.log("\u274C Invalid Solana address");
4071
+ return;
4072
+ }
4073
+ console.log("\n\u{1F6B0} Solana Devnet Faucet (Gasless Mode)\n");
4074
+ console.log(` Address: ${address}
4075
+ `);
4076
+ let usdcSuccess = false;
4077
+ try {
4078
+ console.log(" \u23F3 Requesting 1 USDC from faucet...");
4079
+ const FAUCET_API = process.env.MOLTSPAY_FAUCET_API || "https://moltspay.com/api/v1/faucet";
4080
+ const response = await fetch(FAUCET_API, {
4081
+ method: "POST",
4082
+ headers: { "Content-Type": "application/json" },
4083
+ body: JSON.stringify({ address, chain: "solana_devnet" })
4084
+ });
4085
+ const result = await response.json();
4086
+ if (!response.ok) {
4087
+ console.log(` \u26A0\uFE0F USDC faucet: ${result.error || "Request failed"}`);
4088
+ if (result.hint) console.log(` ${result.hint}`);
4089
+ if (result.retry_after) console.log(` Retry after: ${result.retry_after}`);
4090
+ } else {
4091
+ console.log(` \u2705 Received ${result.amount} USDC!`);
4092
+ console.log(` Transaction: ${result.explorer}`);
4093
+ if (result.faucet_balance) {
4094
+ console.log(` Faucet balance: ${result.faucet_balance} USDC remaining`);
4095
+ }
4096
+ usdcSuccess = true;
4097
+ }
4098
+ } catch (error) {
4099
+ console.log(` \u26A0\uFE0F USDC faucet error: ${error.message}`);
4100
+ }
4101
+ console.log("");
4102
+ if (usdcSuccess) {
4103
+ console.log("\u{1F4A1} Check your balance:");
4104
+ console.log(" npx moltspay status\n");
4105
+ } else {
4106
+ console.log("\u274C Faucet request failed. Try again in a few minutes.\n");
4107
+ }
4108
+ return;
4109
+ }
2002
4110
  if (!address) {
2003
4111
  const client = new MoltsPayClient({ configDir: options.configDir });
2004
4112
  if (client.isInitialized) {
@@ -2013,37 +4121,110 @@ program.command("faucet").description("Request testnet USDC from MoltsPay faucet
2013
4121
  return;
2014
4122
  }
2015
4123
  console.log("\n\u{1F6B0} MoltsPay Testnet Faucet\n");
2016
- console.log(` Requesting 1 USDC on Base Sepolia...`);
2017
- console.log(` Address: ${address}
4124
+ if (chain === "tempo_moderato") {
4125
+ console.log(` Requesting testnet tokens on Tempo Moderato...`);
4126
+ console.log(` Address: ${address}
2018
4127
  `);
2019
- try {
2020
- const FAUCET_API = process.env.MOLTSPAY_FAUCET_API || "https://moltspay.com/api/v1/faucet";
2021
- const response = await fetch(FAUCET_API, {
2022
- method: "POST",
2023
- headers: { "Content-Type": "application/json" },
2024
- body: JSON.stringify({ address })
2025
- });
2026
- const result = await response.json();
2027
- if (!response.ok) {
2028
- console.log(`\u274C ${result.error || "Request failed"}`);
2029
- if (result.hint) console.log(` ${result.hint}`);
2030
- if (result.retry_after) console.log(` Retry after: ${result.retry_after}`);
2031
- return;
4128
+ try {
4129
+ const TEMPO_FAUCET_API = "https://docs.tempo.xyz/api/faucet";
4130
+ const response = await fetch(TEMPO_FAUCET_API, {
4131
+ method: "POST",
4132
+ headers: { "Content-Type": "application/json" },
4133
+ body: JSON.stringify({ address })
4134
+ });
4135
+ const result = await response.json();
4136
+ if (response.ok && result.data && result.data.length > 0) {
4137
+ console.log(`\u2705 Received testnet tokens!
4138
+ `);
4139
+ console.log(` Tokens: pathUSD, AlphaUSD, BetaUSD, ThetaUSD (1M each)`);
4140
+ console.log(` Transactions:`);
4141
+ for (const tx of result.data) {
4142
+ console.log(` https://explore.testnet.tempo.xyz/tx/${tx.hash}`);
4143
+ }
4144
+ console.log("\n\u{1F4A1} Use these tokens to test MPP payments:");
4145
+ console.log(` npx moltspay pay <service-url> <service-id> --chain tempo_moderato
4146
+ `);
4147
+ } else {
4148
+ console.log(`\u274C ${result.error || "Faucet request failed"}`);
4149
+ console.log("\n Try again later or use Tempo Wallet: https://wallet.tempo.xyz\n");
4150
+ }
4151
+ } catch (error) {
4152
+ console.log(`\u274C ${error.message}`);
4153
+ console.log("\n Try Tempo Wallet instead: https://wallet.tempo.xyz\n");
2032
4154
  }
2033
- console.log(`\u2705 Received ${result.amount} USDC!
4155
+ } else if (chain === "bnb_testnet") {
4156
+ console.log(` Requesting 1 USDC on BNB Testnet...`);
4157
+ console.log(` Address: ${address}
2034
4158
  `);
2035
- console.log(` Transaction: ${result.transaction}`);
2036
- console.log(` Explorer: ${result.explorer}`);
2037
- console.log(` Faucet balance: ${result.faucet_balance} USDC remaining
4159
+ try {
4160
+ const FAUCET_API = process.env.MOLTSPAY_FAUCET_API || "https://moltspay.com/api/v1/faucet";
4161
+ const response = await fetch(FAUCET_API, {
4162
+ method: "POST",
4163
+ headers: { "Content-Type": "application/json" },
4164
+ body: JSON.stringify({ address, chain: "bnb_testnet" })
4165
+ });
4166
+ const result = await response.json();
4167
+ if (!response.ok) {
4168
+ console.log(`\u274C ${result.error || "Request failed"}`);
4169
+ if (result.hint) console.log(` ${result.hint}`);
4170
+ if (result.retry_after) console.log(` Retry after: ${result.retry_after}`);
4171
+ console.log("\n\u{1F4A1} Alternatively, get tokens manually:");
4172
+ console.log(` 1. Get test BNB: https://www.bnbchain.org/en/testnet-faucet`);
4173
+ console.log(` 2. Select "Peggy Tokens" -> USDC`);
4174
+ console.log(` 3. Enter: ${address}
2038
4175
  `);
2039
- console.log("\u{1F4A1} Use this USDC to test x402 payments:");
2040
- console.log(` npx moltspay pay <service-url> <service-id> --chain base_sepolia
4176
+ return;
4177
+ }
4178
+ console.log(`\u2705 Received ${result.amount} ${result.token || "USDC"} on ${result.chain_name || "BNB Testnet"}!
2041
4179
  `);
2042
- } catch (error) {
2043
- console.log(`\u274C ${error.message}`);
4180
+ console.log(` Transaction: ${result.explorer || `https://testnet.bscscan.com/tx/${result.transaction}`}`);
4181
+ if (result.faucet_balance) {
4182
+ console.log(` Faucet balance: ${result.faucet_balance} USDC`);
4183
+ }
4184
+ console.log("\n\u{1F4A1} Now you can test BNB payments:");
4185
+ console.log(` npx moltspay pay <service-url> <service-id> --chain bnb_testnet
4186
+ `);
4187
+ } catch (error) {
4188
+ console.log(`\u274C ${error.message}`);
4189
+ console.log("\n\u{1F4A1} Get tokens manually:");
4190
+ console.log(` 1. Get test BNB: https://www.bnbchain.org/en/testnet-faucet`);
4191
+ console.log(` 2. Select "Peggy Tokens" -> USDC`);
4192
+ console.log(` 3. Enter: ${address}
4193
+ `);
4194
+ }
4195
+ } else {
4196
+ console.log(` Requesting 1 USDC on Base Sepolia...`);
4197
+ console.log(` Address: ${address}
4198
+ `);
4199
+ try {
4200
+ const FAUCET_API = process.env.MOLTSPAY_FAUCET_API || "https://moltspay.com/api/v1/faucet";
4201
+ const response = await fetch(FAUCET_API, {
4202
+ method: "POST",
4203
+ headers: { "Content-Type": "application/json" },
4204
+ body: JSON.stringify({ address, chain: "base_sepolia" })
4205
+ });
4206
+ const result = await response.json();
4207
+ if (!response.ok) {
4208
+ console.log(`\u274C ${result.error || "Request failed"}`);
4209
+ if (result.hint) console.log(` ${result.hint}`);
4210
+ if (result.retry_after) console.log(` Retry after: ${result.retry_after}`);
4211
+ return;
4212
+ }
4213
+ console.log(`\u2705 Received ${result.amount} USDC!
4214
+ `);
4215
+ console.log(` Transaction: ${result.transaction}`);
4216
+ console.log(` Explorer: ${result.explorer}`);
4217
+ console.log(` Faucet balance: ${result.faucet_balance} USDC remaining
4218
+ `);
4219
+ console.log("\u{1F4A1} Use this USDC to test x402 payments:");
4220
+ console.log(` npx moltspay pay <service-url> <service-id> --chain base_sepolia
4221
+ `);
4222
+ } catch (error) {
4223
+ console.log(`\u274C ${error.message}`);
4224
+ }
2044
4225
  }
2045
4226
  });
2046
- program.command("status").description("Show wallet status and balance").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR).option("--json", "Output as JSON").action(async (options) => {
4227
+ program.command("status").description("Show wallet status and balance").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).option("--json", "Output as JSON").action(async (options) => {
2047
4228
  const client = new MoltsPayClient({ configDir: options.configDir });
2048
4229
  if (!client.isInitialized) {
2049
4230
  if (options.json) {
@@ -2060,29 +4241,138 @@ program.command("status").description("Show wallet status and balance").option("
2060
4241
  } catch (err) {
2061
4242
  console.error("Warning: Could not fetch balances:", err.message);
2062
4243
  }
4244
+ const solanaAddress = getSolanaAddress(options.configDir);
4245
+ let solanaBalances = {};
4246
+ if (solanaAddress) {
4247
+ try {
4248
+ solanaBalances.devnet = await getSolanaBalances(solanaAddress, "solana_devnet");
4249
+ } catch {
4250
+ }
4251
+ try {
4252
+ solanaBalances.mainnet = await getSolanaBalances(solanaAddress, "solana");
4253
+ } catch {
4254
+ }
4255
+ }
2063
4256
  if (options.json) {
2064
- console.log(JSON.stringify({
4257
+ const output = {
2065
4258
  address: client.address,
2066
4259
  balances: allBalances,
2067
4260
  limits: config.limits
2068
- }, null, 2));
4261
+ };
4262
+ if (solanaAddress) {
4263
+ output.solana = {
4264
+ address: solanaAddress,
4265
+ balances: solanaBalances
4266
+ };
4267
+ }
4268
+ console.log(JSON.stringify(output, null, 2));
2069
4269
  } else {
2070
4270
  console.log("\n\u{1F4CA} MoltsPay Wallet Status\n");
2071
4271
  console.log(` Address: ${client.address}`);
2072
4272
  console.log("");
2073
4273
  console.log(" Balances:");
2074
4274
  for (const [chainName, balance] of Object.entries(allBalances)) {
2075
- const chainLabel = chainName.charAt(0).toUpperCase() + chainName.slice(1);
2076
- console.log(` ${chainLabel.padEnd(10)} ${balance.usdc.toFixed(2)} USDC | ${balance.usdt.toFixed(2)} USDT`);
4275
+ let chainLabel;
4276
+ if (chainName === "base_sepolia") {
4277
+ chainLabel = "Base Sepolia";
4278
+ } else if (chainName === "tempo_moderato") {
4279
+ chainLabel = "Tempo Moderato";
4280
+ } else {
4281
+ chainLabel = chainName.charAt(0).toUpperCase() + chainName.slice(1);
4282
+ }
4283
+ if (chainName === "tempo_moderato" && balance.tempo) {
4284
+ const tempo = balance.tempo;
4285
+ const nativeStr = balance.native > 1e12 ? balance.native.toExponential(2) : balance.native.toFixed(2);
4286
+ console.log(` ${chainLabel}:`);
4287
+ console.log(` Native: ${nativeStr} TEMPO (for gas)`);
4288
+ console.log(` pathUSD: ${tempo.pathUSD.toFixed(2)}`);
4289
+ console.log(` alphaUSD: ${tempo.alphaUSD.toFixed(2)}`);
4290
+ console.log(` betaUSD: ${tempo.betaUSD.toFixed(2)}`);
4291
+ console.log(` thetaUSD: ${tempo.thetaUSD.toFixed(2)}`);
4292
+ } else if (chainName === "bnb" || chainName === "bnb_testnet") {
4293
+ const bnbBalance = balance.native;
4294
+ const bnbWarning = bnbBalance < 5e-4 ? " \u26A0\uFE0F Low gas" : "";
4295
+ console.log(` ${chainLabel.padEnd(14)} ${balance.usdc.toFixed(2)} USDC | ${balance.usdt.toFixed(2)} USDT | ${bnbBalance.toFixed(4)} BNB${bnbWarning}`);
4296
+ } else {
4297
+ console.log(` ${chainLabel.padEnd(14)} ${balance.usdc.toFixed(2)} USDC | ${balance.usdt.toFixed(2)} USDT`);
4298
+ }
4299
+ }
4300
+ const address = client.address;
4301
+ let bnbApprovalStatus = null;
4302
+ let bnbTestnetApprovalStatus = null;
4303
+ try {
4304
+ if (allBalances["bnb"]) {
4305
+ bnbApprovalStatus = await checkBNBApprovals(address, "bnb", options.configDir);
4306
+ }
4307
+ if (allBalances["bnb_testnet"]) {
4308
+ bnbTestnetApprovalStatus = await checkBNBApprovals(address, "bnb_testnet", options.configDir);
4309
+ }
4310
+ } catch {
4311
+ }
4312
+ if (bnbApprovalStatus || bnbTestnetApprovalStatus) {
4313
+ console.log("");
4314
+ console.log(" BNB Approvals (pay-for-success):");
4315
+ if (bnbApprovalStatus) {
4316
+ if (!bnbApprovalStatus.spender) {
4317
+ console.log(" BNB: \u26A0\uFE0F No spender configured");
4318
+ console.log(" \u2514\u2500 Run a payment first, or: npx moltspay approve --chain bnb --spender <address>");
4319
+ } else {
4320
+ const status = bnbApprovalStatus.usdt && bnbApprovalStatus.usdc ? "\u2705" : "\u26A0\uFE0F";
4321
+ const tokens = [
4322
+ bnbApprovalStatus.usdt ? "USDT\u2713" : "USDT\u2717",
4323
+ bnbApprovalStatus.usdc ? "USDC\u2713" : "USDC\u2717"
4324
+ ].join(", ");
4325
+ console.log(` BNB: ${status} ${tokens}`);
4326
+ const bnbNative = allBalances["bnb"]?.native || 0;
4327
+ if (!bnbApprovalStatus.usdc && !bnbApprovalStatus.usdt && bnbNative < 5e-4) {
4328
+ console.log(" \u26A0\uFE0F Need ~0.001 BNB for first approval tx. Get from exchange.");
4329
+ }
4330
+ }
4331
+ }
4332
+ if (bnbTestnetApprovalStatus) {
4333
+ if (!bnbTestnetApprovalStatus.spender) {
4334
+ console.log(" BNB Testnet: \u26A0\uFE0F No spender configured");
4335
+ console.log(" \u2514\u2500 Run a payment first, or: npx moltspay approve --chain bnb_testnet --spender <address>");
4336
+ } else {
4337
+ const status = bnbTestnetApprovalStatus.usdt && bnbTestnetApprovalStatus.usdc ? "\u2705" : "\u26A0\uFE0F";
4338
+ const tokens = [
4339
+ bnbTestnetApprovalStatus.usdt ? "USDT\u2713" : "USDT\u2717",
4340
+ bnbTestnetApprovalStatus.usdc ? "USDC\u2713" : "USDC\u2717"
4341
+ ].join(", ");
4342
+ console.log(` BNB Testnet: ${status} ${tokens}`);
4343
+ const tbnbNative = allBalances["bnb_testnet"]?.native || 0;
4344
+ if (!bnbTestnetApprovalStatus.usdc && !bnbTestnetApprovalStatus.usdt && tbnbNative < 5e-4) {
4345
+ console.log(" \u26A0\uFE0F Need tBNB for approval. Run: npx moltspay faucet --chain bnb_testnet");
4346
+ }
4347
+ }
4348
+ }
2077
4349
  }
2078
4350
  console.log("");
2079
4351
  console.log(" Spending Limits:");
2080
4352
  console.log(` Per Transaction: $${config.limits.maxPerTx}`);
2081
4353
  console.log(` Daily: $${config.limits.maxPerDay}`);
4354
+ const solanaAddress2 = getSolanaAddress(options.configDir);
4355
+ if (solanaAddress2) {
4356
+ console.log("");
4357
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
4358
+ console.log(` \u{1F7E3} Solana: ${solanaAddress2}`);
4359
+ try {
4360
+ const devnetBalances = await getSolanaBalances(solanaAddress2, "solana_devnet");
4361
+ console.log(` Devnet: ${devnetBalances.sol.toFixed(4)} SOL | ${devnetBalances.usdc.toFixed(2)} USDC`);
4362
+ } catch (err) {
4363
+ console.log(` Devnet: (unable to fetch)`);
4364
+ }
4365
+ try {
4366
+ const mainnetBalances = await getSolanaBalances(solanaAddress2, "solana");
4367
+ console.log(` Mainnet: ${mainnetBalances.sol.toFixed(4)} SOL | ${mainnetBalances.usdc.toFixed(2)} USDC`);
4368
+ } catch (err) {
4369
+ console.log(` Mainnet: (unable to fetch)`);
4370
+ }
4371
+ }
2082
4372
  console.log("");
2083
4373
  }
2084
4374
  });
2085
- program.command("list").description("List recent transactions").option("--days <n>", "Number of days to look back", "7").option("--chain <chain>", "Chain to query (base, polygon, base_sepolia, or all)", "all").option("--limit <n>", "Max transactions to show", "20").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR).action(async (options) => {
4375
+ program.command("list").description("List recent transactions").option("--days <n>", "Number of days to look back", "7").option("--chain <chain>", "Chain to query (base, polygon, base_sepolia, or all)", "all").option("--limit <n>", "Max transactions to show", "20").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (options) => {
2086
4376
  const client = new MoltsPayClient({ configDir: options.configDir });
2087
4377
  if (!client.isInitialized) {
2088
4378
  console.log("\u274C Not initialized. Run: npx moltspay init");
@@ -2091,8 +4381,8 @@ program.command("list").description("List recent transactions").option("--days <
2091
4381
  const days = parseInt(options.days) || 7;
2092
4382
  const limit = parseInt(options.limit) || 20;
2093
4383
  const chain = options.chain?.toLowerCase() || "all";
2094
- if (!["base", "polygon", "base_sepolia", "all"].includes(chain)) {
2095
- console.log("\u274C Invalid chain. Use: base, polygon, base_sepolia, or all");
4384
+ if (!["base", "polygon", "base_sepolia", "tempo_moderato", "all"].includes(chain)) {
4385
+ console.log("\u274C Invalid chain. Use: base, polygon, base_sepolia, tempo_moderato, or all");
2096
4386
  return;
2097
4387
  }
2098
4388
  const wallet = client.address;
@@ -2112,9 +4402,16 @@ program.command("list").description("List recent transactions").option("--days <
2112
4402
  api: "https://base-sepolia.blockscout.com/api/v2",
2113
4403
  usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
2114
4404
  name: "Base Sepolia"
4405
+ },
4406
+ // Tempo explorer doesn't have public API yet
4407
+ tempo_moderato: {
4408
+ api: "",
4409
+ // No API available
4410
+ usdc: "0x20c0000000000000000000000000000000000000",
4411
+ name: "Tempo Moderato"
2115
4412
  }
2116
4413
  };
2117
- const chainsToQuery = chain === "all" ? ["base", "polygon", "base_sepolia"] : [chain];
4414
+ const chainsToQuery = chain === "all" ? ["base", "polygon", "base_sepolia", "tempo_moderato"] : [chain];
2118
4415
  console.log(`
2119
4416
  \u{1F4DC} Transactions (last ${days} day${days > 1 ? "s" : ""})
2120
4417
  `);
@@ -2122,27 +4419,136 @@ program.command("list").description("List recent transactions").option("--days <
2122
4419
  for (const c of chainsToQuery) {
2123
4420
  const explorer = explorers[c];
2124
4421
  try {
2125
- const url = `${explorer.api}/addresses/${wallet}/token-transfers?type=ERC-20&token=${explorer.usdc}`;
2126
- const response = await fetch(url);
2127
- const data = await response.json();
2128
- if (data.items && Array.isArray(data.items)) {
2129
- for (const tx of data.items) {
2130
- const timestamp = new Date(tx.timestamp).getTime();
2131
- if (timestamp < cutoffTime) continue;
2132
- const isIncoming = tx.to.hash.toLowerCase() === wallet.toLowerCase();
2133
- const decimals = parseInt(tx.total.decimals) || 6;
2134
- allTxns.push({
2135
- chain: c,
2136
- timestamp,
2137
- type: isIncoming ? "IN" : "OUT",
2138
- amount: parseInt(tx.total.value) / Math.pow(10, decimals),
2139
- other: isIncoming ? tx.from.hash : tx.to.hash,
2140
- hash: tx.transaction_hash
2141
- });
4422
+ if (c === "tempo_moderato") {
4423
+ const tempoTokens = [
4424
+ { address: "0x20c0000000000000000000000000000000000000", name: "pathUSD" },
4425
+ { address: "0x20c0000000000000000000000000000000000001", name: "alphaUSD" },
4426
+ { address: "0x20c0000000000000000000000000000000000002", name: "betaUSD" },
4427
+ { address: "0x20c0000000000000000000000000000000000003", name: "thetaUSD" }
4428
+ ];
4429
+ const transferTopic = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
4430
+ const walletTopic = "0x000000000000000000000000" + wallet.toLowerCase().slice(2);
4431
+ let latestBlock = 0;
4432
+ for (let attempt = 0; attempt < 3; attempt++) {
4433
+ try {
4434
+ const blockRes = await fetch("https://rpc.moderato.tempo.xyz", {
4435
+ method: "POST",
4436
+ headers: { "Content-Type": "application/json" },
4437
+ body: JSON.stringify({ jsonrpc: "2.0", method: "eth_blockNumber", params: [], id: 1 })
4438
+ });
4439
+ const blockData = await blockRes.json();
4440
+ if (blockData.result) {
4441
+ latestBlock = parseInt(blockData.result, 16);
4442
+ break;
4443
+ }
4444
+ } catch (e) {
4445
+ if (attempt === 2) throw e;
4446
+ await new Promise((r) => setTimeout(r, 500));
4447
+ }
4448
+ }
4449
+ if (latestBlock === 0) {
4450
+ console.log(" \u26A0\uFE0F Tempo Moderato: Could not get latest block");
4451
+ continue;
4452
+ }
4453
+ const maxBlocks = 1e5;
4454
+ const blocksPerDay = 172800;
4455
+ const requestedBlocks = blocksPerDay * days;
4456
+ const actualBlocks = Math.min(requestedBlocks, maxBlocks);
4457
+ const fromBlock = "0x" + Math.max(0, latestBlock - actualBlocks).toString(16);
4458
+ const toBlock = "0x" + latestBlock.toString(16);
4459
+ if (requestedBlocks > maxBlocks) {
4460
+ console.log(` \u2139\uFE0F Tempo: querying last ~14 hours (RPC limit: 100k blocks)`);
4461
+ }
4462
+ for (const token of tempoTokens) {
4463
+ try {
4464
+ const inRes = await fetch("https://rpc.moderato.tempo.xyz", {
4465
+ method: "POST",
4466
+ headers: { "Content-Type": "application/json" },
4467
+ body: JSON.stringify({
4468
+ jsonrpc: "2.0",
4469
+ method: "eth_getLogs",
4470
+ params: [{ fromBlock, toBlock, address: token.address, topics: [transferTopic, null, walletTopic] }],
4471
+ id: 1
4472
+ })
4473
+ });
4474
+ const inData = await inRes.json();
4475
+ if (inData.error) {
4476
+ console.log(` \u26A0\uFE0F ${token.name}: ${inData.error.message}`);
4477
+ continue;
4478
+ }
4479
+ if (inData.result && Array.isArray(inData.result)) {
4480
+ for (const log of inData.result) {
4481
+ const timestamp = parseInt(log.blockTimestamp, 16) * 1e3;
4482
+ if (timestamp < cutoffTime) continue;
4483
+ const amount = parseInt(log.data, 16) / 1e6;
4484
+ const from = "0x" + log.topics[1].slice(26);
4485
+ allTxns.push({
4486
+ chain: c,
4487
+ timestamp,
4488
+ type: "IN",
4489
+ amount,
4490
+ other: from,
4491
+ hash: log.transactionHash,
4492
+ token: token.name
4493
+ });
4494
+ }
4495
+ }
4496
+ const outRes = await fetch("https://rpc.moderato.tempo.xyz", {
4497
+ method: "POST",
4498
+ headers: { "Content-Type": "application/json" },
4499
+ body: JSON.stringify({
4500
+ jsonrpc: "2.0",
4501
+ method: "eth_getLogs",
4502
+ params: [{ fromBlock, toBlock, address: token.address, topics: [transferTopic, walletTopic, null] }],
4503
+ id: 1
4504
+ })
4505
+ });
4506
+ const outData = await outRes.json();
4507
+ if (outData.result && Array.isArray(outData.result)) {
4508
+ for (const log of outData.result) {
4509
+ const timestamp = parseInt(log.blockTimestamp, 16) * 1e3;
4510
+ if (timestamp < cutoffTime) continue;
4511
+ const amount = parseInt(log.data, 16) / 1e6;
4512
+ const to = "0x" + log.topics[2].slice(26);
4513
+ allTxns.push({
4514
+ chain: c,
4515
+ timestamp,
4516
+ type: "OUT",
4517
+ amount,
4518
+ other: to,
4519
+ hash: log.transactionHash,
4520
+ token: token.name
4521
+ });
4522
+ }
4523
+ }
4524
+ } catch (tokenError) {
4525
+ continue;
4526
+ }
4527
+ }
4528
+ } else {
4529
+ const url = `${explorer.api}/addresses/${wallet}/token-transfers?type=ERC-20&token=${explorer.usdc}`;
4530
+ const response = await fetch(url);
4531
+ const data = await response.json();
4532
+ if (data.items && Array.isArray(data.items)) {
4533
+ for (const tx of data.items) {
4534
+ const timestamp = new Date(tx.timestamp).getTime();
4535
+ if (timestamp < cutoffTime) continue;
4536
+ const isIncoming = tx.to.hash.toLowerCase() === wallet.toLowerCase();
4537
+ const decimals = parseInt(tx.total.decimals) || 6;
4538
+ allTxns.push({
4539
+ chain: c,
4540
+ timestamp,
4541
+ type: isIncoming ? "IN" : "OUT",
4542
+ amount: parseInt(tx.total.value) / Math.pow(10, decimals),
4543
+ other: isIncoming ? tx.from.hash : tx.to.hash,
4544
+ hash: tx.transaction_hash
4545
+ });
4546
+ }
2142
4547
  }
2143
4548
  }
2144
4549
  } catch (error) {
2145
- console.log(` \u26A0\uFE0F ${explorer.name}: API error`);
4550
+ const errMsg = error instanceof Error ? error.message : String(error);
4551
+ console.log(` \u26A0\uFE0F ${explorer.name}: ${errMsg}`);
2146
4552
  }
2147
4553
  }
2148
4554
  allTxns.sort((a, b) => b.timestamp - a.timestamp);
@@ -2155,8 +4561,12 @@ program.command("list").description("List recent transactions").option("--days <
2155
4561
  const color = tx.type === "IN" ? "\x1B[32m" : "\x1B[31m";
2156
4562
  const reset = "\x1B[0m";
2157
4563
  const date = new Date(tx.timestamp).toISOString().slice(5, 16).replace("T", " ");
2158
- const chainTag = chain === "all" ? `[${tx.chain.toUpperCase()}] ` : "";
2159
- console.log(` ${color}${sign}${tx.amount.toFixed(2)} USDC${reset} | ${chainTag}${tx.type === "IN" ? "from" : "to"} ${tx.other.slice(0, 10)}...${tx.other.slice(-4)} | ${date}`);
4564
+ let chainLabel = tx.chain.toUpperCase();
4565
+ if (tx.chain === "tempo_moderato") chainLabel = "TEMPO";
4566
+ else if (tx.chain === "base_sepolia") chainLabel = "BASE_SEPOLIA";
4567
+ const chainTag = chain === "all" ? `[${chainLabel}] ` : "";
4568
+ const tokenName = tx.token || "USDC";
4569
+ console.log(` ${color}${sign}${tx.amount.toFixed(2)} ${tokenName}${reset} | ${chainTag}${tx.type === "IN" ? "from" : "to"} ${tx.other.slice(0, 10)}...${tx.other.slice(-4)} | ${date}`);
2160
4570
  }
2161
4571
  const inTotal = allTxns.filter((t) => t.type === "IN").reduce((s, t) => s + t.amount, 0);
2162
4572
  const outTotal = allTxns.filter((t) => t.type === "OUT").reduce((s, t) => s + t.amount, 0);
@@ -2165,39 +4575,88 @@ program.command("list").description("List recent transactions").option("--days <
2165
4575
  `);
2166
4576
  }
2167
4577
  });
2168
- program.command("services <url>").description("List services from a provider").option("--json", "Output as JSON").action(async (url, options) => {
4578
+ program.command("services [url]").description("List services from registry or a specific provider").option("-q, --query <keyword>", "Search by keyword (name, description, tags)").option("--max-price <price>", "Maximum price in USD").option("--type <type>", "Filter by type: api_service | file_download").option("--tag <tag>", "Filter by tag").option("--json", "Output as JSON").action(async (url, options) => {
4579
+ const MOLTSPAY_REGISTRY = "https://moltspay.com";
2169
4580
  try {
2170
- const client = new MoltsPayClient();
2171
- const services = await client.getServices(url);
4581
+ let services;
4582
+ let isRegistry = false;
4583
+ if (url) {
4584
+ const client = new MoltsPayClient();
4585
+ services = await client.getServices(url);
4586
+ } else {
4587
+ isRegistry = true;
4588
+ const params = new URLSearchParams();
4589
+ if (options.query) params.set("q", options.query);
4590
+ if (options.maxPrice) params.set("maxPrice", options.maxPrice);
4591
+ if (options.type) params.set("type", options.type);
4592
+ if (options.tag) params.set("tag", options.tag);
4593
+ const queryString = params.toString();
4594
+ const registryUrl = `${MOLTSPAY_REGISTRY}/registry/services${queryString ? "?" + queryString : ""}`;
4595
+ const res = await fetch(registryUrl);
4596
+ if (!res.ok) {
4597
+ throw new Error(`Registry request failed: ${res.status}`);
4598
+ }
4599
+ services = await res.json();
4600
+ }
2172
4601
  if (options.json) {
2173
4602
  console.log(JSON.stringify(services, null, 2));
2174
4603
  } else {
2175
- if (services.provider) {
2176
- console.log(`
2177
- \u{1F3EA} ${services.provider.name}
4604
+ const serviceList = services.services || [];
4605
+ if (isRegistry) {
4606
+ if (options.query) {
4607
+ console.log(`
4608
+ \u{1F50D} Search: "${options.query}" (${serviceList.length} results)
4609
+ `);
4610
+ } else {
4611
+ const filters = [];
4612
+ if (options.maxPrice) filters.push(`max $${options.maxPrice}`);
4613
+ if (options.type) filters.push(options.type);
4614
+ if (options.tag) filters.push(`#${options.tag}`);
4615
+ const filterStr = filters.length > 0 ? ` (${filters.join(", ")})` : "";
4616
+ console.log(`
4617
+ \u{1F50D} MoltsPay Registry${filterStr} - ${serviceList.length} services
2178
4618
  `);
2179
- console.log(` ${services.provider.description || ""}`);
2180
- console.log(` Wallet: ${services.provider.wallet}`);
2181
- const chains = services.provider.chains ? Array.isArray(services.provider.chains) ? services.provider.chains.map((c) => typeof c === "string" ? c : c.chain).join(", ") : services.provider.chains : services.provider.chain || "base";
2182
- console.log(` Chains: ${chains}`);
4619
+ }
4620
+ for (const svc of serviceList) {
4621
+ const name = (svc.name || svc.id).slice(0, 30).padEnd(30);
4622
+ const price = `$${svc.price}`.padEnd(8);
4623
+ const type = (svc.type || "unknown").padEnd(14);
4624
+ const provider = `@${svc.provider?.username || "unknown"}`;
4625
+ console.log(` ${name} ${price} ${type} ${provider}`);
4626
+ }
4627
+ if (serviceList.length > 0) {
4628
+ console.log(`
4629
+ \u{1F4A1} Use: moltspay pay <provider-url> <service-id>
4630
+ `);
4631
+ }
2183
4632
  } else {
2184
- console.log(`
2185
- \u{1F3EA} MoltsPay Service Registry
4633
+ if (services.provider) {
4634
+ console.log(`
4635
+ \u{1F3EA} ${services.provider.name}
4636
+ `);
4637
+ console.log(` ${services.provider.description || ""}`);
4638
+ console.log(` Wallet: ${services.provider.wallet}`);
4639
+ const chains = services.provider.chains ? Array.isArray(services.provider.chains) ? services.provider.chains.map((c) => typeof c === "string" ? c : c.chain).join(", ") : services.provider.chains : services.provider.chain || "base";
4640
+ console.log(` Chains: ${chains}`);
4641
+ } else {
4642
+ console.log(`
4643
+ \u{1F3EA} Provider Services
2186
4644
  `);
2187
- console.log(` ${services.services?.length || 0} services available`);
2188
- }
2189
- console.log("\n\u{1F4E6} Services:\n");
2190
- for (const svc of services.services) {
2191
- const status = svc.available !== false ? "\u2705" : "\u274C";
2192
- console.log(` ${status} ${svc.id || svc.name}`);
2193
- console.log(` ${svc.name} - $${svc.price} ${svc.currency}`);
2194
- if (svc.description) {
2195
- console.log(` ${svc.description}`);
4645
+ console.log(` ${serviceList.length} services available`);
2196
4646
  }
2197
- if (svc.provider && !services.provider) {
2198
- console.log(` Provider: ${svc.provider.name || svc.provider.username}`);
4647
+ console.log("\n\u{1F4E6} Services:\n");
4648
+ for (const svc of serviceList) {
4649
+ const status = svc.available !== false ? "\u2705" : "\u274C";
4650
+ console.log(` ${status} ${svc.id || svc.name}`);
4651
+ console.log(` ${svc.name} - $${svc.price} ${svc.currency}`);
4652
+ if (svc.description) {
4653
+ console.log(` ${svc.description}`);
4654
+ }
4655
+ if (svc.provider && !services.provider) {
4656
+ console.log(` Provider: ${svc.provider.name || svc.provider.username}`);
4657
+ }
4658
+ console.log("");
2199
4659
  }
2200
- console.log("");
2201
4660
  }
2202
4661
  }
2203
4662
  } catch (err) {
@@ -2216,18 +4675,18 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
2216
4675
  const handlers = /* @__PURE__ */ new Map();
2217
4676
  let provider = null;
2218
4677
  for (const inputPath of allPaths) {
2219
- const resolvedPath = (0, import_path2.resolve)(inputPath);
4678
+ const resolvedPath = (0, import_path3.resolve)(inputPath);
2220
4679
  let manifestPath;
2221
4680
  let skillDir;
2222
4681
  let isSkillDir = false;
2223
- if ((0, import_fs4.existsSync)((0, import_path2.join)(resolvedPath, "moltspay.services.json"))) {
2224
- manifestPath = (0, import_path2.join)(resolvedPath, "moltspay.services.json");
4682
+ if ((0, import_fs5.existsSync)((0, import_path3.join)(resolvedPath, "moltspay.services.json"))) {
4683
+ manifestPath = (0, import_path3.join)(resolvedPath, "moltspay.services.json");
2225
4684
  skillDir = resolvedPath;
2226
4685
  isSkillDir = true;
2227
- } else if ((0, import_fs4.existsSync)(resolvedPath) && resolvedPath.endsWith(".json")) {
4686
+ } else if ((0, import_fs5.existsSync)(resolvedPath) && resolvedPath.endsWith(".json")) {
2228
4687
  manifestPath = resolvedPath;
2229
- skillDir = (0, import_path2.dirname)(resolvedPath);
2230
- } else if ((0, import_fs4.existsSync)(resolvedPath)) {
4688
+ skillDir = (0, import_path3.dirname)(resolvedPath);
4689
+ } else if ((0, import_fs5.existsSync)(resolvedPath)) {
2231
4690
  console.error(`\u274C No moltspay.services.json found in: ${resolvedPath}`);
2232
4691
  continue;
2233
4692
  } else {
@@ -2236,25 +4695,25 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
2236
4695
  }
2237
4696
  console.log(`\u{1F4E6} Loading: ${manifestPath}`);
2238
4697
  try {
2239
- const manifestContent = JSON.parse((0, import_fs4.readFileSync)(manifestPath, "utf-8"));
4698
+ const manifestContent = JSON.parse((0, import_fs5.readFileSync)(manifestPath, "utf-8"));
2240
4699
  if (!provider) {
2241
4700
  provider = manifestContent.provider;
2242
4701
  }
2243
4702
  let skillModule = null;
2244
4703
  if (isSkillDir) {
2245
4704
  let entryPoint = "index.js";
2246
- const pkgJsonPath = (0, import_path2.join)(skillDir, "package.json");
2247
- if ((0, import_fs4.existsSync)(pkgJsonPath)) {
4705
+ const pkgJsonPath = (0, import_path3.join)(skillDir, "package.json");
4706
+ if ((0, import_fs5.existsSync)(pkgJsonPath)) {
2248
4707
  try {
2249
- const pkgJson = JSON.parse((0, import_fs4.readFileSync)(pkgJsonPath, "utf-8"));
4708
+ const pkgJson = JSON.parse((0, import_fs5.readFileSync)(pkgJsonPath, "utf-8"));
2250
4709
  if (pkgJson.main) {
2251
4710
  entryPoint = pkgJson.main;
2252
4711
  }
2253
4712
  } catch {
2254
4713
  }
2255
4714
  }
2256
- const modulePath = (0, import_path2.join)(skillDir, entryPoint);
2257
- if ((0, import_fs4.existsSync)(modulePath)) {
4715
+ const modulePath = (0, import_path3.join)(skillDir, entryPoint);
4716
+ if ((0, import_fs5.existsSync)(modulePath)) {
2258
4717
  try {
2259
4718
  skillModule = await import(modulePath);
2260
4719
  console.log(` \u2705 Loaded module: ${modulePath}`);
@@ -2332,8 +4791,8 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
2332
4791
  provider,
2333
4792
  services: allServices
2334
4793
  };
2335
- const tempManifestPath = (0, import_path2.join)(DEFAULT_CONFIG_DIR, "combined-manifest.json");
2336
- (0, import_fs4.writeFileSync)(tempManifestPath, JSON.stringify(combinedManifest, null, 2));
4794
+ const tempManifestPath = (0, import_path3.join)(DEFAULT_CONFIG_DIR2, "combined-manifest.json");
4795
+ (0, import_fs5.writeFileSync)(tempManifestPath, JSON.stringify(combinedManifest, null, 2));
2337
4796
  console.log(`
2338
4797
  \u{1F4CB} Combined manifest: ${allServices.length} services`);
2339
4798
  console.log(` Provider: ${provider.name}`);
@@ -2346,12 +4805,12 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
2346
4805
  server.skill(serviceId, handler);
2347
4806
  }
2348
4807
  const pidData = { pid: process.pid, port, paths: allPaths };
2349
- (0, import_fs4.writeFileSync)(PID_FILE, JSON.stringify(pidData, null, 2));
4808
+ (0, import_fs5.writeFileSync)(PID_FILE, JSON.stringify(pidData, null, 2));
2350
4809
  server.listen(port);
2351
4810
  const cleanup = () => {
2352
4811
  try {
2353
- if ((0, import_fs4.existsSync)(PID_FILE)) (0, import_fs4.unlinkSync)(PID_FILE);
2354
- if ((0, import_fs4.existsSync)(tempManifestPath)) (0, import_fs4.unlinkSync)(tempManifestPath);
4812
+ if ((0, import_fs5.existsSync)(PID_FILE)) (0, import_fs5.unlinkSync)(PID_FILE);
4813
+ if ((0, import_fs5.existsSync)(tempManifestPath)) (0, import_fs5.unlinkSync)(tempManifestPath);
2355
4814
  } catch {
2356
4815
  }
2357
4816
  };
@@ -2372,12 +4831,12 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
2372
4831
  }
2373
4832
  });
2374
4833
  program.command("stop").description("Stop the running MoltsPay server").action(async () => {
2375
- if (!(0, import_fs4.existsSync)(PID_FILE)) {
4834
+ if (!(0, import_fs5.existsSync)(PID_FILE)) {
2376
4835
  console.log("\u274C No running server found (no PID file)");
2377
4836
  process.exit(1);
2378
4837
  }
2379
4838
  try {
2380
- const pidData = JSON.parse((0, import_fs4.readFileSync)(PID_FILE, "utf-8"));
4839
+ const pidData = JSON.parse((0, import_fs5.readFileSync)(PID_FILE, "utf-8"));
2381
4840
  const { pid, port, manifest } = pidData;
2382
4841
  console.log(`
2383
4842
  \u{1F6D1} Stopping MoltsPay Server
@@ -2390,7 +4849,7 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
2390
4849
  process.kill(pid, 0);
2391
4850
  } catch {
2392
4851
  console.log("\u26A0\uFE0F Process not running, cleaning up PID file...");
2393
- (0, import_fs4.unlinkSync)(PID_FILE);
4852
+ (0, import_fs5.unlinkSync)(PID_FILE);
2394
4853
  process.exit(0);
2395
4854
  }
2396
4855
  process.kill(pid, "SIGTERM");
@@ -2402,8 +4861,8 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
2402
4861
  process.kill(pid, "SIGKILL");
2403
4862
  } catch {
2404
4863
  }
2405
- if ((0, import_fs4.existsSync)(PID_FILE)) {
2406
- (0, import_fs4.unlinkSync)(PID_FILE);
4864
+ if ((0, import_fs5.existsSync)(PID_FILE)) {
4865
+ (0, import_fs5.unlinkSync)(PID_FILE);
2407
4866
  }
2408
4867
  console.log("\u2705 Server stopped\n");
2409
4868
  } catch (err) {
@@ -2411,8 +4870,8 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
2411
4870
  process.exit(1);
2412
4871
  }
2413
4872
  });
2414
- program.command("pay <server> <service> [params]").description("Pay for a service and get the result").option("--prompt <text>", "Prompt for the service").option("--image <path>", "Image URL or local file path").option("--token <token>", "Token to pay with (USDC or USDT)", "USDC").option("--chain <chain>", "Chain to pay on (base, polygon, or base_sepolia). Required if server accepts multiple chains.").option("--json", "Output raw JSON only").action(async (server, service, paramsJson, options) => {
2415
- const client = new MoltsPayClient();
4873
+ program.command("pay <server> <service> [params]").description("Pay for a service and get the result").option("--prompt <text>", "Prompt for the service").option("--image <path>", "Image URL or local file path").option("--token <token>", "Token to pay with (USDC or USDT)", "USDC").option("--chain <chain>", "Chain to pay on (base, polygon, base_sepolia, tempo_moderato, solana, or solana_devnet).").option("--config-dir <dir>", "Config directory with wallet.json", DEFAULT_CONFIG_DIR2).option("--json", "Output raw JSON only").action(async (server, service, paramsJson, options) => {
4874
+ const client = new MoltsPayClient({ configDir: options.configDir });
2416
4875
  if (!client.isInitialized) {
2417
4876
  console.error("\u274C Wallet not initialized. Run: npx moltspay init");
2418
4877
  process.exit(1);
@@ -2432,22 +4891,19 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
2432
4891
  if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
2433
4892
  params.image_url = imagePath;
2434
4893
  } else {
2435
- const filePath = (0, import_path2.resolve)(imagePath);
2436
- if (!(0, import_fs4.existsSync)(filePath)) {
4894
+ const filePath = (0, import_path3.resolve)(imagePath);
4895
+ if (!(0, import_fs5.existsSync)(filePath)) {
2437
4896
  console.error(`\u274C Image file not found: ${filePath}`);
2438
4897
  process.exit(1);
2439
4898
  }
2440
- const imageData = (0, import_fs4.readFileSync)(filePath);
4899
+ const imageData = (0, import_fs5.readFileSync)(filePath);
2441
4900
  params.image_base64 = imageData.toString("base64");
2442
4901
  }
2443
4902
  }
2444
- if (!params.prompt) {
2445
- console.error("\u274C Missing prompt. Use --prompt or pass JSON params");
2446
- process.exit(1);
2447
- }
4903
+ const supportedPayChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
2448
4904
  const chain = options.chain?.toLowerCase();
2449
- if (chain && !["base", "polygon", "base_sepolia"].includes(chain)) {
2450
- console.error(`\u274C Unknown chain: ${chain}. Supported: base, polygon, base_sepolia`);
4905
+ if (chain && !supportedPayChains.includes(chain)) {
4906
+ console.error(`\u274C Unknown chain: ${chain}. Supported: ${supportedPayChains.join(", ")}`);
2451
4907
  process.exit(1);
2452
4908
  }
2453
4909
  const imageDisplay = params.image_url || (params.image_base64 ? `[local file: ${options.image}]` : null);
@@ -2499,11 +4955,11 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
2499
4955
  }
2500
4956
  });
2501
4957
  program.command("validate <path>").description("Validate a moltspay.services.json file against the schema").action(async (inputPath) => {
2502
- const resolvedPath = (0, import_path2.resolve)(inputPath);
4958
+ const resolvedPath = (0, import_path3.resolve)(inputPath);
2503
4959
  let manifestPath;
2504
- if ((0, import_fs4.existsSync)((0, import_path2.join)(resolvedPath, "moltspay.services.json"))) {
2505
- manifestPath = (0, import_path2.join)(resolvedPath, "moltspay.services.json");
2506
- } else if (resolvedPath.endsWith(".json") && (0, import_fs4.existsSync)(resolvedPath)) {
4960
+ if ((0, import_fs5.existsSync)((0, import_path3.join)(resolvedPath, "moltspay.services.json"))) {
4961
+ manifestPath = (0, import_path3.join)(resolvedPath, "moltspay.services.json");
4962
+ } else if (resolvedPath.endsWith(".json") && (0, import_fs5.existsSync)(resolvedPath)) {
2507
4963
  manifestPath = resolvedPath;
2508
4964
  } else {
2509
4965
  console.error(`\u274C Not found: ${resolvedPath}`);
@@ -2513,7 +4969,7 @@ program.command("validate <path>").description("Validate a moltspay.services.jso
2513
4969
  \u{1F4CB} Validating: ${manifestPath}
2514
4970
  `);
2515
4971
  try {
2516
- const content = JSON.parse((0, import_fs4.readFileSync)(manifestPath, "utf-8"));
4972
+ const content = JSON.parse((0, import_fs5.readFileSync)(manifestPath, "utf-8"));
2517
4973
  const errors = [];
2518
4974
  if (!content.provider) {
2519
4975
  errors.push("Missing required field: provider");