@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.
- package/lib/commonjs/adapter.js +121 -3
- package/lib/commonjs/adapter.js.map +1 -1
- package/lib/commonjs/connectors/PhantomConnector.js +28 -24
- package/lib/commonjs/connectors/PhantomConnector.js.map +1 -1
- package/lib/commonjs/helpers.js +0 -1
- package/lib/commonjs/helpers.js.map +1 -1
- package/lib/commonjs/index.js +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/providers/PhantomProvider.js +124 -84
- package/lib/commonjs/providers/PhantomProvider.js.map +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/utils/createSendTransaction.js +44 -0
- package/lib/commonjs/utils/createSendTransaction.js.map +1 -0
- package/lib/module/adapter.js +122 -3
- package/lib/module/adapter.js.map +1 -1
- package/lib/module/connectors/PhantomConnector.js +31 -25
- package/lib/module/connectors/PhantomConnector.js.map +1 -1
- package/lib/module/helpers.js +2 -1
- package/lib/module/helpers.js.map +1 -1
- package/lib/module/index.js +6 -4
- package/lib/module/index.js.map +1 -1
- package/lib/module/providers/PhantomProvider.js +126 -84
- package/lib/module/providers/PhantomProvider.js.map +1 -1
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/utils/createSendTransaction.js +41 -0
- package/lib/module/utils/createSendTransaction.js.map +1 -0
- package/lib/typescript/adapter.d.ts +11 -3
- package/lib/typescript/adapter.d.ts.map +1 -1
- package/lib/typescript/connectors/PhantomConnector.d.ts +2 -1
- package/lib/typescript/connectors/PhantomConnector.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -2
- package/lib/typescript/index.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/lib/typescript/utils/createSendTransaction.d.ts +10 -0
- package/lib/typescript/utils/createSendTransaction.d.ts.map +1 -0
- package/package.json +9 -3
- package/src/adapter.ts +152 -3
- package/src/connectors/PhantomConnector.ts +39 -38
- package/src/index.ts +4 -4
- package/src/providers/PhantomProvider.ts +165 -113
- 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,
|
|
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
|
-
|
|
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>;
|
|
@@ -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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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(
|
|
357
|
+
reject(error);
|
|
311
358
|
}
|
|
312
359
|
};
|
|
313
|
-
|
|
360
|
+
|
|
361
|
+
const subscription = Linking.addEventListener('url', handleDeepLink);
|
|
362
|
+
this.setActiveSubscription(subscription);
|
|
363
|
+
|
|
314
364
|
Linking.openURL(url).catch(err => {
|
|
315
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
reject(
|
|
563
|
+
} catch (error) {
|
|
564
|
+
this.cleanupActiveSubscription();
|
|
565
|
+
reject(error);
|
|
515
566
|
}
|
|
516
567
|
};
|
|
517
|
-
|
|
568
|
+
|
|
569
|
+
const subscription = Linking.addEventListener('url', handleDeepLink);
|
|
570
|
+
this.setActiveSubscription(subscription);
|
|
571
|
+
|
|
518
572
|
Linking.openURL(deeplinkUrl).catch(err => {
|
|
519
|
-
|
|
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
|
+
}
|