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

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 (47) hide show
  1. package/lib/commonjs/adapter.js +3 -3
  2. package/lib/commonjs/adapter.js.map +1 -1
  3. package/lib/commonjs/connectors/DeeplinkConnector.js +271 -0
  4. package/lib/commonjs/connectors/DeeplinkConnector.js.map +1 -0
  5. package/lib/commonjs/connectors/PhantomConnector.js +15 -230
  6. package/lib/commonjs/connectors/PhantomConnector.js.map +1 -1
  7. package/lib/commonjs/connectors/SolflareConnector.js +36 -0
  8. package/lib/commonjs/connectors/SolflareConnector.js.map +1 -0
  9. package/lib/commonjs/index.js +7 -0
  10. package/lib/commonjs/index.js.map +1 -1
  11. package/lib/commonjs/providers/{PhantomProvider.js → DeeplinkProvider.js} +155 -154
  12. package/lib/commonjs/providers/DeeplinkProvider.js.map +1 -0
  13. package/lib/module/adapter.js +3 -3
  14. package/lib/module/adapter.js.map +1 -1
  15. package/lib/module/connectors/DeeplinkConnector.js +265 -0
  16. package/lib/module/connectors/DeeplinkConnector.js.map +1 -0
  17. package/lib/module/connectors/PhantomConnector.js +16 -230
  18. package/lib/module/connectors/PhantomConnector.js.map +1 -1
  19. package/lib/module/connectors/SolflareConnector.js +31 -0
  20. package/lib/module/connectors/SolflareConnector.js.map +1 -0
  21. package/lib/module/index.js +1 -0
  22. package/lib/module/index.js.map +1 -1
  23. package/lib/module/providers/{PhantomProvider.js → DeeplinkProvider.js} +153 -152
  24. package/lib/module/providers/DeeplinkProvider.js.map +1 -0
  25. package/lib/typescript/connectors/DeeplinkConnector.d.ts +30 -0
  26. package/lib/typescript/connectors/DeeplinkConnector.d.ts.map +1 -0
  27. package/lib/typescript/connectors/PhantomConnector.d.ts +8 -23
  28. package/lib/typescript/connectors/PhantomConnector.d.ts.map +1 -1
  29. package/lib/typescript/connectors/SolflareConnector.d.ts +12 -0
  30. package/lib/typescript/connectors/SolflareConnector.d.ts.map +1 -0
  31. package/lib/typescript/index.d.ts +2 -1
  32. package/lib/typescript/index.d.ts.map +1 -1
  33. package/lib/typescript/providers/{PhantomProvider.d.ts → DeeplinkProvider.d.ts} +16 -8
  34. package/lib/typescript/providers/DeeplinkProvider.d.ts.map +1 -0
  35. package/lib/typescript/types.d.ts +27 -32
  36. package/lib/typescript/types.d.ts.map +1 -1
  37. package/package.json +5 -4
  38. package/src/adapter.ts +3 -3
  39. package/src/connectors/DeeplinkConnector.ts +353 -0
  40. package/src/connectors/PhantomConnector.ts +17 -313
  41. package/src/connectors/SolflareConnector.ts +33 -0
  42. package/src/index.ts +2 -1
  43. package/src/providers/{PhantomProvider.ts → DeeplinkProvider.ts} +256 -233
  44. package/src/types.ts +29 -37
  45. package/lib/commonjs/providers/PhantomProvider.js.map +0 -1
  46. package/lib/module/providers/PhantomProvider.js.map +0 -1
  47. package/lib/typescript/providers/PhantomProvider.d.ts.map +0 -1
@@ -9,27 +9,24 @@ import type {
9
9
  Storage
10
10
  } from '@reown/appkit-common-react-native';
