@reown/appkit-solana-react-native 2.0.0-alpha.3 → 2.0.0-alpha.4
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 +3 -3
- package/lib/commonjs/adapter.js.map +1 -1
- package/lib/commonjs/connectors/DeeplinkConnector.js +271 -0
- package/lib/commonjs/connectors/DeeplinkConnector.js.map +1 -0
- package/lib/commonjs/connectors/PhantomConnector.js +15 -230
- package/lib/commonjs/connectors/PhantomConnector.js.map +1 -1
- package/lib/commonjs/connectors/SolflareConnector.js +36 -0
- package/lib/commonjs/connectors/SolflareConnector.js.map +1 -0
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/providers/{PhantomProvider.js → DeeplinkProvider.js} +155 -154
- package/lib/commonjs/providers/DeeplinkProvider.js.map +1 -0
- package/lib/module/adapter.js +3 -3
- package/lib/module/adapter.js.map +1 -1
- package/lib/module/connectors/DeeplinkConnector.js +265 -0
- package/lib/module/connectors/DeeplinkConnector.js.map +1 -0
- package/lib/module/connectors/PhantomConnector.js +16 -230
- package/lib/module/connectors/PhantomConnector.js.map +1 -1
- package/lib/module/connectors/SolflareConnector.js +31 -0
- package/lib/module/connectors/SolflareConnector.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/providers/{PhantomProvider.js → DeeplinkProvider.js} +153 -152
- package/lib/module/providers/DeeplinkProvider.js.map +1 -0
- package/lib/typescript/connectors/DeeplinkConnector.d.ts +30 -0
- package/lib/typescript/connectors/DeeplinkConnector.d.ts.map +1 -0
- package/lib/typescript/connectors/PhantomConnector.d.ts +8 -23
- package/lib/typescript/connectors/PhantomConnector.d.ts.map +1 -1
- package/lib/typescript/connectors/SolflareConnector.d.ts +12 -0
- package/lib/typescript/connectors/SolflareConnector.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/providers/{PhantomProvider.d.ts → DeeplinkProvider.d.ts} +16 -8
- package/lib/typescript/providers/DeeplinkProvider.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +27 -32
- package/lib/typescript/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/adapter.ts +3 -3
- package/src/connectors/DeeplinkConnector.ts +353 -0
- package/src/connectors/PhantomConnector.ts +17 -313
- package/src/connectors/SolflareConnector.ts +33 -0
- package/src/index.ts +2 -1
- package/src/providers/{PhantomProvider.ts → DeeplinkProvider.ts} +256 -233
- package/src/types.ts +29 -37
- package/lib/commonjs/providers/PhantomProvider.js.map +0 -1
- package/lib/module/providers/PhantomProvider.js.map +0 -1
- 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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
DeeplinkProviderConfig,
|
|
13
|
+
DeeplinkConnectResult,
|
|
14
|
+
DeeplinkSession,
|
|
15
15
|
DecryptedConnectData,
|
|
16
|
-
|
|
16
|
+
DeeplinkResponse,
|
|
17
17
|
SignAllTransactionsRequestParams,
|
|
18
18
|
SignMessageRequestParams,
|
|
19
19
|
SignTransactionRequestParams,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
47
|
-
private readonly config:
|
|
43
|
+
export class DeeplinkProvider extends EventEmitter implements Provider {
|
|
44
|
+
private readonly config: DeeplinkProviderConfig;
|
|
48
45
|
private dappEncryptionKeyPair: nacl.BoxKeyPair;
|
|
49
|
-
private currentCluster:
|
|
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
|
|
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:
|
|
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
|
-
|
|
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 `${
|
|
117
|
+
return `${this.config.baseUrl}/${rpcMethod}?${query}`;
|
|
111
118
|
}
|
|
112
119
|
|
|
113
|
-
|
|
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
|
-
|
|
191
|
+
walletPublicKeyBs58: string
|
|
132
192
|
): { nonce: string; encryptedPayload: string } | null {
|
|
133
|
-
if (!
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
nonce,
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
226
|
+
walletSenderPublicKeyBs58: string
|
|
160
227
|
): T | null {
|
|
161
228
|
try {
|
|
162
|
-
const
|
|
229
|
+
const formattedEncryptedDataBs58 = encryptedDataBs58.replace('#', '');
|
|
230
|
+
const encryptedDataBytes = bs58.decode(formattedEncryptedDataBs58);
|
|
163
231
|
const nonceBytes = bs58.decode(nonceBs58);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
encryptedDataBytes,
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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<
|
|
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(
|
|
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.
|
|
286
|
+
if (!this.sessionToken || !this.userPublicKey || !this.walletEncryptionPublicKeyBs58) {
|
|
201
287
|
return; // Cannot save incomplete session
|
|
202
288
|
}
|
|
203
|
-
const session:
|
|
289
|
+
const session: DeeplinkSession = {
|
|
204
290
|
sessionToken: this.sessionToken,
|
|
205
291
|
userPublicKey: this.userPublicKey,
|
|
206
|
-
|
|
292
|
+
walletEncryptionPublicKeyBs58: this.walletEncryptionPublicKeyBs58,
|
|
207
293
|
cluster: this.currentCluster
|
|
208
294
|
};
|
|
209
295
|
try {
|
|
210
|
-
await this.storage.setItem(
|
|
296
|
+
await this.storage.setItem(this.getSessionStorageKey(), session);
|
|
211
297
|
} catch (error) {
|
|
212
|
-
// console.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(
|
|
304
|
+
await this.storage.removeItem(this.getSessionStorageKey());
|
|
219
305
|
} catch (error) {
|
|
220
|
-
// console.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 =
|
|
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:
|
|
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
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
304
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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.
|
|
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.
|
|
387
|
+
this.walletEncryptionPublicKeyBs58
|
|
325
388
|
);
|
|
326
389
|
|
|
327
390
|
if (!encryptedDisconnectPayload) {
|
|
328
|
-
// console.warn(
|
|
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:
|
|
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
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
427
|
+
public setSession(session: DeeplinkSession): void {
|
|
381
428
|
this.sessionToken = session.sessionToken;
|
|
382
429
|
this.userPublicKey = session.userPublicKey;
|
|
383
|
-
this.
|
|
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
|
-
|
|
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.
|
|
443
|
+
if (!this.isConnected() || !this.sessionToken || !this.walletEncryptionPublicKeyBs58) {
|
|
397
444
|
throw new Error(
|
|
398
|
-
|
|
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.
|
|
468
|
+
this.walletEncryptionPublicKeyBs58
|
|
422
469
|
);
|
|
423
470
|
if (!encryptedData) {
|
|
424
|
-
throw new Error(
|
|
471
|
+
throw new Error(
|
|
472
|
+
`${this.config.type} provider: Failed to encrypt payload for ${signingMethod}.`
|
|
473
|
+
);
|
|
425
474
|
}
|
|
426
475
|
|
|
427
|
-
const signTxDeeplinkParams:
|
|
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(
|
|
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(
|
|
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.
|
|
518
|
+
this.walletEncryptionPublicKeyBs58
|
|
466
519
|
);
|
|
467
520
|
|
|
468
521
|
if (!encryptedPayloadData) {
|
|
469
|
-
throw new Error(
|
|
522
|
+
throw new Error(
|
|
523
|
+
`${this.config.type} provider: Failed to encrypt payload for signMessage.`
|
|
524
|
+
);
|
|
470
525
|
}
|
|
471
526
|
|
|
472
|
-
const signMsgDeeplinkQueryPayload:
|
|
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
|
-
|
|
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.
|
|
554
|
+
this.walletEncryptionPublicKeyBs58
|
|
500
555
|
);
|
|
501
556
|
if (!encryptedData) {
|
|
502
|
-
throw new Error(
|
|
557
|
+
throw new Error(
|
|
558
|
+
`${this.config.type} provider: Failed to encrypt payload for ${signingMethod}.`
|
|
559
|
+
);
|
|
503
560
|
}
|
|
504
561
|
|
|
505
|
-
const signAllTxDeeplinkParams:
|
|
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(
|
|
573
|
+
throw new Error(`${this.config.type} provider: Unhandled signing method: ${signingMethod}`);
|
|
517
574
|
}
|
|
518
575
|
}
|
|
519
576
|
|
|
520
|
-
return
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
570
|
-
|
|
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
|
|