moltspay 1.4.1 → 1.6.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,1623 @@
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/node/index.ts
37
+ var import_fs2 = require("fs");
38
+ var import_os2 = require("os");
39
+ var import_path2 = require("path");
40
+ var import_ethers2 = 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, connection) {
282
+ const chainConfig = SOLANA_CHAINS[chain];
283
+ const conn = 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)(conn, 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 conn.getLatestBlockhash();
322
+ transaction.recentBlockhash = blockhash;
323
+ transaction.feePayer = actualFeePayer;
324
+ return transaction;
325
+ }
326
+
327
+ // src/client/node/index.ts
328
+ var import_web35 = require("@solana/web3.js");
329
+
330
+ // src/client/core/types.ts
331
+ var X402_VERSION = 2;
332
+ var PAYMENT_REQUIRED_HEADER = "x-payment-required";
333
+ var PAYMENT_HEADER = "x-payment";
334
+
335
+ // src/client/core/chain-map.ts
336
+ var NETWORK_TO_CHAIN = {
337
+ "eip155:8453": "base",
338
+ "eip155:137": "polygon",
339
+ "eip155:84532": "base_sepolia",
340
+ "eip155:42431": "tempo_moderato",
341
+ "eip155:56": "bnb",
342
+ "eip155:97": "bnb_testnet",
343
+ "solana:mainnet": "solana",
344
+ "solana:devnet": "solana_devnet"
345
+ };
346
+ var CHAIN_TO_NETWORK = Object.fromEntries(
347
+ Object.entries(NETWORK_TO_CHAIN).map(([network, chain]) => [chain, network])
348
+ );
349
+ function networkToChainName(network) {
350
+ return NETWORK_TO_CHAIN[network] ?? null;
351
+ }
352
+
353
+ // src/client/core/base64.ts
354
+ var BufferCtor = globalThis.Buffer;
355
+
356
+ // src/client/core/eip3009.ts
357
+ var EIP3009_TYPES = {
358
+ TransferWithAuthorization: [
359
+ { name: "from", type: "address" },
360
+ { name: "to", type: "address" },
361
+ { name: "value", type: "uint256" },
362
+ { name: "validAfter", type: "uint256" },
363
+ { name: "validBefore", type: "uint256" },
364
+ { name: "nonce", type: "bytes32" }
365
+ ]
366
+ };
367
+ function buildEIP3009TypedData(args) {
368
+ const validAfter = args.validAfter ?? "0";
369
+ const validBefore = args.validBefore ?? (Math.floor(Date.now() / 1e3) + 3600).toString();
370
+ const authorization = {
371
+ from: args.from,
372
+ to: args.to,
373
+ value: args.value,
374
+ validAfter,
375
+ validBefore,
376
+ nonce: args.nonce
377
+ };
378
+ return {
379
+ domain: {
380
+ name: args.tokenName,
381
+ version: args.tokenVersion,
382
+ chainId: args.chainId,
383
+ verifyingContract: args.tokenAddress
384
+ },
385
+ types: EIP3009_TYPES,
386
+ primaryType: "TransferWithAuthorization",
387
+ message: authorization
388
+ };
389
+ }
390
+
391
+ // src/client/core/bnb-intent.ts
392
+ var BNB_INTENT_TYPES = {
393
+ PaymentIntent: [
394
+ { name: "from", type: "address" },
395
+ { name: "to", type: "address" },
396
+ { name: "amount", type: "uint256" },
397
+ { name: "token", type: "address" },
398
+ { name: "service", type: "string" },
399
+ { name: "nonce", type: "uint256" },
400
+ { name: "deadline", type: "uint256" }
401
+ ]
402
+ };
403
+ var BNB_DOMAIN_NAME = "MoltsPay";
404
+ var BNB_DOMAIN_VERSION = "1";
405
+ function buildBnbIntentTypedData(args) {
406
+ const intent = {
407
+ from: args.from,
408
+ to: args.to,
409
+ amount: args.amount,
410
+ token: args.tokenAddress,
411
+ service: args.service,
412
+ nonce: args.nonce,
413
+ deadline: args.deadline
414
+ };
415
+ return {
416
+ domain: {
417
+ name: BNB_DOMAIN_NAME,
418
+ version: BNB_DOMAIN_VERSION,
419
+ chainId: args.chainId
420
+ },
421
+ types: BNB_INTENT_TYPES,
422
+ primaryType: "PaymentIntent",
423
+ message: intent
424
+ };
425
+ }
426
+
427
+ // src/client/node/signer.ts
428
+ var import_ethers = require("ethers");
429
+ var import_web34 = require("@solana/web3.js");
430
+ var NodeSigner = class {
431
+ evmWallet;
432
+ getSolanaKeypair;
433
+ constructor(evmWallet, options = {}) {
434
+ this.evmWallet = evmWallet;
435
+ this.getSolanaKeypair = options.getSolanaKeypair ?? (() => null);
436
+ }
437
+ async getEvmAddress() {
438
+ return this.evmWallet.address;
439
+ }
440
+ async getSolanaAddress() {
441
+ const kp = this.getSolanaKeypair();
442
+ return kp ? kp.publicKey.toBase58() : null;
443
+ }
444
+ async signTypedData(envelope) {
445
+ const mutableTypes = {};
446
+ for (const [key, fields] of Object.entries(envelope.types)) {
447
+ mutableTypes[key] = [...fields];
448
+ }
449
+ return this.evmWallet.signTypedData(
450
+ envelope.domain,
451
+ mutableTypes,
452
+ envelope.message
453
+ );
454
+ }
455
+ async sendEvmTransaction(args) {
456
+ const chain = findChainByChainId(args.chainId);
457
+ if (!chain) {
458
+ throw new Error(`sendEvmTransaction: unknown chainId ${args.chainId}`);
459
+ }
460
+ const provider = new import_ethers.ethers.JsonRpcProvider(chain.rpc);
461
+ const connected = this.evmWallet.connect(provider);
462
+ const tx = await connected.sendTransaction({
463
+ to: args.to,
464
+ data: args.data,
465
+ value: args.value ? BigInt(args.value) : 0n
466
+ });
467
+ return tx.hash;
468
+ }
469
+ async signSolanaTransaction(args) {
470
+ const kp = this.getSolanaKeypair();
471
+ if (!kp) {
472
+ throw new Error("signSolanaTransaction: no Solana wallet configured");
473
+ }
474
+ const tx = import_web34.Transaction.from(Buffer.from(args.transactionBase64, "base64"));
475
+ if (args.partialSign) {
476
+ tx.partialSign(kp);
477
+ } else {
478
+ tx.sign(kp);
479
+ }
480
+ return tx.serialize({ requireAllSignatures: false }).toString("base64");
481
+ }
482
+ };
483
+ function findChainByChainId(chainId) {
484
+ for (const cfg of Object.values(CHAINS)) {
485
+ if (cfg.chainId === chainId) {
486
+ return cfg;
487
+ }
488
+ }
489
+ return void 0;
490
+ }
491
+
492
+ // src/client/node/index.ts
493
+ var DEFAULT_CONFIG = {
494
+ chain: "base",
495
+ limits: {
496
+ maxPerTx: 100,
497
+ maxPerDay: 1e3
498
+ }
499
+ };
500
+ var MoltsPayClient = class {
501
+ configDir;
502
+ config;
503
+ walletData = null;
504
+ wallet = null;
505
+ signer = null;
506
+ todaySpending = 0;
507
+ lastSpendingReset = 0;
508
+ constructor(options = {}) {
509
+ this.configDir = options.configDir || (0, import_path2.join)((0, import_os2.homedir)(), ".moltspay");
510
+ this.config = this.loadConfig();
511
+ this.walletData = this.loadWallet();
512
+ this.loadSpending();
513
+ if (this.walletData) {
514
+ this.wallet = new import_ethers2.Wallet(this.walletData.privateKey);
515
+ const configDir = this.configDir;
516
+ this.signer = new NodeSigner(this.wallet, {
517
+ getSolanaKeypair: () => loadSolanaWallet(configDir)
518
+ });
519
+ }
520
+ }
521
+ /**
522
+ * Check if client is initialized (has wallet)
523
+ */
524
+ get isInitialized() {
525
+ return this.wallet !== null;
526
+ }
527
+ /**
528
+ * Get wallet address
529
+ */
530
+ get address() {
531
+ return this.wallet?.address || null;
532
+ }
533
+ /**
534
+ * Get wallet instance (for direct operations like approvals)
535
+ */
536
+ getWallet() {
537
+ return this.wallet;
538
+ }
539
+ /**
540
+ * Get current config
541
+ */
542
+ getConfig() {
543
+ return { ...this.config };
544
+ }
545
+ /**
546
+ * Update config
547
+ */
548
+ updateConfig(updates) {
549
+ if (updates.maxPerTx !== void 0) {
550
+ this.config.limits.maxPerTx = updates.maxPerTx;
551
+ }
552
+ if (updates.maxPerDay !== void 0) {
553
+ this.config.limits.maxPerDay = updates.maxPerDay;
554
+ }
555
+ this.saveConfig();
556
+ }
557
+ /**
558
+ * Get services from a provider
559
+ */
560
+ async getServices(serverUrl) {
561
+ const normalizedUrl = serverUrl.replace(/\/(services|api\/services|registry\/services)\/?$/, "");
562
+ const endpoints = ["/services", "/api/services", "/registry/services"];
563
+ for (const endpoint of endpoints) {
564
+ try {
565
+ const res = await fetch(`${normalizedUrl}${endpoint}`);
566
+ if (!res.ok) continue;
567
+ const contentType = res.headers.get("content-type") || "";
568
+ if (!contentType.includes("application/json")) continue;
569
+ return await res.json();
570
+ } catch {
571
+ continue;
572
+ }
573
+ }
574
+ throw new Error(`Failed to get services: no valid endpoint found at ${normalizedUrl}`);
575
+ }
576
+ /**
577
+ * Pay for a service and get the result (x402 protocol)
578
+ *
579
+ * This is GASLESS for the client - server pays gas to claim payment.
580
+ * This is PAY-FOR-SUCCESS - payment only claimed if service succeeds.
581
+ *
582
+ * @param serverUrl - Server URL
583
+ * @param service - Service ID
584
+ * @param params - Service parameters
585
+ * @param options - Payment options (token selection)
586
+ */
587
+ async pay(serverUrl, service, params, options = {}) {
588
+ if (!this.wallet || !this.walletData) {
589
+ throw new Error("Client not initialized. Run: npx moltspay init");
590
+ }
591
+ console.log(`[MoltsPay] Requesting service: ${service}`);
592
+ let executeUrl = `${serverUrl}/execute`;
593
+ try {
594
+ const services = await this.getServices(serverUrl);
595
+ const svc = services.services?.find((s) => s.id === service);
596
+ if (svc?.endpoint) {
597
+ executeUrl = `${serverUrl}${svc.endpoint}`;
598
+ console.log(`[MoltsPay] Using service endpoint: ${svc.endpoint}`);
599
+ }
600
+ } catch {
601
+ }
602
+ let requestBody;
603
+ if (options.rawData) {
604
+ requestBody = { service, ...params };
605
+ } else {
606
+ requestBody = { service, params };
607
+ }
608
+ if (options.chain) {
609
+ requestBody.chain = options.chain;
610
+ }
611
+ const initialRes = await fetch(executeUrl, {
612
+ method: "POST",
613
+ headers: { "Content-Type": "application/json" },
614
+ body: JSON.stringify(requestBody)
615
+ });
616
+ if (initialRes.status !== 402) {
617
+ const data = await initialRes.json();
618
+ if (initialRes.ok && data.result) {
619
+ return data.result;
620
+ }
621
+ throw new Error(data.error || "Unexpected response");
622
+ }
623
+ const wwwAuthHeader = initialRes.headers.get("www-authenticate");
624
+ const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER);
625
+ if (wwwAuthHeader && wwwAuthHeader.toLowerCase().includes("payment")) {
626
+ console.log("[MoltsPay] Detected MPP protocol, using Tempo flow...");
627
+ return await this.handleMPPPayment(executeUrl, service, params, wwwAuthHeader, options);
628
+ }
629
+ if (!paymentRequiredHeader) {
630
+ throw new Error("Missing payment header (x-payment-required or www-authenticate)");
631
+ }
632
+ let requirements;
633
+ try {
634
+ const decoded = Buffer.from(paymentRequiredHeader, "base64").toString("utf-8");
635
+ const parsed = JSON.parse(decoded);
636
+ if (Array.isArray(parsed)) {
637
+ requirements = parsed;
638
+ } else if (parsed.accepts && Array.isArray(parsed.accepts)) {
639
+ requirements = parsed.accepts;
640
+ } else {
641
+ requirements = [parsed];
642
+ }
643
+ } catch {
644
+ throw new Error("Invalid x-payment-required header");
645
+ }
646
+ const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
647
+ const userSpecifiedChain = options.chain;
648
+ let selectedChain;
649
+ if (userSpecifiedChain) {
650
+ if (!serverChains.includes(userSpecifiedChain)) {
651
+ throw new Error(
652
+ `Server doesn't accept '${userSpecifiedChain}'.
653
+ Server accepts: ${serverChains.join(", ")}`
654
+ );
655
+ }
656
+ selectedChain = userSpecifiedChain;
657
+ } else {
658
+ if (serverChains.length === 1 && serverChains[0] === "base") {
659
+ selectedChain = "base";
660
+ } else {
661
+ throw new Error(
662
+ `Server accepts: ${serverChains.join(", ")}
663
+ Please specify: --chain <chain_name>`
664
+ );
665
+ }
666
+ }
667
+ if (selectedChain === "solana" || selectedChain === "solana_devnet") {
668
+ const solanaChain = selectedChain;
669
+ const network2 = solanaChain === "solana" ? "solana:mainnet" : "solana:devnet";
670
+ const req2 = requirements.find((r) => r.network === network2);
671
+ if (!req2) {
672
+ throw new Error(`Failed to find payment requirement for ${selectedChain}`);
673
+ }
674
+ return await this.handleSolanaPayment(executeUrl, service, params, req2, solanaChain, options);
675
+ }
676
+ const chainName = selectedChain;
677
+ const chain = getChain(chainName);
678
+ const network = `eip155:${chain.chainId}`;
679
+ const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
680
+ if (!req) {
681
+ throw new Error(`Failed to find payment requirement for ${chainName}`);
682
+ }
683
+ const amountRaw = req.amount || req.maxAmountRequired;
684
+ if (!amountRaw) {
685
+ throw new Error("Missing amount in payment requirements");
686
+ }
687
+ const amount = Number(amountRaw) / 1e6;
688
+ this.checkLimits(amount);
689
+ let token = options.token || "USDC";
690
+ if (options.autoSelect) {
691
+ const balances = await this.getBalance();
692
+ if (balances.usdc >= amount) {
693
+ token = "USDC";
694
+ } else if (balances.usdt >= amount) {
695
+ token = "USDT";
696
+ } else {
697
+ throw new Error(`Insufficient balance: need $${amount}, have ${balances.usdc} USDC / ${balances.usdt} USDT`);
698
+ }
699
+ }
700
+ if (token === "USDT") {
701
+ const balances = await this.getBalance();
702
+ if (balances.native < 1e-4) {
703
+ throw new Error(
704
+ `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).`
705
+ );
706
+ }
707
+ console.log(`[MoltsPay] \u26A0\uFE0F USDT requires gas (~$0.01). Proceeding with payment...`);
708
+ } else {
709
+ console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
710
+ }
711
+ if (chainName === "bnb" || chainName === "bnb_testnet") {
712
+ console.log(`[MoltsPay] Using BNB intent-based payment flow...`);
713
+ const payTo2 = req.payTo || req.resource;
714
+ if (!payTo2) {
715
+ throw new Error("Missing payTo address in payment requirements");
716
+ }
717
+ const bnbSpender = req.extra?.bnbSpender;
718
+ if (!bnbSpender) {
719
+ throw new Error("Server did not provide bnbSpender address. Server may not support BNB payments.");
720
+ }
721
+ return await this.handleBNBPayment(executeUrl, service, params, {
722
+ to: payTo2,
723
+ amount,
724
+ token,
725
+ chainName,
726
+ chain,
727
+ spender: bnbSpender
728
+ }, options);
729
+ }
730
+ const payTo = req.payTo || req.resource;
731
+ if (!payTo) {
732
+ throw new Error("Missing payTo address in payment requirements");
733
+ }
734
+ const domainOverride = req.extra && typeof req.extra === "object" && req.extra.name ? { name: req.extra.name, version: req.extra.version || "2" } : void 0;
735
+ const authorization = await this.signEIP3009(payTo, amount, chain, token, domainOverride);
736
+ const tokenConfig = chain.tokens[token];
737
+ const extra = req.extra && typeof req.extra === "object" ? req.extra : {
738
+ name: tokenConfig.eip712Name || "USD Coin",
739
+ version: "2"
740
+ };
741
+ const payload = {
742
+ x402Version: X402_VERSION,
743
+ scheme: "exact",
744
+ network,
745
+ payload: authorization,
746
+ // { authorization: {...}, signature: "0x..." }
747
+ accepted: {
748
+ scheme: "exact",
749
+ network,
750
+ asset: tokenConfig.address,
751
+ amount: amountRaw,
752
+ payTo,
753
+ maxTimeoutSeconds: req.maxTimeoutSeconds || 300,
754
+ extra
755
+ }
756
+ };
757
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
758
+ console.log(`[MoltsPay] Sending request with payment...`);
759
+ const paidRequestBody = options.rawData ? { service, ...params } : { service, params };
760
+ if (options.chain) {
761
+ paidRequestBody.chain = options.chain;
762
+ }
763
+ const paidRes = await fetch(executeUrl, {
764
+ method: "POST",
765
+ headers: {
766
+ "Content-Type": "application/json",
767
+ [PAYMENT_HEADER]: paymentHeader
768
+ },
769
+ body: JSON.stringify(paidRequestBody)
770
+ });
771
+ const result = await paidRes.json();
772
+ if (!paidRes.ok) {
773
+ throw new Error(result.error || "Service execution failed");
774
+ }
775
+ this.recordSpending(amount);
776
+ console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
777
+ return result.result || result;
778
+ }
779
+ /**
780
+ * Handle MPP (Machine Payments Protocol) payment flow
781
+ * Called when pay() detects WWW-Authenticate header in 402 response
782
+ */
783
+ async handleMPPPayment(executeUrl, service, params, wwwAuthHeader, options = {}) {
784
+ const { privateKeyToAccount } = await import("viem/accounts");
785
+ const { createWalletClient, createPublicClient, http } = await import("viem");
786
+ const { tempoModerato } = await import("viem/chains");
787
+ const { Actions } = await import("viem/tempo");
788
+ const privateKey = this.walletData.privateKey;
789
+ const account = privateKeyToAccount(privateKey);
790
+ console.log(`[MoltsPay] Using MPP protocol on Tempo`);
791
+ console.log(`[MoltsPay] Account: ${account.address}`);
792
+ const parseAuthParam = (header, key) => {
793
+ const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
794
+ return match ? match[1] : null;
795
+ };
796
+ const challengeId = parseAuthParam(wwwAuthHeader, "id");
797
+ const method = parseAuthParam(wwwAuthHeader, "method");
798
+ const realm = parseAuthParam(wwwAuthHeader, "realm");
799
+ const requestB64 = parseAuthParam(wwwAuthHeader, "request");
800
+ if (method !== "tempo") {
801
+ throw new Error(`Unsupported payment method: ${method}`);
802
+ }
803
+ if (!requestB64) {
804
+ throw new Error("Missing request in WWW-Authenticate");
805
+ }
806
+ const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
807
+ const paymentRequest = JSON.parse(requestJson);
808
+ const { amount, currency, recipient, methodDetails } = paymentRequest;
809
+ const chainId = methodDetails?.chainId || 42431;
810
+ const amountDisplay = Number(amount) / 1e6;
811
+ console.log(`[MoltsPay] Payment: $${amountDisplay} to ${recipient}`);
812
+ this.checkLimits(amountDisplay);
813
+ console.log(`[MoltsPay] Sending transaction on Tempo...`);
814
+ const tempoChain = { ...tempoModerato, feeToken: currency };
815
+ const publicClient = createPublicClient({
816
+ chain: tempoChain,
817
+ transport: http("https://rpc.moderato.tempo.xyz")
818
+ });
819
+ const walletClient = createWalletClient({
820
+ account,
821
+ chain: tempoChain,
822
+ transport: http("https://rpc.moderato.tempo.xyz")
823
+ });
824
+ const txHash = await Actions.token.transfer(walletClient, {
825
+ to: recipient,
826
+ amount: BigInt(amount),
827
+ token: currency
828
+ });
829
+ console.log(`[MoltsPay] Transaction: ${txHash}`);
830
+ await publicClient.waitForTransactionReceipt({ hash: txHash });
831
+ console.log(`[MoltsPay] Confirmed! Retrying with credential...`);
832
+ const credential = {
833
+ challenge: {
834
+ id: challengeId,
835
+ realm,
836
+ method: "tempo",
837
+ intent: "charge",
838
+ request: paymentRequest
839
+ },
840
+ payload: { hash: txHash, type: "hash" },
841
+ source: `did:pkh:eip155:${chainId}:${account.address}`
842
+ };
843
+ const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
844
+ const retryBody = options.rawData ? { service, ...params, chain: "tempo_moderato" } : { service, params, chain: "tempo_moderato" };
845
+ const paidRes = await fetch(executeUrl, {
846
+ method: "POST",
847
+ headers: {
848
+ "Content-Type": "application/json",
849
+ "Authorization": `Payment ${credentialB64}`
850
+ },
851
+ body: JSON.stringify(retryBody)
852
+ });
853
+ const result = await paidRes.json();
854
+ if (!paidRes.ok) {
855
+ throw new Error(result.error || "Payment verification failed");
856
+ }
857
+ this.recordSpending(amountDisplay);
858
+ console.log(`[MoltsPay] Success!`);
859
+ return result.result || result;
860
+ }
861
+ /**
862
+ * Handle BNB Chain payment flow (pre-approval + intent signature)
863
+ *
864
+ * Flow:
865
+ * 1. Check client has approved server wallet (done via `moltspay init`)
866
+ * 2. Sign EIP-712 payment intent (no gas, just signature)
867
+ * 3. Send intent to server
868
+ * 4. Server executes service
869
+ * 5. Server calls transferFrom if successful (pay-for-success)
870
+ */
871
+ async handleBNBPayment(executeUrl, service, params, paymentDetails, options = {}) {
872
+ const { to, amount, token, chainName, chain, spender } = paymentDetails;
873
+ const tokenConfig = chain.tokens[token];
874
+ const provider = new import_ethers2.ethers.JsonRpcProvider(chain.rpc);
875
+ const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
876
+ const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
877
+ if (allowance < amountWeiCheck) {
878
+ const nativeBalance = await provider.getBalance(this.wallet.address);
879
+ const minGasBalance = import_ethers2.ethers.parseEther("0.0005");
880
+ if (nativeBalance < minGasBalance) {
881
+ const nativeBNB = parseFloat(import_ethers2.ethers.formatEther(nativeBalance)).toFixed(4);
882
+ const isTestnet = chainName === "bnb_testnet";
883
+ if (isTestnet) {
884
+ throw new Error(
885
+ `\u274C Insufficient tBNB for approval transaction
886
+
887
+ Current tBNB: ${nativeBNB}
888
+ Required: ~0.001 tBNB
889
+
890
+ Get testnet tokens: npx moltspay faucet --chain bnb_testnet
891
+ (Gives USDC + tBNB for gas)`
892
+ );
893
+ } else {
894
+ throw new Error(
895
+ `\u274C Insufficient BNB for approval transaction
896
+
897
+ Current BNB: ${nativeBNB}
898
+ Required: ~0.001 BNB (~$0.60)
899
+
900
+ To get BNB:
901
+ \u2022 Withdraw from Binance/exchange to your wallet
902
+ \u2022 Most exchanges include BNB dust with withdrawals
903
+
904
+ After funding, run:
905
+ npx moltspay approve --chain ${chainName} --spender ${spender}`
906
+ );
907
+ }
908
+ }
909
+ throw new Error(
910
+ `Insufficient allowance for ${spender.slice(0, 10)}...
911
+ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
912
+ );
913
+ }
914
+ const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
915
+ const intentNonce = Date.now();
916
+ const intentDeadline = Date.now() + 36e5;
917
+ const envelope = buildBnbIntentTypedData({
918
+ from: this.wallet.address,
919
+ to,
920
+ amount: amountWei,
921
+ tokenAddress: tokenConfig.address,
922
+ service,
923
+ nonce: intentNonce,
924
+ deadline: intentDeadline,
925
+ chainId: chain.chainId
926
+ });
927
+ console.log(`[MoltsPay] Signing BNB payment intent...`);
928
+ const signature = await this.signer.signTypedData(envelope);
929
+ const intent = envelope.message;
930
+ const network = `eip155:${chain.chainId}`;
931
+ const payload = {
932
+ x402Version: 2,
933
+ scheme: "exact",
934
+ network,
935
+ payload: {
936
+ intent: {
937
+ ...intent,
938
+ signature
939
+ },
940
+ chainId: chain.chainId
941
+ },
942
+ accepted: {
943
+ scheme: "exact",
944
+ network,
945
+ asset: tokenConfig.address,
946
+ amount: amountWei,
947
+ payTo: to,
948
+ maxTimeoutSeconds: 300
949
+ }
950
+ };
951
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
952
+ console.log(`[MoltsPay] Sending BNB payment request...`);
953
+ const bnbRequestBody = options.rawData ? { service, ...params, chain: chainName } : { service, params, chain: chainName };
954
+ const paidRes = await fetch(executeUrl, {
955
+ method: "POST",
956
+ headers: {
957
+ "Content-Type": "application/json",
958
+ "X-Payment": paymentHeader
959
+ },
960
+ body: JSON.stringify(bnbRequestBody)
961
+ });
962
+ const result = await paidRes.json();
963
+ if (!paidRes.ok) {
964
+ throw new Error(result.error || "BNB payment failed");
965
+ }
966
+ this.recordSpending(amount);
967
+ console.log(`[MoltsPay] Success! BNB payment settled.`);
968
+ return result.result || result;
969
+ }
970
+ /**
971
+ * Handle Solana payment flow
972
+ *
973
+ * Solana uses SPL token transfers with pay-for-success model:
974
+ * 1. Client creates and signs a transfer transaction
975
+ * 2. Server submits the transaction after service completes
976
+ */
977
+ async handleSolanaPayment(executeUrl, service, params, requirements, chain, options = {}) {
978
+ const solanaWallet = loadSolanaWallet(this.configDir);
979
+ if (!solanaWallet) {
980
+ throw new Error("No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
981
+ }
982
+ const amount = Number(requirements.amount);
983
+ const amountUSDC = amount / 1e6;
984
+ this.checkLimits(amountUSDC);
985
+ console.log(`[MoltsPay] Creating Solana payment: $${amountUSDC} USDC`);
986
+ if (!requirements.payTo) {
987
+ throw new Error("Missing payTo address in payment requirements");
988
+ }
989
+ const solanaFeePayer = requirements.extra?.solanaFeePayer;
990
+ const feePayerPubkey = solanaFeePayer ? new import_web35.PublicKey(solanaFeePayer) : void 0;
991
+ if (feePayerPubkey) {
992
+ console.log(`[MoltsPay] Gasless mode: server pays fees`);
993
+ }
994
+ const recipientPubkey = new import_web35.PublicKey(requirements.payTo);
995
+ const transaction = await createSolanaPaymentTransaction(
996
+ solanaWallet.publicKey,
997
+ recipientPubkey,
998
+ BigInt(amount),
999
+ chain,
1000
+ feePayerPubkey
1001
+ // Optional fee payer for gasless mode
1002
+ );
1003
+ const unsignedBase64 = transaction.serialize({ requireAllSignatures: false, verifySignatures: false }).toString("base64");
1004
+ const signedTx = await this.signer.signSolanaTransaction({
1005
+ transactionBase64: unsignedBase64,
1006
+ partialSign: !!feePayerPubkey
1007
+ });
1008
+ console.log(`[MoltsPay] Transaction signed, sending to server...`);
1009
+ const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
1010
+ const payload = {
1011
+ x402Version: 2,
1012
+ scheme: "exact",
1013
+ network,
1014
+ payload: {
1015
+ signedTransaction: signedTx,
1016
+ sender: solanaWallet.publicKey.toBase58(),
1017
+ chain
1018
+ },
1019
+ accepted: {
1020
+ scheme: "exact",
1021
+ network,
1022
+ asset: requirements.asset,
1023
+ amount: requirements.amount,
1024
+ payTo: requirements.payTo,
1025
+ maxTimeoutSeconds: 300
1026
+ }
1027
+ };
1028
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
1029
+ const solanaRequestBody = options.rawData ? { service, ...params, chain } : { service, params, chain };
1030
+ const paidRes = await fetch(executeUrl, {
1031
+ method: "POST",
1032
+ headers: {
1033
+ "Content-Type": "application/json",
1034
+ "X-Payment": paymentHeader
1035
+ },
1036
+ body: JSON.stringify(solanaRequestBody)
1037
+ });
1038
+ const result = await paidRes.json();
1039
+ if (!paidRes.ok) {
1040
+ throw new Error(result.error || "Solana payment failed");
1041
+ }
1042
+ this.recordSpending(amountUSDC);
1043
+ console.log(`[MoltsPay] Success! Solana payment settled.`);
1044
+ if (result.payment?.transaction) {
1045
+ const explorerUrl = chain === "solana" ? `https://solscan.io/tx/${result.payment.transaction}` : `https://solscan.io/tx/${result.payment.transaction}?cluster=devnet`;
1046
+ console.log(`[MoltsPay] Transaction: ${explorerUrl}`);
1047
+ }
1048
+ return result.result || result;
1049
+ }
1050
+ /**
1051
+ * Check ERC20 allowance for a spender
1052
+ */
1053
+ async checkAllowance(tokenAddress, spender, provider) {
1054
+ const contract = new import_ethers2.ethers.Contract(
1055
+ tokenAddress,
1056
+ ["function allowance(address owner, address spender) view returns (uint256)"],
1057
+ provider
1058
+ );
1059
+ return await contract.allowance(this.wallet.address, spender);
1060
+ }
1061
+ /**
1062
+ * Sign EIP-3009 transferWithAuthorization (GASLESS)
1063
+ * This only signs - no on-chain transaction, no gas needed.
1064
+ * Supports both USDC and USDT.
1065
+ *
1066
+ * Delegates typed-data construction to `core/eip3009.ts` and the signature
1067
+ * itself to `this.signer`. That way Web Client (Phase 4) can reuse the same
1068
+ * flow with an EIP-1193 signer without duplicating typed-data layout.
1069
+ */
1070
+ async signEIP3009(to, amount, chain, token = "USDC", domainOverride) {
1071
+ const tokenConfig = chain.tokens[token];
1072
+ const value = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
1073
+ const nonce = import_ethers2.ethers.hexlify(import_ethers2.ethers.randomBytes(32));
1074
+ const tokenName = domainOverride?.name || tokenConfig.eip712Name || (token === "USDC" ? "USD Coin" : "Tether USD");
1075
+ const tokenVersion = domainOverride?.version || "2";
1076
+ const envelope = buildEIP3009TypedData({
1077
+ from: this.wallet.address,
1078
+ to,
1079
+ value,
1080
+ nonce,
1081
+ chainId: chain.chainId,
1082
+ tokenAddress: tokenConfig.address,
1083
+ tokenName,
1084
+ tokenVersion
1085
+ });
1086
+ const signature = await this.signer.signTypedData(envelope);
1087
+ return { authorization: envelope.message, signature };
1088
+ }
1089
+ /**
1090
+ * Check spending limits
1091
+ */
1092
+ checkLimits(amount) {
1093
+ if (amount > this.config.limits.maxPerTx) {
1094
+ throw new Error(
1095
+ `Amount $${amount} exceeds max per transaction ($${this.config.limits.maxPerTx})`
1096
+ );
1097
+ }
1098
+ const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
1099
+ if (today > this.lastSpendingReset) {
1100
+ this.todaySpending = 0;
1101
+ this.lastSpendingReset = today;
1102
+ this.saveSpending();
1103
+ }
1104
+ if (this.todaySpending + amount > this.config.limits.maxPerDay) {
1105
+ throw new Error(
1106
+ `Would exceed daily limit ($${this.todaySpending} + $${amount} > $${this.config.limits.maxPerDay})`
1107
+ );
1108
+ }
1109
+ }
1110
+ /**
1111
+ * Record spending and persist to disk
1112
+ */
1113
+ recordSpending(amount) {
1114
+ this.todaySpending += amount;
1115
+ this.saveSpending();
1116
+ }
1117
+ // --- Config & Wallet Management ---
1118
+ loadConfig() {
1119
+ const configPath = (0, import_path2.join)(this.configDir, "config.json");
1120
+ if ((0, import_fs2.existsSync)(configPath)) {
1121
+ const content = (0, import_fs2.readFileSync)(configPath, "utf-8");
1122
+ return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
1123
+ }
1124
+ return { ...DEFAULT_CONFIG };
1125
+ }
1126
+ saveConfig() {
1127
+ (0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
1128
+ const configPath = (0, import_path2.join)(this.configDir, "config.json");
1129
+ (0, import_fs2.writeFileSync)(configPath, JSON.stringify(this.config, null, 2));
1130
+ }
1131
+ /**
1132
+ * Load spending data from disk
1133
+ */
1134
+ loadSpending() {
1135
+ const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
1136
+ if ((0, import_fs2.existsSync)(spendingPath)) {
1137
+ try {
1138
+ const data = JSON.parse((0, import_fs2.readFileSync)(spendingPath, "utf-8"));
1139
+ const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
1140
+ if (data.date && data.date === today) {
1141
+ this.todaySpending = data.amount || 0;
1142
+ this.lastSpendingReset = data.date;
1143
+ } else {
1144
+ this.todaySpending = 0;
1145
+ this.lastSpendingReset = today;
1146
+ }
1147
+ } catch {
1148
+ this.todaySpending = 0;
1149
+ this.lastSpendingReset = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
1150
+ }
1151
+ }
1152
+ }
1153
+ /**
1154
+ * Save spending data to disk
1155
+ */
1156
+ saveSpending() {
1157
+ (0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
1158
+ const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
1159
+ const data = {
1160
+ date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
1161
+ amount: this.todaySpending,
1162
+ updatedAt: Date.now()
1163
+ };
1164
+ (0, import_fs2.writeFileSync)(spendingPath, JSON.stringify(data, null, 2));
1165
+ }
1166
+ loadWallet() {
1167
+ const walletPath = (0, import_path2.join)(this.configDir, "wallet.json");
1168
+ if ((0, import_fs2.existsSync)(walletPath)) {
1169
+ if (process.platform !== "win32") {
1170
+ try {
1171
+ const stats = (0, import_fs2.statSync)(walletPath);
1172
+ const mode = stats.mode & 511;
1173
+ if (mode !== 384) {
1174
+ console.warn(`[MoltsPay] WARNING: wallet.json has insecure permissions (${mode.toString(8)})`);
1175
+ console.warn(`[MoltsPay] Fixing permissions to 0600...`);
1176
+ (0, import_fs2.chmodSync)(walletPath, 384);
1177
+ }
1178
+ } catch {
1179
+ }
1180
+ }
1181
+ const content = (0, import_fs2.readFileSync)(walletPath, "utf-8");
1182
+ return JSON.parse(content);
1183
+ }
1184
+ return null;
1185
+ }
1186
+ /**
1187
+ * Initialize a new wallet (called by CLI)
1188
+ */
1189
+ static init(configDir, options) {
1190
+ (0, import_fs2.mkdirSync)(configDir, { recursive: true });
1191
+ const wallet = import_ethers2.Wallet.createRandom();
1192
+ const walletData = {
1193
+ address: wallet.address,
1194
+ privateKey: wallet.privateKey,
1195
+ createdAt: Date.now()
1196
+ };
1197
+ const walletPath = (0, import_path2.join)(configDir, "wallet.json");
1198
+ (0, import_fs2.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
1199
+ const config = {
1200
+ chain: options.chain,
1201
+ limits: {
1202
+ maxPerTx: options.maxPerTx,
1203
+ maxPerDay: options.maxPerDay
1204
+ }
1205
+ };
1206
+ const configPath = (0, import_path2.join)(configDir, "config.json");
1207
+ (0, import_fs2.writeFileSync)(configPath, JSON.stringify(config, null, 2));
1208
+ return { address: wallet.address, configDir };
1209
+ }
1210
+ /**
1211
+ * Get wallet balance (USDC, USDT, and native token) on default chain
1212
+ */
1213
+ async getBalance() {
1214
+ if (!this.wallet) {
1215
+ throw new Error("Client not initialized");
1216
+ }
1217
+ let chain;
1218
+ try {
1219
+ chain = getChain(this.config.chain);
1220
+ } catch {
1221
+ throw new Error(`Unknown chain: ${this.config.chain}`);
1222
+ }
1223
+ const provider = new import_ethers2.ethers.JsonRpcProvider(chain.rpc);
1224
+ const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
1225
+ const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
1226
+ provider.getBalance(this.wallet.address),
1227
+ new import_ethers2.ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
1228
+ new import_ethers2.ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
1229
+ ]);
1230
+ return {
1231
+ usdc: parseFloat(import_ethers2.ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
1232
+ usdt: parseFloat(import_ethers2.ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
1233
+ native: parseFloat(import_ethers2.ethers.formatEther(nativeBalance))
1234
+ };
1235
+ }
1236
+ /**
1237
+ * Get wallet balances on all supported chains (Base + Polygon + Tempo)
1238
+ */
1239
+ async getAllBalances() {
1240
+ if (!this.wallet) {
1241
+ throw new Error("Client not initialized");
1242
+ }
1243
+ const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
1244
+ const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
1245
+ const results = {};
1246
+ const tempoTokens = {
1247
+ pathUSD: "0x20c0000000000000000000000000000000000000",
1248
+ alphaUSD: "0x20c0000000000000000000000000000000000001",
1249
+ betaUSD: "0x20c0000000000000000000000000000000000002",
1250
+ thetaUSD: "0x20c0000000000000000000000000000000000003"
1251
+ };
1252
+ await Promise.all(
1253
+ supportedChains.map(async (chainName) => {
1254
+ try {
1255
+ const chain = getChain(chainName);
1256
+ const provider = new import_ethers2.ethers.JsonRpcProvider(chain.rpc);
1257
+ if (chainName === "tempo_moderato") {
1258
+ const [nativeBalance, pathUSD, alphaUSD, betaUSD, thetaUSD] = await Promise.all([
1259
+ provider.getBalance(this.wallet.address),
1260
+ new import_ethers2.ethers.Contract(tempoTokens.pathUSD, tokenAbi, provider).balanceOf(this.wallet.address),
1261
+ new import_ethers2.ethers.Contract(tempoTokens.alphaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
1262
+ new import_ethers2.ethers.Contract(tempoTokens.betaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
1263
+ new import_ethers2.ethers.Contract(tempoTokens.thetaUSD, tokenAbi, provider).balanceOf(this.wallet.address)
1264
+ ]);
1265
+ results[chainName] = {
1266
+ usdc: parseFloat(import_ethers2.ethers.formatUnits(pathUSD, 6)),
1267
+ // pathUSD as default USDC
1268
+ usdt: parseFloat(import_ethers2.ethers.formatUnits(alphaUSD, 6)),
1269
+ // alphaUSD as default USDT
1270
+ native: parseFloat(import_ethers2.ethers.formatEther(nativeBalance)),
1271
+ tempo: {
1272
+ pathUSD: parseFloat(import_ethers2.ethers.formatUnits(pathUSD, 6)),
1273
+ alphaUSD: parseFloat(import_ethers2.ethers.formatUnits(alphaUSD, 6)),
1274
+ betaUSD: parseFloat(import_ethers2.ethers.formatUnits(betaUSD, 6)),
1275
+ thetaUSD: parseFloat(import_ethers2.ethers.formatUnits(thetaUSD, 6))
1276
+ }
1277
+ };
1278
+ } else {
1279
+ const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
1280
+ provider.getBalance(this.wallet.address),
1281
+ new import_ethers2.ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
1282
+ new import_ethers2.ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
1283
+ ]);
1284
+ results[chainName] = {
1285
+ usdc: parseFloat(import_ethers2.ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
1286
+ usdt: parseFloat(import_ethers2.ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
1287
+ native: parseFloat(import_ethers2.ethers.formatEther(nativeBalance))
1288
+ };
1289
+ }
1290
+ } catch (err2) {
1291
+ results[chainName] = { usdc: 0, usdt: 0, native: 0 };
1292
+ }
1293
+ })
1294
+ );
1295
+ return results;
1296
+ }
1297
+ /**
1298
+ * Pay for a service using MPP (Machine Payments Protocol)
1299
+ *
1300
+ * This implements the MPP flow manually for EOA wallets:
1301
+ * 1. Request service → get 402 with WWW-Authenticate
1302
+ * 2. Parse payment requirements
1303
+ * 3. Execute transfer on Tempo chain
1304
+ * 4. Retry with transaction hash as credential
1305
+ *
1306
+ * @param url - Full URL of the MPP-enabled endpoint
1307
+ * @param options - Request options (body, headers)
1308
+ * @returns Response from the service
1309
+ */
1310
+ async payWithMPP(url, options = {}) {
1311
+ if (!this.wallet || !this.walletData) {
1312
+ throw new Error("Client not initialized. Run: npx moltspay init");
1313
+ }
1314
+ const { privateKeyToAccount } = await import("viem/accounts");
1315
+ const { createWalletClient, createPublicClient, http } = await import("viem");
1316
+ const { tempoModerato } = await import("viem/chains");
1317
+ const { Actions } = await import("viem/tempo");
1318
+ const privateKey = this.walletData.privateKey;
1319
+ const account = privateKeyToAccount(privateKey);
1320
+ console.log(`[MoltsPay] Making MPP request to: ${url}`);
1321
+ console.log(`[MoltsPay] Using account: ${account.address}`);
1322
+ const initResponse = await fetch(url, {
1323
+ method: "POST",
1324
+ headers: {
1325
+ "Content-Type": "application/json",
1326
+ ...options.headers
1327
+ },
1328
+ body: options.body ? JSON.stringify(options.body) : void 0
1329
+ });
1330
+ if (initResponse.status !== 402) {
1331
+ if (initResponse.ok) {
1332
+ return initResponse.json();
1333
+ }
1334
+ const errorText = await initResponse.text();
1335
+ throw new Error(`Request failed (${initResponse.status}): ${errorText}`);
1336
+ }
1337
+ const wwwAuth = initResponse.headers.get("www-authenticate");
1338
+ if (!wwwAuth || !wwwAuth.toLowerCase().includes("payment")) {
1339
+ throw new Error("No WWW-Authenticate Payment challenge in 402 response");
1340
+ }
1341
+ console.log(`[MoltsPay] Got 402, parsing payment challenge...`);
1342
+ const parseAuthParam = (header, key) => {
1343
+ const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
1344
+ return match ? match[1] : null;
1345
+ };
1346
+ const challengeId = parseAuthParam(wwwAuth, "id");
1347
+ const method = parseAuthParam(wwwAuth, "method");
1348
+ const realm = parseAuthParam(wwwAuth, "realm");
1349
+ const requestB64 = parseAuthParam(wwwAuth, "request");
1350
+ if (method !== "tempo") {
1351
+ throw new Error(`Unsupported payment method: ${method}`);
1352
+ }
1353
+ if (!requestB64) {
1354
+ throw new Error("Missing request in WWW-Authenticate");
1355
+ }
1356
+ const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
1357
+ const paymentRequest = JSON.parse(requestJson);
1358
+ console.log(`[MoltsPay] Payment request:`, paymentRequest);
1359
+ const { amount, currency, recipient, methodDetails } = paymentRequest;
1360
+ const chainId = methodDetails?.chainId || 42431;
1361
+ console.log(`[MoltsPay] Executing transfer on Tempo (chainId: ${chainId})...`);
1362
+ console.log(`[MoltsPay] Amount: ${amount}, To: ${recipient}`);
1363
+ const tempoChain = { ...tempoModerato, feeToken: currency };
1364
+ const publicClient = createPublicClient({
1365
+ chain: tempoChain,
1366
+ transport: http("https://rpc.moderato.tempo.xyz")
1367
+ });
1368
+ const walletClient = createWalletClient({
1369
+ account,
1370
+ chain: tempoChain,
1371
+ transport: http("https://rpc.moderato.tempo.xyz")
1372
+ });
1373
+ const txHash = await Actions.token.transfer(walletClient, {
1374
+ to: recipient,
1375
+ amount: BigInt(amount),
1376
+ token: currency
1377
+ });
1378
+ console.log(`[MoltsPay] Transaction sent: ${txHash}`);
1379
+ console.log(`[MoltsPay] Waiting for confirmation...`);
1380
+ await publicClient.waitForTransactionReceipt({ hash: txHash });
1381
+ console.log(`[MoltsPay] Transaction confirmed!`);
1382
+ const challenge = {
1383
+ id: challengeId,
1384
+ realm,
1385
+ method: "tempo",
1386
+ intent: "charge",
1387
+ request: paymentRequest
1388
+ };
1389
+ const credential = {
1390
+ challenge,
1391
+ payload: { hash: txHash, type: "hash" },
1392
+ source: `did:pkh:eip155:${chainId}:${account.address}`
1393
+ };
1394
+ const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1395
+ console.log(`[MoltsPay] Retrying with payment credential...`);
1396
+ const paidResponse = await fetch(url, {
1397
+ method: "POST",
1398
+ headers: {
1399
+ "Content-Type": "application/json",
1400
+ "Authorization": `Payment ${credentialB64}`,
1401
+ ...options.headers
1402
+ },
1403
+ body: options.body ? JSON.stringify(options.body) : void 0
1404
+ });
1405
+ if (!paidResponse.ok) {
1406
+ const errorText = await paidResponse.text();
1407
+ throw new Error(`Payment verification failed (${paidResponse.status}): ${errorText}`);
1408
+ }
1409
+ console.log(`[MoltsPay] Payment verified! Service completed.`);
1410
+ return paidResponse.json();
1411
+ }
1412
+ };
1413
+
1414
+ // src/mcp/server.ts
1415
+ var CHAIN_NAMES = [
1416
+ ...Object.keys(CHAINS),
1417
+ ...Object.keys(SOLANA_CHAINS)
1418
+ ];
1419
+ var TOKEN_SYMBOLS = ["USDC", "USDT"];
1420
+ function ok(payload) {
1421
+ return {
1422
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
1423
+ };
1424
+ }
1425
+ function err(message) {
1426
+ return {
1427
+ isError: true,
1428
+ content: [{ type: "text", text: `Error: ${message}` }]
1429
+ };
1430
+ }
1431
+ function errorMessage(e) {
1432
+ return e instanceof Error ? e.message : String(e);
1433
+ }
1434
+ function wrap(fn) {
1435
+ return async (args) => {
1436
+ try {
1437
+ return await fn(args);
1438
+ } catch (e) {
1439
+ return err(errorMessage(e));
1440
+ }
1441
+ };
1442
+ }
1443
+ function getPackageVersion() {
1444
+ const candidates = [
1445
+ (0, import_path3.join)(__dirname, "../../package.json"),
1446
+ (0, import_path3.join)(__dirname, "../package.json"),
1447
+ (0, import_path3.join)(process.cwd(), "node_modules/moltspay/package.json")
1448
+ ];
1449
+ for (const loc of candidates) {
1450
+ try {
1451
+ if ((0, import_fs3.existsSync)(loc)) {
1452
+ const pkg = JSON.parse((0, import_fs3.readFileSync)(loc, "utf-8"));
1453
+ if (pkg.name === "moltspay") return pkg.version;
1454
+ }
1455
+ } catch {
1456
+ }
1457
+ }
1458
+ return "0.0.0";
1459
+ }
1460
+ function createMoltsPayMcpServer(options = {}) {
1461
+ const client = new MoltsPayClient({ configDir: options.configDir });
1462
+ if (!client.isInitialized) {
1463
+ throw new Error(
1464
+ "MoltsPay wallet not found. Run `moltspay init` before starting the MCP server."
1465
+ );
1466
+ }
1467
+ const dryRun = options.dryRun === true;
1468
+ const requireConfirm = process.env.MOLTSPAY_MCP_REQUIRE_CONFIRM === "1";
1469
+ const server = new import_mcp.McpServer({
1470
+ name: "moltspay",
1471
+ version: getPackageVersion()
1472
+ });
1473
+ server.registerTool(
1474
+ "moltspay_status",
1475
+ {
1476
+ title: "Wallet Status",
1477
+ description: "Return the MoltsPay wallet address, balances across all supported chains, and current spending limits.",
1478
+ inputSchema: {},
1479
+ annotations: { readOnlyHint: true, openWorldHint: true }
1480
+ },
1481
+ wrap(async () => {
1482
+ const config = client.getConfig();
1483
+ const balances = await client.getAllBalances();
1484
+ return ok({
1485
+ address: client.address,
1486
+ defaultChain: config.chain,
1487
+ balances,
1488
+ limits: config.limits
1489
+ });
1490
+ })
1491
+ );
1492
+ server.registerTool(
1493
+ "moltspay_services",
1494
+ {
1495
+ title: "List Provider Services",
1496
+ 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.",
1497
+ inputSchema: {
1498
+ url: import_zod.z.url().describe("Provider base URL."),
1499
+ maxPrice: import_zod.z.number().positive().optional().describe("Only return services priced at or below this amount."),
1500
+ query: import_zod.z.string().optional().describe("Case-insensitive substring match on service id / name / description.")
1501
+ },
1502
+ annotations: { readOnlyHint: true, openWorldHint: true }
1503
+ },
1504
+ wrap(async ({ url, maxPrice, query }) => {
1505
+ const res = await client.getServices(url);
1506
+ let services = res.services ?? [];
1507
+ if (typeof maxPrice === "number") {
1508
+ services = services.filter((s) => typeof s.price !== "number" || s.price <= maxPrice);
1509
+ }
1510
+ if (query) {
1511
+ const q = query.toLowerCase();
1512
+ services = services.filter(
1513
+ (s) => [s.id, s.name, s.description].some((f) => f?.toLowerCase().includes(q))
1514
+ );
1515
+ }
1516
+ return ok({ provider: res.provider, services });
1517
+ })
1518
+ );
1519
+ server.registerTool(
1520
+ "moltspay_pay",
1521
+ {
1522
+ title: "Pay For Service",
1523
+ 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.",
1524
+ inputSchema: {
1525
+ url: import_zod.z.url().describe("Provider base URL."),
1526
+ service: import_zod.z.string().min(1).describe("Service id from moltspay_services."),
1527
+ params: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).describe("Service parameters. Wrapped as { params } unless rawData is true."),
1528
+ chain: import_zod.z.enum(CHAIN_NAMES).optional().describe("Chain to pay on. Required unless the provider only accepts one chain."),
1529
+ token: import_zod.z.enum(TOKEN_SYMBOLS).optional().describe("Token symbol. Default USDC."),
1530
+ rawData: import_zod.z.boolean().optional().describe("Send params at top level instead of wrapped in { params }."),
1531
+ confirmed: import_zod.z.boolean().optional().describe("Required when MOLTSPAY_MCP_REQUIRE_CONFIRM=1 is set on the server.")
1532
+ },
1533
+ annotations: {
1534
+ readOnlyHint: false,
1535
+ destructiveHint: true,
1536
+ idempotentHint: false,
1537
+ openWorldHint: true
1538
+ }
1539
+ },
1540
+ wrap(async ({ url, service, params, chain, token, rawData, confirmed }) => {
1541
+ if (dryRun) {
1542
+ return ok({
1543
+ dryRun: true,
1544
+ message: "Dry-run mode: no payment will be executed. Inspect moltspay_services for prices.",
1545
+ intent: { url, service, params, chain, token, rawData }
1546
+ });
1547
+ }
1548
+ if (requireConfirm && confirmed !== true) {
1549
+ return err(
1550
+ "Confirmation required: server was started with MOLTSPAY_MCP_REQUIRE_CONFIRM=1. Re-call with confirmed: true after reviewing the price via moltspay_services."
1551
+ );
1552
+ }
1553
+ const result = await client.pay(url, service, params, { chain, token, rawData });
1554
+ return ok({ success: true, result });
1555
+ })
1556
+ );
1557
+ server.registerTool(
1558
+ "moltspay_config",
1559
+ {
1560
+ title: "Wallet Config",
1561
+ description: "Read or update MoltsPay wallet spending limits (maxPerTx, maxPerDay). Pass no arguments to read.",
1562
+ inputSchema: {
1563
+ maxPerTx: import_zod.z.number().positive().optional().describe("New per-transaction USD limit."),
1564
+ maxPerDay: import_zod.z.number().positive().optional().describe("New daily USD limit.")
1565
+ },
1566
+ annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false }
1567
+ },
1568
+ wrap(async ({ maxPerTx, maxPerDay }) => {
1569
+ if (maxPerTx !== void 0 || maxPerDay !== void 0) {
1570
+ client.updateConfig({ maxPerTx, maxPerDay });
1571
+ }
1572
+ const config = client.getConfig();
1573
+ return ok({
1574
+ address: client.address,
1575
+ defaultChain: config.chain,
1576
+ limits: config.limits
1577
+ });
1578
+ })
1579
+ );
1580
+ return server;
1581
+ }
1582
+
1583
+ // src/mcp/index.ts
1584
+ if (!globalThis.crypto) {
1585
+ globalThis.crypto = import_crypto.webcrypto;
1586
+ }
1587
+ function parseArgs(argv) {
1588
+ const args = argv.slice(2);
1589
+ let dryRun = false;
1590
+ let configDir;
1591
+ for (let i = 0; i < args.length; i++) {
1592
+ const a = args[i];
1593
+ if (a === "--dry-run") {
1594
+ dryRun = true;
1595
+ } else if (a === "--config-dir") {
1596
+ const next = args[i + 1];
1597
+ if (!next || next.startsWith("--")) {
1598
+ throw new Error("--config-dir requires a path argument");
1599
+ }
1600
+ configDir = next;
1601
+ i++;
1602
+ } else {
1603
+ throw new Error(`Unknown argument: ${a}`);
1604
+ }
1605
+ }
1606
+ return { dryRun, configDir };
1607
+ }
1608
+ async function main() {
1609
+ const { dryRun, configDir } = parseArgs(process.argv);
1610
+ const server = createMoltsPayMcpServer({ dryRun, configDir });
1611
+ const transport = new import_stdio.StdioServerTransport();
1612
+ await server.connect(transport);
1613
+ process.stderr.write(`moltspay-mcp ready${dryRun ? " (dry-run)" : ""}
1614
+ `);
1615
+ }
1616
+ main().catch((e) => {
1617
+ process.stderr.write(
1618
+ `moltspay-mcp failed to start: ${e instanceof Error ? e.message : String(e)}
1619
+ `
1620
+ );
1621
+ process.exit(1);
1622
+ });
1623
+ //# sourceMappingURL=index.js.map