uvd-x402-sdk 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +782 -0
  3. package/dist/index-BrBqP1I8.d.ts +199 -0
  4. package/dist/index-D6Sr4ARD.d.mts +429 -0
  5. package/dist/index-D6Sr4ARD.d.ts +429 -0
  6. package/dist/index-DJ4Cvrev.d.mts +199 -0
  7. package/dist/index.d.mts +3 -0
  8. package/dist/index.d.ts +3 -0
  9. package/dist/index.js +1178 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/index.mjs +1146 -0
  12. package/dist/index.mjs.map +1 -0
  13. package/dist/providers/evm/index.d.mts +84 -0
  14. package/dist/providers/evm/index.d.ts +84 -0
  15. package/dist/providers/evm/index.js +740 -0
  16. package/dist/providers/evm/index.js.map +1 -0
  17. package/dist/providers/evm/index.mjs +735 -0
  18. package/dist/providers/evm/index.mjs.map +1 -0
  19. package/dist/providers/near/index.d.mts +99 -0
  20. package/dist/providers/near/index.d.ts +99 -0
  21. package/dist/providers/near/index.js +483 -0
  22. package/dist/providers/near/index.js.map +1 -0
  23. package/dist/providers/near/index.mjs +478 -0
  24. package/dist/providers/near/index.mjs.map +1 -0
  25. package/dist/providers/solana/index.d.mts +115 -0
  26. package/dist/providers/solana/index.d.ts +115 -0
  27. package/dist/providers/solana/index.js +771 -0
  28. package/dist/providers/solana/index.js.map +1 -0
  29. package/dist/providers/solana/index.mjs +765 -0
  30. package/dist/providers/solana/index.mjs.map +1 -0
  31. package/dist/providers/stellar/index.d.mts +67 -0
  32. package/dist/providers/stellar/index.d.ts +67 -0
  33. package/dist/providers/stellar/index.js +306 -0
  34. package/dist/providers/stellar/index.js.map +1 -0
  35. package/dist/providers/stellar/index.mjs +301 -0
  36. package/dist/providers/stellar/index.mjs.map +1 -0
  37. package/dist/react/index.d.mts +73 -0
  38. package/dist/react/index.d.ts +73 -0
  39. package/dist/react/index.js +1218 -0
  40. package/dist/react/index.js.map +1 -0
  41. package/dist/react/index.mjs +1211 -0
  42. package/dist/react/index.mjs.map +1 -0
  43. package/dist/utils/index.d.mts +103 -0
  44. package/dist/utils/index.d.ts +103 -0
  45. package/dist/utils/index.js +575 -0
  46. package/dist/utils/index.js.map +1 -0
  47. package/dist/utils/index.mjs +562 -0
  48. package/dist/utils/index.mjs.map +1 -0
  49. package/package.json +149 -0
  50. package/src/chains/index.ts +539 -0
  51. package/src/client/X402Client.ts +663 -0
  52. package/src/client/index.ts +1 -0
  53. package/src/index.ts +166 -0
  54. package/src/providers/evm/index.ts +394 -0
  55. package/src/providers/near/index.ts +664 -0
  56. package/src/providers/solana/index.ts +489 -0
  57. package/src/providers/stellar/index.ts +376 -0
  58. package/src/react/index.tsx +417 -0
  59. package/src/types/index.ts +561 -0
  60. package/src/utils/index.ts +20 -0
  61. package/src/utils/x402.ts +295 -0