11
11
  import type {
12
- PhantomProviderConfig,
13
- PhantomConnectResult,
14
- PhantomSession,
12
+ DeeplinkProviderConfig,
13
+ DeeplinkConnectResult,
14
+ DeeplinkSession,
15
15
  DecryptedConnectData,
16
- PhantomDeeplinkResponse,
16
+ DeeplinkResponse,
17
17
  SignAllTransactionsRequestParams,
18
18
  SignMessageRequestParams,
19
19
  SignTransactionRequestParams,
20
- PhantomRpcMethod,
21
- PhantomConnectParams,
22
- PhantomDisconnectParams,
23
- PhantomSignTransactionParams,
24
- PhantomSignMessageParams,
25
- PhantomSignAllTransactionsParams,
26
- PhantomCluster
20
+ DeeplinkRpcMethod,
21
+ DeeplinkConnectParams,
22
+ DeeplinkDisconnectParams,
23
+ DeeplinkSignTransactionParams,
24
+ DeeplinkSignMessageParams,
25
+ DeeplinkSignAllTransactionsParams,
26
+ Cluster
27
27
  } from '../types';
28
28
  import EventEmitter from 'events';
29
29
 
30
- const PHANTOM_BASE_URL = 'https://phantom.app/ul/v1';
31
- const PHANTOM_PROVIDER_STORAGE_KEY = '@appkit/phantom-provider-session';
32
-
33
30
  export const SOLANA_SIGNING_METHODS = {
34
31
  SOLANA_SIGN_TRANSACTION: 'solana_signTransaction',
35
32
  SOLANA_SIGN_MESSAGE: 'solana_signMessage',
@@ -43,28 +40,34 @@ function isValidSolanaSigningMethod(method: string): method is SolanaSigningMeth
43
40
  return Object.values(SOLANA_SIGNING_METHODS).includes(method as SolanaSigningMethod);
44
41
  }
45
42
 
