moltspay 1.4.0 → 1.5.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.
@@ -0,0 +1,1492 @@
1
+ #!/usr/bin/env node
2
+
3
+ // node_modules/tsup/assets/esm_shims.js
4
+ import path from "path";
5
+ import { fileURLToPath } from "url";
6
+ var getFilename = () => fileURLToPath(import.meta.url);
7
+ var getDirname = () => path.dirname(getFilename());
8
+ var __dirname = /* @__PURE__ */ getDirname();
9
+
10
+ // src/mcp/index.ts
11
+ import { webcrypto } from "crypto";
12
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+
14
+ // src/mcp/server.ts
15
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
16
+ import { join as join3 } from "path";
17
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
+ import { z } from "zod";
19
+
20
+ // src/client/index.ts
21
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, statSync, chmodSync } from "fs";
22
+ import { homedir as homedir2 } from "os";
23
+ import { join as join2 } from "path";
24
+ import { Wallet, ethers } from "ethers";
25
+
26
+ // src/chains/index.ts
27
+ var CHAINS = {
28
+ // ============ Mainnet ============
29
+ base: {
30
+ name: "Base",
31
+ chainId: 8453,
32
+ rpc: "https://mainnet.base.org",
33
+ tokens: {
34
+ USDC: {
35
+ address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
36
+ decimals: 6,
37
+ symbol: "USDC",
38
+ eip712Name: "USD Coin"
39
+ // EIP-712 domain name
40
+ },
41
+ USDT: {
42
+ address: "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2",
43
+ decimals: 6,
44
+ symbol: "USDT",
45
+ eip712Name: "Tether USD"
46
+ }
47
+ },
48
+ usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
49
+ // deprecated, for backward compat
50
+ explorer: "https://basescan.org/address/",
51
+ explorerTx: "https://basescan.org/tx/",
52
+ avgBlockTime: 2
53
+ },
54
+ polygon: {
55
+ name: "Polygon",
56
+ chainId: 137,
57
+ rpc: "https://polygon-bor-rpc.publicnode.com",
58
+ tokens: {
59
+ USDC: {
60
+ address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
61
+ decimals: 6,
62
+ symbol: "USDC",
63
+ eip712Name: "USD Coin"
64
+ },
65
+ USDT: {
66
+ address: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
67
+ decimals: 6,
68
+ symbol: "USDT",
69
+ eip712Name: "(PoS) Tether USD"
70
+ // Polygon uses this name
71
+ }
72
+ },
73
+ usdc: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
74
+ explorer: "https://polygonscan.com/address/",
75
+ explorerTx: "https://polygonscan.com/tx/",
76
+ avgBlockTime: 2
77
+ },
78
+ // ============ Testnet ============
79
+ base_sepolia: {
80
+ name: "Base Sepolia",
81
+ chainId: 84532,
82
+ rpc: "https://sepolia.base.org",
83
+ tokens: {
84
+ USDC: {
85
+ address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
86
+ decimals: 6,
87
+ symbol: "USDC",
88
+ eip712Name: "USDC"
89
+ // Testnet USDC uses 'USDC' not 'USD Coin'
90
+ },
91
+ USDT: {
92
+ address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
93
+ // Same as USDC on testnet (no official USDT)
94
+ decimals: 6,
95
+ symbol: "USDT",
96
+ eip712Name: "USDC"
97
+ // Uses same contract as USDC
98
+ }
99
+ },
100
+ usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
101
+ explorer: "https://sepolia.basescan.org/address/",
102
+ explorerTx: "https://sepolia.basescan.org/tx/",
103
+ avgBlockTime: 2
104
+ },
105
+ // ============ Tempo Testnet (Moderato) ============
106
+ tempo_moderato: {
107
+ name: "Tempo Moderato",
108
+ chainId: 42431,
109
+ rpc: "https://rpc.moderato.tempo.xyz",
110
+ tokens: {
111
+ // TIP-20 stablecoins on Tempo testnet (from mppx SDK)
112
+ // Note: Tempo uses USD as native gas token, not ETH
113
+ USDC: {
114
+ address: "0x20c0000000000000000000000000000000000000",
115
+ // pathUSD - primary testnet stablecoin
116
+ decimals: 6,
117
+ symbol: "USDC",
118
+ eip712Name: "pathUSD"
119
+ },
120
+ USDT: {
121
+ address: "0x20c0000000000000000000000000000000000001",
122
+ // alphaUSD
123
+ decimals: 6,
124
+ symbol: "USDT",
125
+ eip712Name: "alphaUSD"
126
+ }
127
+ },
128
+ usdc: "0x20c0000000000000000000000000000000000000",
129
+ explorer: "https://explore.testnet.tempo.xyz/address/",
130
+ explorerTx: "https://explore.testnet.tempo.xyz/tx/",
131
+ avgBlockTime: 0.5
132
+ // ~500ms finality
133
+ },
134
+ // ============ BNB Chain Testnet ============
135
+ bnb_testnet: {
136
+ name: "BNB Testnet",
137
+ chainId: 97,
138
+ rpc: "https://data-seed-prebsc-1-s1.binance.org:8545",
139
+ tokens: {
140
+ // Note: BNB uses 18 decimals for stablecoins (unlike Base/Polygon which use 6)
141
+ // Using official Binance-Peg testnet tokens
142
+ USDC: {
143
+ address: "0x64544969ed7EBf5f083679233325356EbE738930",
144
+ // Testnet USDC
145
+ decimals: 18,
146
+ symbol: "USDC",
147
+ eip712Name: "USD Coin"
148
+ },
149
+ USDT: {
150
+ address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
151
+ // Testnet USDT
152
+ decimals: 18,
153
+ symbol: "USDT",
154
+ eip712Name: "Tether USD"
155
+ }
156
+ },
157
+ usdc: "0x64544969ed7EBf5f083679233325356EbE738930",
158
+ explorer: "https://testnet.bscscan.com/address/",
159
+ explorerTx: "https://testnet.bscscan.com/tx/",
160
+ avgBlockTime: 3,
161
+ // BNB-specific: requires approval for pay-for-success flow
162
+ requiresApproval: true
163
+ },
164
+ // ============ BNB Chain Mainnet ============
165
+ bnb: {
166
+ name: "BNB Smart Chain",
167
+ chainId: 56,
168
+ rpc: "https://bsc-dataseed.binance.org",
169
+ tokens: {
170
+ // Note: BNB uses 18 decimals for stablecoins
171
+ USDC: {
172
+ address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
173
+ decimals: 18,
174
+ symbol: "USDC",
175
+ eip712Name: "USD Coin"
176
+ },
177
+ USDT: {
178
+ address: "0x55d398326f99059fF775485246999027B3197955",
179
+ decimals: 18,
180
+ symbol: "USDT",
181
+ eip712Name: "Tether USD"
182
+ }
183
+ },
184
+ usdc: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
185
+ explorer: "https://bscscan.com/address/",
186
+ explorerTx: "https://bscscan.com/tx/",
187
+ avgBlockTime: 3,
188
+ // BNB-specific: requires approval for pay-for-success flow
189
+ requiresApproval: true
190
+ }
191
+ };
192
+ function getChain(name) {
193
+ const config = CHAINS[name];
194
+ if (!config) {
195
+ throw new Error(`Unsupported chain: ${name}. Supported: ${Object.keys(CHAINS).join(", ")}`);
196
+ }
197
+ return config;
198
+ }
199
+
200
+ // src/wallet/solana.ts
201
+ import { Keypair, PublicKey as PublicKey2, LAMPORTS_PER_SOL } from "@solana/web3.js";
202
+ import { getAssociatedTokenAddress, getAccount } from "@solana/spl-token";
203
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
204
+ import { join } from "path";
205
+ import { homedir } from "os";
206
+ import bs58 from "bs58";
207
+
208
+ // src/chains/solana.ts
209
+ import { Connection, PublicKey } from "@solana/web3.js";
210
+ var SOLANA_CHAINS = {
211
+ solana: {
212
+ name: "Solana Mainnet",
213
+ cluster: "mainnet-beta",
214
+ rpc: "https://api.mainnet-beta.solana.com",
215
+ explorer: "https://solscan.io/account/",
216
+ explorerTx: "https://solscan.io/tx/",
217
+ tokens: {
218
+ USDC: {
219
+ mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
220
+ // Circle official USDC
221
+ decimals: 6
222
+ }
223
+ }
224
+ },
225
+ solana_devnet: {
226
+ name: "Solana Devnet",
227
+ cluster: "devnet",
228
+ rpc: "https://api.devnet.solana.com",
229
+ explorer: "https://solscan.io/account/",
230
+ explorerTx: "https://solscan.io/tx/",
231
+ tokens: {
232
+ USDC: {
233
+ // Circle's devnet USDC (if not available, we'll deploy our own test token)
234
+ mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
235
+ decimals: 6
236
+ }
237
+ }
238
+ }
239
+ };
240
+
241
+ // src/wallet/solana.ts
242
+ var DEFAULT_CONFIG_DIR = join(homedir(), ".moltspay");
243
+ var SOLANA_WALLET_FILE = "wallet-solana.json";
244
+ function getSolanaWalletPath(configDir = DEFAULT_CONFIG_DIR) {
245
+ return join(configDir, SOLANA_WALLET_FILE);
246
+ }
247
+ function loadSolanaWallet(configDir = DEFAULT_CONFIG_DIR) {
248
+ const walletPath = getSolanaWalletPath(configDir);
249
+ if (!existsSync(walletPath)) {
250
+ return null;
251
+ }
252
+ try {
253
+ const data = JSON.parse(readFileSync(walletPath, "utf-8"));
254
+ const secretKey = bs58.decode(data.secretKey);
255
+ return Keypair.fromSecretKey(secretKey);
256
+ } catch (error) {
257
+ console.error("Failed to load Solana wallet:", error);
258
+ return null;
259
+ }
260
+ }
261
+
262
+ // src/facilitators/solana.ts
263
+ import {
264
+ Connection as Connection3,
265
+ PublicKey as PublicKey3,
266
+ Transaction,
267
+ VersionedTransaction
268
+ } from "@solana/web3.js";
269
+ import {
270
+ getAssociatedTokenAddress as getAssociatedTokenAddress2,
271
+ createTransferCheckedInstruction,
272
+ getAccount as getAccount2,
273
+ createAssociatedTokenAccountInstruction
274
+ } from "@solana/spl-token";
275
+ async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey) {
276
+ const chainConfig = SOLANA_CHAINS[chain];
277
+ const connection = new Connection3(chainConfig.rpc, "confirmed");
278
+ const mint = new PublicKey3(chainConfig.tokens.USDC.mint);
279
+ const actualFeePayer = feePayerPubkey || senderPubkey;
280
+ const senderATA = await getAssociatedTokenAddress2(mint, senderPubkey);
281
+ const recipientATA = await getAssociatedTokenAddress2(mint, recipientPubkey);
282
+ const transaction = new Transaction();
283
+ try {
284
+ await getAccount2(connection, recipientATA);
285
+ } catch {
286
+ transaction.add(
287
+ createAssociatedTokenAccountInstruction(
288
+ actualFeePayer,
289
+ // payer (fee payer in gasless mode)
290
+ recipientATA,
291
+ // ata to create
292
+ recipientPubkey,
293
+ // owner
294
+ mint
295
+ // mint
296
+ )
297
+ );
298
+ }
299
+ transaction.add(
300
+ createTransferCheckedInstruction(
301
+ senderATA,
302
+ // source
303
+ mint,
304
+ // mint
305
+ recipientATA,
306
+ // destination
307
+ senderPubkey,
308
+ // owner (sender still authorizes the transfer)
309
+ amount,
310
+ // amount
311
+ chainConfig.tokens.USDC.decimals
312
+ // decimals
313
+ )
314
+ );
315
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
316
+ transaction.recentBlockhash = blockhash;
317
+ transaction.feePayer = actualFeePayer;
318
+ return transaction;
319
+ }
320
+
321
+ // src/client/index.ts
322
+ import { PublicKey as PublicKey4 } from "@solana/web3.js";
323
+ var X402_VERSION = 2;
324
+ var PAYMENT_REQUIRED_HEADER = "x-payment-required";
325
+ var PAYMENT_HEADER = "x-payment";
326
+ var DEFAULT_CONFIG = {
327
+ chain: "base",
328
+ limits: {
329
+ maxPerTx: 100,
330
+ maxPerDay: 1e3
331
+ }
332
+ };
333
+ var MoltsPayClient = class {
334
+ configDir;
335
+ config;
336
+ walletData = null;
337
+ wallet = null;
338
+ todaySpending = 0;
339
+ lastSpendingReset = 0;
340
+ constructor(options = {}) {
341
+ this.configDir = options.configDir || join2(homedir2(), ".moltspay");
342
+ this.config = this.loadConfig();
343
+ this.walletData = this.loadWallet();
344
+ this.loadSpending();
345
+ if (this.walletData) {
346
+ this.wallet = new Wallet(this.walletData.privateKey);
347
+ }
348
+ }
349
+ /**
350
+ * Check if client is initialized (has wallet)
351
+ */
352
+ get isInitialized() {
353
+ return this.wallet !== null;
354
+ }
355
+ /**
356
+ * Get wallet address
357
+ */
358
+ get address() {
359
+ return this.wallet?.address || null;
360
+ }
361
+ /**
362
+ * Get wallet instance (for direct operations like approvals)
363
+ */
364
+ getWallet() {
365
+ return this.wallet;
366
+ }
367
+ /**
368
+ * Get current config
369
+ */
370
+ getConfig() {
371
+ return { ...this.config };
372
+ }
373
+ /**
374
+ * Update config
375
+ */
376
+ updateConfig(updates) {
377
+ if (updates.maxPerTx !== void 0) {
378
+ this.config.limits.maxPerTx = updates.maxPerTx;
379
+ }
380
+ if (updates.maxPerDay !== void 0) {
381
+ this.config.limits.maxPerDay = updates.maxPerDay;
382
+ }
383
+ this.saveConfig();
384
+ }
385
+ /**
386
+ * Get services from a provider
387
+ */
388
+ async getServices(serverUrl) {
389
+ const normalizedUrl = serverUrl.replace(/\/(services|api\/services|registry\/services)\/?$/, "");
390
+ const endpoints = ["/services", "/api/services", "/registry/services"];
391
+ for (const endpoint of endpoints) {
392
+ try {
393
+ const res = await fetch(`${normalizedUrl}${endpoint}`);
394
+ if (!res.ok) continue;
395
+ const contentType = res.headers.get("content-type") || "";
396
+ if (!contentType.includes("application/json")) continue;
397
+ return await res.json();
398
+ } catch {
399
+ continue;
400
+ }
401
+ }
402
+ throw new Error(`Failed to get services: no valid endpoint found at ${normalizedUrl}`);
403
+ }
404
+ /**
405
+ * Pay for a service and get the result (x402 protocol)
406
+ *
407
+ * This is GASLESS for the client - server pays gas to claim payment.
408
+ * This is PAY-FOR-SUCCESS - payment only claimed if service succeeds.
409
+ *
410
+ * @param serverUrl - Server URL
411
+ * @param service - Service ID
412
+ * @param params - Service parameters
413
+ * @param options - Payment options (token selection)
414
+ */
415
+ async pay(serverUrl, service, params, options = {}) {
416
+ if (!this.wallet || !this.walletData) {
417
+ throw new Error("Client not initialized. Run: npx moltspay init");
418
+ }
419
+ console.log(`[MoltsPay] Requesting service: ${service}`);
420
+ let executeUrl = `${serverUrl}/execute`;
421
+ try {
422
+ const services = await this.getServices(serverUrl);
423
+ const svc = services.services?.find((s) => s.id === service);
424
+ if (svc?.endpoint) {
425
+ executeUrl = `${serverUrl}${svc.endpoint}`;
426
+ console.log(`[MoltsPay] Using service endpoint: ${svc.endpoint}`);
427
+ }
428
+ } catch {
429
+ }
430
+ let requestBody;
431
+ if (options.rawData) {
432
+ requestBody = { service, ...params };
433
+ } else {
434
+ requestBody = { service, params };
435
+ }
436
+ if (options.chain) {
437
+ requestBody.chain = options.chain;
438
+ }
439
+ const initialRes = await fetch(executeUrl, {
440
+ method: "POST",
441
+ headers: { "Content-Type": "application/json" },
442
+ body: JSON.stringify(requestBody)
443
+ });
444
+ if (initialRes.status !== 402) {
445
+ const data = await initialRes.json();
446
+ if (initialRes.ok && data.result) {
447
+ return data.result;
448
+ }
449
+ throw new Error(data.error || "Unexpected response");
450
+ }
451
+ const wwwAuthHeader = initialRes.headers.get("www-authenticate");
452
+ const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER);
453
+ if (wwwAuthHeader && wwwAuthHeader.toLowerCase().includes("payment")) {
454
+ console.log("[MoltsPay] Detected MPP protocol, using Tempo flow...");
455
+ return await this.handleMPPPayment(executeUrl, service, params, wwwAuthHeader, options);
456
+ }
457
+ if (!paymentRequiredHeader) {
458
+ throw new Error("Missing payment header (x-payment-required or www-authenticate)");
459
+ }
460
+ let requirements;
461
+ try {
462
+ const decoded = Buffer.from(paymentRequiredHeader, "base64").toString("utf-8");
463
+ const parsed = JSON.parse(decoded);
464
+ if (Array.isArray(parsed)) {
465
+ requirements = parsed;
466
+ } else if (parsed.accepts && Array.isArray(parsed.accepts)) {
467
+ requirements = parsed.accepts;
468
+ } else {
469
+ requirements = [parsed];
470
+ }
471
+ } catch {
472
+ throw new Error("Invalid x-payment-required header");
473
+ }
474
+ const networkToChainName = (network2) => {
475
+ if (network2 === "solana:mainnet") return "solana";
476
+ if (network2 === "solana:devnet") return "solana_devnet";
477
+ const match = network2.match(/^eip155:(\d+)$/);
478
+ if (!match) return null;
479
+ const chainId = parseInt(match[1]);
480
+ if (chainId === 8453) return "base";
481
+ if (chainId === 137) return "polygon";
482
+ if (chainId === 84532) return "base_sepolia";
483
+ if (chainId === 42431) return "tempo_moderato";
484
+ if (chainId === 56) return "bnb";
485
+ if (chainId === 97) return "bnb_testnet";
486
+ return null;
487
+ };
488
+ const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
489
+ const userSpecifiedChain = options.chain;
490
+ let selectedChain;
491
+ if (userSpecifiedChain) {
492
+ if (!serverChains.includes(userSpecifiedChain)) {
493
+ throw new Error(
494
+ `Server doesn't accept '${userSpecifiedChain}'.
495
+ Server accepts: ${serverChains.join(", ")}`
496
+ );
497
+ }
498
+ selectedChain = userSpecifiedChain;
499
+ } else {
500
+ if (serverChains.length === 1 && serverChains[0] === "base") {
501
+ selectedChain = "base";
502
+ } else {
503
+ throw new Error(
504
+ `Server accepts: ${serverChains.join(", ")}
505
+ Please specify: --chain <chain_name>`
506
+ );
507
+ }
508
+ }
509
+ if (selectedChain === "solana" || selectedChain === "solana_devnet") {
510
+ const solanaChain = selectedChain;
511
+ const network2 = solanaChain === "solana" ? "solana:mainnet" : "solana:devnet";
512
+ const req2 = requirements.find((r) => r.network === network2);
513
+ if (!req2) {
514
+ throw new Error(`Failed to find payment requirement for ${selectedChain}`);
515
+ }
516
+ return await this.handleSolanaPayment(executeUrl, service, params, req2, solanaChain, options);
517
+ }
518
+ const chainName = selectedChain;
519
+ const chain = getChain(chainName);
520
+ const network = `eip155:${chain.chainId}`;
521
+ const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
522
+ if (!req) {
523
+ throw new Error(`Failed to find payment requirement for ${chainName}`);
524
+ }
525
+ const amountRaw = req.amount || req.maxAmountRequired;
526
+ if (!amountRaw) {
527
+ throw new Error("Missing amount in payment requirements");
528
+ }
529
+ const amount = Number(amountRaw) / 1e6;
530
+ this.checkLimits(amount);
531
+ let token = options.token || "USDC";
532
+ if (options.autoSelect) {
533
+ const balances = await this.getBalance();
534
+ if (balances.usdc >= amount) {
535
+ token = "USDC";
536
+ } else if (balances.usdt >= amount) {
537
+ token = "USDT";
538
+ } else {
539
+ throw new Error(`Insufficient balance: need $${amount}, have ${balances.usdc} USDC / ${balances.usdt} USDT`);
540
+ }
541
+ }
542
+ if (token === "USDT") {
543
+ const balances = await this.getBalance();
544
+ if (balances.native < 1e-4) {
545
+ throw new Error(
546
+ `USDT requires ETH for gas (~$0.01 on Base). Your ETH balance: ${balances.native.toFixed(6)} ETH. Please add a small amount of ETH to your wallet, or use USDC (gasless).`
547
+ );
548
+ }
549
+ console.log(`[MoltsPay] \u26A0\uFE0F USDT requires gas (~$0.01). Proceeding with payment...`);
550
+ } else {
551
+ console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
552
+ }
553
+ if (chainName === "bnb" || chainName === "bnb_testnet") {
554
+ console.log(`[MoltsPay] Using BNB intent-based payment flow...`);
555
+ const payTo2 = req.payTo || req.resource;
556
+ if (!payTo2) {
557
+ throw new Error("Missing payTo address in payment requirements");
558
+ }
559
+ const bnbSpender = req.extra?.bnbSpender;
560
+ if (!bnbSpender) {
561
+ throw new Error("Server did not provide bnbSpender address. Server may not support BNB payments.");
562
+ }
563
+ return await this.handleBNBPayment(executeUrl, service, params, {
564
+ to: payTo2,
565
+ amount,
566
+ token,
567
+ chainName,
568
+ chain,
569
+ spender: bnbSpender
570
+ }, options);
571
+ }
572
+ const payTo = req.payTo || req.resource;
573
+ if (!payTo) {
574
+ throw new Error("Missing payTo address in payment requirements");
575
+ }
576
+ const domainOverride = req.extra && typeof req.extra === "object" && req.extra.name ? { name: req.extra.name, version: req.extra.version || "2" } : void 0;
577
+ const authorization = await this.signEIP3009(payTo, amount, chain, token, domainOverride);
578
+ const tokenConfig = chain.tokens[token];
579
+ const extra = req.extra && typeof req.extra === "object" ? req.extra : {
580
+ name: tokenConfig.eip712Name || "USD Coin",
581
+ version: "2"
582
+ };
583
+ const payload = {
584
+ x402Version: X402_VERSION,
585
+ scheme: "exact",
586
+ network,
587
+ payload: authorization,
588
+ // { authorization: {...}, signature: "0x..." }
589
+ accepted: {
590
+ scheme: "exact",
591
+ network,
592
+ asset: tokenConfig.address,
593
+ amount: amountRaw,
594
+ payTo,
595
+ maxTimeoutSeconds: req.maxTimeoutSeconds || 300,
596
+ extra
597
+ }
598
+ };
599
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
600
+ console.log(`[MoltsPay] Sending request with payment...`);
601
+ const paidRequestBody = options.rawData ? { service, ...params } : { service, params };
602
+ if (options.chain) {
603
+ paidRequestBody.chain = options.chain;
604
+ }
605
+ const paidRes = await fetch(executeUrl, {
606
+ method: "POST",
607
+ headers: {
608
+ "Content-Type": "application/json",
609
+ [PAYMENT_HEADER]: paymentHeader
610
+ },
611
+ body: JSON.stringify(paidRequestBody)
612
+ });
613
+ const result = await paidRes.json();
614
+ if (!paidRes.ok) {
615
+ throw new Error(result.error || "Service execution failed");
616
+ }
617
+ this.recordSpending(amount);
618
+ console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
619
+ return result.result || result;
620
+ }
621
+ /**
622
+ * Handle MPP (Machine Payments Protocol) payment flow
623
+ * Called when pay() detects WWW-Authenticate header in 402 response
624
+ */
625
+ async handleMPPPayment(executeUrl, service, params, wwwAuthHeader, options = {}) {
626
+ const { privateKeyToAccount } = await import("viem/accounts");
627
+ const { createWalletClient, createPublicClient, http } = await import("viem");
628
+ const { tempoModerato } = await import("viem/chains");
629
+ const { Actions } = await import("viem/tempo");
630
+ const privateKey = this.walletData.privateKey;
631
+ const account = privateKeyToAccount(privateKey);
632
+ console.log(`[MoltsPay] Using MPP protocol on Tempo`);
633
+ console.log(`[MoltsPay] Account: ${account.address}`);
634
+ const parseAuthParam = (header, key) => {
635
+ const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
636
+ return match ? match[1] : null;
637
+ };
638
+ const challengeId = parseAuthParam(wwwAuthHeader, "id");
639
+ const method = parseAuthParam(wwwAuthHeader, "method");
640
+ const realm = parseAuthParam(wwwAuthHeader, "realm");
641
+ const requestB64 = parseAuthParam(wwwAuthHeader, "request");
642
+ if (method !== "tempo") {
643
+ throw new Error(`Unsupported payment method: ${method}`);
644
+ }
645
+ if (!requestB64) {
646
+ throw new Error("Missing request in WWW-Authenticate");
647
+ }
648
+ const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
649
+ const paymentRequest = JSON.parse(requestJson);
650
+ const { amount, currency, recipient, methodDetails } = paymentRequest;
651
+ const chainId = methodDetails?.chainId || 42431;
652
+ const amountDisplay = Number(amount) / 1e6;
653
+ console.log(`[MoltsPay] Payment: $${amountDisplay} to ${recipient}`);
654
+ this.checkLimits(amountDisplay);
655
+ console.log(`[MoltsPay] Sending transaction on Tempo...`);
656
+ const tempoChain = { ...tempoModerato, feeToken: currency };
657
+ const publicClient = createPublicClient({
658
+ chain: tempoChain,
659
+ transport: http("https://rpc.moderato.tempo.xyz")
660
+ });
661
+ const walletClient = createWalletClient({
662
+ account,
663
+ chain: tempoChain,
664
+ transport: http("https://rpc.moderato.tempo.xyz")
665
+ });
666
+ const txHash = await Actions.token.transfer(walletClient, {
667
+ to: recipient,
668
+ amount: BigInt(amount),
669
+ token: currency
670
+ });
671
+ console.log(`[MoltsPay] Transaction: ${txHash}`);
672
+ await publicClient.waitForTransactionReceipt({ hash: txHash });
673
+ console.log(`[MoltsPay] Confirmed! Retrying with credential...`);
674
+ const credential = {
675
+ challenge: {
676
+ id: challengeId,
677
+ realm,
678
+ method: "tempo",
679
+ intent: "charge",
680
+ request: paymentRequest
681
+ },
682
+ payload: { hash: txHash, type: "hash" },
683
+ source: `did:pkh:eip155:${chainId}:${account.address}`
684
+ };
685
+ const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
686
+ const retryBody = options.rawData ? { service, ...params, chain: "tempo_moderato" } : { service, params, chain: "tempo_moderato" };
687
+ const paidRes = await fetch(executeUrl, {
688
+ method: "POST",
689
+ headers: {
690
+ "Content-Type": "application/json",
691
+ "Authorization": `Payment ${credentialB64}`
692
+ },
693
+ body: JSON.stringify(retryBody)
694
+ });
695
+ const result = await paidRes.json();
696
+ if (!paidRes.ok) {
697
+ throw new Error(result.error || "Payment verification failed");
698
+ }
699
+ this.recordSpending(amountDisplay);
700
+ console.log(`[MoltsPay] Success!`);
701
+ return result.result || result;
702
+ }
703
+ /**
704
+ * Handle BNB Chain payment flow (pre-approval + intent signature)
705
+ *
706
+ * Flow:
707
+ * 1. Check client has approved server wallet (done via `moltspay init`)
708
+ * 2. Sign EIP-712 payment intent (no gas, just signature)
709
+ * 3. Send intent to server
710
+ * 4. Server executes service
711
+ * 5. Server calls transferFrom if successful (pay-for-success)
712
+ */
713
+ async handleBNBPayment(executeUrl, service, params, paymentDetails, options = {}) {
714
+ const { to, amount, token, chainName, chain, spender } = paymentDetails;
715
+ const tokenConfig = chain.tokens[token];
716
+ const provider = new ethers.JsonRpcProvider(chain.rpc);
717
+ const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
718
+ const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
719
+ if (allowance < amountWeiCheck) {
720
+ const nativeBalance = await provider.getBalance(this.wallet.address);
721
+ const minGasBalance = ethers.parseEther("0.0005");
722
+ if (nativeBalance < minGasBalance) {
723
+ const nativeBNB = parseFloat(ethers.formatEther(nativeBalance)).toFixed(4);
724
+ const isTestnet = chainName === "bnb_testnet";
725
+ if (isTestnet) {
726
+ throw new Error(
727
+ `\u274C Insufficient tBNB for approval transaction
728
+
729
+ Current tBNB: ${nativeBNB}
730
+ Required: ~0.001 tBNB
731
+
732
+ Get testnet tokens: npx moltspay faucet --chain bnb_testnet
733
+ (Gives USDC + tBNB for gas)`
734
+ );
735
+ } else {
736
+ throw new Error(
737
+ `\u274C Insufficient BNB for approval transaction
738
+
739
+ Current BNB: ${nativeBNB}
740
+ Required: ~0.001 BNB (~$0.60)
741
+
742
+ To get BNB:
743
+ \u2022 Withdraw from Binance/exchange to your wallet
744
+ \u2022 Most exchanges include BNB dust with withdrawals
745
+
746
+ After funding, run:
747
+ npx moltspay approve --chain ${chainName} --spender ${spender}`
748
+ );
749
+ }
750
+ }
751
+ throw new Error(
752
+ `Insufficient allowance for ${spender.slice(0, 10)}...
753
+ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
754
+ );
755
+ }
756
+ const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
757
+ const intent = {
758
+ from: this.wallet.address,
759
+ to,
760
+ amount: amountWei,
761
+ token: tokenConfig.address,
762
+ service,
763
+ nonce: Date.now(),
764
+ // Use timestamp as nonce for simplicity
765
+ deadline: Date.now() + 36e5
766
+ // 1 hour
767
+ };
768
+ const domain = {
769
+ name: "MoltsPay",
770
+ version: "1",
771
+ chainId: chain.chainId
772
+ };
773
+ const types = {
774
+ PaymentIntent: [
775
+ { name: "from", type: "address" },
776
+ { name: "to", type: "address" },
777
+ { name: "amount", type: "uint256" },
778
+ { name: "token", type: "address" },
779
+ { name: "service", type: "string" },
780
+ { name: "nonce", type: "uint256" },
781
+ { name: "deadline", type: "uint256" }
782
+ ]
783
+ };
784
+ console.log(`[MoltsPay] Signing BNB payment intent...`);
785
+ const signature = await this.wallet.signTypedData(domain, types, intent);
786
+ const network = `eip155:${chain.chainId}`;
787
+ const payload = {
788
+ x402Version: 2,
789
+ scheme: "exact",
790
+ network,
791
+ payload: {
792
+ intent: {
793
+ ...intent,
794
+ signature
795
+ },
796
+ chainId: chain.chainId
797
+ },
798
+ accepted: {
799
+ scheme: "exact",
800
+ network,
801
+ asset: tokenConfig.address,
802
+ amount: amountWei,
803
+ payTo: to,
804
+ maxTimeoutSeconds: 300
805
+ }
806
+ };
807
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
808
+ console.log(`[MoltsPay] Sending BNB payment request...`);
809
+ const bnbRequestBody = options.rawData ? { service, ...params, chain: chainName } : { service, params, chain: chainName };
810
+ const paidRes = await fetch(executeUrl, {
811
+ method: "POST",
812
+ headers: {
813
+ "Content-Type": "application/json",
814
+ "X-Payment": paymentHeader
815
+ },
816
+ body: JSON.stringify(bnbRequestBody)
817
+ });
818
+ const result = await paidRes.json();
819
+ if (!paidRes.ok) {
820
+ throw new Error(result.error || "BNB payment failed");
821
+ }
822
+ this.recordSpending(amount);
823
+ console.log(`[MoltsPay] Success! BNB payment settled.`);
824
+ return result.result || result;
825
+ }
826
+ /**
827
+ * Handle Solana payment flow
828
+ *
829
+ * Solana uses SPL token transfers with pay-for-success model:
830
+ * 1. Client creates and signs a transfer transaction
831
+ * 2. Server submits the transaction after service completes
832
+ */
833
+ async handleSolanaPayment(executeUrl, service, params, requirements, chain, options = {}) {
834
+ const solanaWallet = loadSolanaWallet(this.configDir);
835
+ if (!solanaWallet) {
836
+ throw new Error("No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
837
+ }
838
+ const amount = Number(requirements.amount);
839
+ const amountUSDC = amount / 1e6;
840
+ this.checkLimits(amountUSDC);
841
+ console.log(`[MoltsPay] Creating Solana payment: $${amountUSDC} USDC`);
842
+ if (!requirements.payTo) {
843
+ throw new Error("Missing payTo address in payment requirements");
844
+ }
845
+ const solanaFeePayer = requirements.extra?.solanaFeePayer;
846
+ const feePayerPubkey = solanaFeePayer ? new PublicKey4(solanaFeePayer) : void 0;
847
+ if (feePayerPubkey) {
848
+ console.log(`[MoltsPay] Gasless mode: server pays fees`);
849
+ }
850
+ const recipientPubkey = new PublicKey4(requirements.payTo);
851
+ const transaction = await createSolanaPaymentTransaction(
852
+ solanaWallet.publicKey,
853
+ recipientPubkey,
854
+ BigInt(amount),
855
+ chain,
856
+ feePayerPubkey
857
+ // Optional fee payer for gasless mode
858
+ );
859
+ if (feePayerPubkey) {
860
+ transaction.partialSign(solanaWallet);
861
+ } else {
862
+ transaction.sign(solanaWallet);
863
+ }
864
+ const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
865
+ console.log(`[MoltsPay] Transaction signed, sending to server...`);
866
+ const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
867
+ const payload = {
868
+ x402Version: 2,
869
+ scheme: "exact",
870
+ network,
871
+ payload: {
872
+ signedTransaction: signedTx,
873
+ sender: solanaWallet.publicKey.toBase58(),
874
+ chain
875
+ },
876
+ accepted: {
877
+ scheme: "exact",
878
+ network,
879
+ asset: requirements.asset,
880
+ amount: requirements.amount,
881
+ payTo: requirements.payTo,
882
+ maxTimeoutSeconds: 300
883
+ }
884
+ };
885
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
886
+ const solanaRequestBody = options.rawData ? { service, ...params, chain } : { service, params, chain };
887
+ const paidRes = await fetch(executeUrl, {
888
+ method: "POST",
889
+ headers: {
890
+ "Content-Type": "application/json",
891
+ "X-Payment": paymentHeader
892
+ },
893
+ body: JSON.stringify(solanaRequestBody)
894
+ });
895
+ const result = await paidRes.json();
896
+ if (!paidRes.ok) {
897
+ throw new Error(result.error || "Solana payment failed");
898
+ }
899
+ this.recordSpending(amountUSDC);
900
+ console.log(`[MoltsPay] Success! Solana payment settled.`);
901
+ if (result.payment?.transaction) {
902
+ const explorerUrl = chain === "solana" ? `https://solscan.io/tx/${result.payment.transaction}` : `https://solscan.io/tx/${result.payment.transaction}?cluster=devnet`;
903
+ console.log(`[MoltsPay] Transaction: ${explorerUrl}`);
904
+ }
905
+ return result.result || result;
906
+ }
907
+ /**
908
+ * Check ERC20 allowance for a spender
909
+ */
910
+ async checkAllowance(tokenAddress, spender, provider) {
911
+ const contract = new ethers.Contract(
912
+ tokenAddress,
913
+ ["function allowance(address owner, address spender) view returns (uint256)"],
914
+ provider
915
+ );
916
+ return await contract.allowance(this.wallet.address, spender);
917
+ }
918
+ /**
919
+ * Sign EIP-3009 transferWithAuthorization (GASLESS)
920
+ * This only signs - no on-chain transaction, no gas needed.
921
+ * Supports both USDC and USDT.
922
+ */
923
+ async signEIP3009(to, amount, chain, token = "USDC", domainOverride) {
924
+ const validAfter = 0;
925
+ const validBefore = Math.floor(Date.now() / 1e3) + 3600;
926
+ const nonce = ethers.hexlify(ethers.randomBytes(32));
927
+ const tokenConfig = chain.tokens[token];
928
+ const value = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
929
+ const authorization = {
930
+ from: this.wallet.address,
931
+ to,
932
+ value,
933
+ validAfter: validAfter.toString(),
934
+ validBefore: validBefore.toString(),
935
+ nonce
936
+ };
937
+ const tokenName = domainOverride?.name || tokenConfig.eip712Name || (token === "USDC" ? "USD Coin" : "Tether USD");
938
+ const tokenVersion = domainOverride?.version || "2";
939
+ const domain = {
940
+ name: tokenName,
941
+ version: tokenVersion,
942
+ chainId: chain.chainId,
943
+ verifyingContract: tokenConfig.address
944
+ };
945
+ const types = {
946
+ TransferWithAuthorization: [
947
+ { name: "from", type: "address" },
948
+ { name: "to", type: "address" },
949
+ { name: "value", type: "uint256" },
950
+ { name: "validAfter", type: "uint256" },
951
+ { name: "validBefore", type: "uint256" },
952
+ { name: "nonce", type: "bytes32" }
953
+ ]
954
+ };
955
+ const signature = await this.wallet.signTypedData(domain, types, authorization);
956
+ return { authorization, signature };
957
+ }
958
+ /**
959
+ * Check spending limits
960
+ */
961
+ checkLimits(amount) {
962
+ if (amount > this.config.limits.maxPerTx) {
963
+ throw new Error(
964
+ `Amount $${amount} exceeds max per transaction ($${this.config.limits.maxPerTx})`
965
+ );
966
+ }
967
+ const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
968
+ if (today > this.lastSpendingReset) {
969
+ this.todaySpending = 0;
970
+ this.lastSpendingReset = today;
971
+ this.saveSpending();
972
+ }
973
+ if (this.todaySpending + amount > this.config.limits.maxPerDay) {
974
+ throw new Error(
975
+ `Would exceed daily limit ($${this.todaySpending} + $${amount} > $${this.config.limits.maxPerDay})`
976
+ );
977
+ }
978
+ }
979
+ /**
980
+ * Record spending and persist to disk
981
+ */
982
+ recordSpending(amount) {
983
+ this.todaySpending += amount;
984
+ this.saveSpending();
985
+ }
986
+ // --- Config & Wallet Management ---
987
+ loadConfig() {
988
+ const configPath = join2(this.configDir, "config.json");
989
+ if (existsSync2(configPath)) {
990
+ const content = readFileSync2(configPath, "utf-8");
991
+ return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
992
+ }
993
+ return { ...DEFAULT_CONFIG };
994
+ }
995
+ saveConfig() {
996
+ mkdirSync2(this.configDir, { recursive: true });
997
+ const configPath = join2(this.configDir, "config.json");
998
+ writeFileSync2(configPath, JSON.stringify(this.config, null, 2));
999
+ }
1000
+ /**
1001
+ * Load spending data from disk
1002
+ */
1003
+ loadSpending() {
1004
+ const spendingPath = join2(this.configDir, "spending.json");
1005
+ if (existsSync2(spendingPath)) {
1006
+ try {
1007
+ const data = JSON.parse(readFileSync2(spendingPath, "utf-8"));
1008
+ const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
1009
+ if (data.date && data.date === today) {
1010
+ this.todaySpending = data.amount || 0;
1011
+ this.lastSpendingReset = data.date;
1012
+ } else {
1013
+ this.todaySpending = 0;
1014
+ this.lastSpendingReset = today;
1015
+ }
1016
+ } catch {
1017
+ this.todaySpending = 0;
1018
+ this.lastSpendingReset = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
1019
+ }
1020
+ }
1021
+ }
1022
+ /**
1023
+ * Save spending data to disk
1024
+ */
1025
+ saveSpending() {
1026
+ mkdirSync2(this.configDir, { recursive: true });
1027
+ const spendingPath = join2(this.configDir, "spending.json");
1028
+ const data = {
1029
+ date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
1030
+ amount: this.todaySpending,
1031
+ updatedAt: Date.now()
1032
+ };
1033
+ writeFileSync2(spendingPath, JSON.stringify(data, null, 2));
1034
+ }
1035
+ loadWallet() {
1036
+ const walletPath = join2(this.configDir, "wallet.json");
1037
+ if (existsSync2(walletPath)) {
1038
+ if (process.platform !== "win32") {
1039
+ try {
1040
+ const stats = statSync(walletPath);
1041
+ const mode = stats.mode & 511;
1042
+ if (mode !== 384) {
1043
+ console.warn(`[MoltsPay] WARNING: wallet.json has insecure permissions (${mode.toString(8)})`);
1044
+ console.warn(`[MoltsPay] Fixing permissions to 0600...`);
1045
+ chmodSync(walletPath, 384);
1046
+ }
1047
+ } catch {
1048
+ }
1049
+ }
1050
+ const content = readFileSync2(walletPath, "utf-8");
1051
+ return JSON.parse(content);
1052
+ }
1053
+ return null;
1054
+ }
1055
+ /**
1056
+ * Initialize a new wallet (called by CLI)
1057
+ */
1058
+ static init(configDir, options) {
1059
+ mkdirSync2(configDir, { recursive: true });
1060
+ const wallet = Wallet.createRandom();
1061
+ const walletData = {
1062
+ address: wallet.address,
1063
+ privateKey: wallet.privateKey,
1064
+ createdAt: Date.now()
1065
+ };
1066
+ const walletPath = join2(configDir, "wallet.json");
1067
+ writeFileSync2(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
1068
+ const config = {
1069
+ chain: options.chain,
1070
+ limits: {
1071
+ maxPerTx: options.maxPerTx,
1072
+ maxPerDay: options.maxPerDay
1073
+ }
1074
+ };
1075
+ const configPath = join2(configDir, "config.json");
1076
+ writeFileSync2(configPath, JSON.stringify(config, null, 2));
1077
+ return { address: wallet.address, configDir };
1078
+ }
1079
+ /**
1080
+ * Get wallet balance (USDC, USDT, and native token) on default chain
1081
+ */
1082
+ async getBalance() {
1083
+ if (!this.wallet) {
1084
+ throw new Error("Client not initialized");
1085
+ }
1086
+ let chain;
1087
+ try {
1088
+ chain = getChain(this.config.chain);
1089
+ } catch {
1090
+ throw new Error(`Unknown chain: ${this.config.chain}`);
1091
+ }
1092
+ const provider = new ethers.JsonRpcProvider(chain.rpc);
1093
+ const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
1094
+ const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
1095
+ provider.getBalance(this.wallet.address),
1096
+ new ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
1097
+ new ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
1098
+ ]);
1099
+ return {
1100
+ usdc: parseFloat(ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
1101
+ usdt: parseFloat(ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
1102
+ native: parseFloat(ethers.formatEther(nativeBalance))
1103
+ };
1104
+ }
1105
+ /**
1106
+ * Get wallet balances on all supported chains (Base + Polygon + Tempo)
1107
+ */
1108
+ async getAllBalances() {
1109
+ if (!this.wallet) {
1110
+ throw new Error("Client not initialized");
1111
+ }
1112
+ const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
1113
+ const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
1114
+ const results = {};
1115
+ const tempoTokens = {
1116
+ pathUSD: "0x20c0000000000000000000000000000000000000",
1117
+ alphaUSD: "0x20c0000000000000000000000000000000000001",
1118
+ betaUSD: "0x20c0000000000000000000000000000000000002",
1119
+ thetaUSD: "0x20c0000000000000000000000000000000000003"
1120
+ };
1121
+ await Promise.all(
1122
+ supportedChains.map(async (chainName) => {
1123
+ try {
1124
+ const chain = getChain(chainName);
1125
+ const provider = new ethers.JsonRpcProvider(chain.rpc);
1126
+ if (chainName === "tempo_moderato") {
1127
+ const [nativeBalance, pathUSD, alphaUSD, betaUSD, thetaUSD] = await Promise.all([
1128
+ provider.getBalance(this.wallet.address),
1129
+ new ethers.Contract(tempoTokens.pathUSD, tokenAbi, provider).balanceOf(this.wallet.address),
1130
+ new ethers.Contract(tempoTokens.alphaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
1131
+ new ethers.Contract(tempoTokens.betaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
1132
+ new ethers.Contract(tempoTokens.thetaUSD, tokenAbi, provider).balanceOf(this.wallet.address)
1133
+ ]);
1134
+ results[chainName] = {
1135
+ usdc: parseFloat(ethers.formatUnits(pathUSD, 6)),
1136
+ // pathUSD as default USDC
1137
+ usdt: parseFloat(ethers.formatUnits(alphaUSD, 6)),
1138
+ // alphaUSD as default USDT
1139
+ native: parseFloat(ethers.formatEther(nativeBalance)),
1140
+ tempo: {
1141
+ pathUSD: parseFloat(ethers.formatUnits(pathUSD, 6)),
1142
+ alphaUSD: parseFloat(ethers.formatUnits(alphaUSD, 6)),
1143
+ betaUSD: parseFloat(ethers.formatUnits(betaUSD, 6)),
1144
+ thetaUSD: parseFloat(ethers.formatUnits(thetaUSD, 6))
1145
+ }
1146
+ };
1147
+ } else {
1148
+ const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
1149
+ provider.getBalance(this.wallet.address),
1150
+ new ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
1151
+ new ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
1152
+ ]);
1153
+ results[chainName] = {
1154
+ usdc: parseFloat(ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
1155
+ usdt: parseFloat(ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
1156
+ native: parseFloat(ethers.formatEther(nativeBalance))
1157
+ };
1158
+ }
1159
+ } catch (err2) {
1160
+ results[chainName] = { usdc: 0, usdt: 0, native: 0 };
1161
+ }
1162
+ })
1163
+ );
1164
+ return results;
1165
+ }
1166
+ /**
1167
+ * Pay for a service using MPP (Machine Payments Protocol)
1168
+ *
1169
+ * This implements the MPP flow manually for EOA wallets:
1170
+ * 1. Request service → get 402 with WWW-Authenticate
1171
+ * 2. Parse payment requirements
1172
+ * 3. Execute transfer on Tempo chain
1173
+ * 4. Retry with transaction hash as credential
1174
+ *
1175
+ * @param url - Full URL of the MPP-enabled endpoint
1176
+ * @param options - Request options (body, headers)
1177
+ * @returns Response from the service
1178
+ */
1179
+ async payWithMPP(url, options = {}) {
1180
+ if (!this.wallet || !this.walletData) {
1181
+ throw new Error("Client not initialized. Run: npx moltspay init");
1182
+ }
1183
+ const { privateKeyToAccount } = await import("viem/accounts");
1184
+ const { createWalletClient, createPublicClient, http } = await import("viem");
1185
+ const { tempoModerato } = await import("viem/chains");
1186
+ const { Actions } = await import("viem/tempo");
1187
+ const privateKey = this.walletData.privateKey;
1188
+ const account = privateKeyToAccount(privateKey);
1189
+ console.log(`[MoltsPay] Making MPP request to: ${url}`);
1190
+ console.log(`[MoltsPay] Using account: ${account.address}`);
1191
+ const initResponse = await fetch(url, {
1192
+ method: "POST",
1193
+ headers: {
1194
+ "Content-Type": "application/json",
1195
+ ...options.headers
1196
+ },
1197
+ body: options.body ? JSON.stringify(options.body) : void 0
1198
+ });
1199
+ if (initResponse.status !== 402) {
1200
+ if (initResponse.ok) {
1201
+ return initResponse.json();
1202
+ }
1203
+ const errorText = await initResponse.text();
1204
+ throw new Error(`Request failed (${initResponse.status}): ${errorText}`);
1205
+ }
1206
+ const wwwAuth = initResponse.headers.get("www-authenticate");
1207
+ if (!wwwAuth || !wwwAuth.toLowerCase().includes("payment")) {
1208
+ throw new Error("No WWW-Authenticate Payment challenge in 402 response");
1209
+ }
1210
+ console.log(`[MoltsPay] Got 402, parsing payment challenge...`);
1211
+ const parseAuthParam = (header, key) => {
1212
+ const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
1213
+ return match ? match[1] : null;
1214
+ };
1215
+ const challengeId = parseAuthParam(wwwAuth, "id");
1216
+ const method = parseAuthParam(wwwAuth, "method");
1217
+ const realm = parseAuthParam(wwwAuth, "realm");
1218
+ const requestB64 = parseAuthParam(wwwAuth, "request");
1219
+ if (method !== "tempo") {
1220
+ throw new Error(`Unsupported payment method: ${method}`);
1221
+ }
1222
+ if (!requestB64) {
1223
+ throw new Error("Missing request in WWW-Authenticate");
1224
+ }
1225
+ const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
1226
+ const paymentRequest = JSON.parse(requestJson);
1227
+ console.log(`[MoltsPay] Payment request:`, paymentRequest);
1228
+ const { amount, currency, recipient, methodDetails } = paymentRequest;
1229
+ const chainId = methodDetails?.chainId || 42431;
1230
+ console.log(`[MoltsPay] Executing transfer on Tempo (chainId: ${chainId})...`);
1231
+ console.log(`[MoltsPay] Amount: ${amount}, To: ${recipient}`);
1232
+ const tempoChain = { ...tempoModerato, feeToken: currency };
1233
+ const publicClient = createPublicClient({
1234
+ chain: tempoChain,
1235
+ transport: http("https://rpc.moderato.tempo.xyz")
1236
+ });
1237
+ const walletClient = createWalletClient({
1238
+ account,
1239
+ chain: tempoChain,
1240
+ transport: http("https://rpc.moderato.tempo.xyz")
1241
+ });
1242
+ const txHash = await Actions.token.transfer(walletClient, {
1243
+ to: recipient,
1244
+ amount: BigInt(amount),
1245
+ token: currency
1246
+ });
1247
+ console.log(`[MoltsPay] Transaction sent: ${txHash}`);
1248
+ console.log(`[MoltsPay] Waiting for confirmation...`);
1249
+ await publicClient.waitForTransactionReceipt({ hash: txHash });
1250
+ console.log(`[MoltsPay] Transaction confirmed!`);
1251
+ const challenge = {
1252
+ id: challengeId,
1253
+ realm,
1254
+ method: "tempo",
1255
+ intent: "charge",
1256
+ request: paymentRequest
1257
+ };
1258
+ const credential = {
1259
+ challenge,
1260
+ payload: { hash: txHash, type: "hash" },
1261
+ source: `did:pkh:eip155:${chainId}:${account.address}`
1262
+ };
1263
+ const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1264
+ console.log(`[MoltsPay] Retrying with payment credential...`);
1265
+ const paidResponse = await fetch(url, {
1266
+ method: "POST",
1267
+ headers: {
1268
+ "Content-Type": "application/json",
1269
+ "Authorization": `Payment ${credentialB64}`,
1270
+ ...options.headers
1271
+ },
1272
+ body: options.body ? JSON.stringify(options.body) : void 0
1273
+ });
1274
+ if (!paidResponse.ok) {
1275
+ const errorText = await paidResponse.text();
1276
+ throw new Error(`Payment verification failed (${paidResponse.status}): ${errorText}`);
1277
+ }
1278
+ console.log(`[MoltsPay] Payment verified! Service completed.`);
1279
+ return paidResponse.json();
1280
+ }
1281
+ };
1282
+
1283
+ // src/mcp/server.ts
1284
+ var CHAIN_NAMES = [
1285
+ ...Object.keys(CHAINS),
1286
+ ...Object.keys(SOLANA_CHAINS)
1287
+ ];
1288
+ var TOKEN_SYMBOLS = ["USDC", "USDT"];
1289
+ function ok(payload) {
1290
+ return {
1291
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
1292
+ };
1293
+ }
1294
+ function err(message) {
1295
+ return {
1296
+ isError: true,
1297
+ content: [{ type: "text", text: `Error: ${message}` }]
1298
+ };
1299
+ }
1300
+ function errorMessage(e) {
1301
+ return e instanceof Error ? e.message : String(e);
1302
+ }
1303
+ function wrap(fn) {
1304
+ return async (args) => {
1305
+ try {
1306
+ return await fn(args);
1307
+ } catch (e) {
1308
+ return err(errorMessage(e));
1309
+ }
1310
+ };
1311
+ }
1312
+ function getPackageVersion() {
1313
+ const candidates = [
1314
+ join3(__dirname, "../../package.json"),
1315
+ join3(__dirname, "../package.json"),
1316
+ join3(process.cwd(), "node_modules/moltspay/package.json")
1317
+ ];
1318
+ for (const loc of candidates) {
1319
+ try {
1320
+ if (existsSync3(loc)) {
1321
+ const pkg = JSON.parse(readFileSync3(loc, "utf-8"));
1322
+ if (pkg.name === "moltspay") return pkg.version;
1323
+ }
1324
+ } catch {
1325
+ }
1326
+ }
1327
+ return "0.0.0";
1328
+ }
1329
+ function createMoltsPayMcpServer(options = {}) {
1330
+ const client = new MoltsPayClient({ configDir: options.configDir });
1331
+ if (!client.isInitialized) {
1332
+ throw new Error(
1333
+ "MoltsPay wallet not found. Run `moltspay init` before starting the MCP server."
1334
+ );
1335
+ }
1336
+ const dryRun = options.dryRun === true;
1337
+ const requireConfirm = process.env.MOLTSPAY_MCP_REQUIRE_CONFIRM === "1";
1338
+ const server = new McpServer({
1339
+ name: "moltspay",
1340
+ version: getPackageVersion()
1341
+ });
1342
+ server.registerTool(
1343
+ "moltspay_status",
1344
+ {
1345
+ title: "Wallet Status",
1346
+ description: "Return the MoltsPay wallet address, balances across all supported chains, and current spending limits.",
1347
+ inputSchema: {},
1348
+ annotations: { readOnlyHint: true, openWorldHint: true }
1349
+ },
1350
+ wrap(async () => {
1351
+ const config = client.getConfig();
1352
+ const balances = await client.getAllBalances();
1353
+ return ok({
1354
+ address: client.address,
1355
+ defaultChain: config.chain,
1356
+ balances,
1357
+ limits: config.limits
1358
+ });
1359
+ })
1360
+ );
1361
+ server.registerTool(
1362
+ "moltspay_services",
1363
+ {
1364
+ title: "List Provider Services",
1365
+ description: "Fetch the services manifest from a MoltsPay provider URL (e.g. https://moltspay.com/a/zen7). Returns service IDs, prices, input schemas, and accepted chains.",
1366
+ inputSchema: {
1367
+ url: z.url().describe("Provider base URL."),
1368
+ maxPrice: z.number().positive().optional().describe("Only return services priced at or below this amount."),
1369
+ query: z.string().optional().describe("Case-insensitive substring match on service id / name / description.")
1370
+ },
1371
+ annotations: { readOnlyHint: true, openWorldHint: true }
1372
+ },
1373
+ wrap(async ({ url, maxPrice, query }) => {
1374
+ const res = await client.getServices(url);
1375
+ let services = res.services ?? [];
1376
+ if (typeof maxPrice === "number") {
1377
+ services = services.filter((s) => typeof s.price !== "number" || s.price <= maxPrice);
1378
+ }
1379
+ if (query) {
1380
+ const q = query.toLowerCase();
1381
+ services = services.filter(
1382
+ (s) => [s.id, s.name, s.description].some((f) => f?.toLowerCase().includes(q))
1383
+ );
1384
+ }
1385
+ return ok({ provider: res.provider, services });
1386
+ })
1387
+ );
1388
+ server.registerTool(
1389
+ "moltspay_pay",
1390
+ {
1391
+ title: "Pay For Service",
1392
+ description: "Execute an x402/MPP/SOL/BNB payment and call a provider service. Honors SDK-level spending limits. When MOLTSPAY_MCP_REQUIRE_CONFIRM=1, the caller must pass confirmed: true.",
1393
+ inputSchema: {
1394
+ url: z.url().describe("Provider base URL."),
1395
+ service: z.string().min(1).describe("Service id from moltspay_services."),
1396
+ params: z.record(z.string(), z.unknown()).describe("Service parameters. Wrapped as { params } unless rawData is true."),
1397
+ chain: z.enum(CHAIN_NAMES).optional().describe("Chain to pay on. Required unless the provider only accepts one chain."),
1398
+ token: z.enum(TOKEN_SYMBOLS).optional().describe("Token symbol. Default USDC."),
1399
+ rawData: z.boolean().optional().describe("Send params at top level instead of wrapped in { params }."),
1400
+ confirmed: z.boolean().optional().describe("Required when MOLTSPAY_MCP_REQUIRE_CONFIRM=1 is set on the server.")
1401
+ },
1402
+ annotations: {
1403
+ readOnlyHint: false,
1404
+ destructiveHint: true,
1405
+ idempotentHint: false,
1406
+ openWorldHint: true
1407
+ }
1408
+ },
1409
+ wrap(async ({ url, service, params, chain, token, rawData, confirmed }) => {
1410
+ if (dryRun) {
1411
+ return ok({
1412
+ dryRun: true,
1413
+ message: "Dry-run mode: no payment will be executed. Inspect moltspay_services for prices.",
1414
+ intent: { url, service, params, chain, token, rawData }
1415
+ });
1416
+ }
1417
+ if (requireConfirm && confirmed !== true) {
1418
+ return err(
1419
+ "Confirmation required: server was started with MOLTSPAY_MCP_REQUIRE_CONFIRM=1. Re-call with confirmed: true after reviewing the price via moltspay_services."
1420
+ );
1421
+ }
1422
+ const result = await client.pay(url, service, params, { chain, token, rawData });
1423
+ return ok({ success: true, result });
1424
+ })
1425
+ );
1426
+ server.registerTool(
1427
+ "moltspay_config",
1428
+ {
1429
+ title: "Wallet Config",
1430
+ description: "Read or update MoltsPay wallet spending limits (maxPerTx, maxPerDay). Pass no arguments to read.",
1431
+ inputSchema: {
1432
+ maxPerTx: z.number().positive().optional().describe("New per-transaction USD limit."),
1433
+ maxPerDay: z.number().positive().optional().describe("New daily USD limit.")
1434
+ },
1435
+ annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false }
1436
+ },
1437
+ wrap(async ({ maxPerTx, maxPerDay }) => {
1438
+ if (maxPerTx !== void 0 || maxPerDay !== void 0) {
1439
+ client.updateConfig({ maxPerTx, maxPerDay });
1440
+ }
1441
+ const config = client.getConfig();
1442
+ return ok({
1443
+ address: client.address,
1444
+ defaultChain: config.chain,
1445
+ limits: config.limits
1446
+ });
1447
+ })
1448
+ );
1449
+ return server;
1450
+ }
1451
+
1452
+ // src/mcp/index.ts
1453
+ if (!globalThis.crypto) {
1454
+ globalThis.crypto = webcrypto;
1455
+ }
1456
+ function parseArgs(argv) {
1457
+ const args = argv.slice(2);
1458
+ let dryRun = false;
1459
+ let configDir;
1460
+ for (let i = 0; i < args.length; i++) {
1461
+ const a = args[i];
1462
+ if (a === "--dry-run") {
1463
+ dryRun = true;
1464
+ } else if (a === "--config-dir") {
1465
+ const next = args[i + 1];
1466
+ if (!next || next.startsWith("--")) {
1467
+ throw new Error("--config-dir requires a path argument");
1468
+ }
1469
+ configDir = next;
1470
+ i++;
1471
+ } else {
1472
+ throw new Error(`Unknown argument: ${a}`);
1473
+ }
1474
+ }
1475
+ return { dryRun, configDir };
1476
+ }
1477
+ async function main() {
1478
+ const { dryRun, configDir } = parseArgs(process.argv);
1479
+ const server = createMoltsPayMcpServer({ dryRun, configDir });
1480
+ const transport = new StdioServerTransport();
1481
+ await server.connect(transport);
1482
+ process.stderr.write(`moltspay-mcp ready${dryRun ? " (dry-run)" : ""}
1483
+ `);
1484
+ }
1485
+ main().catch((e) => {
1486
+ process.stderr.write(
1487
+ `moltspay-mcp failed to start: ${e instanceof Error ? e.message : String(e)}
1488
+ `
1489
+ );
1490
+ process.exit(1);
1491
+ });
1492
+ //# sourceMappingURL=index.mjs.map