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,489 @@
1
+ /**
2
+ * uvd-x402-sdk - SVM Provider (Solana Virtual Machine)
3
+ *
4
+ * Provides wallet connection and payment creation for SVM-based chains via Phantom.
5
+ * Supports: Solana, Fogo
6
+ * Uses partially-signed transactions where the facilitator is the fee payer.
7
+ *
8
+ * @example Solana
9
+ * ```ts
10
+ * import { SVMProvider } from 'uvd-x402-sdk/solana';
11
+ * import { getChainByName } from 'uvd-x402-sdk';
12
+ *
13
+ * const svm = new SVMProvider();
14
+ *
15
+ * // Connect
16
+ * const address = await svm.connect();
17
+ *
18
+ * // Create Solana payment
19
+ * const chainConfig = getChainByName('solana')!;
20
+ * const paymentPayload = await svm.signPayment(paymentInfo, chainConfig);
21
+ * const header = svm.encodePaymentHeader(paymentPayload, chainConfig);
22
+ * ```
23
+ *
24
+ * @example Fogo
25
+ * ```ts
26
+ * import { SVMProvider } from 'uvd-x402-sdk/solana';
27
+ * import { getChainByName } from 'uvd-x402-sdk';
28
+ *
29
+ * const svm = new SVMProvider();
30
+ *
31
+ * // Connect (same wallet works for all SVM chains)
32
+ * const address = await svm.connect();
33
+ *
34
+ * // Create Fogo payment
35
+ * const chainConfig = getChainByName('fogo')!;
36
+ * const paymentPayload = await svm.signPayment(paymentInfo, chainConfig);
37
+ * const header = svm.encodePaymentHeader(paymentPayload, chainConfig);
38
+ * ```
39
+ */
40
+
41
+ import type {
42
+ ChainConfig,
43
+ PaymentInfo,
44
+ SolanaPaymentPayload,
45
+ WalletAdapter,
46
+ } from '../../types';
47
+ import { X402Error } from '../../types';
48
+ import { getChainByName } from '../../chains';
49
+
50
+ /**
51
+ * Browser-compatible base64 encoding for Uint8Array
52
+ * Avoids Node.js Buffer dependency for browser bundlers
53
+ */
54
+ function uint8ArrayToBase64(bytes: Uint8Array): string {
55
+ let binary = '';
56
+ for (let i = 0; i < bytes.length; i++) {
57
+ binary += String.fromCharCode(bytes[i]);
58
+ }
59
+ return btoa(binary);
60
+ }
61
+
62
+ // Lazy import Solana dependencies to avoid bundling when not used
63
+ let Connection: typeof import('@solana/web3.js').Connection;
64
+ let PublicKey: typeof import('@solana/web3.js').PublicKey;
65
+ let TransactionMessage: typeof import('@solana/web3.js').TransactionMessage;
66
+ let VersionedTransaction: typeof import('@solana/web3.js').VersionedTransaction;
67
+ let ComputeBudgetProgram: typeof import('@solana/web3.js').ComputeBudgetProgram;
68
+ let getAssociatedTokenAddress: typeof import('@solana/spl-token').getAssociatedTokenAddress;
69
+ let createTransferCheckedInstruction: typeof import('@solana/spl-token').createTransferCheckedInstruction;
70
+ let createAssociatedTokenAccountIdempotentInstruction: typeof import('@solana/spl-token').createAssociatedTokenAccountIdempotentInstruction;
71
+ let TOKEN_PROGRAM_ID: typeof import('@solana/spl-token').TOKEN_PROGRAM_ID;
72
+
73
+ async function loadSolanaDeps() {
74
+ if (!Connection) {
75
+ const web3 = await import('@solana/web3.js');
76
+ const splToken = await import('@solana/spl-token');
77
+ Connection = web3.Connection;
78
+ PublicKey = web3.PublicKey;
79
+ TransactionMessage = web3.TransactionMessage;
80
+ VersionedTransaction = web3.VersionedTransaction;
81
+ ComputeBudgetProgram = web3.ComputeBudgetProgram;
82
+ getAssociatedTokenAddress = splToken.getAssociatedTokenAddress;
83
+ createTransferCheckedInstruction = splToken.createTransferCheckedInstruction;
84
+ createAssociatedTokenAccountIdempotentInstruction = splToken.createAssociatedTokenAccountIdempotentInstruction;
85
+ TOKEN_PROGRAM_ID = splToken.TOKEN_PROGRAM_ID;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Phantom wallet provider interface
91
+ */
92
+ interface PhantomProvider {
93
+ isPhantom?: boolean;
94
+ isConnected?: boolean;
95
+ publicKey?: { toBase58(): string };
96
+ connect(): Promise<{ publicKey: { toBase58(): string } }>;
97
+ disconnect(): Promise<void>;
98
+ signTransaction<T>(transaction: T): Promise<T>;
99
+ }
100
+
101
+ /**
102
+ * SVMProvider - Wallet adapter for SVM chains (Solana, Fogo) via Phantom
103
+ *
104
+ * @alias SolanaProvider for backward compatibility
105
+ */
106
+ export class SVMProvider implements WalletAdapter {
107
+ readonly id = 'phantom';
108
+ readonly name = 'Phantom';
109
+ readonly networkType = 'svm' as const;
110
+
111
+ private provider: PhantomProvider | null = null;
112
+ private publicKey: InstanceType<typeof PublicKey> | null = null;
113
+ private connections: Map<string, InstanceType<typeof Connection>> = new Map();
114
+ private address: string | null = null;
115
+
116
+ /**
117
+ * Check if Phantom wallet is available
118
+ */
119
+ isAvailable(): boolean {
120
+ if (typeof window === 'undefined') return false;
121
+ return !!(
122
+ (window as Window & { phantom?: { solana?: PhantomProvider } }).phantom?.solana?.isPhantom ||
123
+ (window as Window & { solana?: PhantomProvider }).solana?.isPhantom
124
+ );
125
+ }
126
+
127
+ /**
128
+ * Connect to Phantom wallet
129
+ */
130
+ async connect(): Promise<string> {
131
+ await loadSolanaDeps();
132
+
133
+ // Get Phantom provider
134
+ this.provider = await this.getPhantomProvider();
135
+ if (!this.provider) {
136
+ throw new X402Error(
137
+ 'Phantom wallet not installed. Please install from phantom.app',
138
+ 'WALLET_NOT_FOUND'
139
+ );
140
+ }
141
+
142
+ try {
143
+ // Check if already connected
144
+ if (this.provider.publicKey && this.provider.isConnected) {
145
+ const publicKeyString = this.provider.publicKey.toBase58();
146
+ this.publicKey = new PublicKey(publicKeyString);
147
+ this.address = publicKeyString;
148
+ await this.initConnection();
149
+ return publicKeyString;
150
+ }
151
+
152
+ // Connect
153
+ const resp = await this.provider.connect();
154
+ const publicKeyString = resp.publicKey.toBase58();
155
+
156
+ this.publicKey = new PublicKey(publicKeyString);
157
+ this.address = publicKeyString;
158
+
159
+ await this.initConnection();
160
+
161
+ return publicKeyString;
162
+ } catch (error: unknown) {
163
+ if (error instanceof Error) {
164
+ if (error.message.includes('User rejected') || (error as { code?: number }).code === 4001) {
165
+ throw new X402Error('Connection rejected by user', 'WALLET_CONNECTION_REJECTED');
166
+ }
167
+ }
168
+ throw new X402Error(
169
+ `Failed to connect Phantom: ${error instanceof Error ? error.message : 'Unknown error'}`,
170
+ 'UNKNOWN_ERROR',
171
+ error
172
+ );
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Disconnect from Phantom
178
+ */
179
+ async disconnect(): Promise<void> {
180
+ if (this.provider) {
181
+ try {
182
+ await this.provider.disconnect();
183
+ } catch {
184
+ // Ignore disconnect errors
185
+ }
186
+ }
187
+ this.provider = null;
188
+ this.publicKey = null;
189
+ this.connections.clear();
190
+ this.address = null;
191
+ }
192
+
193
+ /**
194
+ * Get current address
195
+ */
196
+ getAddress(): string | null {
197
+ return this.address;
198
+ }
199
+
200
+ /**
201
+ * Get USDC balance
202
+ */
203
+ async getBalance(chainConfig: ChainConfig): Promise<string> {
204
+ await loadSolanaDeps();
205
+
206
+ if (!this.address) {
207
+ throw new X402Error('Wallet not connected', 'WALLET_NOT_CONNECTED');
208
+ }
209
+
210
+ await this.initConnection(chainConfig);
211
+ const connection = this.connections.get(chainConfig.name);
212
+ if (!connection) {
213
+ throw new X402Error('Failed to connect to Solana RPC', 'NETWORK_ERROR');
214
+ }
215
+
216
+ try {
217
+ const response = await fetch(chainConfig.rpcUrl, {
218
+ method: 'POST',
219
+ headers: { 'Content-Type': 'application/json' },
220
+ body: JSON.stringify({
221
+ jsonrpc: '2.0',
222
+ id: 1,
223
+ method: 'getTokenAccountsByOwner',
224
+ params: [
225
+ this.address,
226
+ { mint: chainConfig.usdc.address },
227
+ { encoding: 'jsonParsed' },
228
+ ],
229
+ }),
230
+ });
231
+
232
+ const data = await response.json();
233
+
234
+ if (!data.result?.value?.length) {
235
+ return '0.00';
236
+ }
237
+
238
+ const tokenAccountInfo = data.result.value[0].account.data.parsed.info;
239
+ const balance = Number(tokenAccountInfo.tokenAmount.amount) / Math.pow(10, tokenAccountInfo.tokenAmount.decimals);
240
+
241
+ return balance.toFixed(2);
242
+ } catch {
243
+ return '0.00';
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Create SVM payment (partially-signed transaction)
249
+ *
250
+ * Works for both Solana and Fogo chains.
251
+ *
252
+ * Transaction structure required by facilitator:
253
+ * 1. SetComputeUnitLimit
254
+ * 2. SetComputeUnitPrice
255
+ * 3. (Optional) CreateAssociatedTokenAccount if recipient ATA doesn't exist
256
+ * 4. TransferChecked (USDC transfer)
257
+ *
258
+ * Fee payer: Facilitator (not user)
259
+ * User pays: ZERO SOL/FOGO
260
+ */
261
+ async signPayment(paymentInfo: PaymentInfo, chainConfig: ChainConfig): Promise<string> {
262
+ await loadSolanaDeps();
263
+
264
+ if (!this.provider || !this.publicKey || !this.address) {
265
+ throw new X402Error('Wallet not connected', 'WALLET_NOT_CONNECTED');
266
+ }
267
+
268
+ const connection = await this.getConnection(chainConfig);
269
+ if (!connection) {
270
+ throw new X402Error(`Failed to connect to ${chainConfig.displayName} RPC`, 'NETWORK_ERROR');
271
+ }
272
+
273
+ // Get recipient and facilitator addresses
274
+ const recipient = paymentInfo.recipients?.solana || paymentInfo.recipient;
275
+ const facilitatorAddress = paymentInfo.facilitator;
276
+
277
+ if (!facilitatorAddress) {
278
+ throw new X402Error('Facilitator address not provided', 'INVALID_CONFIG');
279
+ }
280
+
281
+ const recipientPubkey = new PublicKey(recipient);
282
+ const facilitatorPubkey = new PublicKey(facilitatorAddress);
283
+ const usdcMint = new PublicKey(chainConfig.usdc.address);
284
+
285
+ // Parse amount (6 decimals for USDC)
286
+ const amount = Math.floor(parseFloat(paymentInfo.amount) * 1_000_000);
287
+
288
+ // Get token accounts
289
+ const fromTokenAccount = await getAssociatedTokenAddress(
290
+ usdcMint,
291
+ this.publicKey,
292
+ true,
293
+ TOKEN_PROGRAM_ID
294
+ );
295
+
296
+ const toTokenAccount = await getAssociatedTokenAddress(
297
+ usdcMint,
298
+ recipientPubkey,
299
+ true,
300
+ TOKEN_PROGRAM_ID
301
+ );
302
+
303
+ // Check if recipient ATA exists
304
+ const toTokenAccountInfo = await connection.getAccountInfo(toTokenAccount);
305
+ const needsATACreation = toTokenAccountInfo === null;
306
+
307
+ // Get recent blockhash
308
+ const { blockhash } = await connection.getLatestBlockhash('finalized');
309
+
310
+ // Build instructions in exact order required by facilitator
311
+ const instructions = [];
312
+
313
+ // Instruction 0: SetComputeUnitLimit
314
+ instructions.push(
315
+ ComputeBudgetProgram.setComputeUnitLimit({
316
+ units: needsATACreation ? 50_000 : 20_000,
317
+ })
318
+ );
319
+
320
+ // Instruction 1: SetComputeUnitPrice
321
+ instructions.push(
322
+ ComputeBudgetProgram.setComputeUnitPrice({
323
+ microLamports: 1,
324
+ })
325
+ );
326
+
327
+ // Instruction 2 (optional): CreateAssociatedTokenAccountIdempotent
328
+ // User pays for ATA creation (not facilitator - security check in x402-rs)
329
+ if (needsATACreation) {
330
+ instructions.push(
331
+ createAssociatedTokenAccountIdempotentInstruction(
332
+ this.publicKey, // User pays for ATA creation
333
+ toTokenAccount,
334
+ recipientPubkey,
335
+ usdcMint,
336
+ TOKEN_PROGRAM_ID
337
+ )
338
+ );
339
+ }
340
+
341
+ // Instruction 2/3: TransferChecked
342
+ instructions.push(
343
+ createTransferCheckedInstruction(
344
+ fromTokenAccount,
345
+ usdcMint,
346
+ toTokenAccount,
347
+ this.publicKey,
348
+ amount,
349
+ 6,
350
+ [],
351
+ TOKEN_PROGRAM_ID
352
+ )
353
+ );
354
+
355
+ // Build VersionedTransaction with facilitator as fee payer
356
+ const messageV0 = new TransactionMessage({
357
+ payerKey: facilitatorPubkey,
358
+ recentBlockhash: blockhash,
359
+ instructions,
360
+ }).compileToV0Message();
361
+
362
+ const transaction = new VersionedTransaction(messageV0);
363
+
364
+ // User signs (partial signature - facilitator will co-sign)
365
+ let signedTransaction: InstanceType<typeof VersionedTransaction>;
366
+ try {
367
+ signedTransaction = await this.provider.signTransaction(transaction);
368
+ } catch (error: unknown) {
369
+ if (error instanceof Error && error.message.includes('User rejected')) {
370
+ throw new X402Error('Signature rejected by user', 'SIGNATURE_REJECTED');
371
+ }
372
+ throw new X402Error(
373
+ `Failed to sign transaction: ${error instanceof Error ? error.message : 'Unknown error'}`,
374
+ 'PAYMENT_FAILED',
375
+ error
376
+ );
377
+ }
378
+
379
+ // Serialize partially-signed transaction (VersionedTransaction.serialize takes no args)
380
+ const serialized = signedTransaction.serialize();
381
+
382
+ const payload: SolanaPaymentPayload = {
383
+ transaction: uint8ArrayToBase64(serialized),
384
+ };
385
+
386
+ return JSON.stringify(payload);
387
+ }
388
+
389
+ /**
390
+ * Encode SVM payment as X-PAYMENT header
391
+ *
392
+ * @param paymentPayload - The payment payload JSON string
393
+ * @param chainConfig - Optional chain config (defaults to 'solana' if not provided)
394
+ */
395
+ encodePaymentHeader(paymentPayload: string, chainConfig?: ChainConfig): string {
396
+ const payload = JSON.parse(paymentPayload) as SolanaPaymentPayload;
397
+
398
+ // Use chain name from config, or default to 'solana' for backward compatibility
399
+ const networkName = chainConfig?.name || 'solana';
400
+
401
+ const x402Payload = {
402
+ x402Version: 1,
403
+ scheme: 'exact',
404
+ network: networkName,
405
+ payload: {
406
+ transaction: payload.transaction,
407
+ },
408
+ };
409
+
410
+ return btoa(JSON.stringify(x402Payload));
411
+ }
412
+
413
+ // Private helpers
414
+
415
+ private async getPhantomProvider(): Promise<PhantomProvider | null> {
416
+ if (typeof window === 'undefined') return null;
417
+
418
+ // Try window.phantom.solana first
419
+ const win = window as Window & {
420
+ phantom?: { solana?: PhantomProvider };
421
+ solana?: PhantomProvider;
422
+ };
423
+
424
+ if (win.phantom?.solana?.isPhantom) {
425
+ return win.phantom.solana;
426
+ }
427
+
428
+ // Fallback to window.solana
429
+ if (win.solana?.isPhantom) {
430
+ return win.solana;
431
+ }
432
+
433
+ // Wait a bit for Phantom to inject itself
434
+ for (let i = 0; i < 5; i++) {
435
+ await new Promise(resolve => setTimeout(resolve, 100));
436
+ if (win.phantom?.solana?.isPhantom) {
437
+ return win.phantom.solana;
438
+ }
439
+ if (win.solana?.isPhantom) {
440
+ return win.solana;
441
+ }
442
+ }
443
+
444
+ return null;
445
+ }
446
+
447
+ /**
448
+ * Get or create a connection for a specific chain
449
+ */
450
+ private async getConnection(chainConfig?: ChainConfig): Promise<InstanceType<typeof Connection>> {
451
+ await loadSolanaDeps();
452
+
453
+ const config = chainConfig || getChainByName('solana');
454
+ if (!config) {
455
+ throw new X402Error('Chain config not found', 'CHAIN_NOT_SUPPORTED');
456
+ }
457
+
458
+ // Check if we already have a connection for this chain
459
+ if (this.connections.has(config.name)) {
460
+ return this.connections.get(config.name)!;
461
+ }
462
+
463
+ // Create new connection for this chain
464
+ const connection = new Connection(config.rpcUrl, 'confirmed');
465
+ this.connections.set(config.name, connection);
466
+
467
+ return connection;
468
+ }
469
+
470
+ /**
471
+ * @deprecated Use getConnection instead
472
+ */
473
+ private async initConnection(chainConfig?: ChainConfig): Promise<void> {
474
+ await this.getConnection(chainConfig);
475
+ }
476
+ }
477
+
478
+ /**
479
+ * @deprecated Use SVMProvider instead
480
+ */
481
+ export class SolanaProvider extends SVMProvider {
482
+ constructor() {
483
+ super();
484
+ console.warn('SolanaProvider is deprecated. Use SVMProvider instead.');
485
+ }
486
+ }
487
+
488
+ // Default export
489
+ export default SVMProvider;