uvd-x402-sdk 2.10.1 → 2.11.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.
@@ -1,9 +1,14 @@
1
1
  /**
2
2
  * uvd-x402-sdk - Algorand Provider
3
3
  *
4
- * Provides wallet connection and payment creation for Algorand via Pera Wallet.
4
+ * Provides wallet connection and payment creation for Algorand.
5
+ * Supports both Lute Wallet (desktop browser extension) and Pera Wallet (mobile).
5
6
  * Uses ASA (Algorand Standard Assets) transfers for USDC payments.
6
7
  *
8
+ * Wallet Priority:
9
+ * 1. Lute Wallet - Desktop browser extension (preferred for desktop)
10
+ * 2. Pera Wallet - Mobile via WalletConnect (fallback/mobile)
11
+ *
7
12
  * USDC ASA IDs:
8
13
  * - Mainnet: 31566704
9
14
  * - Testnet: 10458941
@@ -15,7 +20,7 @@
15
20
  *
16
21
  * const algorand = new AlgorandProvider();
17
22
  *
18
- * // Connect to Pera Wallet
23
+ * // Connect to Lute (desktop) or Pera (mobile) automatically
19
24
  * const address = await algorand.connect();
20
25
  *
21
26
  * // Create Algorand payment
@@ -50,27 +55,60 @@ function uint8ArrayToBase64(bytes: Uint8Array): string {
50
55
  // Lazy import Algorand dependencies
51
56
  let algosdk: typeof import('algosdk') | null = null;
52
57
  let PeraWalletConnect: typeof import('@perawallet/connect').PeraWalletConnect | null = null;
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ let LuteConnect: any = null;
53
60
 
54
61
  async function loadAlgorandDeps() {
55
62
  if (!algosdk) {
56
63
  algosdk = await import('algosdk');
57
64
  }
65
+ }
66
+
67
+ async function loadPeraWallet() {
58
68
  if (!PeraWalletConnect) {
59
69
  const peraModule = await import('@perawallet/connect');
60
70
  PeraWalletConnect = peraModule.PeraWalletConnect;
61
71
  }
62
72
  }
63
73
 
74
+ async function loadLuteWallet() {
75
+ if (!LuteConnect) {
76
+ try {
77
+ const luteModule = await import('lute-connect');
78
+ LuteConnect = luteModule.default;
79
+ } catch {
80
+ // Lute not installed, will fall back to Pera
81
+ LuteConnect = null;
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Check if Lute wallet extension is installed
88
+ */
89
+ function isLuteAvailable(): boolean {
90
+ if (typeof window === 'undefined') return false;
91
+ // Lute injects itself into window.algorand or window.lute
92
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
93
+ const win = window as any;
94
+ return !!(win.algorand || win.lute);
95
+ }
96
+
64
97
  /**
65
- * AlgorandProvider - Wallet adapter for Algorand via Pera Wallet
98
+ * AlgorandProvider - Wallet adapter for Algorand
66
99
  *
67
- * Supports both mainnet and testnet through chain configuration.
100
+ * Supports Lute Wallet (desktop) and Pera Wallet (mobile).
101
+ * Automatically detects and uses the best available wallet.
68
102
  */