46
- export class PhantomProvider extends EventEmitter implements Provider {
47
- private readonly config: PhantomProviderConfig;
43
+ export class DeeplinkProvider extends EventEmitter implements Provider {
44
+ private readonly config: DeeplinkProviderConfig;
48
45
  private dappEncryptionKeyPair: nacl.BoxKeyPair;
49
- private currentCluster: PhantomCluster = 'mainnet-beta';
46
+ private currentCluster: Cluster;
47
+ private sharedKey: Uint8Array | null = null;
50
48
 
51
49
  private storage: Storage;
52
50
 
53
51
  private sessionToken: string | null = null;
54
52
  private userPublicKey: string | null = null;
55
- private phantomEncryptionPublicKeyBs58: string | null = null;
53
+ private walletEncryptionPublicKeyBs58: string | null = null;
56
54
 
57
55
  // Single subscription management - deep links are sequential by nature
58
56
  private activeSubscription: { remove: () => void } | null = null;
59
57
  private isOperationPending = false;
60
58
 
61
- constructor(config: PhantomProviderConfig) {
59
+ constructor(config: DeeplinkProviderConfig) {
62
60
  super();
63
61
  this.config = config;
62
+ this.currentCluster = config.cluster ?? 'mainnet-beta';
64
63
  this.dappEncryptionKeyPair = config.dappEncryptionKeyPair;
65
64
  this.storage = config.storage;
66
65
  }
67
66
 
67
+ private getSessionStorageKey(): string {
68
+ return `@appkit/${this.config.type}-provider-session`;
69
+ }
70
+
68
71
  /**
69
72
  * Cleanup method to be called when the provider is destroyed
70
73
  */
@@ -104,13 +107,70 @@ export class PhantomProvider extends EventEmitter implements Provider {
104
107
  return !!this.sessionToken && !!this.userPublicKey && !!this.dappEncryptionKeyPair;
105
108
  }
106
109
 
107
- private buildUrl(rpcMethod: PhantomRpcMethod, params: Record<string, string>): string {
110
+ public getCurrentCluster(): Cluster {
111
+ return this.currentCluster;
112
+ }
113
+
114
+ private buildUrl(rpcMethod: DeeplinkRpcMethod, params: Record<string, string>): string {
108
115
  const query = new URLSearchParams(params).toString();
109
116
 
110
- return `${PHANTOM_BASE_URL}/${rpcMethod}?${query}`;
117
+ return `${this.config.baseUrl}/${rpcMethod}?${query}`;
111
118
  }
112
119
 
113
- private getRpcMethodName(method: SolanaSigningMethod): PhantomRpcMethod {
120
+ /**
121
+ * Open a deeplink URL and wait for a redirect back to the app. Handles subscription
122
+ * lifecycle and common error extraction from `errorCode`/`errorMessage` query params.
123
+ */
124
+ private async openDeeplinkAndWait<T>(
125
+ deeplinkUrl: string,
126
+ processParams: (params: URLSearchParams) => Promise<T> | T,
127
+ contextLabel: string
128
+ ): Promise<T> {
129
+ return new Promise<T>((resolve, reject) => {
130
+ const handleDeepLink = async (event: { url: string }) => {
131
+ try {
132
+ this.cleanupActiveSubscription();
133
+ const fullUrl = event.url;
134
+ if (!fullUrl.startsWith(this.config.appScheme)) {
135
+ return reject(
136
+ new Error(`${this.config.type} provider: ${contextLabel}: Unexpected redirect URI.`)
137
+ );
138
+ }
139
+ const params = new URLSearchParams(fullUrl.substring(fullUrl.indexOf('?') + 1));
140
+ const errorCode = params.get('errorCode');
141
+ const errorMessage = params.get('errorMessage');
142
+ if (errorCode) {
143
+ return reject(
144
+ new Error(
145
+ `${this.config.type} provider: ${contextLabel} Failed: ${
146
+ errorMessage || 'Unknown error'
147
+ } (Code: ${errorCode})`
148
+ )
149
+ );
150
+ }
151
+ const result = await Promise.resolve(processParams(params));
152
+ resolve(result);
153
+ } catch (error) {
154
+ this.cleanupActiveSubscription();
155
+ reject(error);
156
+ }
157
+ };
158
+
159
+ const subscription = Linking.addEventListener('url', handleDeepLink);
160
+ this.setActiveSubscription(subscription);
161
+
162
+ Linking.openURL(deeplinkUrl).catch(err => {
163
+ this.cleanupActiveSubscription();
164
+ reject(
165
+ new Error(
166
+ `${this.config.type} provider: Failed to open wallet for ${contextLabel}: ${err.message}.`
167
+ )
168
+ );
169
+ });
170
+ });
171
+ }
172
+
173
+ private getRpcMethodName(method: SolanaSigningMethod): DeeplinkRpcMethod {
114
174
  switch (method) {
115
175
  case SOLANA_SIGNING_METHODS.SOLANA_SIGN_TRANSACTION:
116
176
  return 'signTransaction';
@@ -128,27 +188,34 @@ export class PhantomProvider extends EventEmitter implements Provider {
128
188
 
129
189
  private encryptPayload(
130
190
  payload: Record<string, unknown>,
131
- phantomPublicKeyBs58ToEncryptFor: string
191
+ walletPublicKeyBs58: string
132
192
  ): { nonce: string; encryptedPayload: string } | null {
133
- if (!phantomPublicKeyBs58ToEncryptFor) {
193
+ if (!walletPublicKeyBs58) {
134
194
  return null;
135
195
  }
136
196
  try {
137
- const phantomPublicKeyBytes = bs58.decode(phantomPublicKeyBs58ToEncryptFor);
138
197
  const nonce = nacl.randomBytes(nacl.box.nonceLength);
139
198
  const payloadBytes = Buffer.from(JSON.stringify(payload), 'utf8');
140
- const encryptedPayload = nacl.box(
141
- payloadBytes,
142
- nonce,
143
- phantomPublicKeyBytes,
144
- this.dappEncryptionKeyPair.secretKey
145
- );
199
+ let encryptedPayload: Uint8Array | null;
200
+ if (this.sharedKey) {
201
+ encryptedPayload = nacl.box.after(payloadBytes, nonce, this.sharedKey);
202
+ } else {
203
+ const walletPublicKeyBytes = bs58.decode(walletPublicKeyBs58);
204
+ encryptedPayload = nacl.box(
205
+ payloadBytes,
206
+ nonce,
207
+ walletPublicKeyBytes,
208
+ this.dappEncryptionKeyPair.secretKey
209
+ );
210
+ }
146
211
 
147
212
  return {
148
213
  nonce: bs58.encode(nonce),
149
214
  encryptedPayload: bs58.encode(encryptedPayload)
150
215
  };
151
216
  } catch (error) {
217
+ console.warn(`${this.config.type} provider: Failed to encrypt payload.`, error);
218
+
152
219
  return null;
153
220
  }
154
221
  }
@@ -156,40 +223,59 @@ export class PhantomProvider extends EventEmitter implements Provider {
156
223
  private decryptPayload<T>(
157
224
  encryptedDataBs58: string,
158
225
  nonceBs58: string,
159
- phantomSenderPublicKeyBs58: string
226
+ walletSenderPublicKeyBs58: string
160
227
  ): T | null {
161
228
  try {
162
- const encryptedDataBytes = bs58.decode(encryptedDataBs58);
229
+ const formattedEncryptedDataBs58 = encryptedDataBs58.replace('#', '');
230
+ const encryptedDataBytes = bs58.decode(formattedEncryptedDataBs58);
163
231
  const nonceBytes = bs58.decode(nonceBs58);
164
- const phantomSenderPublicKeyBytes = bs58.decode(phantomSenderPublicKeyBs58);
165
- const decryptedPayloadBytes = nacl.box.open(
166
- encryptedDataBytes,
167
- nonceBytes,
168
- phantomSenderPublicKeyBytes,
169
- this.dappEncryptionKeyPair.secretKey
170
- );
232
+ let decryptedPayloadBytes: Uint8Array | null;
233
+ if (this.sharedKey) {
234
+ decryptedPayloadBytes = nacl.box.open.after(encryptedDataBytes, nonceBytes, this.sharedKey);
235
+ } else {
236
+ const walletSenderPublicKeyBytes = bs58.decode(walletSenderPublicKeyBs58);
237
+ decryptedPayloadBytes = nacl.box.open(
238
+ encryptedDataBytes,
239
+ nonceBytes,
240
+ walletSenderPublicKeyBytes,
241
+ this.dappEncryptionKeyPair.secretKey
242
+ );
243
+ }
171
244
  if (!decryptedPayloadBytes) {
172
245
  return null;
173
246
  }
174
247
 
175
248
  return JSON.parse(Buffer.from(decryptedPayloadBytes).toString('utf8')) as T;
176
249
  } catch (error) {
250
+ console.warn(`${this.config.type} provider: Failed to decrypt payload.`, error);
251
+
177
252
  return null;
178
253
  }
179
254
  }
180
255
 
181
256
  public async restoreSession(): Promise<boolean> {
182
257
  try {
183
- const session = await this.storage.getItem<PhantomSession>(PHANTOM_PROVIDER_STORAGE_KEY);
258
+ const session = await this.storage.getItem<DeeplinkSession>(this.getSessionStorageKey());
184
259
  if (session) {
185
260
  this.setSession(session);
186
261
 
262
+ // Recompute shared key on session restore
263
+ try {
264
+ const walletPublicKeyBytes = bs58.decode(session.walletEncryptionPublicKeyBs58);
265
+ this.sharedKey = nacl.box.before(
266
+ walletPublicKeyBytes,
267
+ this.dappEncryptionKeyPair.secretKey
268
+ );
269
+ } catch (e) {
270
+ this.sharedKey = null;
271
+ }
272
+
187
273
  return true;
188
274
  }
189
275
 
190
276
  return false;
191
277
  } catch (error) {
192
- // console.error('PhantomProvider: Failed to restore session.', error);
278
+ // console.error(`${this.config.type} provider: Failed to restore session.`, error);
193
279
  await this.clearSessionStorage(); // Clear potentially corrupt data
194
280
 
195
281
  return false;
@@ -197,36 +283,34 @@ export class PhantomProvider extends EventEmitter implements Provider {
197
283
  }
198
284
 
199
285
  private async saveSession(): Promise<void> {
200
- if (!this.sessionToken || !this.userPublicKey || !this.phantomEncryptionPublicKeyBs58) {
286
+ if (!this.sessionToken || !this.userPublicKey || !this.walletEncryptionPublicKeyBs58) {
201
287
  return; // Cannot save incomplete session
202
288
  }
203
- const session: PhantomSession = {
289
+ const session: DeeplinkSession = {
204
290
  sessionToken: this.sessionToken,
205
291
  userPublicKey: this.userPublicKey,
206
- phantomEncryptionPublicKeyBs58: this.phantomEncryptionPublicKeyBs58,
292
+ walletEncryptionPublicKeyBs58: this.walletEncryptionPublicKeyBs58,
207
293
  cluster: this.currentCluster
208
294
  };
209
295
  try {
210
- await this.storage.setItem(PHANTOM_PROVIDER_STORAGE_KEY, session);
296
+ await this.storage.setItem(this.getSessionStorageKey(), session);
211
297
  } catch (error) {
212
- // console.error('PhantomProvider: Failed to save session.', error);
298
+ // console.error(`${this.config.type} provider: Failed to save session.`, error);
213
299
  }
214
300
  }
215
301
 
216
302
  private async clearSessionStorage(): Promise<void> {
217
303
  try {
218
- await this.storage.removeItem(PHANTOM_PROVIDER_STORAGE_KEY);
304
+ await this.storage.removeItem(this.getSessionStorageKey());
219
305
  } catch (error) {
220
- // console.error('PhantomProvider: Failed to clear session storage.', error);
306
+ // console.error(`${this.config.type} provider: Failed to clear session storage.`, error);
221
307
  }
222
308
  }
223
309
 
224
- public async connect<T = PhantomConnectResult>(params?: {
225
- cluster?: PhantomCluster;
226
- }): Promise<T> {
310
+ public async connect<T = DeeplinkConnectResult>(params?: { cluster?: Cluster }): Promise<T> {
227
311
  const cluster = params?.cluster ?? 'mainnet-beta';
228
312
  this.currentCluster = cluster;
229
- const connectDeeplinkParams: PhantomConnectParams = {
313
+ const connectDeeplinkParams: DeeplinkConnectParams = {
230
314
  app_url: this.config.dappUrl,
231
315
  dapp_encryption_public_key: bs58.encode(this.dappEncryptionKeyPair.publicKey),
232
316
  redirect_link: this.config.appScheme,
@@ -234,84 +318,63 @@ export class PhantomProvider extends EventEmitter implements Provider {
234
318
  };
235
319
  const url = this.buildUrl('connect', connectDeeplinkParams as any);
236
320
 
237
- return new Promise<PhantomConnectResult>((resolve, reject) => {
238
- const handleDeepLink = async (event: { url: string }) => {
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)
245
- );
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
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.'));
296
- }
297
- } catch (error) {
298
- this.cleanupActiveSubscription();
299
- reject(error);
321
+ return this.openDeeplinkAndWait<DeeplinkConnectResult>(
322
+ url,
323
+ (responseUrlParams: URLSearchParams) => {
324
+ const responsePayload: DeeplinkResponse = {
325
+ wallet_encryption_public_key: responseUrlParams.get(this.config.encryptionKeyFieldName)!,
326
+ nonce: responseUrlParams.get('nonce')!,
327
+ data: responseUrlParams.get('data')!
328
+ };
329
+ if (
330
+ !responsePayload.wallet_encryption_public_key ||
331
+ !responsePayload.nonce ||
332
+ !responsePayload.data
333
+ ) {
334
+ throw new Error(`${this.config.type} provider: Invalid response - missing parameters.`);
300
335
  }
301
- };
302
336
 
303
- const subscription = Linking.addEventListener('url', handleDeepLink);
304
- this.setActiveSubscription(subscription);
337
+ const decryptedData = this.decryptPayload<DecryptedConnectData>(
338
+ responsePayload.data,
339
+ responsePayload.nonce,
340
+ responsePayload.wallet_encryption_public_key
341
+ );
342
+ if (!decryptedData || !decryptedData.public_key || !decryptedData.session) {
343
+ throw new Error(
344
+ `${this.config.type} provider: Failed to decrypt or invalid decrypted data.`
345
+ );
346
+ }
347
+ this.userPublicKey = decryptedData.public_key;
348
+ this.sessionToken = decryptedData.session;
349
+ this.walletEncryptionPublicKeyBs58 = responsePayload.wallet_encryption_public_key;
305
350
 
306
- Linking.openURL(url).catch(err => {
307
- this.cleanupActiveSubscription();
308
- reject(new Error(`Failed to open Phantom wallet: ${err.message}. Is it installed?`));
309
- });
310
- }) as Promise<T>;
351
+ // Precompute shared key for subsequent communications
352
+ try {
353
+ const walletPublicKeyBytes = bs58.decode(this.walletEncryptionPublicKeyBs58);
354
+ this.sharedKey = nacl.box.before(
355
+ walletPublicKeyBytes,
356
+ this.dappEncryptionKeyPair.secretKey
357
+ );
358
+ } catch (e) {
359
+ this.sharedKey = null;
360
+ }
361
+
362
+ // Save session on successful connect
363
+ this.saveSession();
364
+
365
+ return {
366
+ userPublicKey: this.userPublicKey!,
367
+ sessionToken: this.sessionToken!,
368
+ walletEncryptionPublicKeyBs58: this.walletEncryptionPublicKeyBs58!,
369
+ cluster
370
+ } as DeeplinkConnectResult;
371
+ },
372
+ 'Connection'
373
+ ) as Promise<T>;
311
374
  }
312
375
 
313
376
  public async disconnect(): Promise<void> {
314
- if (!this.sessionToken || !this.phantomEncryptionPublicKeyBs58) {
377
+ if (!this.sessionToken || !this.walletEncryptionPublicKeyBs58) {
315
378
  await this.clearSession();
316
379
  this.emit('disconnect');
317
380
 
@@ -321,18 +384,18 @@ export class PhantomProvider extends EventEmitter implements Provider {
321
384
  const payloadToEncrypt = { session: this.sessionToken };
322
385
  const encryptedDisconnectPayload = this.encryptPayload(
323
386
  payloadToEncrypt,
324
- this.phantomEncryptionPublicKeyBs58
387
+ this.walletEncryptionPublicKeyBs58
325
388
  );
326
389
 
327
390
  if (!encryptedDisconnectPayload) {
328
- // console.warn('PhantomProvider: Failed to encrypt disconnect payload. Clearing session locally.');
391
+ // console.warn(`${this.config.type} provider: Failed to encrypt disconnect payload. Clearing session locally.`);
329
392
  await this.clearSession();
330
393
  this.emit('disconnect');
331
394
 
332
395
  return Promise.resolve(); // Or reject, depending on desired strictness
333
396
  }
334
397
 
335
- const disconnectDeeplinkParams: PhantomDisconnectParams = {
398
+ const disconnectDeeplinkParams: DeeplinkDisconnectParams = {
336
399
  dapp_encryption_public_key: bs58.encode(this.dappEncryptionKeyPair.publicKey),
337
400
  redirect_link: this.config.appScheme,
338
401
  payload: encryptedDisconnectPayload.encryptedPayload,
@@ -340,62 +403,46 @@ export class PhantomProvider extends EventEmitter implements Provider {
340
403
  };
341
404
  const url = this.buildUrl('disconnect', disconnectDeeplinkParams as any);
342
405
 
343
- return new Promise<void>((resolve, reject) => {
344
- const handleDeepLink = (event: { url: string }) => {
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();
356
- this.clearSession();
357
- reject(error);
358
- }
359
- };
360
-
361
- const subscription = Linking.addEventListener('url', handleDeepLink);
362
- this.setActiveSubscription(subscription);
363
-
364
- Linking.openURL(url).catch(err => {
365
- this.cleanupActiveSubscription();
406
+ return this.openDeeplinkAndWait<void>(
407
+ url,
408
+ () => {
366
409
  this.clearSession();
367
- reject(new Error(`Failed to open Phantom for disconnection: ${err.message}.`));
368
- });
369
- });
410
+ },
411
+ 'Disconnection'
412
+ );
370
413
  }
371
414
 
372
415
  public async clearSession(): Promise<void> {
373
416
  this.sessionToken = null;
374
417
  this.userPublicKey = null;
375
- this.phantomEncryptionPublicKeyBs58 = null;
418
+ this.walletEncryptionPublicKeyBs58 = null;
419
+ if (this.sharedKey) {
420
+ this.sharedKey.fill(0);
421
+ }
422
+ this.sharedKey = null;
376
423
  this.cleanupActiveSubscription();
377
424
  await this.clearSessionStorage();
378
425
  }
379
426
 
380
- public setSession(session: PhantomSession): void {
427
+ public setSession(session: DeeplinkSession): void {
381
428
  this.sessionToken = session.sessionToken;
382
429
  this.userPublicKey = session.userPublicKey;
383
- this.phantomEncryptionPublicKeyBs58 = session.phantomEncryptionPublicKeyBs58;
430
+ this.walletEncryptionPublicKeyBs58 = session.walletEncryptionPublicKeyBs58;
384
431
  this.currentCluster = session.cluster;
385
432
  }
386
433
 
387
434
  public async request<T>(args: RequestArguments, _chainId?: CaipNetworkId): Promise<T> {
388
435
  if (!isValidSolanaSigningMethod(args.method)) {
389
436
  throw new Error(
390
- `PhantomProvider: Unsupported method: ${args.method}. Only Solana signing methods are supported.`
437
+ `${this.config.type} provider: Unsupported method: ${args.method}. Only Solana signing methods are supported.`
391
438
  );
392
439
  }
393
440
  const signingMethod = args.method as SolanaSigningMethod;
394
441
  const requestParams = args.params as any;
395
442
 
396
- if (!this.isConnected() || !this.sessionToken || !this.phantomEncryptionPublicKeyBs58) {
443
+ if (!this.isConnected() || !this.sessionToken || !this.walletEncryptionPublicKeyBs58) {
397
444
  throw new Error(
398
- 'PhantomProvider: Not connected or session details missing. Cannot process request.'
445
+ `${this.config.type} provider: Not connected or session details missing. Cannot process request.`
399
446
  );
400
447
  }
401
448
 
@@ -413,18 +460,20 @@ export class PhantomProvider extends EventEmitter implements Provider {
413
460
  }
414
461
 
415
462
  const dataToEncrypt = {
416
- session: this.sessionToken!,
463
+ session: this.sessionToken,
417
464
  transaction: typedParams.transaction
418
465
  };
419
466
  const encryptedData = this.encryptPayload(
420
467
  dataToEncrypt,
421
- this.phantomEncryptionPublicKeyBs58!
468
+ this.walletEncryptionPublicKeyBs58
422
469
  );
423
470
  if (!encryptedData) {
424
- throw new Error(`PhantomProvider: Failed to encrypt payload for ${signingMethod}.`);
471
+ throw new Error(
472
+ `${this.config.type} provider: Failed to encrypt payload for ${signingMethod}.`
473
+ );
425
474
  }
426
475
 
427
- const signTxDeeplinkParams: PhantomSignTransactionParams = {
476
+ const signTxDeeplinkParams: DeeplinkSignTransactionParams = {
428
477
  dapp_encryption_public_key: bs58.encode(this.dappEncryptionKeyPair.publicKey),
429
478
  redirect_link: this.config.appScheme,
430
479
  cluster: this.currentCluster,
@@ -437,7 +486,9 @@ export class PhantomProvider extends EventEmitter implements Provider {
437
486
  case SOLANA_SIGNING_METHODS.SOLANA_SIGN_MESSAGE: {
438
487
  const typedParams = requestParams as SignMessageRequestParams;
439
488
  if (!typedParams || typeof typedParams.message === 'undefined') {
440
- throw new Error(`Missing 'message' in params for ${signingMethod}`);
489
+ throw new Error(
490
+ `${this.config.type} provider: Missing 'message' in params for ${signingMethod}`
491
+ );
441
492
  }
442
493
 
443
494
  let messageBs58: string;
@@ -451,25 +502,29 @@ export class PhantomProvider extends EventEmitter implements Provider {
451
502
  messageBs58 = bs58.encode(Buffer.from(typedParams.message));
452
503
  }
453
504
  } else {
454
- throw new Error('Invalid message format for signMessage. Expected Uint8Array or string.');
505
+ throw new Error(
506
+ `${this.config.type} provider: Invalid message format for signMessage. Expected Uint8Array or string.`
507
+ );
455
508
  }
456
509
 
457
510
  const dataToEncrypt = {
458
511
  message: messageBs58,
459
- session: this.sessionToken!,
512
+ session: this.sessionToken,
460
513
  display: typedParams.display || 'utf8'
461
514
  };
462
515
 
463
516
  const encryptedPayloadData = this.encryptPayload(
464
517
  dataToEncrypt,
465
- this.phantomEncryptionPublicKeyBs58!
518
+ this.walletEncryptionPublicKeyBs58
466
519
  );
467
520
 
468
521
  if (!encryptedPayloadData) {
469
- throw new Error('PhantomProvider: Failed to encrypt payload for signMessage.');
522
+ throw new Error(
523
+ `${this.config.type} provider: Failed to encrypt payload for signMessage.`
524
+ );
470
525
  }
471
526
 
472
- const signMsgDeeplinkQueryPayload: PhantomSignMessageParams = {
527
+ const signMsgDeeplinkQueryPayload: DeeplinkSignMessageParams = {
473
528
  dapp_encryption_public_key: bs58.encode(this.dappEncryptionKeyPair.publicKey),
474
529
  redirect_link: this.config.appScheme,
475
530
  payload: encryptedPayloadData.encryptedPayload,
@@ -486,23 +541,25 @@ export class PhantomProvider extends EventEmitter implements Provider {
486
541
  !typedParams.transactions.every((t: any) => typeof t === 'string')
487
542
  ) {
488
543
  throw new Error(
489
- `Missing or invalid 'transactions' (array of base58 strings) in params for ${signingMethod}`
544
+ `${this.config.type} provider: Missing or invalid 'transactions' (array of base58 strings) in params for ${signingMethod}`
490
545
  );
491
546
  }
492
547
 
493
548
  const dataToEncrypt = {
494
- session: this.sessionToken!,
549
+ session: this.sessionToken,
495
550
  transactions: typedParams.transactions
496
551
  };
497
552
  const encryptedData = this.encryptPayload(
498
553
  dataToEncrypt,
499
- this.phantomEncryptionPublicKeyBs58!
554
+ this.walletEncryptionPublicKeyBs58
500
555
  );
501
556
  if (!encryptedData) {
502
- throw new Error(`PhantomProvider: Failed to encrypt payload for ${signingMethod}.`);
557
+ throw new Error(
558
+ `${this.config.type} provider: Failed to encrypt payload for ${signingMethod}.`
559
+ );
503
560
  }
504
561
 
505
- const signAllTxDeeplinkParams: PhantomSignAllTransactionsParams = {
562
+ const signAllTxDeeplinkParams: DeeplinkSignAllTransactionsParams = {
506
563
  dapp_encryption_public_key: bs58.encode(this.dappEncryptionKeyPair.publicKey),
507
564
  redirect_link: this.config.appScheme,
508
565
  cluster: this.currentCluster,
@@ -513,69 +570,35 @@ export class PhantomProvider extends EventEmitter implements Provider {
513
570
  break;
514
571
  }
515
572
  default: {
516
- throw new Error(`PhantomProvider: Unhandled signing method: ${signingMethod}`);
573
+ throw new Error(`${this.config.type} provider: Unhandled signing method: ${signingMethod}`);
517
574
  }
518
575
  }
519
576
 
520
- return new Promise<T>((resolve, reject) => {
521
- const handleDeepLink = async (event: { url: string }) => {
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)
528
- );
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!
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.`));
562
- }
563
- } catch (error) {
564
- this.cleanupActiveSubscription();
565
- reject(error);
577
+ return this.openDeeplinkAndWait<T>(
578
+ deeplinkUrl,
579
+ (responseUrlParams: URLSearchParams) => {
580
+ const responseNonce = responseUrlParams.get('nonce');
581
+ const responseData = responseUrlParams.get('data');
582
+ if (!responseNonce || !responseData) {
583
+ throw new Error(
584
+ `${this.config.type} provider: ${signingMethod}: Invalid response - missing nonce or data.`
585
+ );
566
586
  }
567
- };
568
-
569
- const subscription = Linking.addEventListener('url', handleDeepLink);
570
- this.setActiveSubscription(subscription);
571
-
572
- Linking.openURL(deeplinkUrl).catch(err => {
573
- this.cleanupActiveSubscription();
574
- reject(
575
- new Error(`Failed to open Phantom for ${signingMethod}: ${err.message}. Is it installed?`)
587
+ const decryptedResult = this.decryptPayload<any>(
588
+ responseData,
589
+ responseNonce,
590
+ this.walletEncryptionPublicKeyBs58!
576
591
  );
577
- });
578
- });
592
+ if (!decryptedResult) {
593
+ throw new Error(
594
+ `${this.config.type} provider: ${signingMethod}: Failed to decrypt response or invalid decrypted data.`
595
+ );
596
+ }
597
+
598
+ return decryptedResult as T;
599
+ },
600
+ signingMethod
601
+ );
579
602
  }
580
603
  }
581
604