@reown/appkit-solana-react-native 2.0.0-alpha.1 → 2.0.0-alpha.3

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 (44) hide show
  1. package/lib/commonjs/adapter.js +121 -3
  2. package/lib/commonjs/adapter.js.map +1 -1
  3. package/lib/commonjs/connectors/PhantomConnector.js +28 -24
  4. package/lib/commonjs/connectors/PhantomConnector.js.map +1 -1
  5. package/lib/commonjs/helpers.js +0 -1
  6. package/lib/commonjs/helpers.js.map +1 -1
  7. package/lib/commonjs/index.js +1 -1
  8. package/lib/commonjs/index.js.map +1 -1
  9. package/lib/commonjs/package.json +1 -0
  10. package/lib/commonjs/providers/PhantomProvider.js +124 -84
  11. package/lib/commonjs/providers/PhantomProvider.js.map +1 -1
  12. package/lib/commonjs/types.js.map +1 -1
  13. package/lib/commonjs/utils/createSendTransaction.js +44 -0
  14. package/lib/commonjs/utils/createSendTransaction.js.map +1 -0
  15. package/lib/module/adapter.js +122 -3
  16. package/lib/module/adapter.js.map +1 -1
  17. package/lib/module/connectors/PhantomConnector.js +31 -25
  18. package/lib/module/connectors/PhantomConnector.js.map +1 -1
  19. package/lib/module/helpers.js +2 -1
  20. package/lib/module/helpers.js.map +1 -1
  21. package/lib/module/index.js +6 -4
  22. package/lib/module/index.js.map +1 -1
  23. package/lib/module/providers/PhantomProvider.js +126 -84
  24. package/lib/module/providers/PhantomProvider.js.map +1 -1
  25. package/lib/module/types.js +2 -0
  26. package/lib/module/types.js.map +1 -1
  27. package/lib/module/utils/createSendTransaction.js +41 -0
  28. package/lib/module/utils/createSendTransaction.js.map +1 -0
  29. package/lib/typescript/adapter.d.ts +11 -3
  30. package/lib/typescript/adapter.d.ts.map +1 -1
  31. package/lib/typescript/connectors/PhantomConnector.d.ts +2 -1
  32. package/lib/typescript/connectors/PhantomConnector.d.ts.map +1 -1
  33. package/lib/typescript/index.d.ts +2 -2
  34. package/lib/typescript/index.d.ts.map +1 -1
  35. package/lib/typescript/providers/PhantomProvider.d.ts +14 -0
  36. package/lib/typescript/providers/PhantomProvider.d.ts.map +1 -1
  37. package/lib/typescript/utils/createSendTransaction.d.ts +10 -0
  38. package/lib/typescript/utils/createSendTransaction.d.ts.map +1 -0
  39. package/package.json +9 -3
  40. package/src/adapter.ts +152 -3
  41. package/src/connectors/PhantomConnector.ts +39 -38
  42. package/src/index.ts +4 -4
  43. package/src/providers/PhantomProvider.ts +165 -113
  44. package/src/utils/createSendTransaction.ts +57 -0