69
103
  export class AlgorandProvider implements WalletAdapter {
70
- readonly id = 'pera';
71
- readonly name = 'Pera Wallet';
104
+ readonly id = 'algorand';
105
+ readonly name = 'Algorand Wallet';
72
106
  readonly networkType = 'algorand' as const;
73
107
 
108
+ // Active wallet type
109
+ private walletType: 'lute' | 'pera' | null = null;
110
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
111
+ private luteWallet: any = null;
74
112
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
113
  private peraWallet: any = null;
76
114
  private address: string | null = null;
@@ -78,19 +116,91 @@ export class AlgorandProvider implements WalletAdapter {
78
116
  private algodClients: Map<string, any> = new Map();
79
117
 
80
118
  /**
81
- * Check if Pera Wallet is available
82
- * Note: Pera works as a WalletConnect modal, so it's always "available"
119
+ * Check if any Algorand wallet is available
120
+ * Returns true if Lute extension is installed OR we can use Pera (always available via WalletConnect)
83
121
  */
84
122
  isAvailable(): boolean {
85
123
  return typeof window !== 'undefined';
86
124
  }
87
125
 
88
126
  /**
89
- * Connect to Pera Wallet
127
+ * Get the name of the currently connected wallet
128
+ */
129
+ getWalletName(): string {
130
+ if (this.walletType === 'lute') return 'Lute Wallet';
131
+ if (this.walletType === 'pera') return 'Pera Wallet';
132
+ return 'Algorand Wallet';
133
+ }
134
+
135
+ /**
136
+ * Connect to Algorand wallet
137
+ * Priority: Lute (desktop extension) > Pera (mobile via WalletConnect)
90
138
  */
91
139
  async connect(_chainName?: string): Promise<string> {
92
140
  await loadAlgorandDeps();
93
141
 
142
+ // Try Lute first (better desktop UX)
143
+ if (isLuteAvailable()) {
144
+ try {
145
+ return await this.connectLute();
146
+ } catch (error) {
147
+ // Lute failed, try Pera
148
+ console.warn('Lute connection failed, falling back to Pera:', error);
149
+ }
150
+ }
151
+
152
+ // Fall back to Pera (mobile/WalletConnect)
153
+ return await this.connectPera();
154
+ }
155
+
156
+ /**
157
+ * Connect to Lute Wallet (desktop browser extension)
158
+ */
159
+ private async connectLute(): Promise<string> {
160
+ await loadLuteWallet();
161
+
162
+ if (!LuteConnect) {
163
+ throw new X402Error('Lute Wallet SDK not available', 'WALLET_NOT_FOUND');
164
+ }
165
+
166
+ try {
167
+ this.luteWallet = new LuteConnect('402milly');
168
+
169
+ // Get Algorand genesis ID for mainnet
170
+ const genesisId = 'mainnet-v1.0';
171
+
172
+ // Connect and get accounts
173
+ const accounts = await this.luteWallet.connect(genesisId);
174
+
175
+ if (!accounts || accounts.length === 0) {
176
+ throw new X402Error('No accounts returned from Lute Wallet', 'WALLET_CONNECTION_REJECTED');
177
+ }
178
+
179
+ this.address = accounts[0];
180
+ this.walletType = 'lute';
181
+
182
+ return accounts[0];
183
+ } catch (error: unknown) {
184
+ if (error instanceof X402Error) throw error;
185
+ if (error instanceof Error) {
186
+ if (error.message.includes('rejected') || error.message.includes('cancelled')) {
187
+ throw new X402Error('Connection rejected by user', 'WALLET_CONNECTION_REJECTED');
188
+ }
189
+ }
190
+ throw new X402Error(
191
+ `Failed to connect Lute Wallet: ${error instanceof Error ? error.message : 'Unknown error'}`,
192
+ 'UNKNOWN_ERROR',
193
+ error
194
+ );
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Connect to Pera Wallet (mobile via WalletConnect)
200
+ */
201
+ private async connectPera(): Promise<string> {
202
+ await loadPeraWallet();
203
+
94
204
  if (!PeraWalletConnect) {
95
205
  throw new X402Error('Failed to load Pera Wallet SDK', 'WALLET_NOT_FOUND');
96
206
  }
@@ -104,6 +214,7 @@ export class AlgorandProvider implements WalletAdapter {
104
214
 
105
215
  if (accounts.length > 0) {
106
216
  this.address = accounts[0];
217
+ this.walletType = 'pera';
107
218
  return accounts[0];
108
219
  }
109
220
 
@@ -115,10 +226,12 @@ export class AlgorandProvider implements WalletAdapter {
115
226
  }
116
227
 
117
228
  this.address = newAccounts[0];
229
+ this.walletType = 'pera';
118
230
 
119
231
  // Set up disconnect handler
120
232
  this.peraWallet.connector?.on('disconnect', () => {
121
233
  this.address = null;
234
+ this.walletType = null;
122
235
  });
123
236
 
124
237
  return newAccounts[0];
@@ -137,18 +250,27 @@ export class AlgorandProvider implements WalletAdapter {
137
250
  }
138
251
 
139
252
  /**
140
- * Disconnect from Pera Wallet
253
+ * Disconnect from wallet
141
254
  */
142
255
  async disconnect(): Promise<void> {
143
- if (this.peraWallet) {
256
+ if (this.walletType === 'lute' && this.luteWallet) {
257
+ try {
258
+ // Lute doesn't have a disconnect method, just clear state
259
+ } catch {
260
+ // Ignore disconnect errors
261
+ }
262
+ this.luteWallet = null;
263
+ }
264
+ if (this.walletType === 'pera' && this.peraWallet) {
144
265
  try {
145
266
  await this.peraWallet.disconnect();
146
267
  } catch {
147
268
  // Ignore disconnect errors
148
269
  }
270
+ this.peraWallet = null;
149
271
  }
150
- this.peraWallet = null;
151
272
  this.address = null;
273
+ this.walletType = null;
152
274
  this.algodClients.clear();
153
275
  }
154
276
 
@@ -205,7 +327,7 @@ export class AlgorandProvider implements WalletAdapter {
205
327
  async signPayment(paymentInfo: PaymentInfo, chainConfig: ChainConfig): Promise<string> {
206
328
  await loadAlgorandDeps();
207
329
 
208
- if (!this.peraWallet || !this.address) {
330
+ if (!this.address || !this.walletType) {
209
331
  throw new X402Error('Wallet not connected', 'WALLET_NOT_CONNECTED');
210
332
  }
211
333
 
@@ -237,15 +359,29 @@ export class AlgorandProvider implements WalletAdapter {
237
359
  note: new TextEncoder().encode('x402 payment via uvd-x402-sdk'),
238
360
  } as any);
239
361
 
240
- // Sign with Pera Wallet
241
- const signedTxns = await this.peraWallet.signTransaction([[{ txn }]]);
362
+ // Sign with the active wallet (Lute or Pera)
363
+ let signedTxn: Uint8Array;
242
364
 
243
- if (!signedTxns || signedTxns.length === 0) {
244
- throw new X402Error('No signed transaction returned', 'SIGNATURE_REJECTED');
365
+ if (this.walletType === 'lute' && this.luteWallet) {
366
+ // Lute uses signTxns with base64 encoded transactions
367
+ const txnBase64 = uint8ArrayToBase64(txn.toByte());
368
+ const signedTxns = await this.luteWallet.signTxns([{ txn: txnBase64 }]);
369
+ if (!signedTxns || signedTxns.length === 0 || !signedTxns[0]) {
370
+ throw new X402Error('No signed transaction returned', 'SIGNATURE_REJECTED');
371
+ }
372
+ // Lute returns base64 encoded signed transaction
373
+ signedTxn = Uint8Array.from(atob(signedTxns[0]), c => c.charCodeAt(0));
374
+ } else if (this.walletType === 'pera' && this.peraWallet) {
375
+ // Pera uses signTransaction with transaction objects
376
+ const signedTxns = await this.peraWallet.signTransaction([[{ txn }]]);
377
+ if (!signedTxns || signedTxns.length === 0) {
378
+ throw new X402Error('No signed transaction returned', 'SIGNATURE_REJECTED');
379
+ }
380
+ signedTxn = signedTxns[0];
381
+ } else {
382
+ throw new X402Error('No wallet available for signing', 'WALLET_NOT_CONNECTED');
245
383
  }
246
384
 
247
- const signedTxn = signedTxns[0];
248
-
249
385
  const payload: AlgorandPaymentPayload = {
250
386
  from: this.address,
251
387
  to: recipient,