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