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.
- package/LICENSE +21 -0
- package/README.md +782 -0
- package/dist/index-BrBqP1I8.d.ts +199 -0
- package/dist/index-D6Sr4ARD.d.mts +429 -0
- package/dist/index-D6Sr4ARD.d.ts +429 -0
- package/dist/index-DJ4Cvrev.d.mts +199 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1178 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1146 -0
- package/dist/index.mjs.map +1 -0
- package/dist/providers/evm/index.d.mts +84 -0
- package/dist/providers/evm/index.d.ts +84 -0
- package/dist/providers/evm/index.js +740 -0
- package/dist/providers/evm/index.js.map +1 -0
- package/dist/providers/evm/index.mjs +735 -0
- package/dist/providers/evm/index.mjs.map +1 -0
- package/dist/providers/near/index.d.mts +99 -0
- package/dist/providers/near/index.d.ts +99 -0
- package/dist/providers/near/index.js +483 -0
- package/dist/providers/near/index.js.map +1 -0
- package/dist/providers/near/index.mjs +478 -0
- package/dist/providers/near/index.mjs.map +1 -0
- package/dist/providers/solana/index.d.mts +115 -0
- package/dist/providers/solana/index.d.ts +115 -0
- package/dist/providers/solana/index.js +771 -0
- package/dist/providers/solana/index.js.map +1 -0
- package/dist/providers/solana/index.mjs +765 -0
- package/dist/providers/solana/index.mjs.map +1 -0
- package/dist/providers/stellar/index.d.mts +67 -0
- package/dist/providers/stellar/index.d.ts +67 -0
- package/dist/providers/stellar/index.js +306 -0
- package/dist/providers/stellar/index.js.map +1 -0
- package/dist/providers/stellar/index.mjs +301 -0
- package/dist/providers/stellar/index.mjs.map +1 -0
- package/dist/react/index.d.mts +73 -0
- package/dist/react/index.d.ts +73 -0
- package/dist/react/index.js +1218 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/index.mjs +1211 -0
- package/dist/react/index.mjs.map +1 -0
- package/dist/utils/index.d.mts +103 -0
- package/dist/utils/index.d.ts +103 -0
- package/dist/utils/index.js +575 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/index.mjs +562 -0
- package/dist/utils/index.mjs.map +1 -0
- package/package.json +149 -0
- package/src/chains/index.ts +539 -0
- package/src/client/X402Client.ts +663 -0
- package/src/client/index.ts +1 -0
- package/src/index.ts +166 -0
- package/src/providers/evm/index.ts +394 -0
- package/src/providers/near/index.ts +664 -0
- package/src/providers/solana/index.ts +489 -0
- package/src/providers/stellar/index.ts +376 -0
- package/src/react/index.tsx +417 -0
- package/src/types/index.ts +561 -0
- package/src/utils/index.ts +20 -0
- 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';
|