@@ -0,0 +1,663 @@
1
+ /**
2
+ * uvd-x402-sdk - Main Client
3
+ *
4
+ * The X402Client is the primary entry point for the SDK.
5
+ * It manages wallet connections, chain switching, and payment creation.
6
+ */
7
+
8
+ import { ethers } from 'ethers';
9
+ import type {
10
+ ChainConfig,
11
+ NetworkType,
12
+ PaymentInfo,
13
+ PaymentResult,
14
+ WalletState,
15
+ X402ClientConfig,
16
+ X402Event,
17
+ X402EventData,
18
+ X402EventHandler,
19
+ EVMPaymentPayload,
20
+ } from '../types';
21
+ import { X402Error, DEFAULT_CONFIG } from '../types';
22
+ import {
23
+ SUPPORTED_CHAINS,
24
+ getChainByName,
25
+ getChainById,
26
+ getEnabledChains,
27
+ } from '../chains';
28
+
29
+ /**
30
+ * X402Client - Main SDK client for x402 payments
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { X402Client } from 'uvd-x402-sdk';
35
+ *
36
+ * const client = new X402Client({ defaultChain: 'base' });
37
+ *
38
+ * // Connect wallet
39
+ * await client.connect('base');
40
+ *
41
+ * // Create payment
42
+ * const result = await client.createPayment({
43
+ * recipient: '0x...',
44
+ * amount: '10.00',
45
+ * });
46
+ *
47
+ * // Use result.paymentHeader in your API request
48
+ * ```
49
+ */
50
+ export class X402Client {
51
+ // Configuration
52
+ private readonly config: Required<Pick<X402ClientConfig, 'facilitatorUrl' | 'defaultChain' | 'autoConnect' | 'debug'>> & X402ClientConfig;
53
+
54
+ // Wallet state
55
+ private provider: ethers.BrowserProvider | null = null;
56
+ private signer: ethers.Signer | null = null;
57
+ private connectedAddress: string | null = null;
58
+ private currentChainId: number | null = null;
59
+ private currentNetwork: NetworkType | null = null;
60
+ private currentChainName: string | null = null;
61
+
62
+ // Event emitter
63
+ private eventHandlers: Map<X402Event, Set<X402EventHandler<X402Event>>> = new Map();
64
+
65
+ constructor(config: X402ClientConfig = {}) {
66
+ this.config = {
67
+ ...DEFAULT_CONFIG,
68
+ ...config,
69
+ };
70
+
71
+ // Apply custom RPC overrides
72
+ if (config.rpcOverrides) {
73
+ for (const [chainName, rpcUrl] of Object.entries(config.rpcOverrides)) {
74
+ if (SUPPORTED_CHAINS[chainName]) {
75
+ SUPPORTED_CHAINS[chainName].rpcUrl = rpcUrl;
76
+ }
77
+ }
78
+ }
79
+
80
+ // Apply custom chain configurations
81
+ if (config.customChains) {
82
+ for (const [chainName, chainConfig] of Object.entries(config.customChains)) {
83
+ if (SUPPORTED_CHAINS[chainName]) {
84
+ Object.assign(SUPPORTED_CHAINS[chainName], chainConfig);
85
+ }
86
+ }
87
+ }
88
+
89
+ this.log('X402Client initialized', { config: this.config });
90
+ }
91
+
92
+ // ============================================================================
93
+ // PUBLIC API - Wallet Connection
94
+ // ============================================================================
95
+
96
+ /**
97
+ * Connect to a wallet on the specified chain
98
+ */
99
+ async connect(chainName?: string): Promise<string> {
100
+ const targetChain = chainName || this.config.defaultChain;
101
+ const chain = getChainByName(targetChain);
102
+
103
+ if (!chain) {
104
+ throw new X402Error(`Unsupported chain: ${targetChain}`, 'CHAIN_NOT_SUPPORTED');
105
+ }
106
+
107
+ if (!chain.x402.enabled) {
108
+ throw new X402Error(`Chain ${targetChain} is not enabled for x402 payments`, 'CHAIN_NOT_SUPPORTED');
109
+ }
110
+
111
+ this.log(`Connecting wallet on ${chain.displayName}...`);
112
+
113
+ // Route to appropriate connection method based on network type
114
+ switch (chain.networkType) {
115
+ case 'evm':
116
+ return this.connectEVMWallet(chain);
117
+ case 'solana':
118
+ throw new X402Error(
119
+ 'Solana support requires importing from "uvd-x402-sdk/solana"',
120
+ 'CHAIN_NOT_SUPPORTED'
121
+ );
122
+ case 'stellar':
123
+ throw new X402Error(
124
+ 'Stellar support requires importing from "uvd-x402-sdk/stellar"',
125
+ 'CHAIN_NOT_SUPPORTED'
126
+ );
127
+ case 'near':
128
+ throw new X402Error('NEAR is not yet supported by the facilitator', 'CHAIN_NOT_SUPPORTED');
129
+ default:
130
+ throw new X402Error(`Unknown network type for chain ${targetChain}`, 'CHAIN_NOT_SUPPORTED');
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Disconnect the current wallet
136
+ */
137
+ async disconnect(): Promise<void> {
138
+ this.provider = null;
139
+ this.signer = null;
140
+ this.connectedAddress = null;
141
+ this.currentChainId = null;
142
+ this.currentNetwork = null;
143
+ this.currentChainName = null;
144
+
145
+ this.emit('disconnect', undefined);
146
+ this.log('Wallet disconnected');
147
+ }
148
+
149
+ /**
150
+ * Switch to a different chain (EVM only)
151
+ */
152
+ async switchChain(chainName: string): Promise<void> {
153
+ const chain = getChainByName(chainName);
154
+
155
+ if (!chain) {
156
+ throw new X402Error(`Unsupported chain: ${chainName}`, 'CHAIN_NOT_SUPPORTED');
157
+ }
158
+
159
+ if (chain.networkType !== 'evm') {
160
+ throw new X402Error(
161
+ 'switchChain is only supported for EVM networks. Reconnect with connect() for other networks.',
162
+ 'CHAIN_NOT_SUPPORTED'
163
+ );
164
+ }
165
+
166
+ if (!this.provider) {
167
+ throw new X402Error('Wallet not connected', 'WALLET_NOT_CONNECTED');
168
+ }
169
+
170
+ await this.switchEVMChain(chain);
171
+ }
172
+
173
+ // ============================================================================
174
+ // PUBLIC API - Payment Creation
175
+ // ============================================================================
176
+
177
+ /**
178
+ * Create a payment authorization
179
+ *
180
+ * @param paymentInfo - Payment information from 402 response
181
+ * @returns Payment result with encoded X-PAYMENT header
182
+ */
183
+ async createPayment(paymentInfo: PaymentInfo): Promise<PaymentResult> {
184
+ if (!this.connectedAddress) {
185
+ throw new X402Error('Wallet not connected', 'WALLET_NOT_CONNECTED');
186
+ }
187
+
188
+ if (!this.currentChainName) {
189
+ throw new X402Error('Chain not set', 'CHAIN_NOT_SUPPORTED');
190
+ }
191
+
192
+ const chain = getChainByName(this.currentChainName);
193
+ if (!chain) {
194
+ throw new X402Error(`Chain ${this.currentChainName} not found`, 'CHAIN_NOT_SUPPORTED');
195
+ }
196
+
197
+ this.emit('paymentStarted', { amount: paymentInfo.amount, network: chain.name });
198
+ this.log('Creating payment...', { paymentInfo, chain: chain.name });
199
+
200
+ try {
201
+ // Route to appropriate payment method
202
+ switch (chain.networkType) {
203
+ case 'evm':
204
+ return await this.createEVMPayment(paymentInfo, chain);
205
+ default:
206
+ throw new X402Error(
207
+ `Payment creation for ${chain.networkType} requires the appropriate provider`,
208
+ 'CHAIN_NOT_SUPPORTED'
209
+ );
210
+ }
211
+ } catch (error) {
212
+ const x402Error = error instanceof X402Error
213
+ ? error
214
+ : new X402Error(
215
+ error instanceof Error ? error.message : 'Unknown error',
216
+ 'PAYMENT_FAILED',
217
+ error
218
+ );
219
+
220
+ this.emit('paymentFailed', { error: x402Error.message, code: x402Error.code });
221
+ throw x402Error;
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Check USDC balance on current chain
227
+ */
228
+ async getBalance(): Promise<string> {
229
+ if (!this.connectedAddress || !this.currentChainName) {
230
+ throw new X402Error('Wallet not connected', 'WALLET_NOT_CONNECTED');
231
+ }
232
+
233
+ const chain = getChainByName(this.currentChainName);
234
+ if (!chain) {
235
+ throw new X402Error(`Chain ${this.currentChainName} not found`, 'CHAIN_NOT_SUPPORTED');
236
+ }
237
+
238
+ switch (chain.networkType) {
239
+ case 'evm':
240
+ return this.getEVMBalance(chain);
241
+ default:
242
+ throw new X402Error(
243
+ `Balance check for ${chain.networkType} requires the appropriate provider`,
244
+ 'CHAIN_NOT_SUPPORTED'
245
+ );
246
+ }
247
+ }
248
+
249
+ // ============================================================================
250
+ // PUBLIC API - Getters
251
+ // ============================================================================
252
+
253
+ /**
254
+ * Get current wallet state
255
+ */
256
+ getState(): WalletState {
257
+ return {
258
+ connected: this.connectedAddress !== null,
259
+ address: this.connectedAddress,
260
+ chainId: this.currentChainId,
261
+ network: this.currentChainName,
262
+ networkType: this.currentNetwork,
263
+ balance: null, // Call getBalance() separately
264
+ };
265
+ }
266
+
267
+ /**
268
+ * Get connected wallet address
269
+ */
270
+ getAddress(): string | null {
271
+ return this.connectedAddress;
272
+ }
273
+
274
+ /**
275
+ * Get current chain ID
276
+ */
277
+ getChainId(): number | null {
278
+ return this.currentChainId;
279
+ }
280
+
281
+ /**
282
+ * Get current chain name
283
+ */
284
+ getChainName(): string | null {
285
+ return this.currentChainName;
286
+ }
287
+
288
+ /**
289
+ * Get current chain display name
290
+ */
291
+ getChainDisplayName(): string | null {
292
+ if (!this.currentChainName) return null;
293
+ const chain = getChainByName(this.currentChainName);
294
+ return chain?.displayName ?? null;
295
+ }
296
+
297
+ /**
298
+ * Check if wallet is connected
299
+ */
300
+ isConnected(): boolean {
301
+ return this.connectedAddress !== null;
302
+ }
303
+
304
+ /**
305
+ * Get list of enabled chains
306
+ */
307
+ getEnabledChains(): ChainConfig[] {
308
+ return getEnabledChains();
309
+ }
310
+
311
+ /**
312
+ * Get chain config by name
313
+ */
314
+ getChain(name: string): ChainConfig | undefined {
315
+ return getChainByName(name);
316
+ }
317
+
318
+ // ============================================================================
319
+ // PUBLIC API - Events
320
+ // ============================================================================
321
+
322
+ /**
323
+ * Subscribe to an event
324
+ */
325
+ on<E extends X402Event>(event: E, handler: X402EventHandler<E>): () => void {
326
+ if (!this.eventHandlers.has(event)) {
327
+ this.eventHandlers.set(event, new Set());
328
+ }
329
+ this.eventHandlers.get(event)!.add(handler as X402EventHandler<X402Event>);
330
+
331
+ // Return unsubscribe function
332
+ return () => this.off(event, handler);
333
+ }
334
+
335
+ /**
336
+ * Unsubscribe from an event
337
+ */
338
+ off<E extends X402Event>(event: E, handler: X402EventHandler<E>): void {
339
+ this.eventHandlers.get(event)?.delete(handler as X402EventHandler<X402Event>);
340
+ }
341
+
342
+ // ============================================================================
343
+ // PRIVATE - EVM Wallet Connection
344
+ // ============================================================================
345
+
346
+ private async connectEVMWallet(chain: ChainConfig): Promise<string> {
347
+ // Check for injected provider
348
+ if (typeof window === 'undefined' || !window.ethereum) {
349
+ throw new X402Error(
350
+ 'No Ethereum wallet found. Please install MetaMask or another EVM wallet.',
351
+ 'WALLET_NOT_FOUND'
352
+ );
353
+ }
354
+
355
+ try {
356
+ this.provider = new ethers.BrowserProvider(window.ethereum);
357
+
358
+ // Request account access
359
+ await this.provider.send('eth_requestAccounts', []);
360
+
361
+ // Switch to target chain
362
+ await this.switchEVMChain(chain);
363
+
364
+ // Get signer and address
365
+ this.signer = await this.provider.getSigner();
366
+ this.connectedAddress = await this.signer.getAddress();
367
+ this.currentChainId = chain.chainId;
368
+ this.currentNetwork = 'evm';
369
+ this.currentChainName = chain.name;
370
+
371
+ // Setup event listeners
372
+ this.setupEVMEventListeners();
373
+
374
+ const state = this.getState();
375
+ this.emit('connect', state);
376
+ this.log('EVM wallet connected', { address: this.connectedAddress, chain: chain.name });
377
+
378
+ return this.connectedAddress;
379
+ } catch (error: unknown) {
380
+ if (error instanceof Error) {
381
+ if (error.message.includes('User rejected') || (error as { code?: number }).code === 4001) {
382
+ throw new X402Error('Connection rejected by user', 'WALLET_CONNECTION_REJECTED');
383
+ }
384
+ }
385
+ throw new X402Error(
386
+ `Failed to connect wallet: ${error instanceof Error ? error.message : 'Unknown error'}`,
387
+ 'UNKNOWN_ERROR',
388
+ error
389
+ );
390
+ }
391
+ }
392
+
393
+ private async switchEVMChain(chain: ChainConfig): Promise<void> {
394
+ if (!this.provider) {
395
+ throw new X402Error('Wallet not connected', 'WALLET_NOT_CONNECTED');
396
+ }
397
+
398
+ try {
399
+ await this.provider.send('wallet_switchEthereumChain', [{ chainId: chain.chainIdHex }]);
400
+ } catch (switchError: unknown) {
401
+ // Chain not added - try to add it
402
+ if ((switchError as { code?: number }).code === 4902) {
403
+ try {
404
+ await this.provider.send('wallet_addEthereumChain', [
405
+ {
406
+ chainId: chain.chainIdHex,
407
+ chainName: chain.displayName,
408
+ nativeCurrency: chain.nativeCurrency,
409
+ rpcUrls: [chain.rpcUrl],
410
+ blockExplorerUrls: [chain.explorerUrl],
411
+ },
412
+ ]);
413
+ } catch (addError) {
414
+ throw new X402Error(
415
+ `Failed to add ${chain.displayName} network`,
416
+ 'CHAIN_SWITCH_REJECTED',
417
+ addError
418
+ );
419
+ }
420
+ } else if ((switchError as { code?: number }).code === 4001) {
421
+ throw new X402Error('Network switch rejected by user', 'CHAIN_SWITCH_REJECTED');
422
+ } else {
423
+ throw new X402Error(
424
+ `Failed to switch to ${chain.displayName}`,
425
+ 'CHAIN_SWITCH_REJECTED',
426
+ switchError
427
+ );
428
+ }
429
+ }
430
+
431
+ this.currentChainId = chain.chainId;
432
+ this.currentChainName = chain.name;
433
+ this.emit('chainChanged', { chainId: chain.chainId, chainName: chain.name });
434
+ }
435
+
436
+ private setupEVMEventListeners(): void {
437
+ if (typeof window === 'undefined' || !window.ethereum) return;
438
+
439
+ window.ethereum.on?.('accountsChanged', ((...args: unknown[]) => {
440
+ const accounts = args[0] as string[];
441
+ if (accounts.length === 0) {
442
+ this.disconnect();
443
+ } else if (accounts[0] !== this.connectedAddress) {
444
+ this.connectedAddress = accounts[0];
445
+ this.emit('accountChanged', { address: accounts[0] });
446
+ }
447
+ }) as (...args: unknown[]) => void);
448
+
449
+ window.ethereum.on?.('chainChanged', ((...args: unknown[]) => {
450
+ const chainIdHex = args[0] as string;
451
+ const chainId = parseInt(chainIdHex, 16);
452
+ const chain = getChainById(chainId);
453
+ if (chain) {
454
+ this.currentChainId = chainId;
455
+ this.currentChainName = chain.name;
456
+ this.emit('chainChanged', { chainId, chainName: chain.name });
457
+ }
458
+ }) as (...args: unknown[]) => void);
459
+ }
460
+
461
+ // ============================================================================
462
+ // PRIVATE - EVM Payment Creation
463
+ // ============================================================================
464
+
465
+ private async createEVMPayment(paymentInfo: PaymentInfo, chain: ChainConfig): Promise<PaymentResult> {
466
+ if (!this.signer) {
467
+ throw new X402Error('Wallet not connected', 'WALLET_NOT_CONNECTED');
468
+ }
469
+
470
+ // Get recipient address for EVM
471
+ const recipient = this.getRecipientForNetwork(paymentInfo, 'evm');
472
+
473
+ // Generate random nonce
474
+ const nonceBytes = new Uint8Array(32);
475
+ if (typeof window !== 'undefined' && window.crypto) {
476
+ window.crypto.getRandomValues(nonceBytes);
477
+ } else {
478
+ for (let i = 0; i < 32; i++) {
479
+ nonceBytes[i] = Math.floor(Math.random() * 256);
480
+ }
481
+ }
482
+ const nonce = ethers.hexlify(nonceBytes);
483
+
484
+ // Set validity window (5 minutes for congested networks, 1 minute otherwise)
485
+ const validAfter = 0;
486
+ const validityWindowSeconds = chain.name === 'base' ? 300 : 60;
487
+ const validBefore = Math.floor(Date.now() / 1000) + validityWindowSeconds;
488
+
489
+ // EIP-712 domain
490
+ const domain = {
491
+ name: chain.usdc.name,
492
+ version: chain.usdc.version,
493
+ chainId: chain.chainId,
494
+ verifyingContract: chain.usdc.address,
495
+ };
496
+
497
+ // EIP-712 types for TransferWithAuthorization (ERC-3009)
498
+ const types = {
499
+ TransferWithAuthorization: [
500
+ { name: 'from', type: 'address' },
501
+ { name: 'to', type: 'address' },
502
+ { name: 'value', type: 'uint256' },
503
+ { name: 'validAfter', type: 'uint256' },
504
+ { name: 'validBefore', type: 'uint256' },
505
+ { name: 'nonce', type: 'bytes32' },
506
+ ],
507
+ };
508
+
509
+ // Parse amount
510
+ const value = ethers.parseUnits(paymentInfo.amount, chain.usdc.decimals);
511
+ const from = await this.signer.getAddress();
512
+ const to = ethers.getAddress(recipient);
513
+
514
+ // Message to sign
515
+ const message = {
516
+ from,
517
+ to,
518
+ value,
519
+ validAfter,
520
+ validBefore,
521
+ nonce,
522
+ };
523
+
524
+ this.log('Signing EIP-712 message...', { domain, message });
525
+
526
+ // Sign the EIP-712 message
527
+ let signature: string;
528
+ try {
529
+ signature = await this.signer.signTypedData(domain, types, message);
530
+ } catch (error: unknown) {
531
+ if (error instanceof Error && (error.message.includes('User rejected') || (error as { code?: number }).code === 4001)) {
532
+ throw new X402Error('Signature rejected by user', 'SIGNATURE_REJECTED');
533
+ }
534
+ throw new X402Error(
535
+ `Failed to sign payment: ${error instanceof Error ? error.message : 'Unknown error'}`,
536
+ 'PAYMENT_FAILED',
537
+ error
538
+ );
539
+ }
540
+
541
+ const sig = ethers.Signature.from(signature);
542
+
543
+ // Construct payload
544
+ const payload: EVMPaymentPayload = {
545
+ from,
546
+ to,
547
+ value: value.toString(),
548
+ validAfter,
549
+ validBefore,
550
+ nonce,
551
+ v: sig.v,
552
+ r: sig.r,
553
+ s: sig.s,
554
+ chainId: chain.chainId,
555
+ token: chain.usdc.address,
556
+ };
557
+
558
+ // Encode as X-PAYMENT header
559
+ const paymentHeader = this.encodeEVMPaymentHeader(payload, chain);
560
+
561
+ this.emit('paymentSigned', { paymentHeader });
562
+
563
+ const result: PaymentResult = {
564
+ success: true,
565
+ paymentHeader,
566
+ network: chain.name,
567
+ payer: from,
568
+ };
569
+
570
+ this.emit('paymentCompleted', result);
571
+ this.log('Payment created successfully', { network: chain.name, from });
572
+
573
+ return result;
574
+ }
575
+
576
+ private encodeEVMPaymentHeader(payload: EVMPaymentPayload, chain: ChainConfig): string {
577
+ // Reconstruct full signature from v, r, s
578
+ const fullSignature = payload.r + payload.s.slice(2) + payload.v.toString(16).padStart(2, '0');
579
+
580
+ // Format in x402 standard format
581
+ const x402Payload = {
582
+ x402Version: 1,
583
+ scheme: 'exact',
584
+ network: chain.name,
585
+ payload: {
586
+ signature: fullSignature,
587
+ authorization: {
588
+ from: payload.from,
589
+ to: payload.to,
590
+ value: payload.value,
591
+ validAfter: payload.validAfter.toString(),
592
+ validBefore: payload.validBefore.toString(),
593
+ nonce: payload.nonce,
594
+ },
595
+ },
596
+ };
597
+
598
+ // Base64 encode
599
+ const jsonString = JSON.stringify(x402Payload);
600
+ return btoa(jsonString);
601
+ }
602
+
603
+ // ============================================================================
604
+ // PRIVATE - EVM Balance Check
605
+ // ============================================================================
606
+
607
+ private async getEVMBalance(chain: ChainConfig): Promise<string> {
608
+ // Use public RPC for balance check (more reliable than wallet provider)
609
+ const publicProvider = new ethers.JsonRpcProvider(chain.rpcUrl);
610
+
611
+ const usdcAbi = ['function balanceOf(address owner) view returns (uint256)'];
612
+ const usdcContract = new ethers.Contract(chain.usdc.address, usdcAbi, publicProvider);
613
+
614
+ try {
615
+ const balance = await usdcContract.balanceOf(this.connectedAddress);
616
+ const formatted = ethers.formatUnits(balance, chain.usdc.decimals);
617
+ return parseFloat(formatted).toFixed(2);
618
+ } catch {
619
+ return '0.00';
620
+ }
621
+ }
622
+
623
+ // ============================================================================
624
+ // PRIVATE - Utilities
625
+ // ============================================================================
626
+
627
+ private getRecipientForNetwork(paymentInfo: PaymentInfo, network: NetworkType): string {
628
+ // Map SVM to solana for recipient lookup
629
+ const lookupNetwork = network === 'svm' ? 'solana' : network;
630
+ const recipients = paymentInfo.recipients as Record<string, string> | undefined;
631
+ if (recipients?.[lookupNetwork]) {
632
+ return recipients[lookupNetwork];
633
+ }
634
+ return paymentInfo.recipient;
635
+ }
636
+
637
+ private emit<E extends X402Event>(event: E, data: X402EventData[E]): void {
638
+ this.eventHandlers.get(event)?.forEach(handler => {
639
+ try {
640
+ handler(data);
641
+ } catch (error) {
642
+ console.error(`Error in ${event} handler:`, error);
643
+ }
644
+ });
645
+ }
646
+
647
+ private log(message: string, data?: unknown): void {
648
+ if (this.config.debug) {
649
+ console.log(`[X402Client] ${message}`, data ?? '');
650
+ }
651
+ }
652
+ }
653
+
654
+ // Type augmentation for window.ethereum
655
+ declare global {
656
+ interface Window {
657
+ ethereum?: {
658
+ request: (args: { method: string; params?: unknown[] }) => Promise<unknown>;
659
+ on?: (event: string, handler: (...args: unknown[]) => void) => void;
660
+ removeListener?: (event: string, handler: (...args: unknown[]) => void) => void;
661
+ };
662
+ }
663
+ }
@@ -0,0 +1 @@
1
+ export { X402Client } from './X402Client';