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,1289 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ // src/client/web/index.ts
6
+ import { ethers } from "ethers";
7
+ import { Connection as Connection3, PublicKey as PublicKey3 } from "@solana/web3.js";
8
+
9
+ // src/client/core/types.ts
10
+ var X402_VERSION = 2;
11
+ var PAYMENT_REQUIRED_HEADER = "x-payment-required";
12
+ var PAYMENT_HEADER = "x-payment";
13
+
14
+ // src/client/core/chain-map.ts
15
+ var NETWORK_TO_CHAIN = {
16
+ "eip155:8453": "base",
17
+ "eip155:137": "polygon",
18
+ "eip155:84532": "base_sepolia",
19
+ "eip155:42431": "tempo_moderato",
20
+ "eip155:56": "bnb",
21
+ "eip155:97": "bnb_testnet",
22
+ "solana:mainnet": "solana",
23
+ "solana:devnet": "solana_devnet"
24
+ };
25
+ var CHAIN_TO_NETWORK = Object.fromEntries(
26
+ Object.entries(NETWORK_TO_CHAIN).map(([network, chain]) => [chain, network])
27
+ );
28
+ function networkToChainName(network) {
29
+ return NETWORK_TO_CHAIN[network] ?? null;
30
+ }
31
+ function chainNameToNetwork(chain) {
32
+ return CHAIN_TO_NETWORK[chain];
33
+ }
34
+
35
+ // src/client/core/base64.ts
36
+ var BufferCtor = globalThis.Buffer;
37
+ function encodeBase64(input) {
38
+ if (BufferCtor) {
39
+ return BufferCtor.from(input, "utf-8").toString("base64");
40
+ }
41
+ const bytes = new TextEncoder().encode(input);
42
+ let binary = "";
43
+ for (let i = 0; i < bytes.length; i++) {
44
+ binary += String.fromCharCode(bytes[i]);
45
+ }
46
+ return btoa(binary);
47
+ }
48
+ function decodeBase64(input) {
49
+ if (BufferCtor) {
50
+ return BufferCtor.from(input, "base64").toString("utf-8");
51
+ }
52
+ const binary = atob(input);
53
+ const bytes = new Uint8Array(binary.length);
54
+ for (let i = 0; i < binary.length; i++) {
55
+ bytes[i] = binary.charCodeAt(i);
56
+ }
57
+ return new TextDecoder().decode(bytes);
58
+ }
59
+ function base64ToUint8Array(input) {
60
+ if (BufferCtor) {
61
+ const buf = BufferCtor.from(input, "base64");
62
+ return new Uint8Array(buf);
63
+ }
64
+ const binary = atob(input);
65
+ const bytes = new Uint8Array(binary.length);
66
+ for (let i = 0; i < binary.length; i++) {
67
+ bytes[i] = binary.charCodeAt(i);
68
+ }
69
+ return bytes;
70
+ }
71
+ function uint8ArrayToBase64(bytes) {
72
+ if (BufferCtor) {
73
+ return BufferCtor.from(bytes).toString("base64");
74
+ }
75
+ let binary = "";
76
+ for (let i = 0; i < bytes.length; i++) {
77
+ binary += String.fromCharCode(bytes[i]);
78
+ }
79
+ return btoa(binary);
80
+ }
81
+
82
+ // src/client/core/errors.ts
83
+ var MoltsPayError = class extends Error {
84
+ constructor(code, message) {
85
+ super(message);
86
+ __publicField(this, "code");
87
+ this.name = "MoltsPayError";
88
+ this.code = code;
89
+ }
90
+ };
91
+ var NotInitializedError = class extends MoltsPayError {
92
+ constructor(message = "Client not initialized") {
93
+ super("NOT_INITIALIZED", message);
94
+ this.name = "NotInitializedError";
95
+ }
96
+ };
97
+ var UnsupportedChainError = class extends MoltsPayError {
98
+ constructor(chain, message) {
99
+ super("UNSUPPORTED_CHAIN", message ?? `Chain not supported: ${chain}`);
100
+ this.chain = chain;
101
+ this.name = "UnsupportedChainError";
102
+ }
103
+ };
104
+ var NeedsApprovalError = class extends MoltsPayError {
105
+ constructor(details, message) {
106
+ super(
107
+ "NEEDS_APPROVAL",
108
+ message ?? `Insufficient allowance for ${details.spender}. Current=${details.currentAllowance}, required=${details.required}.`
109
+ );
110
+ this.details = details;
111
+ this.name = "NeedsApprovalError";
112
+ }
113
+ };
114
+ var InsufficientBalanceError = class extends MoltsPayError {
115
+ constructor(message) {
116
+ super("INSUFFICIENT_BALANCE", message);
117
+ this.name = "InsufficientBalanceError";
118
+ }
119
+ };
120
+ var SpendingLimitExceededError = class extends MoltsPayError {
121
+ constructor(message) {
122
+ super("SPENDING_LIMIT_EXCEEDED", message);
123
+ this.name = "SpendingLimitExceededError";
124
+ }
125
+ };
126
+ var PaymentRejectedError = class extends MoltsPayError {
127
+ constructor(message) {
128
+ super("PAYMENT_REJECTED", message);
129
+ this.name = "PaymentRejectedError";
130
+ }
131
+ };
132
+ var ServerError = class extends MoltsPayError {
133
+ constructor(status, message) {
134
+ super("SERVER_ERROR", message);
135
+ this.status = status;
136
+ this.name = "ServerError";
137
+ }
138
+ };
139
+ var InvalidPaymentHeaderError = class extends MoltsPayError {
140
+ constructor(message) {
141
+ super("INVALID_PAYMENT_HEADER", message);
142
+ this.name = "InvalidPaymentHeaderError";
143
+ }
144
+ };
145
+
146
+ // src/client/core/eip3009.ts
147
+ var EIP3009_TYPES = {
148
+ TransferWithAuthorization: [
149
+ { name: "from", type: "address" },
150
+ { name: "to", type: "address" },
151
+ { name: "value", type: "uint256" },
152
+ { name: "validAfter", type: "uint256" },
153
+ { name: "validBefore", type: "uint256" },
154
+ { name: "nonce", type: "bytes32" }
155
+ ]
156
+ };
157
+ function buildEIP3009TypedData(args) {
158
+ const validAfter = args.validAfter ?? "0";
159
+ const validBefore = args.validBefore ?? (Math.floor(Date.now() / 1e3) + 3600).toString();
160
+ const authorization = {
161
+ from: args.from,
162
+ to: args.to,
163
+ value: args.value,
164
+ validAfter,
165
+ validBefore,
166
+ nonce: args.nonce
167
+ };
168
+ return {
169
+ domain: {
170
+ name: args.tokenName,
171
+ version: args.tokenVersion,
172
+ chainId: args.chainId,
173
+ verifyingContract: args.tokenAddress
174
+ },
175
+ types: EIP3009_TYPES,
176
+ primaryType: "TransferWithAuthorization",
177
+ message: authorization
178
+ };
179
+ }
180
+
181
+ // src/client/core/eip2612.ts
182
+ var EIP2612_TYPES = {
183
+ Permit: [
184
+ { name: "owner", type: "address" },
185
+ { name: "spender", type: "address" },
186
+ { name: "value", type: "uint256" },
187
+ { name: "nonce", type: "uint256" },
188
+ { name: "deadline", type: "uint256" }
189
+ ]
190
+ };
191
+ function buildEIP2612PermitTypedData(args) {
192
+ const message = {
193
+ owner: args.owner,
194
+ spender: args.spender,
195
+ value: args.value,
196
+ nonce: args.nonce,
197
+ deadline: args.deadline
198
+ };
199
+ return {
200
+ domain: {
201
+ name: args.tokenName,
202
+ version: args.tokenVersion,
203
+ chainId: args.chainId,
204
+ verifyingContract: args.tokenAddress
205
+ },
206
+ types: EIP2612_TYPES,
207
+ primaryType: "Permit",
208
+ message
209
+ };
210
+ }
211
+
212
+ // src/client/core/bnb-intent.ts
213
+ var BNB_INTENT_TYPES = {
214
+ PaymentIntent: [
215
+ { name: "from", type: "address" },
216
+ { name: "to", type: "address" },
217
+ { name: "amount", type: "uint256" },
218
+ { name: "token", type: "address" },
219
+ { name: "service", type: "string" },
220
+ { name: "nonce", type: "uint256" },
221
+ { name: "deadline", type: "uint256" }
222
+ ]
223
+ };
224
+ var BNB_DOMAIN_NAME = "MoltsPay";
225
+ var BNB_DOMAIN_VERSION = "1";
226
+ function buildBnbIntentTypedData(args) {
227
+ const intent = {
228
+ from: args.from,
229
+ to: args.to,
230
+ amount: args.amount,
231
+ token: args.tokenAddress,
232
+ service: args.service,
233
+ nonce: args.nonce,
234
+ deadline: args.deadline
235
+ };
236
+ return {
237
+ domain: {
238
+ name: BNB_DOMAIN_NAME,
239
+ version: BNB_DOMAIN_VERSION,
240
+ chainId: args.chainId
241
+ },
242
+ types: BNB_INTENT_TYPES,
243
+ primaryType: "PaymentIntent",
244
+ message: intent
245
+ };
246
+ }
247
+
248
+ // src/facilitators/solana.ts
249
+ import {
250
+ Connection as Connection2,
251
+ PublicKey as PublicKey2,
252
+ Transaction,
253
+ VersionedTransaction
254
+ } from "@solana/web3.js";
255
+ import {
256
+ getAssociatedTokenAddress,
257
+ createTransferCheckedInstruction,
258
+ getAccount,
259
+ createAssociatedTokenAccountInstruction
260
+ } from "@solana/spl-token";
261
+
262
+ // src/chains/solana.ts
263
+ import { Connection, PublicKey } from "@solana/web3.js";
264
+ var SOLANA_CHAINS = {
265
+ solana: {
266
+ name: "Solana Mainnet",
267
+ cluster: "mainnet-beta",
268
+ rpc: "https://api.mainnet-beta.solana.com",
269
+ explorer: "https://solscan.io/account/",
270
+ explorerTx: "https://solscan.io/tx/",
271
+ tokens: {
272
+ USDC: {
273
+ mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
274
+ // Circle official USDC
275
+ decimals: 6
276
+ }
277
+ }
278
+ },
279
+ solana_devnet: {
280
+ name: "Solana Devnet",
281
+ cluster: "devnet",
282
+ rpc: "https://api.devnet.solana.com",
283
+ explorer: "https://solscan.io/account/",
284
+ explorerTx: "https://solscan.io/tx/",
285
+ tokens: {
286
+ USDC: {
287
+ // Circle's devnet USDC (if not available, we'll deploy our own test token)
288
+ mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
289
+ decimals: 6
290
+ }
291
+ }
292
+ }
293
+ };
294
+
295
+ // src/facilitators/solana.ts
296
+ async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey, connection) {
297
+ const chainConfig = SOLANA_CHAINS[chain];
298
+ const conn = connection ?? new Connection2(chainConfig.rpc, "confirmed");
299
+ const mint = new PublicKey2(chainConfig.tokens.USDC.mint);
300
+ const actualFeePayer = feePayerPubkey || senderPubkey;
301
+ const senderATA = await getAssociatedTokenAddress(mint, senderPubkey);
302
+ const recipientATA = await getAssociatedTokenAddress(mint, recipientPubkey);
303
+ const transaction = new Transaction();
304
+ try {
305
+ await getAccount(conn, recipientATA);
306
+ } catch {
307
+ transaction.add(
308
+ createAssociatedTokenAccountInstruction(
309
+ actualFeePayer,
310
+ // payer (fee payer in gasless mode)
311
+ recipientATA,
312
+ // ata to create
313
+ recipientPubkey,
314
+ // owner
315
+ mint
316
+ // mint
317
+ )
318
+ );
319
+ }
320
+ transaction.add(
321
+ createTransferCheckedInstruction(
322
+ senderATA,
323
+ // source
324
+ mint,
325
+ // mint
326
+ recipientATA,
327
+ // destination
328
+ senderPubkey,
329
+ // owner (sender still authorizes the transfer)
330
+ amount,
331
+ // amount
332
+ chainConfig.tokens.USDC.decimals
333
+ // decimals
334
+ )
335
+ );
336
+ const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash();
337
+ transaction.recentBlockhash = blockhash;
338
+ transaction.feePayer = actualFeePayer;
339
+ return transaction;
340
+ }
341
+
342
+ // src/client/core/x402.ts
343
+ function parsePaymentRequiredHeader(header) {
344
+ let parsed;
345
+ try {
346
+ parsed = JSON.parse(decodeBase64(header));
347
+ } catch {
348
+ throw new InvalidPaymentHeaderError("Invalid x-payment-required header");
349
+ }
350
+ if (Array.isArray(parsed)) {
351
+ return parsed;
352
+ }
353
+ if (parsed && typeof parsed === "object" && Array.isArray(parsed.accepts)) {
354
+ return parsed.accepts;
355
+ }
356
+ return [parsed];
357
+ }
358
+ function serverAcceptedChains(requirements) {
359
+ return requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
360
+ }
361
+ function selectChain(requirements, userSpecifiedChain) {
362
+ const accepted = serverAcceptedChains(requirements);
363
+ if (userSpecifiedChain) {
364
+ if (!accepted.includes(userSpecifiedChain)) {
365
+ throw new UnsupportedChainError(
366
+ userSpecifiedChain,
367
+ `Server doesn't accept '${userSpecifiedChain}'. Server accepts: ${accepted.join(", ")}`
368
+ );
369
+ }
370
+ return userSpecifiedChain;
371
+ }
372
+ if (accepted.length === 1 && accepted[0] === "base") {
373
+ return "base";
374
+ }
375
+ throw new UnsupportedChainError(
376
+ "unspecified",
377
+ `Server accepts: ${accepted.join(", ")}. Please specify a chain explicitly.`
378
+ );
379
+ }
380
+ function findRequirementForChain(requirements, chain) {
381
+ const network = chainNameToNetwork(chain);
382
+ return requirements.find((r) => r.network === network) ?? null;
383
+ }
384
+ function buildPaymentPayload(args) {
385
+ return {
386
+ x402Version: X402_VERSION,
387
+ scheme: args.scheme,
388
+ network: args.network,
389
+ payload: args.payload,
390
+ accepted: args.accepted
391
+ };
392
+ }
393
+ function encodePaymentHeader(payload) {
394
+ return encodeBase64(JSON.stringify(payload));
395
+ }
396
+
397
+ // src/chains/index.ts
398
+ var CHAINS = {
399
+ // ============ Mainnet ============
400
+ base: {
401
+ name: "Base",
402
+ chainId: 8453,
403
+ rpc: "https://mainnet.base.org",
404
+ tokens: {
405
+ USDC: {
406
+ address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
407
+ decimals: 6,
408
+ symbol: "USDC",
409
+ eip712Name: "USD Coin"
410
+ // EIP-712 domain name
411
+ },
412
+ USDT: {
413
+ address: "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2",
414
+ decimals: 6,
415
+ symbol: "USDT",
416
+ eip712Name: "Tether USD"
417
+ }
418
+ },
419
+ usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
420
+ // deprecated, for backward compat
421
+ explorer: "https://basescan.org/address/",
422
+ explorerTx: "https://basescan.org/tx/",
423
+ avgBlockTime: 2
424
+ },
425
+ polygon: {
426
+ name: "Polygon",
427
+ chainId: 137,
428
+ rpc: "https://polygon-bor-rpc.publicnode.com",
429
+ tokens: {
430
+ USDC: {
431
+ address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
432
+ decimals: 6,
433
+ symbol: "USDC",
434
+ eip712Name: "USD Coin"
435
+ },
436
+ USDT: {
437
+ address: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
438
+ decimals: 6,
439
+ symbol: "USDT",
440
+ eip712Name: "(PoS) Tether USD"
441
+ // Polygon uses this name
442
+ }
443
+ },
444
+ usdc: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
445
+ explorer: "https://polygonscan.com/address/",
446
+ explorerTx: "https://polygonscan.com/tx/",
447
+ avgBlockTime: 2
448
+ },
449
+ // ============ Testnet ============
450
+ base_sepolia: {
451
+ name: "Base Sepolia",
452
+ chainId: 84532,
453
+ rpc: "https://sepolia.base.org",
454
+ tokens: {
455
+ USDC: {
456
+ address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
457
+ decimals: 6,
458
+ symbol: "USDC",
459
+ eip712Name: "USDC"
460
+ // Testnet USDC uses 'USDC' not 'USD Coin'
461
+ },
462
+ USDT: {
463
+ address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
464
+ // Same as USDC on testnet (no official USDT)
465
+ decimals: 6,
466
+ symbol: "USDT",
467
+ eip712Name: "USDC"
468
+ // Uses same contract as USDC
469
+ }
470
+ },
471
+ usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
472
+ explorer: "https://sepolia.basescan.org/address/",
473
+ explorerTx: "https://sepolia.basescan.org/tx/",
474
+ avgBlockTime: 2
475
+ },
476
+ // ============ Tempo Testnet (Moderato) ============
477
+ tempo_moderato: {
478
+ name: "Tempo Moderato",
479
+ chainId: 42431,
480
+ rpc: "https://rpc.moderato.tempo.xyz",
481
+ tokens: {
482
+ // TIP-20 stablecoins on Tempo testnet (from mppx SDK)
483
+ // Note: Tempo uses USD as native gas token, not ETH
484
+ USDC: {
485
+ address: "0x20c0000000000000000000000000000000000000",
486
+ // pathUSD - primary testnet stablecoin
487
+ decimals: 6,
488
+ symbol: "USDC",
489
+ eip712Name: "pathUSD"
490
+ },
491
+ USDT: {
492
+ address: "0x20c0000000000000000000000000000000000001",
493
+ // alphaUSD
494
+ decimals: 6,
495
+ symbol: "USDT",
496
+ eip712Name: "alphaUSD"
497
+ }
498
+ },
499
+ usdc: "0x20c0000000000000000000000000000000000000",
500
+ explorer: "https://explore.testnet.tempo.xyz/address/",
501
+ explorerTx: "https://explore.testnet.tempo.xyz/tx/",
502
+ avgBlockTime: 0.5
503
+ // ~500ms finality
504
+ },
505
+ // ============ BNB Chain Testnet ============
506
+ bnb_testnet: {
507
+ name: "BNB Testnet",
508
+ chainId: 97,
509
+ rpc: "https://data-seed-prebsc-1-s1.binance.org:8545",
510
+ tokens: {
511
+ // Note: BNB uses 18 decimals for stablecoins (unlike Base/Polygon which use 6)
512
+ // Using official Binance-Peg testnet tokens
513
+ USDC: {
514
+ address: "0x64544969ed7EBf5f083679233325356EbE738930",
515
+ // Testnet USDC
516
+ decimals: 18,
517
+ symbol: "USDC",
518
+ eip712Name: "USD Coin"
519
+ },
520
+ USDT: {
521
+ address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
522
+ // Testnet USDT
523
+ decimals: 18,
524
+ symbol: "USDT",
525
+ eip712Name: "Tether USD"
526
+ }
527
+ },
528
+ usdc: "0x64544969ed7EBf5f083679233325356EbE738930",
529
+ explorer: "https://testnet.bscscan.com/address/",
530
+ explorerTx: "https://testnet.bscscan.com/tx/",
531
+ avgBlockTime: 3,
532
+ // BNB-specific: requires approval for pay-for-success flow
533
+ requiresApproval: true
534
+ },
535
+ // ============ BNB Chain Mainnet ============
536
+ bnb: {
537
+ name: "BNB Smart Chain",
538
+ chainId: 56,
539
+ rpc: "https://bsc-dataseed.binance.org",
540
+ tokens: {
541
+ // Note: BNB uses 18 decimals for stablecoins
542
+ USDC: {
543
+ address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
544
+ decimals: 18,
545
+ symbol: "USDC",
546
+ eip712Name: "USD Coin"
547
+ },
548
+ USDT: {
549
+ address: "0x55d398326f99059fF775485246999027B3197955",
550
+ decimals: 18,
551
+ symbol: "USDT",
552
+ eip712Name: "Tether USD"
553
+ }
554
+ },
555
+ usdc: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
556
+ explorer: "https://bscscan.com/address/",
557
+ explorerTx: "https://bscscan.com/tx/",
558
+ avgBlockTime: 3,
559
+ // BNB-specific: requires approval for pay-for-success flow
560
+ requiresApproval: true
561
+ }
562
+ };
563
+
564
+ // src/client/web/storage.ts
565
+ var DEFAULT_STORAGE_KEY = "moltspay:spending";
566
+ function getStorage() {
567
+ const g = globalThis;
568
+ return g.localStorage ?? null;
569
+ }
570
+ function todayKey() {
571
+ return (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
572
+ }
573
+ var SpendingLedger = class {
574
+ constructor(config) {
575
+ __publicField(this, "storageKey");
576
+ __publicField(this, "maxPerTx");
577
+ __publicField(this, "maxPerDay");
578
+ this.maxPerTx = config.maxPerTx;
579
+ this.maxPerDay = config.maxPerDay;
580
+ this.storageKey = config.storageKey ?? DEFAULT_STORAGE_KEY;
581
+ }
582
+ /** Load the persisted total for today. Fails silently on parse errors. */
583
+ read() {
584
+ const storage = getStorage();
585
+ const today = todayKey();
586
+ if (!storage) return { date: today, amount: 0, updatedAt: Date.now() };
587
+ try {
588
+ const raw = storage.getItem(this.storageKey);
589
+ if (!raw) return { date: today, amount: 0, updatedAt: Date.now() };
590
+ const parsed = JSON.parse(raw);
591
+ if (parsed.date !== today) {
592
+ return { date: today, amount: 0, updatedAt: Date.now() };
593
+ }
594
+ return parsed;
595
+ } catch {
596
+ return { date: today, amount: 0, updatedAt: Date.now() };
597
+ }
598
+ }
599
+ write(entry) {
600
+ const storage = getStorage();
601
+ if (!storage) return;
602
+ try {
603
+ storage.setItem(this.storageKey, JSON.stringify(entry));
604
+ } catch {
605
+ }
606
+ }
607
+ /**
608
+ * Throw `SpendingLimitExceededError` if the requested charge would push
609
+ * per-tx or per-day limits over. Does NOT mutate state — callers record
610
+ * only after the payment clears via {@link record}.
611
+ */
612
+ check(amount) {
613
+ if (amount > this.maxPerTx) {
614
+ throw new SpendingLimitExceededError(
615
+ `Amount $${amount} exceeds max per transaction ($${this.maxPerTx})`
616
+ );
617
+ }
618
+ const current = this.read();
619
+ if (current.amount + amount > this.maxPerDay) {
620
+ throw new SpendingLimitExceededError(
621
+ `Would exceed daily limit ($${current.amount} + $${amount} > $${this.maxPerDay})`
622
+ );
623
+ }
624
+ }
625
+ /** Persist a successful charge. Silently no-ops in non-browser runtimes. */
626
+ record(amount) {
627
+ const current = this.read();
628
+ this.write({
629
+ date: current.date,
630
+ amount: current.amount + amount,
631
+ updatedAt: Date.now()
632
+ });
633
+ }
634
+ /** Current aggregate for the active day. Intended for UI display. */
635
+ get todaySpending() {
636
+ return this.read().amount;
637
+ }
638
+ /** Manually reset — mainly useful for tests. */
639
+ reset() {
640
+ this.write({ date: todayKey(), amount: 0, updatedAt: Date.now() });
641
+ }
642
+ };
643
+
644
+ // src/client/web/signers/eip1193.ts
645
+ var USER_REJECTED_CODES = /* @__PURE__ */ new Set([4001, -32603]);
646
+ function isUserRejection(err) {
647
+ const code = err?.code;
648
+ return code !== void 0 && USER_REJECTED_CODES.has(code);
649
+ }
650
+ function toHexChainId(chainId) {
651
+ return "0x" + chainId.toString(16);
652
+ }
653
+ function eip1193Signer(provider, options = {}) {
654
+ async function getEvmAddress() {
655
+ const accounts = await provider.request({
656
+ method: "eth_requestAccounts"
657
+ });
658
+ if (!accounts || accounts.length === 0) {
659
+ throw new PaymentRejectedError("No EVM account available from provider");
660
+ }
661
+ return accounts[0];
662
+ }
663
+ async function ensureChainId(chainId) {
664
+ const hexId = toHexChainId(chainId);
665
+ try {
666
+ const current = await provider.request({ method: "eth_chainId" });
667
+ if (current?.toLowerCase() === hexId.toLowerCase()) return;
668
+ await provider.request({
669
+ method: "wallet_switchEthereumChain",
670
+ params: [{ chainId: hexId }]
671
+ });
672
+ } catch (err) {
673
+ const code = err?.code;
674
+ if (code === 4902 || code === -32603) {
675
+ const meta = options.addChainMetadata?.[chainId];
676
+ if (!meta) {
677
+ throw new Error(
678
+ `Wallet does not know chainId ${chainId}. Provide addChainMetadata to eip1193Signer for automatic addition.`
679
+ );
680
+ }
681
+ await provider.request({
682
+ method: "wallet_addEthereumChain",
683
+ params: [
684
+ {
685
+ chainId: hexId,
686
+ chainName: meta.chainName,
687
+ rpcUrls: meta.rpcUrls,
688
+ nativeCurrency: meta.nativeCurrency,
689
+ blockExplorerUrls: meta.blockExplorerUrls
690
+ }
691
+ ]
692
+ });
693
+ return;
694
+ }
695
+ if (isUserRejection(err)) {
696
+ throw new PaymentRejectedError("User rejected chain switch");
697
+ }
698
+ throw err;
699
+ }
700
+ }
701
+ return {
702
+ getEvmAddress,
703
+ async signTypedData(envelope) {
704
+ if (typeof envelope.domain?.chainId === "number") {
705
+ await ensureChainId(envelope.domain.chainId);
706
+ }
707
+ const from = await getEvmAddress();
708
+ const typesForWire = {
709
+ EIP712Domain: [
710
+ { name: "name", type: "string" },
711
+ { name: "version", type: "string" },
712
+ { name: "chainId", type: "uint256" },
713
+ { name: "verifyingContract", type: "address" }
714
+ ]
715
+ };
716
+ for (const [key, fields] of Object.entries(envelope.types)) {
717
+ typesForWire[key] = [...fields];
718
+ }
719
+ const payload = JSON.stringify({
720
+ domain: envelope.domain,
721
+ types: typesForWire,
722
+ primaryType: envelope.primaryType,
723
+ message: envelope.message
724
+ });
725
+ try {
726
+ return await provider.request({
727
+ method: "eth_signTypedData_v4",
728
+ params: [from, payload]
729
+ });
730
+ } catch (err) {
731
+ if (isUserRejection(err)) {
732
+ throw new PaymentRejectedError("User rejected signature");
733
+ }
734
+ throw err;
735
+ }
736
+ },
737
+ async sendEvmTransaction(args) {
738
+ await ensureChainId(args.chainId);
739
+ const from = await getEvmAddress();
740
+ try {
741
+ return await provider.request({
742
+ method: "eth_sendTransaction",
743
+ params: [
744
+ {
745
+ from,
746
+ to: args.to,
747
+ data: args.data,
748
+ ...args.value ? { value: args.value } : {}
749
+ }
750
+ ]
751
+ });
752
+ } catch (err) {
753
+ if (isUserRejection(err)) {
754
+ throw new PaymentRejectedError("User rejected transaction");
755
+ }
756
+ throw err;
757
+ }
758
+ }
759
+ };
760
+ }
761
+
762
+ // src/client/web/signers/solana-adapter.ts
763
+ import { Transaction as Transaction2 } from "@solana/web3.js";
764
+ function isUserRejection2(err) {
765
+ const name = err?.name ?? "";
766
+ const message = String(err?.message ?? "");
767
+ return name.toLowerCase().includes("reject") || /user rejected|user denied|rejected by user/i.test(message);
768
+ }
769
+ function solanaSigner(adapter) {
770
+ return {
771
+ async getEvmAddress() {
772
+ throw new Error(
773
+ "solanaSigner does not support EVM. Compose with eip1193Signer for multi-chain."
774
+ );
775
+ },
776
+ async getSolanaAddress() {
777
+ return adapter.publicKey ? adapter.publicKey.toBase58() : null;
778
+ },
779
+ async signTypedData() {
780
+ throw new Error("solanaSigner does not support EIP-712 signing.");
781
+ },
782
+ async signSolanaTransaction(args) {
783
+ if (!adapter.publicKey) {
784
+ throw new PaymentRejectedError("Solana wallet not connected");
785
+ }
786
+ const tx = Transaction2.from(base64ToUint8Array(args.transactionBase64));
787
+ let signed;
788
+ try {
789
+ signed = await adapter.signTransaction(tx);
790
+ } catch (err) {
791
+ if (isUserRejection2(err)) {
792
+ throw new PaymentRejectedError("User rejected Solana signature");
793
+ }
794
+ throw err;
795
+ }
796
+ const serialized = signed.serialize({
797
+ requireAllSignatures: false,
798
+ verifySignatures: false
799
+ });
800
+ return uint8ArrayToBase64(serialized);
801
+ }
802
+ };
803
+ }
804
+
805
+ // src/client/web/signers/compose.ts
806
+ function composeSigners(...signers) {
807
+ if (signers.length === 0) {
808
+ throw new Error("composeSigners requires at least one signer");
809
+ }
810
+ if (signers.length === 1) {
811
+ return signers[0];
812
+ }
813
+ async function tryEach(pick) {
814
+ let lastError = new Error("composeSigners: no signer supported the operation");
815
+ for (const signer of signers) {
816
+ const candidate = pick(signer);
817
+ if (candidate === void 0) continue;
818
+ try {
819
+ return await candidate;
820
+ } catch (err) {
821
+ lastError = err;
822
+ }
823
+ }
824
+ throw lastError;
825
+ }
826
+ return {
827
+ getEvmAddress: () => tryEach((s) => s.getEvmAddress?.()),
828
+ getSolanaAddress: () => tryEach(
829
+ (s) => s.getSolanaAddress ? s.getSolanaAddress() : void 0
830
+ ),
831
+ signTypedData: (envelope) => tryEach((s) => s.signTypedData?.(envelope)),
832
+ sendEvmTransaction: (args) => tryEach(
833
+ (s) => s.sendEvmTransaction ? s.sendEvmTransaction(args) : void 0
834
+ ),
835
+ signSolanaTransaction: (args) => tryEach(
836
+ (s) => s.signSolanaTransaction ? s.signSolanaTransaction(args) : void 0
837
+ )
838
+ };
839
+ }
840
+
841
+ // src/client/web/index.ts
842
+ var MoltsPayWebClient = class {
843
+ constructor(options) {
844
+ __publicField(this, "signer");
845
+ __publicField(this, "defaultChain");
846
+ __publicField(this, "ledger");
847
+ __publicField(this, "fetchImpl");
848
+ __publicField(this, "solanaRpc");
849
+ if (!options.signer) {
850
+ throw new NotInitializedError("MoltsPayWebClient: signer is required");
851
+ }
852
+ this.signer = options.signer;
853
+ this.defaultChain = options.defaultChain;
854
+ this.ledger = options.spendingLimits ? new SpendingLedger(options.spendingLimits) : null;
855
+ this.fetchImpl = (options.fetch ?? globalThis.fetch).bind(globalThis);
856
+ this.solanaRpc = options.solanaRpc;
857
+ }
858
+ getSolanaConnection(chain) {
859
+ const rpc = this.solanaRpc?.[chain] ?? SOLANA_CHAINS[chain].rpc;
860
+ return new Connection3(rpc, "confirmed");
861
+ }
862
+ /** Fetch a provider's service manifest. Mirrors `MoltsPayClient.getServices`. */
863
+ async getServices(serverUrl) {
864
+ const normalizedUrl = serverUrl.replace(/\/(services|api\/services|registry\/services)\/?$/, "");
865
+ const endpoints = ["/services", "/api/services", "/registry/services"];
866
+ for (const endpoint of endpoints) {
867
+ try {
868
+ const res = await this.fetchImpl(`${normalizedUrl}${endpoint}`);
869
+ if (!res.ok) continue;
870
+ const contentType = res.headers.get("content-type") ?? "";
871
+ if (!contentType.includes("application/json")) continue;
872
+ return await res.json();
873
+ } catch {
874
+ continue;
875
+ }
876
+ }
877
+ throw new ServerError(0, `Failed to get services: no valid endpoint found at ${normalizedUrl}`);
878
+ }
879
+ /**
880
+ * Pay for a service via x402. Returns the service `result` field (or the
881
+ * whole JSON body if the server doesn't wrap). Throws `NeedsApprovalError`
882
+ * for BNB when allowance is insufficient — call {@link approveBnb} and retry.
883
+ */
884
+ async pay(serverUrl, service, params, options = {}) {
885
+ const executeUrl = await this.resolveExecuteUrl(serverUrl, service);
886
+ const requestBody = this.buildRequestBody(service, params, options);
887
+ const initialRes = await this.fetchImpl(executeUrl, {
888
+ method: "POST",
889
+ headers: { "Content-Type": "application/json" },
890
+ body: JSON.stringify(requestBody)
891
+ });
892
+ if (initialRes.status !== 402) {
893
+ const data = await initialRes.json();
894
+ if (initialRes.ok && data.result) {
895
+ return data.result;
896
+ }
897
+ if (initialRes.ok) {
898
+ return data;
899
+ }
900
+ throw new ServerError(initialRes.status, data.error ?? "Unexpected response");
901
+ }
902
+ const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER);
903
+ if (!paymentRequiredHeader) {
904
+ throw new ServerError(402, "Missing X-Payment-Required header in 402 response");
905
+ }
906
+ const requirements = parsePaymentRequiredHeader(paymentRequiredHeader);
907
+ const chain = this.chooseChain(requirements, options.chain);
908
+ const req = findRequirementForChain(requirements, chain);
909
+ if (!req) {
910
+ throw new UnsupportedChainError(chain, `No payment requirement for chain '${chain}' in server response`);
911
+ }
912
+ if (chain === "solana" || chain === "solana_devnet") {
913
+ return this.paySolana(executeUrl, service, params, req, chain, options);
914
+ }
915
+ if (chain === "tempo_moderato") {
916
+ return this.payTempoPermit(executeUrl, service, params, req, options);
917
+ }
918
+ if (chain === "bnb" || chain === "bnb_testnet") {
919
+ return this.payBnb(executeUrl, service, params, req, chain, options);
920
+ }
921
+ return this.payEIP3009(executeUrl, service, params, req, chain, options);
922
+ }
923
+ /** Read-only balance check on the specified chain (or `defaultChain` if configured). */
924
+ async getBalance(chain) {
925
+ const target = chain ?? this.defaultChain;
926
+ if (!target) {
927
+ throw new UnsupportedChainError("unspecified", "No chain provided and no defaultChain configured");
928
+ }
929
+ if (target === "solana" || target === "solana_devnet") {
930
+ const conn = this.getSolanaConnection(target);
931
+ const owner2 = await this.signer.getSolanaAddress?.();
932
+ if (!owner2) {
933
+ throw new NotInitializedError("No Solana address available from signer");
934
+ }
935
+ const native = await conn.getBalance(new PublicKey3(owner2));
936
+ return { usdc: 0, native: native / 1e9 };
937
+ }
938
+ const evmChain = CHAINS[target];
939
+ const provider = new ethers.JsonRpcProvider(evmChain.rpc);
940
+ const owner = await this.signer.getEvmAddress();
941
+ const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
942
+ const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
943
+ provider.getBalance(owner),
944
+ new ethers.Contract(evmChain.tokens.USDC.address, tokenAbi, provider).balanceOf(owner),
945
+ new ethers.Contract(evmChain.tokens.USDT.address, tokenAbi, provider).balanceOf(owner)
946
+ ]);
947
+ return {
948
+ usdc: parseFloat(ethers.formatUnits(usdcBalance, evmChain.tokens.USDC.decimals)),
949
+ usdt: parseFloat(ethers.formatUnits(usdtBalance, evmChain.tokens.USDT.decimals)),
950
+ native: parseFloat(ethers.formatEther(nativeBalance))
951
+ };
952
+ }
953
+ /**
954
+ * Approve a spender for the BNB chain's USDC / USDT (one-time, costs BNB gas).
955
+ * The spender address is what the server advertised as `bnbSpender` in the
956
+ * 402 response. Amount defaults to `MaxUint256` so only one approval is ever
957
+ * needed per (chain, token, spender) tuple.
958
+ */
959
+ async approveBnb(args) {
960
+ if (!this.signer.sendEvmTransaction) {
961
+ throw new NotInitializedError("Signer does not support sendEvmTransaction");
962
+ }
963
+ const chain = CHAINS[args.chain];
964
+ const tokenAddr = chain.tokens[args.token].address;
965
+ const amount = args.amount ?? ethers.MaxUint256.toString();
966
+ const iface = new ethers.Interface([
967
+ "function approve(address spender, uint256 amount) returns (bool)"
968
+ ]);
969
+ const data = iface.encodeFunctionData("approve", [args.spender, amount]);
970
+ return this.signer.sendEvmTransaction({
971
+ chainId: chain.chainId,
972
+ to: tokenAddr,
973
+ data
974
+ });
975
+ }
976
+ // ===== internals =====
977
+ async resolveExecuteUrl(serverUrl, service) {
978
+ try {
979
+ const services = await this.getServices(serverUrl);
980
+ const svc = services.services?.find((s) => s.id === service);
981
+ if (svc?.endpoint) {
982
+ return `${serverUrl}${svc.endpoint}`;
983
+ }
984
+ } catch {
985
+ }
986
+ return `${serverUrl}/execute`;
987
+ }
988
+ buildRequestBody(service, params, options) {
989
+ const body = options.rawData ? { service, ...params } : { service, params };
990
+ if (options.chain) body.chain = options.chain;
991
+ return body;
992
+ }
993
+ chooseChain(requirements, userChain) {
994
+ const preferred = userChain ?? this.defaultChain;
995
+ if (preferred) {
996
+ return selectChain(requirements, preferred);
997
+ }
998
+ const accepted = serverAcceptedChains(requirements);
999
+ if (accepted.length === 1) {
1000
+ return accepted[0];
1001
+ }
1002
+ return selectChain(requirements);
1003
+ }
1004
+ /**
1005
+ * Submit the x402 payload and return the service result. Shared by all
1006
+ * scheme branches — the only thing they vary is how they build `payload`.
1007
+ */
1008
+ async submitPayment(executeUrl, service, params, options, paymentHeader, chainForBody, charge) {
1009
+ const body = options.rawData ? { service, ...params, chain: chainForBody } : { service, params, chain: chainForBody };
1010
+ const paidRes = await this.fetchImpl(executeUrl, {
1011
+ method: "POST",
1012
+ headers: {
1013
+ "Content-Type": "application/json",
1014
+ [PAYMENT_HEADER]: paymentHeader
1015
+ },
1016
+ body: JSON.stringify(body)
1017
+ });
1018
+ const result = await paidRes.json();
1019
+ if (!paidRes.ok) {
1020
+ throw new ServerError(paidRes.status, result.error ?? "Payment failed");
1021
+ }
1022
+ if (this.ledger && charge > 0) {
1023
+ this.ledger.record(charge);
1024
+ }
1025
+ return result.result ?? result;
1026
+ }
1027
+ // ----- EIP-3009 (Base / Polygon / Base Sepolia) -----
1028
+ async payEIP3009(executeUrl, service, params, req, chainName, options) {
1029
+ const evmChain = CHAINS[chainName];
1030
+ const amountRaw = req.amount ?? req.maxAmountRequired;
1031
+ if (!amountRaw) {
1032
+ throw new ServerError(402, "Missing amount in payment requirements");
1033
+ }
1034
+ const amountDisplay = Number(amountRaw) / 1e6;
1035
+ this.ledger?.check(amountDisplay);
1036
+ const payTo = req.payTo ?? req.resource;
1037
+ if (!payTo) {
1038
+ throw new ServerError(402, "Missing payTo in payment requirements");
1039
+ }
1040
+ const owner = await this.signer.getEvmAddress();
1041
+ const nonce = ethers.hexlify(ethers.randomBytes(32));
1042
+ const extra = req.extra ?? {};
1043
+ const tokenConfig = evmChain.tokens.USDC;
1044
+ const tokenName = extra.name ?? tokenConfig.eip712Name ?? "USD Coin";
1045
+ const tokenVersion = extra.version ?? "2";
1046
+ const envelope = buildEIP3009TypedData({
1047
+ from: owner,
1048
+ to: payTo,
1049
+ value: amountRaw,
1050
+ nonce,
1051
+ chainId: evmChain.chainId,
1052
+ tokenAddress: req.asset ?? tokenConfig.address,
1053
+ tokenName,
1054
+ tokenVersion
1055
+ });
1056
+ const signature = await this.signer.signTypedData(envelope);
1057
+ const payload = buildPaymentPayload({
1058
+ scheme: "exact",
1059
+ network: chainNameToNetwork(chainName),
1060
+ payload: { authorization: envelope.message, signature },
1061
+ accepted: {
1062
+ scheme: "exact",
1063
+ network: chainNameToNetwork(chainName),
1064
+ asset: req.asset ?? tokenConfig.address,
1065
+ amount: amountRaw,
1066
+ payTo,
1067
+ maxTimeoutSeconds: req.maxTimeoutSeconds ?? 300,
1068
+ extra: { name: tokenName, version: tokenVersion }
1069
+ }
1070
+ });
1071
+ const header = encodePaymentHeader(payload);
1072
+ return this.submitPayment(executeUrl, service, params, options, header, chainName, amountDisplay);
1073
+ }
1074
+ // ----- EIP-2612 permit (Tempo Moderato) -----
1075
+ async payTempoPermit(executeUrl, service, params, req, options) {
1076
+ const amountRaw = req.amount ?? req.maxAmountRequired;
1077
+ if (!amountRaw) {
1078
+ throw new ServerError(402, "Missing amount in Tempo requirements");
1079
+ }
1080
+ const amountDisplay = Number(amountRaw) / 1e6;
1081
+ this.ledger?.check(amountDisplay);
1082
+ const extra = req.extra ?? {};
1083
+ const spender = extra.tempoSpender;
1084
+ if (!spender) {
1085
+ throw new ServerError(
1086
+ 402,
1087
+ "Tempo requirement missing extra.tempoSpender \u2014 server has no settler configured (TEMPO_SETTLER_KEY)"
1088
+ );
1089
+ }
1090
+ if (!req.asset) {
1091
+ throw new ServerError(402, "Tempo requirement missing asset (token address)");
1092
+ }
1093
+ if (!extra.name || !extra.version) {
1094
+ throw new ServerError(402, "Tempo requirement missing extra.name / extra.version for EIP-712 domain");
1095
+ }
1096
+ const owner = await this.signer.getEvmAddress();
1097
+ const tempoRpc = CHAINS.tempo_moderato.rpc;
1098
+ const provider = new ethers.JsonRpcProvider(tempoRpc);
1099
+ const token = new ethers.Contract(
1100
+ req.asset,
1101
+ ["function nonces(address owner) view returns (uint256)"],
1102
+ provider
1103
+ );
1104
+ const nonce = await token.nonces(owner);
1105
+ const deadline = Math.floor(Date.now() / 1e3) + 3600;
1106
+ const envelope = buildEIP2612PermitTypedData({
1107
+ owner,
1108
+ spender,
1109
+ value: amountRaw,
1110
+ nonce: nonce.toString(),
1111
+ deadline: deadline.toString(),
1112
+ chainId: CHAINS.tempo_moderato.chainId,
1113
+ tokenAddress: req.asset,
1114
+ tokenName: extra.name,
1115
+ tokenVersion: extra.version
1116
+ });
1117
+ const rawSig = await this.signer.signTypedData(envelope);
1118
+ const split = ethers.Signature.from(rawSig);
1119
+ const payload = buildPaymentPayload({
1120
+ scheme: "permit",
1121
+ network: chainNameToNetwork("tempo_moderato"),
1122
+ payload: {
1123
+ permit: {
1124
+ owner,
1125
+ spender,
1126
+ value: amountRaw,
1127
+ nonce: nonce.toString(),
1128
+ deadline: deadline.toString(),
1129
+ v: split.v,
1130
+ r: split.r,
1131
+ s: split.s
1132
+ }
1133
+ },
1134
+ accepted: {
1135
+ scheme: "permit",
1136
+ network: chainNameToNetwork("tempo_moderato"),
1137
+ asset: req.asset,
1138
+ amount: amountRaw,
1139
+ payTo: req.payTo ?? "",
1140
+ maxTimeoutSeconds: req.maxTimeoutSeconds ?? 300,
1141
+ extra: { name: extra.name, version: extra.version, tempoSpender: spender }
1142
+ }
1143
+ });
1144
+ const header = encodePaymentHeader(payload);
1145
+ return this.submitPayment(executeUrl, service, params, options, header, "tempo_moderato", amountDisplay);
1146
+ }
1147
+ // ----- BNB intent (BNB / BNB Testnet) -----
1148
+ async payBnb(executeUrl, service, params, req, chainName, options) {
1149
+ const evmChain = CHAINS[chainName];
1150
+ const extra = req.extra ?? {};
1151
+ const spender = extra.bnbSpender;
1152
+ if (!spender) {
1153
+ throw new ServerError(402, "BNB requirement missing extra.bnbSpender");
1154
+ }
1155
+ if (!req.amount || !req.payTo) {
1156
+ throw new ServerError(402, "BNB requirement missing amount or payTo");
1157
+ }
1158
+ const owner = await this.signer.getEvmAddress();
1159
+ const tokenConfig = evmChain.tokens.USDC;
1160
+ const tokenAddress = req.asset ?? tokenConfig.address;
1161
+ const amountDisplay = Number(req.amount) / 1e6;
1162
+ this.ledger?.check(amountDisplay);
1163
+ const amountWei = BigInt(Math.floor(amountDisplay * 10 ** tokenConfig.decimals));
1164
+ const amountWeiStr = amountWei.toString();
1165
+ const provider = new ethers.JsonRpcProvider(evmChain.rpc);
1166
+ const erc20 = new ethers.Contract(
1167
+ tokenAddress,
1168
+ ["function allowance(address owner, address spender) view returns (uint256)"],
1169
+ provider
1170
+ );
1171
+ const allowance = await erc20.allowance(owner, spender);
1172
+ if (allowance < amountWei) {
1173
+ throw new NeedsApprovalError({
1174
+ chain: chainName,
1175
+ spender,
1176
+ token: "USDC",
1177
+ currentAllowance: allowance.toString(),
1178
+ required: amountWei.toString()
1179
+ });
1180
+ }
1181
+ if (chainName === "bnb") {
1182
+ const nativeBalance = await provider.getBalance(owner);
1183
+ if (nativeBalance < ethers.parseEther("0.0001") && allowance < amountWei) {
1184
+ throw new InsufficientBalanceError(
1185
+ `Insufficient BNB for approve gas (have ${ethers.formatEther(nativeBalance)}, need ~0.001 BNB)`
1186
+ );
1187
+ }
1188
+ }
1189
+ const envelope = buildBnbIntentTypedData({
1190
+ from: owner,
1191
+ to: req.payTo,
1192
+ amount: amountWeiStr,
1193
+ tokenAddress,
1194
+ service,
1195
+ nonce: Date.now(),
1196
+ deadline: Date.now() + 36e5,
1197
+ chainId: evmChain.chainId
1198
+ });
1199
+ const signature = await this.signer.signTypedData(envelope);
1200
+ const payload = buildPaymentPayload({
1201
+ scheme: "exact",
1202
+ network: chainNameToNetwork(chainName),
1203
+ payload: {
1204
+ intent: { ...envelope.message, signature },
1205
+ chainId: evmChain.chainId
1206
+ },
1207
+ accepted: {
1208
+ scheme: "exact",
1209
+ network: chainNameToNetwork(chainName),
1210
+ asset: tokenAddress,
1211
+ amount: amountWeiStr,
1212
+ payTo: req.payTo,
1213
+ maxTimeoutSeconds: req.maxTimeoutSeconds ?? 300
1214
+ }
1215
+ });
1216
+ const header = encodePaymentHeader(payload);
1217
+ return this.submitPayment(executeUrl, service, params, options, header, chainName, amountDisplay);
1218
+ }
1219
+ // ----- Solana (solana / solana_devnet) -----
1220
+ async paySolana(executeUrl, service, params, req, chainName, options) {
1221
+ if (!this.signer.signSolanaTransaction) {
1222
+ throw new NotInitializedError("Signer does not support Solana \u2014 use solanaSigner(adapter) or composeSigners");
1223
+ }
1224
+ const ownerBase58 = await this.signer.getSolanaAddress?.();
1225
+ if (!ownerBase58) {
1226
+ throw new NotInitializedError("Solana wallet not connected");
1227
+ }
1228
+ if (!req.amount || !req.payTo) {
1229
+ throw new ServerError(402, "Solana requirement missing amount or payTo");
1230
+ }
1231
+ const amountDisplay = Number(req.amount) / 1e6;
1232
+ this.ledger?.check(amountDisplay);
1233
+ const ownerPubkey = new PublicKey3(ownerBase58);
1234
+ const recipientPubkey = new PublicKey3(req.payTo);
1235
+ const extra = req.extra ?? {};
1236
+ const feePayerPubkey = extra.solanaFeePayer ? new PublicKey3(extra.solanaFeePayer) : void 0;
1237
+ const unsignedTx = await createSolanaPaymentTransaction(
1238
+ ownerPubkey,
1239
+ recipientPubkey,
1240
+ BigInt(req.amount),
1241
+ chainName,
1242
+ feePayerPubkey,
1243
+ this.getSolanaConnection(chainName)
1244
+ );
1245
+ const unsignedBytes = unsignedTx.serialize({
1246
+ requireAllSignatures: false,
1247
+ verifySignatures: false
1248
+ });
1249
+ const transactionBase64 = uint8ArrayToBase64(unsignedBytes);
1250
+ const signedTx = await this.signer.signSolanaTransaction({
1251
+ transactionBase64,
1252
+ partialSign: !!feePayerPubkey
1253
+ });
1254
+ const payload = buildPaymentPayload({
1255
+ scheme: "exact",
1256
+ network: chainNameToNetwork(chainName),
1257
+ payload: {
1258
+ signedTransaction: signedTx,
1259
+ sender: ownerBase58,
1260
+ chain: chainName
1261
+ },
1262
+ accepted: {
1263
+ scheme: "exact",
1264
+ network: chainNameToNetwork(chainName),
1265
+ asset: req.asset ?? SOLANA_CHAINS[chainName].tokens.USDC.mint,
1266
+ amount: req.amount,
1267
+ payTo: req.payTo,
1268
+ maxTimeoutSeconds: req.maxTimeoutSeconds ?? 300
1269
+ }
1270
+ });
1271
+ const header = encodePaymentHeader(payload);
1272
+ return this.submitPayment(executeUrl, service, params, options, header, chainName, amountDisplay);
1273
+ }
1274
+ };
1275
+ export {
1276
+ InsufficientBalanceError,
1277
+ MoltsPayError,
1278
+ MoltsPayWebClient,
1279
+ NeedsApprovalError,
1280
+ PaymentRejectedError,
1281
+ ServerError,
1282
+ SpendingLedger,
1283
+ SpendingLimitExceededError,
1284
+ UnsupportedChainError,
1285
+ composeSigners,
1286
+ eip1193Signer,
1287
+ solanaSigner
1288
+ };
1289
+ //# sourceMappingURL=index.mjs.map