@phantom/react-native-sdk 0.1.5 → 0.1.7
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/README.md +32 -13
- package/dist/index.d.ts +8 -5
- package/dist/index.js +121 -64
- package/dist/index.mjs +121 -64
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -99,7 +99,6 @@ export default function App() {
|
|
|
99
99
|
redirectUrl: "mywalletapp://phantom-auth-callback",
|
|
100
100
|
},
|
|
101
101
|
appName: "My Wallet App", // Optional branding
|
|
102
|
-
debug: false, // Optional debug logging
|
|
103
102
|
}}
|
|
104
103
|
>
|
|
105
104
|
<YourAppContent />
|
|
@@ -193,7 +192,7 @@ interface PhantomSDKConfig {
|
|
|
193
192
|
organizationId: string; // Your Phantom organization ID
|
|
194
193
|
scheme: string; // Custom URL scheme for your app
|
|
195
194
|
embeddedWalletType: "user-wallet" | "app-wallet";
|
|
196
|
-
addressTypes: AddressType[]; // e.g., [AddressType.solana]
|
|
195
|
+
addressTypes: [AddressType, ...AddressType[]]; // e.g., [AddressType.solana]
|
|
197
196
|
apiBaseUrl: string; // e.g., "https://api.phantom.app/v1/wallets"
|
|
198
197
|
solanaProvider: "web3js" | "kit"; // Solana provider to use
|
|
199
198
|
authOptions?: {
|
|
@@ -203,7 +202,6 @@ interface PhantomSDKConfig {
|
|
|
203
202
|
appName?: string; // Optional app name for branding
|
|
204
203
|
appLogo?: string; // Optional app logo URL for branding
|
|
205
204
|
autoConnect?: boolean; // Auto-connect to existing session on SDK instantiation (default: true)
|
|
206
|
-
debug?: boolean; // Enable debug logging (optional)
|
|
207
205
|
}
|
|
208
206
|
```
|
|
209
207
|
|
|
@@ -422,19 +420,40 @@ adb shell am start -W -a android.intent.action.VIEW -d "myapp://phantom-auth-cal
|
|
|
422
420
|
- Verify URL scheme configuration
|
|
423
421
|
- Check intent filters (Android) or URL schemes (iOS)
|
|
424
422
|
|
|
425
|
-
### Debug
|
|
423
|
+
### Debug Configuration
|
|
426
424
|
|
|
427
|
-
|
|
425
|
+
The React Native SDK supports separate debug configuration for better performance and dynamic control:
|
|
428
426
|
|
|
429
427
|
```typescript
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
428
|
+
import { PhantomProvider, type PhantomSDKConfig, type PhantomDebugConfig } from "@phantom/react-native-sdk";
|
|
429
|
+
|
|
430
|
+
function App() {
|
|
431
|
+
// SDK configuration - static, won't change when debug settings change
|
|
432
|
+
const config: PhantomSDKConfig = {
|
|
433
|
+
organizationId: "your-org-id",
|
|
434
|
+
scheme: "mywalletapp",
|
|
435
|
+
// ... other config
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// Debug configuration - separate to avoid SDK reinstantiation
|
|
439
|
+
const debugConfig: PhantomDebugConfig = {
|
|
440
|
+
enabled: true, // Enable debug logging
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
return (
|
|
444
|
+
<PhantomProvider config={config} debugConfig={debugConfig}>
|
|
445
|
+
<App />
|
|
446
|
+
</PhantomProvider>
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
**PhantomDebugConfig Interface:**
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
interface PhantomDebugConfig {
|
|
455
|
+
enabled?: boolean; // Enable debug logging (default: false)
|
|
456
|
+
}
|
|
438
457
|
```
|
|
439
458
|
|
|
440
459
|
## Support
|
package/dist/index.d.ts
CHANGED
|
@@ -6,11 +6,13 @@ export { ConnectResult, SignAndSendTransactionParams, SignMessageParams, SignMes
|
|
|
6
6
|
export { AddressType } from '@phantom/client';
|
|
7
7
|
export { NetworkId } from '@phantom/constants';
|
|
8
8
|
|
|
9
|
+
interface PhantomDebugConfig {
|
|
10
|
+
/** Enable debug logging */
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
}
|
|
9
13
|
interface PhantomSDKConfig extends EmbeddedProviderConfig {
|
|
10
14
|
/** Custom URL scheme for your app (e.g., "myapp") */
|
|
11
15
|
scheme: string;
|
|
12
|
-
/** Enable debug logging */
|
|
13
|
-
debug?: boolean;
|
|
14
16
|
/** Enable auto-connect to existing sessions (default: true) */
|
|
15
17
|
autoConnect?: boolean;
|
|
16
18
|
}
|
|
@@ -24,7 +26,7 @@ interface ConnectOptions {
|
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
interface PhantomContextValue {
|
|
27
|
-
sdk: EmbeddedProvider;
|
|
29
|
+
sdk: EmbeddedProvider | null;
|
|
28
30
|
isConnected: boolean;
|
|
29
31
|
isConnecting: boolean;
|
|
30
32
|
connectError: Error | null;
|
|
@@ -35,8 +37,9 @@ interface PhantomContextValue {
|
|
|
35
37
|
interface PhantomProviderProps {
|
|
36
38
|
children: ReactNode;
|
|
37
39
|
config: PhantomSDKConfig;
|
|
40
|
+
debugConfig?: PhantomDebugConfig;
|
|
38
41
|
}
|
|
39
|
-
declare function PhantomProvider({ children, config }: PhantomProviderProps): react_jsx_runtime.JSX.Element;
|
|
42
|
+
declare function PhantomProvider({ children, config, debugConfig }: PhantomProviderProps): react_jsx_runtime.JSX.Element;
|
|
40
43
|
/**
|
|
41
44
|
* Hook to access the Phantom context
|
|
42
45
|
* Must be used within a PhantomProvider
|
|
@@ -73,4 +76,4 @@ declare function useSignAndSendTransaction(): {
|
|
|
73
76
|
error: Error | null;
|
|
74
77
|
};
|
|
75
78
|
|
|
76
|
-
export { ConnectOptions, PhantomProvider, PhantomSDKConfig, useAccounts, useConnect, useDisconnect, usePhantom, useSignAndSendTransaction, useSignMessage };
|
|
79
|
+
export { ConnectOptions, PhantomDebugConfig, PhantomProvider, PhantomSDKConfig, useAccounts, useConnect, useDisconnect, usePhantom, useSignAndSendTransaction, useSignMessage };
|
package/dist/index.js
CHANGED
|
@@ -295,7 +295,8 @@ var import_sdk_types = require("@phantom/sdk-types");
|
|
|
295
295
|
var ReactNativeStamper = class {
|
|
296
296
|
// Optional for PKI, required for OIDC
|
|
297
297
|
constructor(config = {}) {
|
|
298
|
-
this.
|
|
298
|
+
this.activeKeyRecord = null;
|
|
299
|
+
this.pendingKeyRecord = null;
|
|
299
300
|
this.algorithm = import_sdk_types.Algorithm.ed25519;
|
|
300
301
|
this.type = "PKI";
|
|
301
302
|
this.keyPrefix = config.keyPrefix || "phantom-rn-stamper";
|
|
@@ -305,32 +306,27 @@ var ReactNativeStamper = class {
|
|
|
305
306
|
* Initialize the stamper and generate/load cryptographic keys
|
|
306
307
|
*/
|
|
307
308
|
async init() {
|
|
308
|
-
|
|
309
|
-
if (
|
|
310
|
-
|
|
311
|
-
if (keyInfo2) {
|
|
312
|
-
this.keyInfo = keyInfo2;
|
|
313
|
-
return keyInfo2;
|
|
314
|
-
}
|
|
309
|
+
this.activeKeyRecord = await this.loadActiveKeyRecord();
|
|
310
|
+
if (!this.activeKeyRecord) {
|
|
311
|
+
this.activeKeyRecord = await this.generateAndStoreNewKeyRecord("active");
|
|
315
312
|
}
|
|
316
|
-
|
|
317
|
-
this.keyInfo
|
|
318
|
-
return keyInfo;
|
|
313
|
+
this.pendingKeyRecord = await this.loadPendingKeyRecord();
|
|
314
|
+
return this.activeKeyRecord.keyInfo;
|
|
319
315
|
}
|
|
320
316
|
/**
|
|
321
317
|
* Get the current key information
|
|
322
318
|
*/
|
|
323
319
|
getKeyInfo() {
|
|
324
|
-
return this.keyInfo;
|
|
320
|
+
return this.activeKeyRecord?.keyInfo || null;
|
|
325
321
|
}
|
|
326
322
|
/**
|
|
327
323
|
* Generate and store a new key pair, replacing any existing keys
|
|
328
324
|
*/
|
|
329
325
|
async resetKeyPair() {
|
|
330
326
|
await this.clear();
|
|
331
|
-
|
|
332
|
-
this.
|
|
333
|
-
return keyInfo;
|
|
327
|
+
this.activeKeyRecord = await this.generateAndStoreNewKeyRecord("active");
|
|
328
|
+
this.pendingKeyRecord = null;
|
|
329
|
+
return this.activeKeyRecord.keyInfo;
|
|
334
330
|
}
|
|
335
331
|
/**
|
|
336
332
|
* Create X-Phantom-Stamp header value using stored secret key
|
|
@@ -338,79 +334,129 @@ var ReactNativeStamper = class {
|
|
|
338
334
|
* @returns Complete X-Phantom-Stamp header value
|
|
339
335
|
*/
|
|
340
336
|
async stamp(params) {
|
|
341
|
-
if (!this.
|
|
337
|
+
if (!this.activeKeyRecord) {
|
|
342
338
|
throw new Error("Stamper not initialized. Call init() first.");
|
|
343
339
|
}
|
|
344
|
-
const
|
|
345
|
-
if (!storedSecretKey) {
|
|
346
|
-
throw new Error("Secret key not found in secure storage");
|
|
347
|
-
}
|
|
348
|
-
const apiKeyStamper = new import_api_key_stamper.ApiKeyStamper({ apiSecretKey: storedSecretKey });
|
|
340
|
+
const apiKeyStamper = new import_api_key_stamper.ApiKeyStamper({ apiSecretKey: this.activeKeyRecord.secretKey });
|
|
349
341
|
return await apiKeyStamper.stamp(params);
|
|
350
342
|
}
|
|
351
343
|
/**
|
|
352
344
|
* Clear all stored keys from SecureStore
|
|
353
345
|
*/
|
|
354
346
|
async clear() {
|
|
355
|
-
const
|
|
356
|
-
const
|
|
347
|
+
const activeKey = this.getActiveKeyName();
|
|
348
|
+
const pendingKey = this.getPendingKeyName();
|
|
349
|
+
try {
|
|
350
|
+
await SecureStore2.deleteItemAsync(activeKey);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
}
|
|
357
353
|
try {
|
|
358
|
-
await SecureStore2.deleteItemAsync(
|
|
354
|
+
await SecureStore2.deleteItemAsync(pendingKey);
|
|
359
355
|
} catch (error) {
|
|
360
356
|
}
|
|
357
|
+
this.activeKeyRecord = null;
|
|
358
|
+
this.pendingKeyRecord = null;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Generate a new keypair for rotation without making it active
|
|
362
|
+
*/
|
|
363
|
+
async rotateKeyPair() {
|
|
364
|
+
this.pendingKeyRecord = await this.generateAndStoreNewKeyRecord("pending");
|
|
365
|
+
return this.pendingKeyRecord.keyInfo;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Switch to the pending keypair, making it active and cleaning up the old one
|
|
369
|
+
*/
|
|
370
|
+
async commitRotation(authenticatorId) {
|
|
371
|
+
if (!this.pendingKeyRecord) {
|
|
372
|
+
throw new Error("No pending keypair to commit");
|
|
373
|
+
}
|
|
374
|
+
if (this.activeKeyRecord) {
|
|
375
|
+
try {
|
|
376
|
+
await SecureStore2.deleteItemAsync(this.getActiveKeyName());
|
|
377
|
+
} catch (error) {
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
this.pendingKeyRecord.status = "active";
|
|
381
|
+
this.pendingKeyRecord.authenticatorId = authenticatorId;
|
|
382
|
+
this.pendingKeyRecord.keyInfo.authenticatorId = authenticatorId;
|
|
383
|
+
this.activeKeyRecord = this.pendingKeyRecord;
|
|
384
|
+
this.pendingKeyRecord = null;
|
|
385
|
+
await this.storeKeyRecord(this.activeKeyRecord, "active");
|
|
386
|
+
try {
|
|
387
|
+
await SecureStore2.deleteItemAsync(this.getPendingKeyName());
|
|
388
|
+
} catch (error) {
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Discard the pending keypair on rotation failure
|
|
393
|
+
*/
|
|
394
|
+
async rollbackRotation() {
|
|
395
|
+
if (!this.pendingKeyRecord) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
361
398
|
try {
|
|
362
|
-
await SecureStore2.deleteItemAsync(
|
|
399
|
+
await SecureStore2.deleteItemAsync(this.getPendingKeyName());
|
|
363
400
|
} catch (error) {
|
|
364
401
|
}
|
|
365
|
-
this.
|
|
402
|
+
this.pendingKeyRecord = null;
|
|
366
403
|
}
|
|
367
|
-
async
|
|
404
|
+
async generateAndStoreNewKeyRecord(type) {
|
|
368
405
|
const keypair = (0, import_crypto.generateKeyPair)();
|
|
369
406
|
const keyId = this.createKeyId(keypair.publicKey);
|
|
407
|
+
const now = Date.now();
|
|
370
408
|
const keyInfo = {
|
|
371
409
|
keyId,
|
|
372
|
-
publicKey: keypair.publicKey
|
|
410
|
+
publicKey: keypair.publicKey,
|
|
411
|
+
createdAt: now
|
|
373
412
|
};
|
|
374
|
-
|
|
375
|
-
|
|
413
|
+
const record = {
|
|
414
|
+
keyInfo,
|
|
415
|
+
secretKey: keypair.secretKey,
|
|
416
|
+
createdAt: now,
|
|
417
|
+
expiresAt: 0,
|
|
418
|
+
// Not used anymore, kept for backward compatibility
|
|
419
|
+
status: type
|
|
420
|
+
};
|
|
421
|
+
await this.storeKeyRecord(record, type);
|
|
422
|
+
return record;
|
|
376
423
|
}
|
|
377
424
|
createKeyId(publicKey) {
|
|
378
425
|
return (0, import_base64url.base64urlEncode)(new TextEncoder().encode(publicKey)).substring(0, 16);
|
|
379
426
|
}
|
|
380
|
-
async
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
await SecureStore2.setItemAsync(infoKey, JSON.stringify(keyInfo), {
|
|
384
|
-
requireAuthentication: false
|
|
385
|
-
});
|
|
386
|
-
await SecureStore2.setItemAsync(secretKeyName, secretKey, {
|
|
427
|
+
async storeKeyRecord(record, type) {
|
|
428
|
+
const keyName = type === "active" ? this.getActiveKeyName() : this.getPendingKeyName();
|
|
429
|
+
await SecureStore2.setItemAsync(keyName, JSON.stringify(record), {
|
|
387
430
|
requireAuthentication: false
|
|
388
431
|
});
|
|
389
432
|
}
|
|
390
|
-
async
|
|
433
|
+
async loadActiveKeyRecord() {
|
|
391
434
|
try {
|
|
392
|
-
const
|
|
393
|
-
const
|
|
394
|
-
if (
|
|
395
|
-
return JSON.parse(
|
|
435
|
+
const activeKey = this.getActiveKeyName();
|
|
436
|
+
const storedRecord = await SecureStore2.getItemAsync(activeKey);
|
|
437
|
+
if (storedRecord) {
|
|
438
|
+
return JSON.parse(storedRecord);
|
|
396
439
|
}
|
|
397
440
|
} catch (error) {
|
|
398
441
|
}
|
|
399
442
|
return null;
|
|
400
443
|
}
|
|
401
|
-
async
|
|
444
|
+
async loadPendingKeyRecord() {
|
|
402
445
|
try {
|
|
403
|
-
const
|
|
404
|
-
|
|
446
|
+
const pendingKey = this.getPendingKeyName();
|
|
447
|
+
const storedRecord = await SecureStore2.getItemAsync(pendingKey);
|
|
448
|
+
if (storedRecord) {
|
|
449
|
+
return JSON.parse(storedRecord);
|
|
450
|
+
}
|
|
405
451
|
} catch (error) {
|
|
406
|
-
return null;
|
|
407
452
|
}
|
|
453
|
+
return null;
|
|
408
454
|
}
|
|
409
|
-
|
|
410
|
-
return `${this.keyPrefix}-${this.organizationId}-
|
|
455
|
+
getActiveKeyName() {
|
|
456
|
+
return `${this.keyPrefix}-${this.organizationId}-active`;
|
|
411
457
|
}
|
|
412
|
-
|
|
413
|
-
return `${this.keyPrefix}-${this.organizationId}-
|
|
458
|
+
getPendingKeyName() {
|
|
459
|
+
return `${this.keyPrefix}-${this.organizationId}-pending`;
|
|
414
460
|
}
|
|
415
461
|
};
|
|
416
462
|
|
|
@@ -418,23 +464,31 @@ var ReactNativeStamper = class {
|
|
|
418
464
|
var import_react_native2 = require("react-native");
|
|
419
465
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
420
466
|
var PhantomContext = (0, import_react.createContext)(void 0);
|
|
421
|
-
function PhantomProvider({ children, config }) {
|
|
422
|
-
const
|
|
467
|
+
function PhantomProvider({ children, config, debugConfig }) {
|
|
468
|
+
const [isConnected, setIsConnected] = (0, import_react.useState)(false);
|
|
469
|
+
const [isConnecting, setIsConnecting] = (0, import_react.useState)(false);
|
|
470
|
+
const [connectError, setConnectError] = (0, import_react.useState)(null);
|
|
471
|
+
const [addresses, setAddresses] = (0, import_react.useState)([]);
|
|
472
|
+
const [walletId, setWalletId] = (0, import_react.useState)(null);
|
|
473
|
+
const [sdk, setSdk] = (0, import_react.useState)(null);
|
|
474
|
+
const memoizedConfig = (0, import_react.useMemo)(() => {
|
|
423
475
|
const redirectUrl = config.authOptions?.redirectUrl || `${config.scheme}://phantom-auth-callback`;
|
|
424
|
-
|
|
476
|
+
return {
|
|
425
477
|
...config,
|
|
426
478
|
authOptions: {
|
|
427
479
|
...config.authOptions || {},
|
|
428
480
|
redirectUrl
|
|
429
481
|
}
|
|
430
482
|
};
|
|
483
|
+
}, [config]);
|
|
484
|
+
(0, import_react.useEffect)(() => {
|
|
431
485
|
const storage = new ExpoSecureStorage();
|
|
432
486
|
const authProvider = new ExpoAuthProvider();
|
|
433
487
|
const urlParamsAccessor = new ExpoURLParamsAccessor();
|
|
434
|
-
const logger = new ExpoLogger(
|
|
488
|
+
const logger = new ExpoLogger(debugConfig?.enabled || false);
|
|
435
489
|
const stamper = new ReactNativeStamper({
|
|
436
|
-
keyPrefix: `phantom-rn-${
|
|
437
|
-
organizationId:
|
|
490
|
+
keyPrefix: `phantom-rn-${memoizedConfig.organizationId}`,
|
|
491
|
+
organizationId: memoizedConfig.organizationId
|
|
438
492
|
});
|
|
439
493
|
const platform = {
|
|
440
494
|
storage,
|
|
@@ -443,7 +497,7 @@ function PhantomProvider({ children, config }) {
|
|
|
443
497
|
stamper,
|
|
444
498
|
name: `${import_react_native2.Platform.OS}-${import_react_native2.Platform.Version}`
|
|
445
499
|
};
|
|
446
|
-
const sdkInstance = new import_embedded_provider_core.EmbeddedProvider(
|
|
500
|
+
const sdkInstance = new import_embedded_provider_core.EmbeddedProvider(memoizedConfig, platform, logger);
|
|
447
501
|
const handleConnectStart = () => {
|
|
448
502
|
setIsConnecting(true);
|
|
449
503
|
setConnectError(null);
|
|
@@ -478,14 +532,17 @@ function PhantomProvider({ children, config }) {
|
|
|
478
532
|
sdkInstance.on("connect", handleConnect);
|
|
479
533
|
sdkInstance.on("connect_error", handleConnectError);
|
|
480
534
|
sdkInstance.on("disconnect", handleDisconnect);
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
535
|
+
setSdk(sdkInstance);
|
|
536
|
+
return () => {
|
|
537
|
+
sdkInstance.off("connect_start", handleConnectStart);
|
|
538
|
+
sdkInstance.off("connect", handleConnect);
|
|
539
|
+
sdkInstance.off("connect_error", handleConnectError);
|
|
540
|
+
sdkInstance.off("disconnect", handleDisconnect);
|
|
541
|
+
};
|
|
542
|
+
}, [memoizedConfig, debugConfig]);
|
|
488
543
|
(0, import_react.useEffect)(() => {
|
|
544
|
+
if (!sdk)
|
|
545
|
+
return;
|
|
489
546
|
if (config.autoConnect !== false) {
|
|
490
547
|
sdk.autoConnect().catch(() => {
|
|
491
548
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -251,7 +251,8 @@ import { Algorithm } from "@phantom/sdk-types";
|
|
|
251
251
|
var ReactNativeStamper = class {
|
|
252
252
|
// Optional for PKI, required for OIDC
|
|
253
253
|
constructor(config = {}) {
|
|
254
|
-
this.
|
|
254
|
+
this.activeKeyRecord = null;
|
|
255
|
+
this.pendingKeyRecord = null;
|
|
255
256
|
this.algorithm = Algorithm.ed25519;
|
|
256
257
|
this.type = "PKI";
|
|
257
258
|
this.keyPrefix = config.keyPrefix || "phantom-rn-stamper";
|
|
@@ -261,32 +262,27 @@ var ReactNativeStamper = class {
|
|
|
261
262
|
* Initialize the stamper and generate/load cryptographic keys
|
|
262
263
|
*/
|
|
263
264
|
async init() {
|
|
264
|
-
|
|
265
|
-
if (
|
|
266
|
-
|
|
267
|
-
if (keyInfo2) {
|
|
268
|
-
this.keyInfo = keyInfo2;
|
|
269
|
-
return keyInfo2;
|
|
270
|
-
}
|
|
265
|
+
this.activeKeyRecord = await this.loadActiveKeyRecord();
|
|
266
|
+
if (!this.activeKeyRecord) {
|
|
267
|
+
this.activeKeyRecord = await this.generateAndStoreNewKeyRecord("active");
|
|
271
268
|
}
|
|
272
|
-
|
|
273
|
-
this.keyInfo
|
|
274
|
-
return keyInfo;
|
|
269
|
+
this.pendingKeyRecord = await this.loadPendingKeyRecord();
|
|
270
|
+
return this.activeKeyRecord.keyInfo;
|
|
275
271
|
}
|
|
276
272
|
/**
|
|
277
273
|
* Get the current key information
|
|
278
274
|
*/
|
|
279
275
|
getKeyInfo() {
|
|
280
|
-
return this.keyInfo;
|
|
276
|
+
return this.activeKeyRecord?.keyInfo || null;
|
|
281
277
|
}
|
|
282
278
|
/**
|
|
283
279
|
* Generate and store a new key pair, replacing any existing keys
|
|
284
280
|
*/
|
|
285
281
|
async resetKeyPair() {
|
|
286
282
|
await this.clear();
|
|
287
|
-
|
|
288
|
-
this.
|
|
289
|
-
return keyInfo;
|
|
283
|
+
this.activeKeyRecord = await this.generateAndStoreNewKeyRecord("active");
|
|
284
|
+
this.pendingKeyRecord = null;
|
|
285
|
+
return this.activeKeyRecord.keyInfo;
|
|
290
286
|
}
|
|
291
287
|
/**
|
|
292
288
|
* Create X-Phantom-Stamp header value using stored secret key
|
|
@@ -294,79 +290,129 @@ var ReactNativeStamper = class {
|
|
|
294
290
|
* @returns Complete X-Phantom-Stamp header value
|
|
295
291
|
*/
|
|
296
292
|
async stamp(params) {
|
|
297
|
-
if (!this.
|
|
293
|
+
if (!this.activeKeyRecord) {
|
|
298
294
|
throw new Error("Stamper not initialized. Call init() first.");
|
|
299
295
|
}
|
|
300
|
-
const
|
|
301
|
-
if (!storedSecretKey) {
|
|
302
|
-
throw new Error("Secret key not found in secure storage");
|
|
303
|
-
}
|
|
304
|
-
const apiKeyStamper = new ApiKeyStamper({ apiSecretKey: storedSecretKey });
|
|
296
|
+
const apiKeyStamper = new ApiKeyStamper({ apiSecretKey: this.activeKeyRecord.secretKey });
|
|
305
297
|
return await apiKeyStamper.stamp(params);
|
|
306
298
|
}
|
|
307
299
|
/**
|
|
308
300
|
* Clear all stored keys from SecureStore
|
|
309
301
|
*/
|
|
310
302
|
async clear() {
|
|
311
|
-
const
|
|
312
|
-
const
|
|
303
|
+
const activeKey = this.getActiveKeyName();
|
|
304
|
+
const pendingKey = this.getPendingKeyName();
|
|
305
|
+
try {
|
|
306
|
+
await SecureStore2.deleteItemAsync(activeKey);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
}
|
|
313
309
|
try {
|
|
314
|
-
await SecureStore2.deleteItemAsync(
|
|
310
|
+
await SecureStore2.deleteItemAsync(pendingKey);
|
|
315
311
|
} catch (error) {
|
|
316
312
|
}
|
|
313
|
+
this.activeKeyRecord = null;
|
|
314
|
+
this.pendingKeyRecord = null;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Generate a new keypair for rotation without making it active
|
|
318
|
+
*/
|
|
319
|
+
async rotateKeyPair() {
|
|
320
|
+
this.pendingKeyRecord = await this.generateAndStoreNewKeyRecord("pending");
|
|
321
|
+
return this.pendingKeyRecord.keyInfo;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Switch to the pending keypair, making it active and cleaning up the old one
|
|
325
|
+
*/
|
|
326
|
+
async commitRotation(authenticatorId) {
|
|
327
|
+
if (!this.pendingKeyRecord) {
|
|
328
|
+
throw new Error("No pending keypair to commit");
|
|
329
|
+
}
|
|
330
|
+
if (this.activeKeyRecord) {
|
|
331
|
+
try {
|
|
332
|
+
await SecureStore2.deleteItemAsync(this.getActiveKeyName());
|
|
333
|
+
} catch (error) {
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
this.pendingKeyRecord.status = "active";
|
|
337
|
+
this.pendingKeyRecord.authenticatorId = authenticatorId;
|
|
338
|
+
this.pendingKeyRecord.keyInfo.authenticatorId = authenticatorId;
|
|
339
|
+
this.activeKeyRecord = this.pendingKeyRecord;
|
|
340
|
+
this.pendingKeyRecord = null;
|
|
341
|
+
await this.storeKeyRecord(this.activeKeyRecord, "active");
|
|
342
|
+
try {
|
|
343
|
+
await SecureStore2.deleteItemAsync(this.getPendingKeyName());
|
|
344
|
+
} catch (error) {
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Discard the pending keypair on rotation failure
|
|
349
|
+
*/
|
|
350
|
+
async rollbackRotation() {
|
|
351
|
+
if (!this.pendingKeyRecord) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
317
354
|
try {
|
|
318
|
-
await SecureStore2.deleteItemAsync(
|
|
355
|
+
await SecureStore2.deleteItemAsync(this.getPendingKeyName());
|
|
319
356
|
} catch (error) {
|
|
320
357
|
}
|
|
321
|
-
this.
|
|
358
|
+
this.pendingKeyRecord = null;
|
|
322
359
|
}
|
|
323
|
-
async
|
|
360
|
+
async generateAndStoreNewKeyRecord(type) {
|
|
324
361
|
const keypair = generateKeyPair();
|
|
325
362
|
const keyId = this.createKeyId(keypair.publicKey);
|
|
363
|
+
const now = Date.now();
|
|
326
364
|
const keyInfo = {
|
|
327
365
|
keyId,
|
|
328
|
-
publicKey: keypair.publicKey
|
|
366
|
+
publicKey: keypair.publicKey,
|
|
367
|
+
createdAt: now
|
|
329
368
|
};
|
|
330
|
-
|
|
331
|
-
|
|
369
|
+
const record = {
|
|
370
|
+
keyInfo,
|
|
371
|
+
secretKey: keypair.secretKey,
|
|
372
|
+
createdAt: now,
|
|
373
|
+
expiresAt: 0,
|
|
374
|
+
// Not used anymore, kept for backward compatibility
|
|
375
|
+
status: type
|
|
376
|
+
};
|
|
377
|
+
await this.storeKeyRecord(record, type);
|
|
378
|
+
return record;
|
|
332
379
|
}
|
|
333
380
|
createKeyId(publicKey) {
|
|
334
381
|
return base64urlEncode(new TextEncoder().encode(publicKey)).substring(0, 16);
|
|
335
382
|
}
|
|
336
|
-
async
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
await SecureStore2.setItemAsync(infoKey, JSON.stringify(keyInfo), {
|
|
340
|
-
requireAuthentication: false
|
|
341
|
-
});
|
|
342
|
-
await SecureStore2.setItemAsync(secretKeyName, secretKey, {
|
|
383
|
+
async storeKeyRecord(record, type) {
|
|
384
|
+
const keyName = type === "active" ? this.getActiveKeyName() : this.getPendingKeyName();
|
|
385
|
+
await SecureStore2.setItemAsync(keyName, JSON.stringify(record), {
|
|
343
386
|
requireAuthentication: false
|
|
344
387
|
});
|
|
345
388
|
}
|
|
346
|
-
async
|
|
389
|
+
async loadActiveKeyRecord() {
|
|
347
390
|
try {
|
|
348
|
-
const
|
|
349
|
-
const
|
|
350
|
-
if (
|
|
351
|
-
return JSON.parse(
|
|
391
|
+
const activeKey = this.getActiveKeyName();
|
|
392
|
+
const storedRecord = await SecureStore2.getItemAsync(activeKey);
|
|
393
|
+
if (storedRecord) {
|
|
394
|
+
return JSON.parse(storedRecord);
|
|
352
395
|
}
|
|
353
396
|
} catch (error) {
|
|
354
397
|
}
|
|
355
398
|
return null;
|
|
356
399
|
}
|
|
357
|
-
async
|
|
400
|
+
async loadPendingKeyRecord() {
|
|
358
401
|
try {
|
|
359
|
-
const
|
|
360
|
-
|
|
402
|
+
const pendingKey = this.getPendingKeyName();
|
|
403
|
+
const storedRecord = await SecureStore2.getItemAsync(pendingKey);
|
|
404
|
+
if (storedRecord) {
|
|
405
|
+
return JSON.parse(storedRecord);
|
|
406
|
+
}
|
|
361
407
|
} catch (error) {
|
|
362
|
-
return null;
|
|
363
408
|
}
|
|
409
|
+
return null;
|
|
364
410
|
}
|
|
365
|
-
|
|
366
|
-
return `${this.keyPrefix}-${this.organizationId}-
|
|
411
|
+
getActiveKeyName() {
|
|
412
|
+
return `${this.keyPrefix}-${this.organizationId}-active`;
|
|
367
413
|
}
|
|
368
|
-
|
|
369
|
-
return `${this.keyPrefix}-${this.organizationId}-
|
|
414
|
+
getPendingKeyName() {
|
|
415
|
+
return `${this.keyPrefix}-${this.organizationId}-pending`;
|
|
370
416
|
}
|
|
371
417
|
};
|
|
372
418
|
|
|
@@ -374,23 +420,31 @@ var ReactNativeStamper = class {
|
|
|
374
420
|
import { Platform } from "react-native";
|
|
375
421
|
import { jsx } from "react/jsx-runtime";
|
|
376
422
|
var PhantomContext = createContext(void 0);
|
|
377
|
-
function PhantomProvider({ children, config }) {
|
|
378
|
-
const
|
|
423
|
+
function PhantomProvider({ children, config, debugConfig }) {
|
|
424
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
425
|
+
const [isConnecting, setIsConnecting] = useState(false);
|
|
426
|
+
const [connectError, setConnectError] = useState(null);
|
|
427
|
+
const [addresses, setAddresses] = useState([]);
|
|
428
|
+
const [walletId, setWalletId] = useState(null);
|
|
429
|
+
const [sdk, setSdk] = useState(null);
|
|
430
|
+
const memoizedConfig = useMemo(() => {
|
|
379
431
|
const redirectUrl = config.authOptions?.redirectUrl || `${config.scheme}://phantom-auth-callback`;
|
|
380
|
-
|
|
432
|
+
return {
|
|
381
433
|
...config,
|
|
382
434
|
authOptions: {
|
|
383
435
|
...config.authOptions || {},
|
|
384
436
|
redirectUrl
|
|
385
437
|
}
|
|
386
438
|
};
|
|
439
|
+
}, [config]);
|
|
440
|
+
useEffect(() => {
|
|
387
441
|
const storage = new ExpoSecureStorage();
|
|
388
442
|
const authProvider = new ExpoAuthProvider();
|
|
389
443
|
const urlParamsAccessor = new ExpoURLParamsAccessor();
|
|
390
|
-
const logger = new ExpoLogger(
|
|
444
|
+
const logger = new ExpoLogger(debugConfig?.enabled || false);
|
|
391
445
|
const stamper = new ReactNativeStamper({
|
|
392
|
-
keyPrefix: `phantom-rn-${
|
|
393
|
-
organizationId:
|
|
446
|
+
keyPrefix: `phantom-rn-${memoizedConfig.organizationId}`,
|
|
447
|
+
organizationId: memoizedConfig.organizationId
|
|
394
448
|
});
|
|
395
449
|
const platform = {
|
|
396
450
|
storage,
|
|
@@ -399,7 +453,7 @@ function PhantomProvider({ children, config }) {
|
|
|
399
453
|
stamper,
|
|
400
454
|
name: `${Platform.OS}-${Platform.Version}`
|
|
401
455
|
};
|
|
402
|
-
const sdkInstance = new EmbeddedProvider(
|
|
456
|
+
const sdkInstance = new EmbeddedProvider(memoizedConfig, platform, logger);
|
|
403
457
|
const handleConnectStart = () => {
|
|
404
458
|
setIsConnecting(true);
|
|
405
459
|
setConnectError(null);
|
|
@@ -434,14 +488,17 @@ function PhantomProvider({ children, config }) {
|
|
|
434
488
|
sdkInstance.on("connect", handleConnect);
|
|
435
489
|
sdkInstance.on("connect_error", handleConnectError);
|
|
436
490
|
sdkInstance.on("disconnect", handleDisconnect);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
491
|
+
setSdk(sdkInstance);
|
|
492
|
+
return () => {
|
|
493
|
+
sdkInstance.off("connect_start", handleConnectStart);
|
|
494
|
+
sdkInstance.off("connect", handleConnect);
|
|
495
|
+
sdkInstance.off("connect_error", handleConnectError);
|
|
496
|
+
sdkInstance.off("disconnect", handleDisconnect);
|
|
497
|
+
};
|
|
498
|
+
}, [memoizedConfig, debugConfig]);
|
|
444
499
|
useEffect(() => {
|
|
500
|
+
if (!sdk)
|
|
501
|
+
return;
|
|
445
502
|
if (config.autoConnect !== false) {
|
|
446
503
|
sdk.autoConnect().catch(() => {
|
|
447
504
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phantom/react-native-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Phantom Wallet SDK for React Native and Expo applications",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -47,11 +47,11 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@phantom/api-key-stamper": "^0.1.5",
|
|
49
49
|
"@phantom/base64url": "^0.1.0",
|
|
50
|
-
"@phantom/client": "^0.1.
|
|
50
|
+
"@phantom/client": "^0.1.10",
|
|
51
51
|
"@phantom/constants": "^0.0.3",
|
|
52
52
|
"@phantom/crypto": "^0.1.2",
|
|
53
|
-
"@phantom/embedded-provider-core": "^0.1.
|
|
54
|
-
"@phantom/sdk-types": "^0.1.
|
|
53
|
+
"@phantom/embedded-provider-core": "^0.1.9",
|
|
54
|
+
"@phantom/sdk-types": "^0.1.5",
|
|
55
55
|
"@types/bs58": "^5.0.0",
|
|
56
56
|
"bs58": "^6.0.0",
|
|
57
57
|
"buffer": "^6.0.3"
|