@@ -54,6 +54,10 @@ export class PhantomProvider extends EventEmitter implements Provider {
54
54
  private userPublicKey: string | null = null;
55
55
  private phantomEncryptionPublicKeyBs58: string | null = null;
56
56
 
57
+ // Single subscription management - deep links are sequential by nature
58
+ private activeSubscription: { remove: () => void } | null = null;
59
+ private isOperationPending = false;
60
+
57
61
  constructor(config: PhantomProviderConfig) {
58
62
  super();
59
63
  this.config = config;
@@ -61,6 +65,37 @@ export class PhantomProvider extends EventEmitter implements Provider {
61
65
  this.storage = config.storage;
62
66
  }
63
67
 
68
+ /**
69
+ * Cleanup method to be called when the provider is destroyed
70
+ */
71
+ public destroy(): void {
72
+ this.cleanupActiveSubscription();
73
+ this.removeAllListeners();
74
+ }
75
+
76
+ /**
77
+ * Safely cleanup the active subscription
78
+ */
79
+ private cleanupActiveSubscription(): void {
80
+ if (this.activeSubscription) {
81
+ this.activeSubscription.remove();
82
+ this.activeSubscription = null;
83
+ }
84
+ this.isOperationPending = false;
85
+ }
86
+
87
+ /**
88
+ * Safely set a new subscription, ensuring no operation is pending
89
+ */
90
+ private setActiveSubscription(subscription: { remove: () => void }): void {
91
+ // If there's already a pending operation, reject it
92
+ if (this.isOperationPending) {
93
+ this.cleanupActiveSubscription();
94
+ }
95
+ this.activeSubscription = subscription;
96
+ this.isOperationPending = true;
97
+ }
98
+
64
99
  getUserPublicKey(): string | null {
65
100
  return this.userPublicKey;
66
101
  }
@@ -172,7 +207,7 @@ export class PhantomProvider extends EventEmitter implements Provider {
172
207
  cluster: this.currentCluster
173
208
  };
174
209
  try {
175
- await this.storage.setItem(PHANTOM_PROVIDER_STORAGE_KEY, JSON.stringify(session));
210
+ await this.storage.setItem(PHANTOM_PROVIDER_STORAGE_KEY, session);
176
211
  } catch (error) {
177
212
  // console.error('PhantomProvider: Failed to save session.', error);
178
213
  }
@@ -200,69 +235,76 @@ export class PhantomProvider extends EventEmitter implements Provider {
200
235
  const url = this.buildUrl('connect', connectDeeplinkParams as any);
201
236
 
202
237
  return new Promise<PhantomConnectResult>((resolve, reject) => {
203
- let subscription: { remove: () => void } | null = null;
204
238
  const handleDeepLink = async (event: { url: string }) => {
205
- if (subscription) {
206
- subscription.remove();
207
- }
208
- const fullUrl = event.url;
209
- if (fullUrl.startsWith(this.config.appScheme)) {
210
- const responseUrlParams = new URLSearchParams(
211
- fullUrl.substring(fullUrl.indexOf('?') + 1)
212
- );
213
- const errorCode = responseUrlParams.get('errorCode');
214
- const errorMessage = responseUrlParams.get('errorMessage');
215
- if (errorCode) {
216
- return reject(
217
- new Error(
218
- `Phantom Connection Failed: ${errorMessage || 'Unknown error'} (Code: ${errorCode})`
219
- )
239
+ try {
240
+ this.cleanupActiveSubscription();
241
+ const fullUrl = event.url;
242
+ if (fullUrl.startsWith(this.config.appScheme)) {
243
+ const responseUrlParams = new URLSearchParams(
244
+ fullUrl.substring(fullUrl.indexOf('?') + 1)
220
245
  );
221
- }
222
- const responsePayload: PhantomDeeplinkResponse = {
223
- phantom_encryption_public_key: responseUrlParams.get('phantom_encryption_public_key')!,
224
- nonce: responseUrlParams.get('nonce')!,
225
- data: responseUrlParams.get('data')!
226
- };
227
- if (
228
- !responsePayload.phantom_encryption_public_key ||
229
- !responsePayload.nonce ||
230
- !responsePayload.data
231
- ) {
232
- return reject(new Error('Phantom Connect: Invalid response - missing parameters.'));
233
- }
234
- const decryptedData = this.decryptPayload<DecryptedConnectData>(
235
- responsePayload.data,
236
- responsePayload.nonce,
237
- responsePayload.phantom_encryption_public_key
238
- );
239
- if (!decryptedData || !decryptedData.public_key || !decryptedData.session) {
240
- return reject(
241
- new Error('Phantom Connect: Failed to decrypt or invalid decrypted data.')
246
+ const errorCode = responseUrlParams.get('errorCode');
247
+ const errorMessage = responseUrlParams.get('errorMessage');
248
+ if (errorCode) {
249
+ return reject(
250
+ new Error(
251
+ `Phantom Connection Failed: ${
252
+ errorMessage || 'Unknown error'
253
+ } (Code: ${errorCode})`
254
+ )
255
+ );
256
+ }
257
+ const responsePayload: PhantomDeeplinkResponse = {
258
+ phantom_encryption_public_key: responseUrlParams.get(
259
+ 'phantom_encryption_public_key'
260
+ )!,
261
+ nonce: responseUrlParams.get('nonce')!,
262
+ data: responseUrlParams.get('data')!
263
+ };
264
+ if (
265
+ !responsePayload.phantom_encryption_public_key ||
266
+ !responsePayload.nonce ||
267
+ !responsePayload.data
268
+ ) {
269
+ return reject(new Error('Phantom Connect: Invalid response - missing parameters.'));
270
+ }
271
+ const decryptedData = this.decryptPayload<DecryptedConnectData>(
272
+ responsePayload.data,
273
+ responsePayload.nonce,
274
+ responsePayload.phantom_encryption_public_key
242
275
  );
276
+ if (!decryptedData || !decryptedData.public_key || !decryptedData.session) {
277
+ return reject(
278
+ new Error('Phantom Connect: Failed to decrypt or invalid decrypted data.')
279
+ );
280
+ }
281
+ this.userPublicKey = decryptedData.public_key;
282
+ this.sessionToken = decryptedData.session;
283
+ this.phantomEncryptionPublicKeyBs58 = responsePayload.phantom_encryption_public_key;
284
+
285
+ // Save session on successful connect
286
+ this.saveSession();
287
+
288
+ resolve({
289
+ userPublicKey: this.userPublicKey,
290
+ sessionToken: this.sessionToken,
291
+ phantomEncryptionPublicKeyBs58: this.phantomEncryptionPublicKeyBs58,
292
+ cluster
293
+ });
294
+ } else {
295
+ reject(new Error('Phantom Connect: Unexpected redirect URI.'));
243
296
  }
244
- this.userPublicKey = decryptedData.public_key;
245
- this.sessionToken = decryptedData.session;
246
- this.phantomEncryptionPublicKeyBs58 = responsePayload.phantom_encryption_public_key;
247
-
248
- // Save session on successful connect
249
- this.saveSession();
250
-
251
- resolve({
252
- userPublicKey: this.userPublicKey,
253
- sessionToken: this.sessionToken,
254
- phantomEncryptionPublicKeyBs58: this.phantomEncryptionPublicKeyBs58,
255
- cluster
256
- });
257
- } else {
258
- reject(new Error('Phantom Connect: Unexpected redirect URI.'));
297
+ } catch (error) {
298
+ this.cleanupActiveSubscription();
299
+ reject(error);
259
300
  }
260
301
  };
261
- subscription = Linking.addEventListener('url', handleDeepLink);
302
+
303
+ const subscription = Linking.addEventListener('url', handleDeepLink);
304
+ this.setActiveSubscription(subscription);
305
+
262
306
  Linking.openURL(url).catch(err => {
263
- if (subscription) {
264
- subscription.remove();
265
- }
307
+ this.cleanupActiveSubscription();
266
308
  reject(new Error(`Failed to open Phantom wallet: ${err.message}. Is it installed?`));
267
309
  });
268
310
  }) as Promise<T>;
@@ -271,6 +313,7 @@ export class PhantomProvider extends EventEmitter implements Provider {
271
313
  public async disconnect(): Promise<void> {
272
314
  if (!this.sessionToken || !this.phantomEncryptionPublicKeyBs58) {
273
315
  await this.clearSession();
316
+ this.emit('disconnect');
274
317
 
275
318
  return Promise.resolve();
276
319
  }
@@ -284,6 +327,7 @@ export class PhantomProvider extends EventEmitter implements Provider {
284
327
  if (!encryptedDisconnectPayload) {
285
328
  // console.warn('PhantomProvider: Failed to encrypt disconnect payload. Clearing session locally.');
286
329
  await this.clearSession();
330
+ this.emit('disconnect');
287
331
 
288
332
  return Promise.resolve(); // Or reject, depending on desired strictness
289
333
  }
@@ -297,24 +341,28 @@ export class PhantomProvider extends EventEmitter implements Provider {
297
341
  const url = this.buildUrl('disconnect', disconnectDeeplinkParams as any);
298
342
 
299
343
  return new Promise<void>((resolve, reject) => {
300
- let subscription: { remove: () => void } | null = null;
301
344
  const handleDeepLink = (event: { url: string }) => {
302
- if (subscription) {
303
- subscription.remove();
304
- }
305
- if (event.url.startsWith(this.config.appScheme)) {
306
- this.clearSession();
307
- resolve();
308
- } else {
345
+ try {
346
+ this.cleanupActiveSubscription();
347
+ if (event.url.startsWith(this.config.appScheme)) {
348
+ this.clearSession();
349
+ resolve();
350
+ } else {
351
+ this.clearSession();
352
+ reject(new Error('Phantom Disconnect: Unexpected redirect URI.'));
353
+ }
354
+ } catch (error) {
355
+ this.cleanupActiveSubscription();
309
356
  this.clearSession();
310
- reject(new Error('Phantom Disconnect: Unexpected redirect URI.'));
357
+ reject(error);
311
358
  }
312
359
  };
313
- subscription = Linking.addEventListener('url', handleDeepLink);
360
+
361
+ const subscription = Linking.addEventListener('url', handleDeepLink);
362
+ this.setActiveSubscription(subscription);
363
+
314
364
  Linking.openURL(url).catch(err => {
315
- if (subscription) {
316
- subscription.remove();
317
- }
365
+ this.cleanupActiveSubscription();
318
366
  this.clearSession();
319
367
  reject(new Error(`Failed to open Phantom for disconnection: ${err.message}.`));
320
368
  });
@@ -325,6 +373,7 @@ export class PhantomProvider extends EventEmitter implements Provider {
325
373
  this.sessionToken = null;
326
374
  this.userPublicKey = null;
327
375
  this.phantomEncryptionPublicKeyBs58 = null;
376
+ this.cleanupActiveSubscription();
328
377
  await this.clearSessionStorage();
329
378
  }
330
379
 
@@ -469,56 +518,59 @@ export class PhantomProvider extends EventEmitter implements Provider {
469
518
  }
470
519
 
471
520
  return new Promise<T>((resolve, reject) => {
472
- let subscription: { remove: () => void } | null = null;
473
521
  const handleDeepLink = async (event: { url: string }) => {
474
- if (subscription) {
475
- subscription.remove();
476
- }
477
- const fullUrl = event.url;
478
- if (fullUrl.startsWith(this.config.appScheme)) {
479
- const responseUrlParams = new URLSearchParams(
480
- fullUrl.substring(fullUrl.indexOf('?') + 1)
481
- );
482
- const errorCode = responseUrlParams.get('errorCode');
483
- const errorMessage = responseUrlParams.get('errorMessage');
484
- if (errorCode) {
485
- return reject(
486
- new Error(
487
- `Phantom ${signingMethod} Failed: ${
488
- errorMessage || 'Unknown error'
489
- } (Code: ${errorCode})`
490
- )
522
+ try {
523
+ this.cleanupActiveSubscription();
524
+ const fullUrl = event.url;
525
+ if (fullUrl.startsWith(this.config.appScheme)) {
526
+ const responseUrlParams = new URLSearchParams(
527
+ fullUrl.substring(fullUrl.indexOf('?') + 1)
491
528
  );
492
- }
493
- const responseNonce = responseUrlParams.get('nonce');
494
- const responseData = responseUrlParams.get('data');
495
- if (!responseNonce || !responseData) {
496
- return reject(
497
- new Error(`Phantom ${signingMethod}: Invalid response - missing nonce or data.`)
498
- );
499
- }
500
- const decryptedResult = this.decryptPayload<any>(
501
- responseData,
502
- responseNonce,
503
- this.phantomEncryptionPublicKeyBs58!
504
- );
505
- if (!decryptedResult) {
506
- return reject(
507
- new Error(
508
- `Phantom ${signingMethod}: Failed to decrypt response or invalid decrypted data.`
509
- )
529
+ const errorCode = responseUrlParams.get('errorCode');
530
+ const errorMessage = responseUrlParams.get('errorMessage');
531
+ if (errorCode) {
532
+ return reject(
533
+ new Error(
534
+ `Phantom ${signingMethod} Failed: ${
535
+ errorMessage || 'Unknown error'
536
+ } (Code: ${errorCode})`
537
+ )
538
+ );
539
+ }
540
+ const responseNonce = responseUrlParams.get('nonce');
541
+ const responseData = responseUrlParams.get('data');
542
+ if (!responseNonce || !responseData) {
543
+ return reject(
544
+ new Error(`Phantom ${signingMethod}: Invalid response - missing nonce or data.`)
545
+ );
546
+ }
547
+ const decryptedResult = this.decryptPayload<any>(
548
+ responseData,
549
+ responseNonce,
550
+ this.phantomEncryptionPublicKeyBs58!
510
551
  );
552
+ if (!decryptedResult) {
553
+ return reject(
554
+ new Error(
555
+ `Phantom ${signingMethod}: Failed to decrypt response or invalid decrypted data.`
556
+ )
557
+ );
558
+ }
559
+ resolve(decryptedResult as T);
560
+ } else {
561
+ reject(new Error(`Phantom ${signingMethod}: Unexpected redirect URI.`));
511
562
  }
512
- resolve(decryptedResult as T);
513
- } else {
514
- reject(new Error(`Phantom ${signingMethod}: Unexpected redirect URI.`));
563
+ } catch (error) {
564
+ this.cleanupActiveSubscription();
565
+ reject(error);
515
566
  }
516
567
  };
517
- subscription = Linking.addEventListener('url', handleDeepLink);
568
+
569
+ const subscription = Linking.addEventListener('url', handleDeepLink);
570
+ this.setActiveSubscription(subscription);
571
+
518
572
  Linking.openURL(deeplinkUrl).catch(err => {
519
- if (subscription) {
520
- subscription.remove();
521
- }
573
+ this.cleanupActiveSubscription();
522
574
  reject(
523
575
  new Error(`Failed to open Phantom for ${signingMethod}: ${err.message}. Is it installed?`)
524
576
  );
@@ -0,0 +1,57 @@
1
+ import {
2
+ ComputeBudgetProgram,
3
+ type Connection,
4
+ LAMPORTS_PER_SOL,
5
+ PublicKey,
6
+ SystemProgram,
7
+ Transaction
8
+ } from '@solana/web3.js';
9
+
10
+ // import type { Provider } from '@reown/appkit-utils/solana'
11
+
12
+ type SendTransactionArgs = {
13
+ connection: Connection;
14
+ fromAddress: string;
15
+ toAddress: string;
16
+ value: number;
17
+ };
18
+
19
+ /**
20
+ * These constants defines the cost of running the program, allowing to calculate the maximum
21
+ * amount of SOL that can be sent in case of cleaning the account and remove the rent exemption error.
22
+ */
23
+ const COMPUTE_BUDGET_CONSTANTS = {
24
+ UNIT_PRICE_MICRO_LAMPORTS: 20000000,
25
+ UNIT_LIMIT: 500
26
+ };
27
+
28
+ export async function createSendTransaction({
29
+ fromAddress,
30
+ toAddress,
31
+ value,
32
+ connection
33
+ }: SendTransactionArgs): Promise<Transaction> {
34
+ const fromPubkey = new PublicKey(fromAddress);
35
+ const toPubkey = new PublicKey(toAddress);
36
+ const lamports = Math.floor(value * LAMPORTS_PER_SOL);
37
+
38
+ const { blockhash } = await connection.getLatestBlockhash();
39
+
40
+ const instructions = [
41
+ ComputeBudgetProgram.setComputeUnitPrice({
42
+ microLamports: COMPUTE_BUDGET_CONSTANTS.UNIT_PRICE_MICRO_LAMPORTS
43
+ }),
44
+ ComputeBudgetProgram.setComputeUnitLimit({ units: COMPUTE_BUDGET_CONSTANTS.UNIT_LIMIT }),
45
+ SystemProgram.transfer({
46
+ fromPubkey,
47
+ toPubkey,
48
+ lamports
49
+ })
50
+ ];
51
+
52
+ const transaction = new Transaction().add(...instructions);
53
+ transaction.feePayer = fromPubkey;
54
+ transaction.recentBlockhash = blockhash;
55
+
56
+ return transaction;
57
+ }