@reown/appkit-solana-react-native 2.0.0-alpha.2 → 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.
- package/lib/commonjs/adapter.js +1 -2
- package/lib/commonjs/adapter.js.map +1 -1
- package/lib/commonjs/connectors/PhantomConnector.js +20 -13
- package/lib/commonjs/connectors/PhantomConnector.js.map +1 -1
- package/lib/commonjs/providers/PhantomProvider.js +121 -81
- package/lib/commonjs/providers/PhantomProvider.js.map +1 -1
- package/lib/module/adapter.js +1 -2
- package/lib/module/adapter.js.map +1 -1
- package/lib/module/connectors/PhantomConnector.js +20 -13
- package/lib/module/connectors/PhantomConnector.js.map +1 -1
- package/lib/module/providers/PhantomProvider.js +121 -81
- package/lib/module/providers/PhantomProvider.js.map +1 -1
- package/lib/typescript/adapter.d.ts +1 -3
- package/lib/typescript/adapter.d.ts.map +1 -1
- package/lib/typescript/connectors/PhantomConnector.d.ts.map +1 -1
- package/lib/typescript/providers/PhantomProvider.d.ts +14 -0
- package/lib/typescript/providers/PhantomProvider.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/adapter.ts +1 -2
- package/src/connectors/PhantomConnector.ts +26 -26
- package/src/providers/PhantomProvider.ts +162 -112
|
@@ -26,10 +26,10 @@ import type {
|
|
|
26
26
|
PhantomProviderConfig
|
|
27
27
|
} from '../types';
|
|
28
28
|
|
|
29
|
-
const
|
|
30
|
-
'mainnet-beta': solana.
|
|
31
|
-
'testnet': solanaTestnet.
|
|
32
|
-
'devnet': solanaDevnet.
|
|
29
|
+
const SOLANA_CLUSTER_TO_CHAIN_ID: Record<PhantomCluster, CaipNetworkId> = {
|
|
30
|
+
'mainnet-beta': solana.caipNetworkId,
|
|
31
|
+
'testnet': solanaTestnet.caipNetworkId,
|
|
32
|
+
'devnet': solanaDevnet.caipNetworkId
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
const PHANTOM_CONNECTOR_STORAGE_KEY = '@appkit/phantom-connector-data';
|
|
@@ -37,7 +37,6 @@ const DAPP_KEYPAIR_STORAGE_KEY = '@appkit/phantom-dapp-secret-key';
|
|
|
37
37
|
|
|
38
38
|
export class PhantomConnector extends WalletConnector {
|
|
39
39
|
private readonly config: PhantomConnectorConfig;
|
|
40
|
-
|
|
41
40
|
private currentCaipNetworkId: CaipNetworkId | null = null;
|
|
42
41
|
private dappEncryptionKeyPair?: nacl.BoxKeyPair;
|
|
43
42
|
|
|
@@ -97,31 +96,31 @@ export class PhantomConnector extends WalletConnector {
|
|
|
97
96
|
return this.namespaces;
|
|
98
97
|
}
|
|
99
98
|
|
|
100
|
-
const defaultChain =
|
|
99
|
+
const defaultChain: CaipNetworkId | undefined =
|
|
101
100
|
opts?.defaultChain?.split(':')?.[0] === 'solana'
|
|
102
|
-
? opts?.defaultChain
|
|
103
|
-
: opts?.namespaces?.['solana']?.chains?.[0]
|
|
101
|
+
? opts?.defaultChain
|
|
102
|
+
: opts?.namespaces?.['solana']?.chains?.[0];
|
|
104
103
|
|
|
105
104
|
const requestedCluster =
|
|
106
105
|
this.config.cluster ??
|
|
107
|
-
(Object.keys(
|
|
108
|
-
key =>
|
|
109
|
-
SOLANA_CLUSTER_TO_CHAIN_ID_PART[key as keyof typeof SOLANA_CLUSTER_TO_CHAIN_ID_PART] ===
|
|
110
|
-
defaultChain
|
|
106
|
+
(Object.keys(SOLANA_CLUSTER_TO_CHAIN_ID).find(
|
|
107
|
+
key => SOLANA_CLUSTER_TO_CHAIN_ID[key as PhantomCluster] === defaultChain
|
|
111
108
|
) as PhantomCluster | undefined);
|
|
112
109
|
|
|
113
110
|
try {
|
|
114
111
|
const connectResult = await this.getProvider().connect({ cluster: requestedCluster });
|
|
115
112
|
|
|
116
|
-
const
|
|
117
|
-
if (!
|
|
113
|
+
const solanaChainId = SOLANA_CLUSTER_TO_CHAIN_ID[connectResult.cluster];
|
|
114
|
+
if (!solanaChainId) {
|
|
118
115
|
throw new Error(
|
|
119
116
|
`Phantom Connect: Internal - Unknown cluster mapping for ${connectResult.cluster}`
|
|
120
117
|
);
|
|
121
118
|
}
|
|
122
|
-
this.currentCaipNetworkId =
|
|
119
|
+
this.currentCaipNetworkId = solanaChainId;
|
|
123
120
|
|
|
124
|
-
this.wallet =
|
|
121
|
+
this.wallet = {
|
|
122
|
+
name: ConstantsUtil.PHANTOM_CUSTOM_WALLET.name
|
|
123
|
+
};
|
|
125
124
|
|
|
126
125
|
const userPublicKey = this.getProvider().getUserPublicKey();
|
|
127
126
|
if (!userPublicKey) {
|
|
@@ -155,6 +154,12 @@ export class PhantomConnector extends WalletConnector {
|
|
|
155
154
|
} catch (error: any) {
|
|
156
155
|
// console.warn(`PhantomConnector: Error during provider disconnect: ${error.message}. Proceeding with local clear.`);
|
|
157
156
|
}
|
|
157
|
+
|
|
158
|
+
// Cleanup provider resources
|
|
159
|
+
if (this.provider) {
|
|
160
|
+
(this.provider as PhantomProvider).destroy();
|
|
161
|
+
}
|
|
162
|
+
|
|
158
163
|
await this.clearSession();
|
|
159
164
|
}
|
|
160
165
|
|
|
@@ -215,21 +220,16 @@ export class PhantomConnector extends WalletConnector {
|
|
|
215
220
|
}
|
|
216
221
|
|
|
217
222
|
override async switchNetwork(network: AppKitNetwork): Promise<void> {
|
|
218
|
-
const targetClusterName = Object.keys(
|
|
219
|
-
key =>
|
|
220
|
-
SOLANA_CLUSTER_TO_CHAIN_ID_PART[key as keyof typeof SOLANA_CLUSTER_TO_CHAIN_ID_PART] ===
|
|
221
|
-
network.id
|
|
223
|
+
const targetClusterName = Object.keys(SOLANA_CLUSTER_TO_CHAIN_ID).find(
|
|
224
|
+
key => SOLANA_CLUSTER_TO_CHAIN_ID[key as PhantomCluster] === network.caipNetworkId
|
|
222
225
|
) as PhantomCluster | undefined;
|
|
223
226
|
|
|
224
227
|
if (!targetClusterName) {
|
|
225
228
|
throw new Error(`Cannot switch to unsupported network ID: ${network.id}`);
|
|
226
229
|
}
|
|
227
230
|
|
|
228
|
-
const currentClusterName = Object.keys(
|
|
229
|
-
key =>
|
|
230
|
-
`solana:${
|
|
231
|
-
SOLANA_CLUSTER_TO_CHAIN_ID_PART[key as keyof typeof SOLANA_CLUSTER_TO_CHAIN_ID_PART]
|
|
232
|
-
}` === this.currentCaipNetworkId
|
|
231
|
+
const currentClusterName = Object.keys(SOLANA_CLUSTER_TO_CHAIN_ID).find(
|
|
232
|
+
key => SOLANA_CLUSTER_TO_CHAIN_ID[key as PhantomCluster] === this.currentCaipNetworkId
|
|
233
233
|
) as PhantomCluster | undefined;
|
|
234
234
|
|
|
235
235
|
if (targetClusterName === currentClusterName && this.isConnected()) {
|
|
@@ -241,7 +241,7 @@ export class PhantomConnector extends WalletConnector {
|
|
|
241
241
|
|
|
242
242
|
// Create a temporary options object to guide the new connection
|
|
243
243
|
const tempConnectOpts: ConnectOptions = {
|
|
244
|
-
defaultChain:
|
|
244
|
+
defaultChain: SOLANA_CLUSTER_TO_CHAIN_ID[targetClusterName]
|
|
245
245
|
};
|
|
246
246
|
|
|
247
247
|
// Attempt to connect to the new cluster
|
|
@@ -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
|
}
|
|
@@ -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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
245
|
-
this.
|
|
246
|
-
|
|
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
|
-
|
|
302
|
+
|
|
303
|
+
const subscription = Linking.addEventListener('url', handleDeepLink);
|
|
304
|
+
this.setActiveSubscription(subscription);
|
|
305
|
+
|
|
262
306
|
Linking.openURL(url).catch(err => {
|
|
263
|
-
|
|
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>;
|
|
@@ -299,24 +341,28 @@ export class PhantomProvider extends EventEmitter implements Provider {
|
|
|
299
341
|
const url = this.buildUrl('disconnect', disconnectDeeplinkParams as any);
|
|
300
342
|
|
|
301
343
|
return new Promise<void>((resolve, reject) => {
|
|
302
|
-
let subscription: { remove: () => void } | null = null;
|
|
303
344
|
const handleDeepLink = (event: { url: string }) => {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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();
|
|
311
356
|
this.clearSession();
|
|
312
|
-
reject(
|
|
357
|
+
reject(error);
|
|
313
358
|
}
|
|
314
359
|
};
|
|
315
|
-
|
|
360
|
+
|
|
361
|
+
const subscription = Linking.addEventListener('url', handleDeepLink);
|
|
362
|
+
this.setActiveSubscription(subscription);
|
|
363
|
+
|
|
316
364
|
Linking.openURL(url).catch(err => {
|
|
317
|
-
|
|
318
|
-
subscription.remove();
|
|
319
|
-
}
|
|
365
|
+
this.cleanupActiveSubscription();
|
|
320
366
|
this.clearSession();
|
|
321
367
|
reject(new Error(`Failed to open Phantom for disconnection: ${err.message}.`));
|
|
322
368
|
});
|
|
@@ -327,6 +373,7 @@ export class PhantomProvider extends EventEmitter implements Provider {
|
|
|
327
373
|
this.sessionToken = null;
|
|
328
374
|
this.userPublicKey = null;
|
|
329
375
|
this.phantomEncryptionPublicKeyBs58 = null;
|
|
376
|
+
this.cleanupActiveSubscription();
|
|
330
377
|
await this.clearSessionStorage();
|
|
331
378
|
}
|
|
332
379
|
|
|
@@ -471,56 +518,59 @@ export class PhantomProvider extends EventEmitter implements Provider {
|
|
|
471
518
|
}
|
|
472
519
|
|
|
473
520
|
return new Promise<T>((resolve, reject) => {
|
|
474
|
-
let subscription: { remove: () => void } | null = null;
|
|
475
521
|
const handleDeepLink = async (event: { url: string }) => {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
fullUrl.substring(fullUrl.indexOf('?') + 1)
|
|
483
|
-
);
|
|
484
|
-
const errorCode = responseUrlParams.get('errorCode');
|
|
485
|
-
const errorMessage = responseUrlParams.get('errorMessage');
|
|
486
|
-
if (errorCode) {
|
|
487
|
-
return reject(
|
|
488
|
-
new Error(
|
|
489
|
-
`Phantom ${signingMethod} Failed: ${
|
|
490
|
-
errorMessage || 'Unknown error'
|
|
491
|
-
} (Code: ${errorCode})`
|
|
492
|
-
)
|
|
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)
|
|
493
528
|
);
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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!
|
|
512
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.`));
|
|
513
562
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
reject(
|
|
563
|
+
} catch (error) {
|
|
564
|
+
this.cleanupActiveSubscription();
|
|
565
|
+
reject(error);
|
|
517
566
|
}
|
|
518
567
|
};
|
|
519
|
-
|
|
568
|
+
|
|
569
|
+
const subscription = Linking.addEventListener('url', handleDeepLink);
|
|
570
|
+
this.setActiveSubscription(subscription);
|
|
571
|
+
|
|
520
572
|
Linking.openURL(deeplinkUrl).catch(err => {
|
|
521
|
-
|
|
522
|
-
subscription.remove();
|
|
523
|
-
}
|
|
573
|
+
this.cleanupActiveSubscription();
|
|
524
574
|
reject(
|
|
525
575
|
new Error(`Failed to open Phantom for ${signingMethod}: ${err.message}. Is it installed?`)
|
|
526
576
|
);
|