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