From 47f3b7c806b7fb6bf0e44c7e1cc5324f5abd69a1 Mon Sep 17 00:00:00 2001 From: B0Y3R-AVA Date: Tue, 5 Aug 2025 13:25:14 -0400 Subject: [PATCH 01/24] ledger connection working in demo --- .../android/app/src/main/AndroidManifest.xml | 8 + .../new/routes/onboarding/ledger/_layout.tsx | 11 + .../onboarding/ledger/connectWallet.tsx | 394 ++++++++++++++++++ .../core-mobile/app/new/routes/signup.tsx | 12 + .../ios/AvaxWallet.xcodeproj/project.pbxproj | 8 + .../core-mobile/ios/AvaxWallet/Info.plist | 5 + packages/core-mobile/ios/Podfile.lock | 1 + 7 files changed, 439 insertions(+) create mode 100644 packages/core-mobile/app/new/routes/onboarding/ledger/_layout.tsx create mode 100644 packages/core-mobile/app/new/routes/onboarding/ledger/connectWallet.tsx diff --git a/packages/core-mobile/android/app/src/main/AndroidManifest.xml b/packages/core-mobile/android/app/src/main/AndroidManifest.xml index 37afe82b0f..ba85a7659d 100644 --- a/packages/core-mobile/android/app/src/main/AndroidManifest.xml +++ b/packages/core-mobile/android/app/src/main/AndroidManifest.xml @@ -20,6 +20,14 @@ + + + + + + + + diff --git a/packages/core-mobile/app/new/routes/onboarding/ledger/_layout.tsx b/packages/core-mobile/app/new/routes/onboarding/ledger/_layout.tsx new file mode 100644 index 0000000000..529742ee0e --- /dev/null +++ b/packages/core-mobile/app/new/routes/onboarding/ledger/_layout.tsx @@ -0,0 +1,11 @@ +import { stackNavigatorScreenOptions } from 'common/consts/screenOptions' +import { Stack } from 'common/components/Stack' +import React from 'react' + +export default function LedgerOnboardingLayout(): JSX.Element { + return ( + + + + ) +} \ No newline at end of file diff --git a/packages/core-mobile/app/new/routes/onboarding/ledger/connectWallet.tsx b/packages/core-mobile/app/new/routes/onboarding/ledger/connectWallet.tsx new file mode 100644 index 0000000000..f536783440 --- /dev/null +++ b/packages/core-mobile/app/new/routes/onboarding/ledger/connectWallet.tsx @@ -0,0 +1,394 @@ +import React, { useEffect, useState, useCallback } from 'react' +import { + View, + Alert, + FlatList, + Platform, + PermissionsAndroid, + Linking +} from 'react-native' + +import TransportBLE from '@ledgerhq/react-native-hw-transport-ble' +import { Button, Card, Text as K2Text, useTheme } from '@avalabs/k2-alpine' + +interface Device { + id: string + name: string + rssi?: number +} + +interface TransportState { + available: boolean + powered: boolean +} + +export default function ConnectWallet(): JSX.Element { + const { + theme: { colors } + } = useTheme() + const [transportState, setTransportState] = useState({ + available: false, + powered: false + }) + const [devices, setDevices] = useState([]) + const [isScanning, setIsScanning] = useState(false) + const [isConnecting, setIsConnecting] = useState(false) + const [connectedDevice, setConnectedDevice] = useState(null) + + // Monitor BLE transport state + useEffect(() => { + const subscription = TransportBLE.observeState({ + next: event => { + setTransportState({ + available: event.available, + powered: false // Remove powered property since it doesn't exist + }) + }, + complete: () => { + // Handle completion + }, + error: error => { + Alert.alert( + 'BLE Error', + `Failed to monitor BLE state: ${error.message}` + ) + } + }) + + return () => { + subscription.unsubscribe() + } + }, []) + + // Request Bluetooth permissions + const requestBluetoothPermissions = useCallback(async () => { + if (Platform.OS === 'android') { + try { + const permissions = [ + PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, + PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT, + PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION + ].filter(Boolean) as any[] + + const granted = await PermissionsAndroid.requestMultiple(permissions) + + return Object.values(granted).every( + permission => permission === 'granted' + ) + } catch (err) { + return false + } + } else if (Platform.OS === 'ios') { + // iOS permissions are handled automatically by the TransportBLE library + return true + } + return false + }, []) + + // Handle scan errors + const handleScanError = useCallback((error: any) => { + setIsScanning(false) + + // Handle specific authorization errors + if ( + error.message?.includes('not authorized') || + error.message?.includes('Origin: 101') + ) { + Alert.alert( + 'Bluetooth Permission Required', + 'Please enable Bluetooth permissions in your device settings to scan for Ledger devices.', + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Open Settings', + onPress: () => { + // You can add navigation to settings here if needed + } + } + ] + ) + } else { + Alert.alert('Scan Error', `Failed to scan for devices: ${error.message}`) + } + }, []) + + // Scan for Ledger devices + const scanForDevices = useCallback(async () => { + if (!transportState.available) { + Alert.alert( + 'Bluetooth Unavailable', + 'Please enable Bluetooth to scan for Ledger devices' + ) + return + } + + // Request permissions before scanning + const hasPermissions = await requestBluetoothPermissions() + if (!hasPermissions) { + Alert.alert( + 'Permission Required', + 'Bluetooth permissions are required to scan for Ledger devices. Please grant permissions in your device settings.' + ) + return + } + + setIsScanning(true) + setDevices([]) + + try { + const subscription = TransportBLE.listen({ + next: event => { + if (event.type === 'add') { + const device: Device = { + id: event.descriptor.id, + name: event.descriptor.name || 'Unknown Device', + rssi: event.descriptor.rssi + } + + setDevices(prev => { + // Avoid duplicates + const exists = prev.find(d => d.id === device.id) + if (!exists) { + return [...prev, device] + } + return prev + }) + } + }, + complete: () => { + setIsScanning(false) + }, + error: handleScanError + }) + + // Stop scanning after 10 seconds + setTimeout(() => { + subscription.unsubscribe() + setIsScanning(false) + }, 10000) + } catch (error) { + setIsScanning(false) + Alert.alert( + 'Scan Error', + `Failed to start scanning: ${ + error instanceof Error ? error.message : 'Unknown error' + }` + ) + } + }, [transportState.available, requestBluetoothPermissions, handleScanError]) + + // Open Bluetooth settings + const openBluetoothSettings = useCallback(() => { + if (Platform.OS === 'ios') { + Linking.openURL('App-Prefs:Bluetooth') + } else { + Linking.openSettings() + } + }, []) + + // Connect to a specific device with error handling for TurboModule issue + const connectToDevice = useCallback( + async (device: Device) => { + setIsConnecting(true) + + try { + const connectionPromise = TransportBLE.open(device.id) + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Connection timeout')), 15000) + ) + + await Promise.race([connectionPromise, timeoutPromise]) + + setConnectedDevice(device) + Alert.alert('Success', `Connected to ${device.name}`) + } catch (error) { + let errorMessage = 'Unknown connection error' + + if (error instanceof Error) { + if (error.message.includes('TurboModule')) { + errorMessage = + 'Connection failed due to compatibility issue. Please try restarting the app or updating to the latest version.' + } else if (error.message.includes('timeout')) { + errorMessage = + 'Connection timed out. Please make sure your Ledger device is unlocked and in pairing mode.' + } else if (error.message.includes('PeerRemovedPairing')) { + Alert.alert( + 'Pairing Removed', + 'The pairing with your Ledger device was removed. Would you like to open Bluetooth settings to re-pair?', + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Open Settings', + onPress: openBluetoothSettings + } + ] + ) + return // Don't show the generic error alert + } else { + errorMessage = error.message + } + } + + Alert.alert( + 'Connection Error', + `Failed to connect to ${device.name}: ${errorMessage}` + ) + } finally { + setIsConnecting(false) + } + }, + [openBluetoothSettings] + ) + + // Disconnect from current device + const disconnectDevice = useCallback(async () => { + try { + // Close any open transport connections + setConnectedDevice(null) + Alert.alert( + 'Disconnected', + 'Successfully disconnected from Ledger device' + ) + } catch (error) { + Alert.alert( + 'Disconnect Error', + `Failed to disconnect: ${ + error instanceof Error ? error.message : 'Unknown error' + }` + ) + } + }, []) + + const renderDevice = ({ item }: { item: Device }) => ( + + + + + {item.name} + + + ID: {item.id} + + {item.rssi && ( + + Signal: {item.rssi} dBm + + )} + + + + + ) + + return ( + + + Connect Ledger Device + + + {/* BLE Status */} + + + Bluetooth Status + + + Available: {transportState.available ? 'Yes' : 'No'} + + + Powered: {transportState.powered ? 'Yes' : 'No'} + + + + {/* Connected Device */} + {connectedDevice && ( + + + Connected Device + + + {connectedDevice.name} + + + + )} + + {/* Scan Button */} + + + {/* Device List */} + {devices.length > 0 && ( + + + Available Devices ({devices.length}) + + item.id} + showsVerticalScrollIndicator={false} + /> + + )} + + {/* Instructions */} + {devices.length === 0 && !isScanning && ( + + + Make sure your Ledger device is: + + + • Unlocked and on the home screen + + + • Has Bluetooth enabled + + + • Is within range of your device + + + )} + + ) +} diff --git a/packages/core-mobile/app/new/routes/signup.tsx b/packages/core-mobile/app/new/routes/signup.tsx index fd34a6b9e1..bac774787b 100644 --- a/packages/core-mobile/app/new/routes/signup.tsx +++ b/packages/core-mobile/app/new/routes/signup.tsx @@ -77,6 +77,11 @@ export default function Signup(): JSX.Element { }) } + const handleContinueWithLedgerDemo = (): void => { + // @ts-ignore TODO: make routes typesafe + router.navigate('/onboarding/ledger') + } + const renderMnemonicOnboarding = (): JSX.Element => { return ( @@ -94,6 +99,13 @@ export default function Signup(): JSX.Element { onPress={handleAccessExistingWallet}> Access existing wallet + ) } diff --git a/packages/core-mobile/ios/AvaxWallet.xcodeproj/project.pbxproj b/packages/core-mobile/ios/AvaxWallet.xcodeproj/project.pbxproj index 5d6f495a81..d42d39d6f0 100644 --- a/packages/core-mobile/ios/AvaxWallet.xcodeproj/project.pbxproj +++ b/packages/core-mobile/ios/AvaxWallet.xcodeproj/project.pbxproj @@ -602,10 +602,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-common-AvaxWalletInternal/Pods-common-AvaxWalletInternal-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-common-AvaxWalletInternal/Pods-common-AvaxWalletInternal-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-common-AvaxWalletInternal/Pods-common-AvaxWalletInternal-frameworks.sh\"\n"; @@ -817,10 +821,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-common-AvaxWallet/Pods-common-AvaxWallet-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-common-AvaxWallet/Pods-common-AvaxWallet-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-common-AvaxWallet/Pods-common-AvaxWallet-resources.sh\"\n"; diff --git a/packages/core-mobile/ios/AvaxWallet/Info.plist b/packages/core-mobile/ios/AvaxWallet/Info.plist index 833328afa0..887093f846 100644 --- a/packages/core-mobile/ios/AvaxWallet/Info.plist +++ b/packages/core-mobile/ios/AvaxWallet/Info.plist @@ -51,6 +51,11 @@ $(CURRENT_PROJECT_VERSION) ITSAppUsesNonExemptEncryption + LSApplicationQueriesSchemes + + twitter + mailto + LSMinimumSystemVersion 13.3.0 LSRequiresIPhoneOS diff --git a/packages/core-mobile/ios/Podfile.lock b/packages/core-mobile/ios/Podfile.lock index 50c8fb8f38..3dfa511c76 100644 --- a/packages/core-mobile/ios/Podfile.lock +++ b/packages/core-mobile/ios/Podfile.lock @@ -62,6 +62,7 @@ PODS: - DatadogWebViewTracking (2.24.1): - DatadogInternal (= 2.24.1) - DGSwiftUtilities (0.47.0) + - DGSwiftUtilities (0.47.0) - DoubleConversion (1.1.6) - EXApplication (6.1.5): - ExpoModulesCore From ea15c43006dbde8fbf5d6fab8e36bf0f9d83e389 Mon Sep 17 00:00:00 2001 From: B0Y3R-AVA Date: Fri, 8 Aug 2025 12:12:48 -0400 Subject: [PATCH 02/24] iOS able to pull addresses from ledger and create a new wallet, also tested that it can recieve funds on avalanche c chain --- .../hooks/useLedgerBasePublickKeyFetcher.ts | 210 ++++++++ .../(modals)/accountSettings/importWallet.tsx | 19 + .../ledger/confirmAddresses.tsx | 236 ++++++++ .../accountSettings}/ledger/connectWallet.tsx | 51 +- .../new/routes/onboarding/ledger/_layout.tsx | 11 - .../app/services/ledger/LedgerService.ts | 503 +++--------------- 6 files changed, 567 insertions(+), 463 deletions(-) create mode 100644 packages/core-mobile/app/new/features/ledger/hooks/useLedgerBasePublickKeyFetcher.ts create mode 100644 packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx rename packages/core-mobile/app/new/routes/{onboarding => (signedIn)/(modals)/accountSettings}/ledger/connectWallet.tsx (90%) delete mode 100644 packages/core-mobile/app/new/routes/onboarding/ledger/_layout.tsx diff --git a/packages/core-mobile/app/new/features/ledger/hooks/useLedgerBasePublickKeyFetcher.ts b/packages/core-mobile/app/new/features/ledger/hooks/useLedgerBasePublickKeyFetcher.ts new file mode 100644 index 0000000000..580ee015cf --- /dev/null +++ b/packages/core-mobile/app/new/features/ledger/hooks/useLedgerBasePublickKeyFetcher.ts @@ -0,0 +1,210 @@ +import { useState, useCallback, useEffect } from 'react' +import { LedgerService } from 'services/ledger/ledgerService' +import { NetworkVMType } from '@avalabs/core-chains-sdk' +import Logger from 'utils/Logger' + +export interface BasePublicKey { + path: string + key: string + chainCode: string + network: NetworkVMType +} + +export interface PublicKeyInfo { + key: string + derivationPath: string + curve: 'secp256k1' | 'ed25519' + network: NetworkVMType +} + +export interface UseLedgerBasePublickKeyFetcherProps { + deviceId?: string + derivationPathSpec?: 'BIP44' | 'LedgerLive' + accountIndex?: number + count?: number +} + +export interface UseLedgerBasePublickKeyFetcherReturn { + basePublicKeys: BasePublicKey[] + publicKeys: PublicKeyInfo[] + isLoading: boolean + error: string | null + connect: (deviceId: string) => Promise + fetchBasePublicKeys: () => Promise + fetchPublicKeys: () => Promise + disconnect: () => Promise + isConnected: boolean +} + +export const useLedgerBasePublickKeyFetcher = ({ + deviceId, + derivationPathSpec = 'BIP44', + accountIndex = 0, + count = 5 +}: UseLedgerBasePublickKeyFetcherProps = {}): UseLedgerBasePublickKeyFetcherReturn => { + const [ledgerService] = useState(() => new LedgerService()) + const [basePublicKeys, setBasePublicKeys] = useState([]) + const [publicKeys, setPublicKeys] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [isConnected, setIsConnected] = useState(false) + + const connect = useCallback(async (deviceId: string) => { + setIsLoading(true) + setError(null) + + try { + await ledgerService.connect(deviceId) + setIsConnected(true) + console.log('Successfully connected to Ledger device') + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error' + setError(`Failed to connect to Ledger: ${errorMessage}`) + Logger.error('Ledger connection failed:', err) + setIsConnected(false) + } finally { + setIsLoading(false) + } + }, [ledgerService]) + + const fetchBasePublicKeys = useCallback(async () => { + if (!isConnected) { + setError('Device not connected') + return + } + + setIsLoading(true) + setError(null) + + try { + // Get extended public keys for BIP44 derivation + const extendedPublicKeys = await ledgerService.getExtendedPublicKeys() + + const baseKeys: BasePublicKey[] = [ + { + path: extendedPublicKeys.evm.path, + key: extendedPublicKeys.evm.key, + chainCode: extendedPublicKeys.evm.chainCode, + network: NetworkVMType.EVM + }, + { + path: extendedPublicKeys.avalanche.path, + key: extendedPublicKeys.avalanche.key, + chainCode: extendedPublicKeys.avalanche.chainCode, + network: NetworkVMType.AVM + } + ] + + setBasePublicKeys(baseKeys) + console.log('Successfully fetched base public keys') + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error' + setError(`Failed to fetch base public keys: ${errorMessage}`) + Logger.error('Base public key fetching failed:', err) + } finally { + setIsLoading(false) + } + }, [isConnected, ledgerService]) + + const fetchPublicKeys = useCallback(async () => { + if (!isConnected) { + setError('Device not connected') + return + } + + setIsLoading(true) + setError(null) + + try { + // Get individual public keys for LedgerLive derivation + const keys = await ledgerService.getPublicKeys(accountIndex, count) + + const publicKeyInfos: PublicKeyInfo[] = keys.map(key => ({ + key: key.key, + derivationPath: key.derivationPath, + curve: key.curve, + network: getNetworkFromDerivationPath(key.derivationPath) + })) + + setPublicKeys(publicKeyInfos) + console.log('Successfully fetched public keys') + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error' + setError(`Failed to fetch public keys: ${errorMessage}`) + Logger.error('Public key fetching failed:', err) + } finally { + setIsLoading(false) + } + }, [isConnected, accountIndex, count, ledgerService]) + + const disconnect = useCallback(async () => { + try { + await ledgerService.disconnect() + setIsConnected(false) + setBasePublicKeys([]) + setPublicKeys([]) + setError(null) + console.log('Successfully disconnected from Ledger device') + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error' + setError(`Failed to disconnect: ${errorMessage}`) + Logger.error('Ledger disconnection failed:', err) + } + }, [ledgerService]) + + // Helper function to determine network from derivation path + const getNetworkFromDerivationPath = (derivationPath: string): NetworkVMType => { + if (derivationPath.includes("'/60'")) { + return NetworkVMType.EVM + } else if (derivationPath.includes("'/9000'")) { + return NetworkVMType.AVM + } else if (derivationPath.includes("'/0'")) { + return NetworkVMType.BITCOIN + } else if (derivationPath.includes("'/501'")) { + return NetworkVMType.SVM + } + return NetworkVMType.EVM // Default fallback + } + + // Auto-connect when deviceId changes + useEffect(() => { + if (deviceId && !isConnected) { + connect(deviceId) + } + }, [deviceId, connect, isConnected]) + + // Auto-fetch base public keys when connected and in BIP44 mode + useEffect(() => { + if (isConnected && derivationPathSpec === 'BIP44') { + fetchBasePublicKeys() + } + }, [isConnected, derivationPathSpec, fetchBasePublicKeys]) + + // Auto-fetch public keys when connected and in LedgerLive mode + useEffect(() => { + if (isConnected && derivationPathSpec === 'LedgerLive') { + fetchPublicKeys() + } + }, [isConnected, derivationPathSpec, fetchPublicKeys]) + + // Cleanup on unmount + useEffect(() => { + return () => { + if (isConnected) { + disconnect() + } + } + }, [isConnected, disconnect]) + + return { + basePublicKeys, + publicKeys, + isLoading, + error, + connect, + fetchBasePublicKeys, + fetchPublicKeys, + disconnect, + isConnected + } +} \ No newline at end of file diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/importWallet.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/importWallet.tsx index a0e1c173e5..958d324d19 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/importWallet.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/importWallet.tsx @@ -113,6 +113,25 @@ const ImportWalletScreen = (): JSX.Element => { ), onPress: handleImportPrivateKey + }, + { + title: 'Import from Ledger', + subtitle: ( + + Access with an existing Ledger + + ), + leftIcon: ( + + ), + accessory: ( + + ), + onPress: handleImportLedger } ] diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx new file mode 100644 index 0000000000..99c51fd8cb --- /dev/null +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx @@ -0,0 +1,236 @@ +import React, { useState, useEffect } from 'react' +import { View } from 'react-native' +import { useLocalSearchParams, useRouter } from 'expo-router' +import { useDispatch } from 'react-redux' + +import { Button, Card, Text as K2Text, useTheme } from '@avalabs/k2-alpine' +import { ScrollScreen } from 'common/components/ScrollScreen' +import LedgerService, { AddressInfo } from 'services/ledger/ledgerService' +import { WalletType } from 'services/wallet/types' +import { storeWallet } from 'store/wallet/thunks' +import { setActiveWallet } from 'store/wallet/slice' +import { setAccount, setActiveAccount } from 'store/account' +import { CoreAccountType } from '@avalabs/types' +import { uuid } from 'utils/uuid' +import Logger from 'utils/Logger' +import { AppThunkDispatch } from 'store/types' +import { PrimaryAccount } from 'store/account/types' + +export default function ConfirmAddresses(): JSX.Element { + const router = useRouter() + const dispatch = useDispatch() + const { + theme: { colors } + } = useTheme() + const params = useLocalSearchParams<{ deviceId: string }>() + + const [isLoading, setIsLoading] = useState(false) + const [addresses, setAddresses] = useState([]) + const [error, setError] = useState(null) + + const connectToDevice = async () => { + if (!params.deviceId) { + setError('No device ID provided') + return + } + + setIsLoading(true) + setError(null) + + try { + await LedgerService.connect(params.deviceId) + Logger.info('Successfully connected to Ledger device') + + // Get addresses for the first account + const deviceAddresses = await LedgerService.getAllAddresses(0, 1) + setAddresses(deviceAddresses) + } catch (err) { + Logger.error('Failed to connect to Ledger:', err) + setError( + err instanceof Error + ? err.message + : 'Failed to connect to Ledger device' + ) + } finally { + setIsLoading(false) + } + } + + const handleConfirm = async () => { + if (!params.deviceId) { + setError('No device ID provided') + return + } + + setIsLoading(true) + setError(null) + + try { + // Get extended public keys for wallet creation + const extendedPublicKeys = await LedgerService.getExtendedPublicKeys() + + // Get individual public keys for the first account + const publicKeys = await LedgerService.getPublicKeys(0, 1) + + // Create wallet data to store + const walletId = uuid() + const walletData = { + deviceId: params.deviceId, + derivationPath: "m/44'/60'/0'/0/0", + vmType: 'EVM', + derivationPathSpec: 'BIP44', + extendedPublicKeys: { + evm: extendedPublicKeys.evm.key, + avalanche: extendedPublicKeys.avalanche.key + }, + publicKeys: publicKeys.map(pk => ({ + key: pk.key, + derivationPath: pk.derivationPath, + curve: pk.curve, + type: 'address-pubkey' + })) + } + + // Store the wallet + await dispatch( + storeWallet({ + walletId, + walletSecret: JSON.stringify(walletData), + type: WalletType.LEDGER + }) + ).unwrap() + + // Set as active wallet + dispatch(setActiveWallet(walletId)) + + // Create account from addresses + const evmAddress = addresses.find( + addr => addr.network === 'AVALANCHE_C_EVM' + ) + const xChainAddress = addresses.find( + addr => addr.network === 'AVALANCHE_X' + ) + const pChainAddress = addresses.find( + addr => addr.network === 'AVALANCHE_P' + ) + const btcAddress = addresses.find(addr => addr.network === 'BITCOIN') + + const accountId = uuid() + const account: PrimaryAccount = { + id: accountId, + walletId, + name: 'Account 1', + type: CoreAccountType.PRIMARY, + index: 0, + addressC: evmAddress?.address || '', + addressAVM: xChainAddress?.address || '', + addressPVM: pChainAddress?.address || '', + addressBTC: btcAddress?.address || '', + addressSVM: '', + addressCoreEth: evmAddress?.address || '' + } + + Logger.info('Created account:', account) + + // Store account and set as active + dispatch(setAccount(account)) + dispatch(setActiveAccount(accountId)) + + Logger.info('Successfully created Ledger wallet and account') + + // Navigate to success screen + router.push( + '/(signedIn)/(modals)/accountSettings/ledger/importSuccess' as any + ) + } catch (err) { + Logger.error('Failed to create Ledger wallet:', err) + setError( + err instanceof Error ? err.message : 'Failed to create Ledger wallet' + ) + } finally { + setIsLoading(false) + } + } + + const handleBack = () => { + router.back() + } + + // Auto-connect when component mounts + useEffect(() => { + connectToDevice() + }, []) + + // Cleanup on unmount + useEffect(() => { + return () => { + LedgerService.disconnect().catch(Logger.error) + } + }, []) + + return ( + + + + Confirm Addresses + + + {error && ( + + + {error} + + + )} + + {isLoading ? ( + + + {addresses.length > 0 + ? 'Creating wallet...' + : 'Connecting to Ledger device...'} + + + ) : addresses.length > 0 ? ( + + + Please verify these addresses on your Ledger device: + + + {addresses.map(address => ( + + + {address.network} + + + {address.address} + + + {address.derivationPath} + + + ))} + + + + + + + ) : null} + + + ) +} diff --git a/packages/core-mobile/app/new/routes/onboarding/ledger/connectWallet.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx similarity index 90% rename from packages/core-mobile/app/new/routes/onboarding/ledger/connectWallet.tsx rename to packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx index f536783440..ad3f70cb8f 100644 --- a/packages/core-mobile/app/new/routes/onboarding/ledger/connectWallet.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx @@ -10,6 +10,9 @@ import { import TransportBLE from '@ledgerhq/react-native-hw-transport-ble' import { Button, Card, Text as K2Text, useTheme } from '@avalabs/k2-alpine' +import { useRouter } from 'expo-router' +import { ScrollScreen } from 'common/components/ScrollScreen' +import { LedgerService } from 'services/ledger/ledgerService' interface Device { id: string @@ -23,9 +26,11 @@ interface TransportState { } export default function ConnectWallet(): JSX.Element { + const router = useRouter() const { theme: { colors } } = useTheme() + const [ledgerService] = useState(() => new LedgerService()) const [transportState, setTransportState] = useState({ available: false, powered: false @@ -41,7 +46,7 @@ export default function ConnectWallet(): JSX.Element { next: event => { setTransportState({ available: event.available, - powered: false // Remove powered property since it doesn't exist + powered: false }) }, complete: () => { @@ -186,21 +191,28 @@ export default function ConnectWallet(): JSX.Element { } }, []) - // Connect to a specific device with error handling for TurboModule issue + // Connect to a specific device using LedgerService const connectToDevice = useCallback( async (device: Device) => { setIsConnecting(true) try { - const connectionPromise = TransportBLE.open(device.id) - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('Connection timeout')), 15000) - ) - - await Promise.race([connectionPromise, timeoutPromise]) + // Use LedgerService to connect + await ledgerService.connect(device.id) setConnectedDevice(device) - Alert.alert('Success', `Connected to ${device.name}`) + Alert.alert('Success', `Connected to ${device.name}`, [ + { + text: 'Continue', + onPress: () => { + // Navigate to confirm addresses screen with device ID + router.push({ + pathname: '/accountSettings/ledger/confirmAddresses' as any, + params: { deviceId: device.id } + }) + } + } + ]) } catch (error) { let errorMessage = 'Unknown connection error' @@ -237,13 +249,14 @@ export default function ConnectWallet(): JSX.Element { setIsConnecting(false) } }, - [openBluetoothSettings] + [openBluetoothSettings, router, ledgerService] ) // Disconnect from current device const disconnectDevice = useCallback(async () => { try { - // Close any open transport connections + // Use LedgerService to disconnect + await ledgerService.disconnect() setConnectedDevice(null) Alert.alert( 'Disconnected', @@ -257,7 +270,7 @@ export default function ConnectWallet(): JSX.Element { }` ) } - }, []) + }, [ledgerService]) const renderDevice = ({ item }: { item: Device }) => ( @@ -292,14 +305,10 @@ export default function ConnectWallet(): JSX.Element { ) return ( - - - Connect Ledger Device - - + {/* BLE Status */} )} - + ) } diff --git a/packages/core-mobile/app/new/routes/onboarding/ledger/_layout.tsx b/packages/core-mobile/app/new/routes/onboarding/ledger/_layout.tsx deleted file mode 100644 index 529742ee0e..0000000000 --- a/packages/core-mobile/app/new/routes/onboarding/ledger/_layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { stackNavigatorScreenOptions } from 'common/consts/screenOptions' -import { Stack } from 'common/components/Stack' -import React from 'react' - -export default function LedgerOnboardingLayout(): JSX.Element { - return ( - - - - ) -} \ No newline at end of file diff --git a/packages/core-mobile/app/services/ledger/LedgerService.ts b/packages/core-mobile/app/services/ledger/LedgerService.ts index e3345668b7..4827b63185 100644 --- a/packages/core-mobile/app/services/ledger/LedgerService.ts +++ b/packages/core-mobile/app/services/ledger/LedgerService.ts @@ -1,69 +1,40 @@ import TransportBLE from '@ledgerhq/react-native-hw-transport-ble' -import Transport from '@ledgerhq/hw-transport' import AppAvalanche from '@avalabs/hw-app-avalanche' -import AppSolana from '@ledgerhq/hw-app-solana' import { NetworkVMType } from '@avalabs/core-chains-sdk' import { getAddressDerivationPath } from 'services/wallet/utils' import { ChainName } from 'services/network/consts' -import { - getBtcAddressFromPubKey, - getSolanaPublicKeyFromLedger, - getLedgerAppInfo -} from '@avalabs/core-wallets-sdk' +import { getBtcAddressFromPubKey } from '@avalabs/core-wallets-sdk' import { networks } from 'bitcoinjs-lib' -import Logger from 'utils/Logger' -import bs58 from 'bs58' -import { - LEDGER_TIMEOUTS, - getSolanaDerivationPath -} from 'new/features/ledger/consts' -import { assertNotNull } from 'utils/assertions' -import { - AddressInfo, - ExtendedPublicKey, - PublicKeyInfo, - LedgerAppType, - LedgerReturnCode, - AppInfo -} from './types' -export class LedgerService { - #transport: TransportBLE | null = null - private currentAppType: LedgerAppType = LedgerAppType.UNKNOWN - private appPollingInterval: number | null = null - private appPollingEnabled = false +export interface AddressInfo { + id: string + address: string + derivationPath: string + network: string +} - // Transport getter/setter with automatic error handling - private get transport(): TransportBLE { - assertNotNull( - this.#transport, - 'Ledger transport is not initialized. Please connect to a device first.' - ) - return this.#transport - } +export interface ExtendedPublicKey { + path: string + key: string + chainCode: string +} - private set transport(transport: TransportBLE) { - this.#transport = transport - } +export interface PublicKeyInfo { + key: string + derivationPath: string + curve: 'secp256k1' | 'ed25519' +} + +export class LedgerService { + private transport: any = null + private avalancheApp: AppAvalanche | null = null - // Connect to Ledger device (transport only, no apps) + // Connect to Ledger device async connect(deviceId: string): Promise { try { - Logger.info('Starting BLE connection attempt with deviceId:', deviceId) - // Use a longer timeout for connection - this.transport = await TransportBLE.open( - deviceId, - LEDGER_TIMEOUTS.CONNECTION_TIMEOUT - ) - Logger.info('BLE transport connected successfully') - this.currentAppType = LedgerAppType.UNKNOWN - - // Start passive app detection - Logger.info('Starting app polling...') - this.startAppPolling() - Logger.info('App polling started') + this.transport = await TransportBLE.open(deviceId) + this.avalancheApp = new AppAvalanche(this.transport) } catch (error) { - Logger.error('Failed to connect to Ledger', error) throw new Error( `Failed to connect to Ledger: ${ error instanceof Error ? error.message : 'Unknown error' @@ -72,166 +43,27 @@ export class LedgerService { } } - // Start passive app detection polling - private startAppPolling(): void { - if (this.appPollingEnabled) return - - this.appPollingEnabled = true - this.appPollingInterval = setInterval(async () => { - try { - if (!this.#transport || !this.#transport.isConnected) { - this.stopAppPolling() - return - } - - const appInfo = await this.getCurrentAppInfo() - const newAppType = this.mapAppNameToType(appInfo.applicationName) - - if (newAppType !== this.currentAppType) { - Logger.info( - `App changed from ${this.currentAppType} to ${newAppType}` - ) - this.currentAppType = newAppType - } - } catch (error) { - Logger.error('Error polling app info', error) - // Don't stop polling on error, just log it - } - }, LEDGER_TIMEOUTS.APP_POLLING_INTERVAL) // Poll every 2 seconds like the extension - } - - // Stop passive app detection polling - private stopAppPolling(): void { - if (this.appPollingInterval) { - clearInterval(this.appPollingInterval) - this.appPollingInterval = null - } - this.appPollingEnabled = false - } - - // Get current app info from device - private async getCurrentAppInfo(): Promise { - return await getLedgerAppInfo(this.transport as Transport) - } - - // Map app name to our enum - private mapAppNameToType(appName: string): LedgerAppType { - switch (appName.toLowerCase()) { - case 'avalanche': - return LedgerAppType.AVALANCHE - case 'solana': - return LedgerAppType.SOLANA - case 'ethereum': - return LedgerAppType.ETHEREUM - default: - return LedgerAppType.UNKNOWN - } - } - - // Get current app type (passive detection) - getCurrentAppType(): LedgerAppType { - return this.currentAppType - } - - // Wait for specific app to be open (passive approach) - async waitForApp( - appType: LedgerAppType, - timeoutMs = LEDGER_TIMEOUTS.APP_WAIT_TIMEOUT - ): Promise { - const startTime = Date.now() - Logger.info(`Waiting for ${appType} app (timeout: ${timeoutMs}ms)...`) - - while (Date.now() - startTime < timeoutMs) { - Logger.info( - `Current app type: ${this.currentAppType}, waiting for: ${appType}` - ) - - if (this.currentAppType === appType) { - Logger.info(`${appType} app is ready`) - return - } - - // Wait before next check - await new Promise(resolve => - setTimeout(resolve, LEDGER_TIMEOUTS.APP_CHECK_DELAY) - ) - } - - Logger.error(`Timeout waiting for ${appType} app after ${timeoutMs}ms`) - throw new Error( - `Timeout waiting for ${appType} app. Please open the ${appType} app on your Ledger device.` - ) - } - - // Check if specific app is currently open - async isAppOpen(appType: LedgerAppType): Promise { - try { - const appInfo = await this.getCurrentAppInfo() - const currentAppType = this.mapAppNameToType(appInfo.applicationName) - return currentAppType === appType - } catch (error) { - Logger.error('Error checking app status', error) - return false - } - } - - // Reconnect to device if disconnected - private async reconnectIfNeeded(deviceId: string): Promise { - Logger.info('Checking if reconnection is needed') - - if (!this.#transport || !this.#transport.isConnected) { - Logger.info('Transport is disconnected, attempting reconnection') - try { - await this.connect(deviceId) - Logger.info('Reconnection successful') - } catch (error) { - Logger.error('Reconnection failed:', error) - throw error // Re-throw to propagate the error - } - } else { - Logger.info('Transport is already connected') - } - } - // Get extended public keys for BIP44 derivation async getExtendedPublicKeys(): Promise<{ evm: ExtendedPublicKey avalanche: ExtendedPublicKey }> { - Logger.info('=== getExtendedPublicKeys STARTED ===') - Logger.info('Current app type:', this.currentAppType) - - // Connect to Avalanche app - Logger.info('Waiting for Avalanche app...') - await this.waitForApp(LedgerAppType.AVALANCHE) - Logger.info('Avalanche app detected, creating app instance...') - - // Create Avalanche app instance - const avalancheApp = new AppAvalanche(this.transport as Transport) - Logger.info('Avalanche app instance created') + if (!this.avalancheApp) { + throw new Error('Avalanche app not initialized') + } try { // Get EVM extended public key (m/44'/60'/0') - Logger.info('Getting EVM extended public key...') - const evmPath = getAddressDerivationPath({ - accountIndex: 0, - vmType: NetworkVMType.EVM - }).replace('/0/0', '') - Logger.info('EVM derivation path:', evmPath) - - const evmXpubResponse = await avalancheApp.getExtendedPubKey( - evmPath, + const evmXpubResponse = await this.avalancheApp.getExtendedPubKey( + getAddressDerivationPath({ + accountIndex: 0, + vmType: NetworkVMType.EVM + }).replace('/0/0', ''), false ) - Logger.info('EVM response return code:', evmXpubResponse.returnCode) - // Check for error response - if (evmXpubResponse.returnCode !== LedgerReturnCode.SUCCESS) { - Logger.error( - 'EVM extended public key error:', - evmXpubResponse.errorMessage - ) + if (evmXpubResponse.returnCode !== 0x9000) { throw new Error( `EVM extended public key error: ${ evmXpubResponse.errorMessage || 'Unknown error' @@ -239,32 +71,17 @@ export class LedgerService { ) } - Logger.info('EVM extended public key retrieved successfully') - // Get Avalanche extended public key (m/44'/9000'/0') - Logger.info('Getting Avalanche extended public key...') - const avalanchePath = getAddressDerivationPath({ - accountIndex: 0, - vmType: NetworkVMType.AVM - }).replace('/0/0', '') - Logger.info('Avalanche derivation path:', avalanchePath) - - const avalancheXpubResponse = await avalancheApp.getExtendedPubKey( - avalanchePath, + const avalancheXpubResponse = await this.avalancheApp.getExtendedPubKey( + getAddressDerivationPath({ + accountIndex: 0, + vmType: NetworkVMType.AVM + }).replace('/0/0', ''), false ) - Logger.info( - 'Avalanche response return code:', - avalancheXpubResponse.returnCode - ) - // Check for error response - if (avalancheXpubResponse.returnCode !== LedgerReturnCode.SUCCESS) { - Logger.error( - 'Avalanche extended public key error:', - avalancheXpubResponse.errorMessage - ) + if (avalancheXpubResponse.returnCode !== 0x9000) { throw new Error( `Avalanche extended public key error: ${ avalancheXpubResponse.errorMessage || 'Unknown error' @@ -272,8 +89,6 @@ export class LedgerService { ) } - Logger.info('Avalanche extended public key retrieved successfully') - return { evm: { path: getAddressDerivationPath({ @@ -313,153 +128,18 @@ export class LedgerService { ) } } - Logger.error('=== getExtendedPublicKeys FAILED ===', error) throw new Error(`Failed to get extended public keys: ${error}`) } } - // Check if Solana app is open - async checkSolanaApp(): Promise { - if (!this.#transport) { - return false - } - - try { - // Create fresh Solana app instance - const transport = await this.getTransport() - const solanaApp = new AppSolana(transport as Transport) - // Try to get a simple address to check if app is open - // Use a standard Solana derivation path - const testPath = "m/44'/501'/0'" - await solanaApp.getAddress(testPath, false) - return true - } catch (error) { - Logger.error('Solana app not open or not available', error) - return false - } - } - - // Get Solana address for a specific derivation path - async getSolanaAddress(derivationPath: string): Promise<{ address: Buffer }> { - await this.waitForApp(LedgerAppType.SOLANA) - const transport = await this.getTransport() - const solanaApp = new AppSolana(transport as Transport) - return await solanaApp.getAddress(derivationPath, false) - } - - // Get Solana public keys using SDK function (like extension) - async getSolanaPublicKeys( - startIndex: number, - count: number - ): Promise { - // Create a fresh AppSolana instance for each call (like the SDK does) - const transport = await this.getTransport() - const freshSolanaApp = new AppSolana(transport as Transport) - const publicKeys: PublicKeyInfo[] = [] - - try { - for (let i = startIndex; i < startIndex + count; i++) { - // Use correct Solana derivation path format - const derivationPath = getSolanaDerivationPath(i) - - // Simple direct call to get Solana address using fresh instance - const result = await freshSolanaApp.getAddress(derivationPath, false) - const publicKey = result.address - - publicKeys.push({ - key: publicKey.toString('hex'), - derivationPath, - curve: 'ed25519' - }) - } - - return publicKeys - } catch (error) { - if (error instanceof Error) { - if (error.message.includes('6a80')) { - throw new Error( - 'Wrong app open. Please open the Solana app on your Ledger device.' - ) - } - throw new Error(`Failed to get Solana address: ${error.message}`) - } - throw new Error('Failed to get Solana address') - } - } - - // Alternative method using the SDK function (like the extension does) - async getSolanaPublicKeysViaSDK( - startIndex: number, - _count: number - ): Promise { - try { - // Use the SDK function directly (like the extension does) - const publicKey = await getSolanaPublicKeyFromLedger( - startIndex, - this.transport as Transport - ) - - const publicKeys: PublicKeyInfo[] = [ - { - key: publicKey.toString('hex'), - derivationPath: getSolanaDerivationPath(startIndex), - curve: 'ed25519' - } - ] - - return publicKeys - } catch (error) { - if (error instanceof Error) { - if (error.message.includes('6a80')) { - throw new Error( - 'Wrong app open. Please open the Solana app on your Ledger device.' - ) - } - throw new Error( - `Failed to get Solana address via SDK: ${error.message}` - ) - } - throw new Error('Failed to get Solana address via SDK') - } - } - - // Get Solana addresses from public keys - async getSolanaAddresses( - startIndex: number, - count: number - ): Promise { - Logger.info('Starting getSolanaAddresses') - try { - const publicKeys = await this.getSolanaPublicKeys(startIndex, count) - Logger.info('Got Solana public keys, converting to addresses') - - return publicKeys.map((pk, index) => { - // Convert public key to Solana address (Base58 encoding) - const address = bs58.encode(Uint8Array.from(Buffer.from(pk.key, 'hex'))) - - return { - id: `solana-${startIndex + index}`, - address, - derivationPath: pk.derivationPath, - network: ChainName.SOLANA - } - }) - } catch (error) { - Logger.error('Failed in getSolanaAddresses', error) - throw error - } - } - // Get individual public keys for LedgerLive derivation async getPublicKeys( startIndex: number, count: number ): Promise { - // Connect to Avalanche app - await this.waitForApp(LedgerAppType.AVALANCHE) - - // Create Avalanche app instance - const avalancheApp = new AppAvalanche(this.transport as Transport) + if (!this.avalancheApp) { + throw new Error('Avalanche app not initialized') + } const publicKeys: PublicKeyInfo[] = [] @@ -470,7 +150,7 @@ export class LedgerService { accountIndex: i, vmType: NetworkVMType.EVM }) - const evmResponse = await avalancheApp.getAddressAndPubKey( + const evmResponse = await this.avalancheApp.getAddressAndPubKey( evmPath, false, 'avax' @@ -486,7 +166,7 @@ export class LedgerService { accountIndex: i, vmType: NetworkVMType.AVM }) - const avmResponse = await avalancheApp.getAddressAndPubKey( + const avmResponse = await this.avalancheApp.getAddressAndPubKey( avmPath, false, 'avax' @@ -502,7 +182,7 @@ export class LedgerService { accountIndex: i, vmType: NetworkVMType.BITCOIN }) - const btcResponse = await avalancheApp.getAddressAndPubKey( + const btcResponse = await this.avalancheApp.getAddressAndPubKey( btcPath, false, 'bc' @@ -525,11 +205,9 @@ export class LedgerService { startIndex: number, count: number ): Promise { - // Connect to Avalanche app - await this.waitForApp(LedgerAppType.AVALANCHE) - - // Create Avalanche app instance - const avalancheApp = new AppAvalanche(this.transport as Transport) + if (!this.avalancheApp) { + throw new Error('Avalanche app not initialized') + } const addresses: AddressInfo[] = [] @@ -541,7 +219,7 @@ export class LedgerService { accountIndex: i, vmType: NetworkVMType.EVM }) - const evmAddressResponse = await avalancheApp.getETHAddress( + const evmAddressResponse = await this.avalancheApp.getETHAddress( evmPath, false // don't display on device ) @@ -557,11 +235,12 @@ export class LedgerService { accountIndex: i, vmType: NetworkVMType.AVM }) - const xChainAddressResponse = await avalancheApp.getAddressAndPubKey( - xChainPath, - false, - 'avax' // hrp for mainnet - ) + const xChainAddressResponse = + await this.avalancheApp.getAddressAndPubKey( + xChainPath, + false, + 'avax' // hrp for mainnet + ) addresses.push({ id: `avalanche-x-${i}`, address: xChainAddressResponse.address, @@ -574,12 +253,13 @@ export class LedgerService { accountIndex: i, vmType: NetworkVMType.AVM }) - const pChainAddressResponse = await avalancheApp.getAddressAndPubKey( - pChainPath, - false, - 'avax', // hrp for mainnet - '2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM' // P-Chain ID - ) + const pChainAddressResponse = + await this.avalancheApp.getAddressAndPubKey( + pChainPath, + false, + 'avax', // hrp for mainnet + '2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM' // P-Chain ID + ) addresses.push({ id: `avalanche-p-${i}`, address: pChainAddressResponse.address, @@ -592,11 +272,12 @@ export class LedgerService { accountIndex: i, vmType: NetworkVMType.EVM // Use EVM path for Bitcoin }) - const btcPublicKeyResponse = await avalancheApp.getAddressAndPubKey( - btcPath, - false, - 'avax' // hrp for mainnet - ) + const btcPublicKeyResponse = + await this.avalancheApp.getAddressAndPubKey( + btcPath, + false, + 'avax' // hrp for mainnet + ) const btcAddress = getBtcAddressFromPubKey( Buffer.from(btcPublicKeyResponse.publicKey.toString('hex'), 'hex'), networks.bitcoin // mainnet @@ -615,54 +296,14 @@ export class LedgerService { return addresses } - // Get all addresses including Solana (requires app switching) - async getAllAddressesWithSolana( - startIndex: number, - count: number - ): Promise { - const addresses: AddressInfo[] = [] - - try { - // Get Avalanche addresses first - const avalancheAddresses = await this.getAllAddresses(startIndex, count) - addresses.push(...avalancheAddresses) - - // Get Solana addresses - const solanaAddresses = await this.getSolanaAddresses(startIndex, count) - addresses.push(...solanaAddresses) - - return addresses - } catch (error) { - Logger.error('Failed to get all addresses with Solana', error) - throw error - } - } - // Disconnect from Ledger device async disconnect(): Promise { - if (this.#transport) { - await this.#transport.close() - this.#transport = null - this.currentAppType = LedgerAppType.UNKNOWN - this.stopAppPolling() // Stop polling on disconnect + if (this.transport) { + await this.transport.close() + this.transport = null + this.avalancheApp = null } } - - // Check if transport is available and connected - isConnected(): boolean { - return this.#transport !== null && this.#transport.isConnected - } - - // Ensure connection is established for a specific device - async ensureConnection(deviceId: string): Promise { - await this.reconnectIfNeeded(deviceId) - return this.transport - } - - // Get the current transport (for compatibility with existing code) - async getTransport(): Promise { - return this.transport - } } export default new LedgerService() From 970b5a0e5cc9aad52fff4863b7a9bfeed11c7fa5 Mon Sep 17 00:00:00 2001 From: B0Y3R-AVA Date: Sun, 10 Aug 2025 16:32:31 -0400 Subject: [PATCH 03/24] connection and wallet creation working --- .../ledger/confirmAddresses.tsx | 696 +++++++++++++----- .../app/services/ledger/LedgerService.ts | 510 +++++++++++-- 2 files changed, 984 insertions(+), 222 deletions(-) diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx index 99c51fd8cb..37a5db4588 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx @@ -1,236 +1,578 @@ -import React, { useState, useEffect } from 'react' -import { View } from 'react-native' -import { useLocalSearchParams, useRouter } from 'expo-router' -import { useDispatch } from 'react-redux' - -import { Button, Card, Text as K2Text, useTheme } from '@avalabs/k2-alpine' -import { ScrollScreen } from 'common/components/ScrollScreen' -import LedgerService, { AddressInfo } from 'services/ledger/ledgerService' +import React, { useState, useCallback, useEffect } from 'react' +import { Alert } from 'react-native' +import { useRouter, useLocalSearchParams } from 'expo-router' +import { useDispatch, useSelector } from 'react-redux' +import { + View, + Text, + Button, + Card, + Icons, + useTheme, + CircularProgress +} from '@avalabs/k2-alpine' +import { LoadingState } from 'new/common/components/LoadingState' +import { LedgerService, LedgerAppType } from 'services/ledger/ledgerService' import { WalletType } from 'services/wallet/types' +import { AppThunkDispatch } from 'store/types' import { storeWallet } from 'store/wallet/thunks' import { setActiveWallet } from 'store/wallet/slice' -import { setAccount, setActiveAccount } from 'store/account' +import { setAccount, setActiveAccount, selectAccounts } from 'store/account' +import { Account } from 'store/account/types' import { CoreAccountType } from '@avalabs/types' +import { showSnackbar } from 'new/common/utils/toast' import { uuid } from 'utils/uuid' import Logger from 'utils/Logger' -import { AppThunkDispatch } from 'store/types' -import { PrimaryAccount } from 'store/account/types' +import { ScrollScreen } from 'common/components/ScrollScreen' -export default function ConfirmAddresses(): JSX.Element { - const router = useRouter() +export default function ConfirmAddresses() { + const params = useLocalSearchParams<{ + deviceId: string + deviceName: string + }>() + const [step, setStep] = useState< + 'connecting' | 'solana' | 'avalanche' | 'complete' + >('connecting') + const [isLoading, setIsLoading] = useState(false) + const [solanaKeys, setSolanaKeys] = useState([]) + const [avalancheKeys, setAvalancheKeys] = useState(null) + const [bitcoinAddress, setBitcoinAddress] = useState('') + const [xpAddress, setXpAddress] = useState('') + const [ledgerService] = useState(() => new LedgerService()) const dispatch = useDispatch() + const router = useRouter() + const allAccounts = useSelector(selectAccounts) const { theme: { colors } } = useTheme() - const params = useLocalSearchParams<{ deviceId: string }>() - const [isLoading, setIsLoading] = useState(false) - const [addresses, setAddresses] = useState([]) - const [error, setError] = useState(null) + const getSolanaKeys = useCallback(async () => { + try { + setIsLoading(true) + Logger.info('Getting Solana keys with passive app detection') + + // Wait for Solana app to be open (passive detection) + await ledgerService.waitForApp(LedgerAppType.SOLANA) - const connectToDevice = async () => { - if (!params.deviceId) { - setError('No device ID provided') - return + // Get Solana keys + const keys = await ledgerService.getSolanaPublicKeys(0) + setSolanaKeys(keys) + Logger.info('Successfully got Solana keys', keys) + + // Prompt for app switch while staying on Solana step + promptForAvalancheSwitch() + } catch (error) { + Logger.error('Failed to get Solana keys', error) + Alert.alert( + 'Solana App Required', + 'Please open the Solana app on your Ledger device, then tap "I\'ve Switched" when ready.', + [ + { text: 'Cancel', style: 'cancel' }, + { text: "I've Switched", onPress: getSolanaKeys } + ] + ) + } finally { + setIsLoading(false) } + }, [ledgerService, promptForAvalancheSwitch]) + + const promptForAvalancheSwitch = useCallback(async () => { + Alert.alert( + 'Switch to Avalanche App', + 'Please switch to the Avalanche app on your Ledger device, then tap "I\'ve Switched" to continue.', + [ + { text: 'Cancel', style: 'cancel' }, + { + text: "I've Switched", + onPress: async () => { + try { + setIsLoading(true) + Logger.info('=== RECONNECTION STARTED ===') + Logger.info( + 'User confirmed Avalanche app switch, reconnecting...' + ) + + // Force disconnect and reconnect to refresh the connection + Logger.info('Disconnecting...') + await ledgerService.disconnect() + Logger.info('Disconnected successfully') - setIsLoading(true) - setError(null) + Logger.info('Waiting 1 second...') + await new Promise(resolve => setTimeout(resolve, 1000)) // Wait 1 second + Logger.info('Reconnecting...') + await ledgerService.connect(params.deviceId!) + Logger.info('Reconnected successfully') + + // Move to Avalanche step and get keys + setStep('avalanche') + Logger.info('Calling getAvalancheKeys...') + await getAvalancheKeys() + Logger.info('=== RECONNECTION COMPLETED ===') + } catch (error) { + Logger.error('Failed to reconnect for Avalanche:', error) + Alert.alert( + 'Reconnection Failed', + 'Failed to reconnect to the device. Please try again.', + [ + { text: 'Cancel', style: 'cancel' }, + { text: 'Retry', onPress: promptForAvalancheSwitch } + ] + ) + } finally { + setIsLoading(false) + } + } + } + ] + ) + }, [ledgerService, params.deviceId, getAvalancheKeys]) + + const getAvalancheKeys = useCallback(async () => { try { - await LedgerService.connect(params.deviceId) - Logger.info('Successfully connected to Ledger device') - - // Get addresses for the first account - const deviceAddresses = await LedgerService.getAllAddresses(0, 1) - setAddresses(deviceAddresses) - } catch (err) { - Logger.error('Failed to connect to Ledger:', err) - setError( - err instanceof Error - ? err.message - : 'Failed to connect to Ledger device' + setIsLoading(true) + Logger.info('=== getAvalancheKeys STARTED ===') + + // Get Avalanche addresses (getAllAddresses will handle app detection internally) + Logger.info('Calling getAllAddresses...') + const addresses = await ledgerService.getAllAddresses(0, 1) + Logger.info('Avalanche app detected successfully') + Logger.info('Successfully got Avalanche addresses:', addresses) + + // Extract addresses from the response + const evmAddress = addresses.find(addr => addr.network === 'Avalanche C-Chain/EVM')?.address || '' + const xpAddress = addresses.find(addr => addr.network === 'Avalanche X-Chain')?.address || '' + const pvmAddress = addresses.find(addr => addr.network === 'Avalanche P-Chain')?.address || '' + const btcAddress = addresses.find(addr => addr.network === 'Bitcoin')?.address || '' + + // Set the addresses + setAvalancheKeys({ + evm: { key: evmAddress }, + avalanche: { key: xpAddress } + }) + setBitcoinAddress(btcAddress) + setXpAddress(xpAddress) + + Logger.info('Successfully extracted addresses:', { + evm: evmAddress, + xp: xpAddress, + pvm: pvmAddress, + btc: btcAddress + }) + + // Successfully got all keys, move to complete step + Logger.info('Setting step to complete...') + setStep('complete') + Logger.info('Ledger setup completed successfully') + } catch (error) { + Logger.error('Failed to get Avalanche keys:', error) + Alert.alert( + 'Avalanche App Required', + 'Please ensure the Avalanche app is open on your Ledger device, then tap "Retry".', + [ + { text: 'Cancel', style: 'cancel' }, + { text: 'Retry', onPress: getAvalancheKeys } + ] ) } finally { setIsLoading(false) + Logger.info('=== getAvalancheKeys FINISHED ===') } - } + }, [ledgerService]) - const handleConfirm = async () => { - if (!params.deviceId) { - setError('No device ID provided') - return + const connectToDevice = useCallback(async () => { + try { + setIsLoading(true) + await ledgerService.connect(params.deviceId) + Logger.info('Connected to Ledger device') + setStep('solana') + // Start with Solana keys (passive detection will handle app switching) + await getSolanaKeys() + } catch (error) { + Logger.error('Failed to connect to device', error) + Alert.alert( + 'Connection Failed', + 'Failed to connect to Ledger device. Please ensure your device is unlocked and try again.', + [{ text: 'OK' }] + ) + } finally { + setIsLoading(false) } + }, [ledgerService, params.deviceId, getSolanaKeys]) - setIsLoading(true) - setError(null) - + const createLedgerWallet = useCallback(async () => { try { - // Get extended public keys for wallet creation - const extendedPublicKeys = await LedgerService.getExtendedPublicKeys() - - // Get individual public keys for the first account - const publicKeys = await LedgerService.getPublicKeys(0, 1) - - // Create wallet data to store - const walletId = uuid() - const walletData = { - deviceId: params.deviceId, - derivationPath: "m/44'/60'/0'/0/0", - vmType: 'EVM', - derivationPathSpec: 'BIP44', - extendedPublicKeys: { - evm: extendedPublicKeys.evm.key, - avalanche: extendedPublicKeys.avalanche.key - }, - publicKeys: publicKeys.map(pk => ({ - key: pk.key, - derivationPath: pk.derivationPath, - curve: pk.curve, - type: 'address-pubkey' - })) + setIsLoading(true) + Logger.info('Creating Ledger wallet with generated keys...') + + if (!avalancheKeys || solanaKeys.length === 0 || !bitcoinAddress) { + throw new Error('Missing required keys for wallet creation') } - // Store the wallet + const newWalletId = uuid() + + // Store the Ledger wallet await dispatch( storeWallet({ - walletId, - walletSecret: JSON.stringify(walletData), + walletId: newWalletId, + walletSecret: JSON.stringify({ + deviceId: params.deviceId, + deviceName: params.deviceName || 'Ledger Device', + avalancheKeys, + solanaKeys + }), type: WalletType.LEDGER }) ).unwrap() - // Set as active wallet - dispatch(setActiveWallet(walletId)) + dispatch(setActiveWallet(newWalletId)) - // Create account from addresses - const evmAddress = addresses.find( - addr => addr.network === 'AVALANCHE_C_EVM' - ) - const xChainAddress = addresses.find( - addr => addr.network === 'AVALANCHE_X' - ) - const pChainAddress = addresses.find( - addr => addr.network === 'AVALANCHE_P' - ) - const btcAddress = addresses.find(addr => addr.network === 'BITCOIN') + // Create addresses from the keys + const addresses = { + EVM: avalancheKeys.evm.key, + AVM: avalancheKeys.avalanche.key, + PVM: avalancheKeys.avalanche.key, // Same as AVM for now + BITCOIN: bitcoinAddress, + SVM: solanaKeys[0]?.key || '', + CoreEth: '' // Not implemented yet + } + + const allAccountsCount = Object.keys(allAccounts).length - const accountId = uuid() - const account: PrimaryAccount = { - id: accountId, - walletId, - name: 'Account 1', + const newAccountId = uuid() + const newAccount: Account = { + id: newAccountId, + walletId: newWalletId, + name: `Account ${allAccountsCount + 1}`, type: CoreAccountType.PRIMARY, index: 0, - addressC: evmAddress?.address || '', - addressAVM: xChainAddress?.address || '', - addressPVM: pChainAddress?.address || '', - addressBTC: btcAddress?.address || '', - addressSVM: '', - addressCoreEth: evmAddress?.address || '' + addressC: addresses.EVM, + addressBTC: addresses.BITCOIN, + addressAVM: addresses.AVM, + addressPVM: addresses.PVM, + addressSVM: addresses.SVM, + addressCoreEth: addresses.CoreEth } - Logger.info('Created account:', account) - - // Store account and set as active - dispatch(setAccount(account)) - dispatch(setActiveAccount(accountId)) + dispatch(setAccount(newAccount)) + dispatch(setActiveAccount(newAccountId)) - Logger.info('Successfully created Ledger wallet and account') + Logger.info('Ledger wallet created successfully:', newWalletId) + showSnackbar('Ledger wallet created successfully!') - // Navigate to success screen - router.push( - '/(signedIn)/(modals)/accountSettings/ledger/importSuccess' as any - ) - } catch (err) { - Logger.error('Failed to create Ledger wallet:', err) - setError( - err instanceof Error ? err.message : 'Failed to create Ledger wallet' + // Navigate to manage accounts + router.push('/accountSettings/manageAccounts' as any) + } catch (error) { + Logger.error('Failed to create Ledger wallet:', error) + showSnackbar( + `Failed to create wallet: ${ + error instanceof Error ? error.message : 'Unknown error' + }` ) } finally { setIsLoading(false) } - } + }, [ + avalancheKeys, + solanaKeys, + bitcoinAddress, + dispatch, + params.deviceId, + params.deviceName, + allAccounts, + router + ]) + + useEffect(() => { + if (params.deviceId) { + connectToDevice() + } else { + Logger.error('No deviceId provided in params') + Alert.alert('Error', 'No device ID provided') + } + }, [connectToDevice, params.deviceId]) + - const handleBack = () => { - router.back() + + const renderStepTitle = () => { + switch (step) { + case 'connecting': + return 'Connecting to Ledger' + case 'solana': + return 'Solana Setup' + case 'avalanche': + return 'Avalanche Setup' + case 'complete': + return 'Setup Complete' + default: + return 'Confirm Addresses' + } } - // Auto-connect when component mounts - useEffect(() => { - connectToDevice() - }, []) + const renderStepContent = () => { + switch (step) { + case 'connecting': + return ( + + + + Connecting to your Ledger device... + + + Please ensure your device is unlocked and nearby + + + ) - // Cleanup on unmount - useEffect(() => { - return () => { - LedgerService.disconnect().catch(Logger.error) + case 'solana': + return ( + + + + {isLoading ? ( + + ) : ( + + )} + + {isLoading ? 'Getting Solana Keys...' : 'Solana Keys Retrieved'} + + + {isLoading + ? 'Please open the Solana app on your Ledger device' + : 'Successfully retrieved Solana public keys'} + + + + + ) + + case 'avalanche': + return ( + + + + {isLoading ? ( + + ) : ( + + )} + + {isLoading ? 'Getting Avalanche Keys...' : 'Avalanche Keys Retrieved'} + + + {isLoading + ? 'Please open the Avalanche app on your Ledger device' + : 'Successfully retrieved Avalanche public keys'} + + + + + ) + + case 'complete': + return ( + + + + + + All Keys Retrieved Successfully + + + Your Ledger wallet is ready to be created + + + + + + + Generated Addresses: + + + {avalancheKeys && avalancheKeys.evm.key && ( + + + Avalanche (EVM): + + + {avalancheKeys.evm.key} + + + )} + + {xpAddress && ( + + + Avalanche (X/P): + + + {xpAddress} + + + )} + + {bitcoinAddress && ( + + + Bitcoin: + + + {bitcoinAddress} + + + )} + + {solanaKeys.length > 0 && ( + + + Solana: + + + {solanaKeys[0].key.substring(0, 20)}... + + + )} + + + + + ) + + default: + return null } - }, []) + } return ( - - - - Confirm Addresses - - - {error && ( - + + + {renderStepTitle()} + + + {step !== 'connecting' && ( + - - {error} - - - )} - - {isLoading ? ( - - - {addresses.length > 0 - ? 'Creating wallet...' - : 'Connecting to Ledger device...'} - - - ) : addresses.length > 0 ? ( - - - Please verify these addresses on your Ledger device: - - - {addresses.map(address => ( - - - {address.network} - - - {address.address} - - - {address.derivationPath} - - - ))} - - - - - + + + Step {step === 'solana' ? '1' : step === 'avalanche' ? '2' : '3'}{' '} + of 3 + - ) : null} + )} + + {renderStepContent()} ) } diff --git a/packages/core-mobile/app/services/ledger/LedgerService.ts b/packages/core-mobile/app/services/ledger/LedgerService.ts index 4827b63185..7c2e0f26f0 100644 --- a/packages/core-mobile/app/services/ledger/LedgerService.ts +++ b/packages/core-mobile/app/services/ledger/LedgerService.ts @@ -1,10 +1,17 @@ import TransportBLE from '@ledgerhq/react-native-hw-transport-ble' import AppAvalanche from '@avalabs/hw-app-avalanche' +import AppSolana from '@ledgerhq/hw-app-solana' import { NetworkVMType } from '@avalabs/core-chains-sdk' import { getAddressDerivationPath } from 'services/wallet/utils' import { ChainName } from 'services/network/consts' -import { getBtcAddressFromPubKey } from '@avalabs/core-wallets-sdk' +import { + getBtcAddressFromPubKey, + getSolanaPublicKeyFromLedger, + getLedgerAppInfo +} from '@avalabs/core-wallets-sdk' import { networks } from 'bitcoinjs-lib' +import Logger from 'utils/Logger' +import bs58 from 'bs58' export interface AddressInfo { id: string @@ -25,16 +32,35 @@ export interface PublicKeyInfo { curve: 'secp256k1' | 'ed25519' } +export enum LedgerAppType { + AVALANCHE = 'Avalanche', + SOLANA = 'Solana', + UNKNOWN = 'Unknown' +} + +export interface AppInfo { + applicationName: string + version: string +} + export class LedgerService { private transport: any = null - private avalancheApp: AppAvalanche | null = null + private currentAppType: LedgerAppType = LedgerAppType.UNKNOWN + private appPollingInterval: NodeJS.Timeout | null = null + private appPollingEnabled = false - // Connect to Ledger device + // Connect to Ledger device (transport only, no apps) async connect(deviceId: string): Promise { try { - this.transport = await TransportBLE.open(deviceId) - this.avalancheApp = new AppAvalanche(this.transport) + // Use a longer timeout for connection (30 seconds) + this.transport = await TransportBLE.open(deviceId, 30000) + Logger.info('BLE transport connected successfully') + this.currentAppType = LedgerAppType.UNKNOWN + + // Start passive app detection + this.startAppPolling() } catch (error) { + Logger.error('Failed to connect to Ledger', error) throw new Error( `Failed to connect to Ledger: ${ error instanceof Error ? error.message : 'Unknown error' @@ -43,51 +69,189 @@ export class LedgerService { } } + // Start passive app detection polling + private startAppPolling(): void { + if (this.appPollingEnabled) return + + this.appPollingEnabled = true + this.appPollingInterval = setInterval(async () => { + try { + if (!this.transport || this.transport.isDisconnected) { + this.stopAppPolling() + return + } + + const appInfo = await this.getCurrentAppInfo() + const newAppType = this.mapAppNameToType(appInfo.applicationName) + + if (newAppType !== this.currentAppType) { + Logger.info( + `App changed from ${this.currentAppType} to ${newAppType}` + ) + this.currentAppType = newAppType + } + } catch (error) { + Logger.error('Error polling app info', error) + // Don't stop polling on error, just log it + } + }, 2000) // Poll every 2 seconds like the extension + } + + // Stop passive app detection polling + private stopAppPolling(): void { + if (this.appPollingInterval) { + clearInterval(this.appPollingInterval) + this.appPollingInterval = null + } + this.appPollingEnabled = false + } + + // Get current app info from device + private async getCurrentAppInfo(): Promise { + if (!this.transport) { + throw new Error('Transport not initialized') + } + + return await getLedgerAppInfo(this.transport) + } + + // Map app name to our enum + private mapAppNameToType(appName: string): LedgerAppType { + switch (appName.toLowerCase()) { + case 'avalanche': + return LedgerAppType.AVALANCHE + case 'solana': + return LedgerAppType.SOLANA + default: + return LedgerAppType.UNKNOWN + } + } + + // Get current app type (passive detection) + getCurrentAppType(): LedgerAppType { + return this.currentAppType + } + + // Wait for specific app to be open (passive approach) + async waitForApp(appType: LedgerAppType, timeoutMs = 30000): Promise { + const startTime = Date.now() + Logger.info(`Waiting for ${appType} app (timeout: ${timeoutMs}ms)...`) + + while (Date.now() - startTime < timeoutMs) { + Logger.info(`Current app type: ${this.currentAppType}, waiting for: ${appType}`) + + if (this.currentAppType === appType) { + Logger.info(`${appType} app is ready`) + return + } + + // Wait 1 second before next check + await new Promise(resolve => setTimeout(resolve, 1000)) + } + + Logger.error(`Timeout waiting for ${appType} app after ${timeoutMs}ms`) + throw new Error( + `Timeout waiting for ${appType} app. Please open the ${appType} app on your Ledger device.` + ) + } + + // Check if specific app is currently open + async isAppOpen(appType: LedgerAppType): Promise { + try { + const appInfo = await this.getCurrentAppInfo() + const currentAppType = this.mapAppNameToType(appInfo.applicationName) + return currentAppType === appType + } catch (error) { + Logger.error('Error checking app status', error) + return false + } + } + + // Reconnect to device if disconnected + private async reconnectIfNeeded(deviceId: string): Promise { + Logger.info('Checking if reconnection is needed') + + if (!this.transport || this.transport.isDisconnected) { + Logger.info('Transport is disconnected, attempting reconnection') + await this.connect(deviceId) + } + } + // Get extended public keys for BIP44 derivation async getExtendedPublicKeys(): Promise<{ evm: ExtendedPublicKey avalanche: ExtendedPublicKey }> { - if (!this.avalancheApp) { - throw new Error('Avalanche app not initialized') + if (!this.transport) { + throw new Error('Transport not initialized') } + Logger.info('=== getExtendedPublicKeys STARTED ===') + Logger.info('Current app type:', this.currentAppType) + + // Connect to Avalanche app + Logger.info('Waiting for Avalanche app...') + await this.waitForApp(LedgerAppType.AVALANCHE) + Logger.info('Avalanche app detected, creating app instance...') + + // Create Avalanche app instance + const avalancheApp = new AppAvalanche(this.transport) + Logger.info('Avalanche app instance created') + try { // Get EVM extended public key (m/44'/60'/0') - const evmXpubResponse = await this.avalancheApp.getExtendedPubKey( - getAddressDerivationPath({ - accountIndex: 0, - vmType: NetworkVMType.EVM - }).replace('/0/0', ''), + Logger.info('Getting EVM extended public key...') + const evmPath = getAddressDerivationPath({ + accountIndex: 0, + vmType: NetworkVMType.EVM + }).replace('/0/0', '') + Logger.info('EVM derivation path:', evmPath) + + const evmXpubResponse = await avalancheApp.getExtendedPubKey( + evmPath, false ) + Logger.info('EVM response return code:', evmXpubResponse.returnCode) + // Check for error response if (evmXpubResponse.returnCode !== 0x9000) { + Logger.error('EVM extended public key error:', evmXpubResponse.errorMessage) throw new Error( `EVM extended public key error: ${ evmXpubResponse.errorMessage || 'Unknown error' }` ) } + + Logger.info('EVM extended public key retrieved successfully') // Get Avalanche extended public key (m/44'/9000'/0') - const avalancheXpubResponse = await this.avalancheApp.getExtendedPubKey( - getAddressDerivationPath({ - accountIndex: 0, - vmType: NetworkVMType.AVM - }).replace('/0/0', ''), + Logger.info('Getting Avalanche extended public key...') + const avalanchePath = getAddressDerivationPath({ + accountIndex: 0, + vmType: NetworkVMType.AVM + }).replace('/0/0', '') + Logger.info('Avalanche derivation path:', avalanchePath) + + const avalancheXpubResponse = await avalancheApp.getExtendedPubKey( + avalanchePath, false ) + Logger.info('Avalanche response return code:', avalancheXpubResponse.returnCode) + // Check for error response if (avalancheXpubResponse.returnCode !== 0x9000) { + Logger.error('Avalanche extended public key error:', avalancheXpubResponse.errorMessage) throw new Error( `Avalanche extended public key error: ${ avalancheXpubResponse.errorMessage || 'Unknown error' }` ) } + + Logger.info('Avalanche extended public key retrieved successfully') return { evm: { @@ -128,8 +292,231 @@ export class LedgerService { ) } } + Logger.error('=== getExtendedPublicKeys FAILED ===', error) throw new Error(`Failed to get extended public keys: ${error}`) } + + Logger.info('=== getExtendedPublicKeys COMPLETED SUCCESSFULLY ===') + } + + // Check if Solana app is open + async checkSolanaApp(): Promise { + if (!this.transport) { + return false + } + + try { + // Create fresh Solana app instance + const solanaApp = new AppSolana(this.transport) + // Try to get a simple address to check if app is open + // Use a standard Solana derivation path + const testPath = "m/44'/501'/0'" + await solanaApp.getAddress(testPath, false) + return true + } catch (error) { + Logger.error('Solana app not open or not available', error) + return false + } + } + + // Get Solana public keys using SDK function (like extension) + async getSolanaPublicKeys(startIndex: number): Promise { + if (!this.transport) { + throw new Error('Transport not initialized') + } + + // Create a fresh AppSolana instance for each call (like the SDK does) + const freshSolanaApp = new AppSolana(this.transport) + + // Use correct Solana derivation path format + const derivationPath = `44'/501'/0'/0'/${startIndex}` + + try { + // Simple direct call to get Solana address using fresh instance + const result = await freshSolanaApp.getAddress(derivationPath, false) + const publicKey = result.address + + console.log('HIT PUBLIC KEY', publicKey) + + const publicKeys: PublicKeyInfo[] = [ + { + key: publicKey.toString('hex'), + derivationPath, + curve: 'ed25519' + } + ] + + return publicKeys + } catch (error) { + if (error instanceof Error) { + if (error.message.includes('6a80')) { + throw new Error( + 'Wrong app open. Please open the Solana app on your Ledger device.' + ) + } + throw new Error(`Failed to get Solana address: ${error.message}`) + } + throw new Error('Failed to get Solana address') + } + } + + // Alternative method using the SDK function (like the extension does) + async getSolanaPublicKeysViaSDK( + startIndex: number, + _count: number + ): Promise { + if (!this.transport) { + throw new Error('Transport not initialized') + } + + try { + // Use the SDK function directly (like the extension does) + const publicKey = await getSolanaPublicKeyFromLedger( + startIndex, + this.transport + ) + + console.log('HIT PUBLIC KEY via SDK', publicKey) + + const publicKeys: PublicKeyInfo[] = [ + { + key: publicKey.toString('hex'), + derivationPath: `44'/501'/0'/0'/${startIndex}`, + curve: 'ed25519' + } + ] + + return publicKeys + } catch (error) { + if (error instanceof Error) { + if (error.message.includes('6a80')) { + throw new Error( + 'Wrong app open. Please open the Solana app on your Ledger device.' + ) + } + throw new Error( + `Failed to get Solana address via SDK: ${error.message}` + ) + } + throw new Error('Failed to get Solana address via SDK') + } + } + + // Robust method with timeout and retry logic + async getSolanaPublicKeysRobust( + startIndex: number, + _count: number + ): Promise { + if (!this.transport) { + throw new Error('Transport not initialized') + } + + const maxRetries = 3 + const retryDelay = 1000 // 1 second + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + Logger.info(`Solana attempt ${attempt}/${maxRetries}`) + + // Create a fresh transport connection for each attempt + const freshTransport = await TransportBLE.open( + this.transport.deviceId || 'unknown', + 15000 + ) + + // Create fresh app instance + const freshSolanaApp = new AppSolana(freshTransport) + + // Use derivation path + const derivationPath = `44'/501'/0'/0'/${startIndex}` + + // Call with timeout wrapper + const timeoutPromise = new Promise((_, reject) => { + setTimeout( + () => reject(new Error('Solana getAddress timeout')), + 10000 + ) + }) + + const getAddressPromise = freshSolanaApp.getAddress( + derivationPath, + false + ) + + const result = await Promise.race([getAddressPromise, timeoutPromise]) + const publicKey = result.address + + console.log('HIT PUBLIC KEY robust method', publicKey) + + // Close the fresh transport + await freshTransport.close() + + const publicKeys: PublicKeyInfo[] = [ + { + key: publicKey.toString('hex'), + derivationPath, + curve: 'ed25519' + } + ] + + return publicKeys + } catch (error) { + Logger.error(`Solana attempt ${attempt} failed:`, error) + + if (attempt === maxRetries) { + if (error instanceof Error) { + if (error.message.includes('6a80')) { + throw new Error( + 'Wrong app open. Please open the Solana app on your Ledger device.' + ) + } + if (error.message.includes('DisconnectedDevice')) { + throw new Error( + 'Ledger device disconnected. Please ensure the Solana app is open and try again.' + ) + } + throw new Error( + `Failed to get Solana address after ${maxRetries} attempts: ${error.message}` + ) + } + throw new Error( + `Failed to get Solana address after ${maxRetries} attempts` + ) + } + + // Wait before retry + await new Promise(resolve => setTimeout(resolve, retryDelay)) + } + } + + throw new Error('Unexpected error in getSolanaPublicKeysRobust') + } + + // Get Solana addresses from public keys + async getSolanaAddresses( + startIndex: number, + count: number + ): Promise { + Logger.info('Starting getSolanaAddresses') + try { + const publicKeys = await this.getSolanaPublicKeys(startIndex) + Logger.info('Got Solana public keys, converting to addresses') + + return publicKeys.map((pk, index) => { + // Convert public key to Solana address (Base58 encoding) + const address = bs58.encode(Uint8Array.from(Buffer.from(pk.key, 'hex'))) + + return { + id: `solana-${startIndex + index}`, + address, + derivationPath: pk.derivationPath, + network: ChainName.SOLANA + } + }) + } catch (error) { + Logger.error('Failed in getSolanaAddresses', error) + throw error + } } // Get individual public keys for LedgerLive derivation @@ -137,10 +524,16 @@ export class LedgerService { startIndex: number, count: number ): Promise { - if (!this.avalancheApp) { - throw new Error('Avalanche app not initialized') + if (!this.transport) { + throw new Error('Transport not initialized') } + // Connect to Avalanche app + await this.waitForApp(LedgerAppType.AVALANCHE) + + // Create Avalanche app instance + const avalancheApp = new AppAvalanche(this.transport) + const publicKeys: PublicKeyInfo[] = [] try { @@ -150,7 +543,7 @@ export class LedgerService { accountIndex: i, vmType: NetworkVMType.EVM }) - const evmResponse = await this.avalancheApp.getAddressAndPubKey( + const evmResponse = await avalancheApp.getAddressAndPubKey( evmPath, false, 'avax' @@ -166,7 +559,7 @@ export class LedgerService { accountIndex: i, vmType: NetworkVMType.AVM }) - const avmResponse = await this.avalancheApp.getAddressAndPubKey( + const avmResponse = await avalancheApp.getAddressAndPubKey( avmPath, false, 'avax' @@ -182,7 +575,7 @@ export class LedgerService { accountIndex: i, vmType: NetworkVMType.BITCOIN }) - const btcResponse = await this.avalancheApp.getAddressAndPubKey( + const btcResponse = await avalancheApp.getAddressAndPubKey( btcPath, false, 'bc' @@ -205,10 +598,16 @@ export class LedgerService { startIndex: number, count: number ): Promise { - if (!this.avalancheApp) { - throw new Error('Avalanche app not initialized') + if (!this.transport) { + throw new Error('Transport not initialized') } + // Connect to Avalanche app + await this.waitForApp(LedgerAppType.AVALANCHE) + + // Create Avalanche app instance + const avalancheApp = new AppAvalanche(this.transport) + const addresses: AddressInfo[] = [] try { @@ -219,7 +618,7 @@ export class LedgerService { accountIndex: i, vmType: NetworkVMType.EVM }) - const evmAddressResponse = await this.avalancheApp.getETHAddress( + const evmAddressResponse = await avalancheApp.getETHAddress( evmPath, false // don't display on device ) @@ -235,12 +634,11 @@ export class LedgerService { accountIndex: i, vmType: NetworkVMType.AVM }) - const xChainAddressResponse = - await this.avalancheApp.getAddressAndPubKey( - xChainPath, - false, - 'avax' // hrp for mainnet - ) + const xChainAddressResponse = await avalancheApp.getAddressAndPubKey( + xChainPath, + false, + 'avax' // hrp for mainnet + ) addresses.push({ id: `avalanche-x-${i}`, address: xChainAddressResponse.address, @@ -253,13 +651,12 @@ export class LedgerService { accountIndex: i, vmType: NetworkVMType.AVM }) - const pChainAddressResponse = - await this.avalancheApp.getAddressAndPubKey( - pChainPath, - false, - 'avax', // hrp for mainnet - '2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM' // P-Chain ID - ) + const pChainAddressResponse = await avalancheApp.getAddressAndPubKey( + pChainPath, + false, + 'avax', // hrp for mainnet + '2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM' // P-Chain ID + ) addresses.push({ id: `avalanche-p-${i}`, address: pChainAddressResponse.address, @@ -272,12 +669,11 @@ export class LedgerService { accountIndex: i, vmType: NetworkVMType.EVM // Use EVM path for Bitcoin }) - const btcPublicKeyResponse = - await this.avalancheApp.getAddressAndPubKey( - btcPath, - false, - 'avax' // hrp for mainnet - ) + const btcPublicKeyResponse = await avalancheApp.getAddressAndPubKey( + btcPath, + false, + 'avax' // hrp for mainnet + ) const btcAddress = getBtcAddressFromPubKey( Buffer.from(btcPublicKeyResponse.publicKey.toString('hex'), 'hex'), networks.bitcoin // mainnet @@ -296,12 +692,36 @@ export class LedgerService { return addresses } + // Get all addresses including Solana (requires app switching) + async getAllAddressesWithSolana( + startIndex: number, + count: number + ): Promise { + const addresses: AddressInfo[] = [] + + try { + // Get Avalanche addresses first + const avalancheAddresses = await this.getAllAddresses(startIndex, count) + addresses.push(...avalancheAddresses) + + // Get Solana addresses + const solanaAddresses = await this.getSolanaAddresses(startIndex, count) + addresses.push(...solanaAddresses) + + return addresses + } catch (error) { + Logger.error('Failed to get all addresses with Solana', error) + throw error + } + } + // Disconnect from Ledger device async disconnect(): Promise { if (this.transport) { await this.transport.close() this.transport = null - this.avalancheApp = null + this.currentAppType = LedgerAppType.UNKNOWN + this.stopAppPolling() // Stop polling on disconnect } } } From 01a806792048838d4d420bfd7d462181d0d63045 Mon Sep 17 00:00:00 2001 From: B0Y3R-AVA Date: Mon, 11 Aug 2025 18:39:46 -0400 Subject: [PATCH 04/24] avax / solana txs working, can pull addresses from ledger, create ledger wallet, and sign tx --- .../ledger/confirmAddresses.tsx | 37 +++++++++++++++++-- .../app/services/ledger/LedgerService.ts | 35 +++++++++++++++++- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx index 37a5db4588..1c2fa1b0d6 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx @@ -24,6 +24,7 @@ import { showSnackbar } from 'new/common/utils/toast' import { uuid } from 'utils/uuid' import Logger from 'utils/Logger' import { ScrollScreen } from 'common/components/ScrollScreen' +import bs58 from 'bs58' export default function ConfirmAddresses() { const params = useLocalSearchParams<{ @@ -148,7 +149,8 @@ export default function ConfirmAddresses() { // Set the addresses setAvalancheKeys({ evm: { key: evmAddress }, - avalanche: { key: xpAddress } + avalanche: { key: xpAddress }, + pvm: { key: pvmAddress } }) setBitcoinAddress(btcAddress) setXpAddress(xpAddress) @@ -218,6 +220,35 @@ export default function ConfirmAddresses() { walletSecret: JSON.stringify({ deviceId: params.deviceId, deviceName: params.deviceName || 'Ledger Device', + derivationPath: "m/44'/60'/0'/0/0", // Standard EVM derivation path + vmType: 'EVM', // NetworkVMType.EVM + derivationPathSpec: 'BIP44', // Use BIP44 derivation + extendedPublicKeys: { + evm: avalancheKeys.evm.key, + avalanche: avalancheKeys.avalanche.key + }, + publicKeys: [ + { + key: avalancheKeys.evm.key, + derivationPath: "m/44'/60'/0'/0/0", + curve: 'secp256k1' + }, + { + key: avalancheKeys.avalanche.key, + derivationPath: "m/44'/9000'/0'/0/0", + curve: 'secp256k1' + }, + { + key: avalancheKeys.pvm?.key || avalancheKeys.avalanche.key, + derivationPath: "m/44'/9000'/0'/0/0", // P-Chain uses same path as AVM + curve: 'secp256k1' + }, + { + key: solanaKeys[0]?.key || '', + derivationPath: "m/44'/501'/0'/0'", + curve: 'ed25519' + } + ], avalancheKeys, solanaKeys }), @@ -231,9 +262,9 @@ export default function ConfirmAddresses() { const addresses = { EVM: avalancheKeys.evm.key, AVM: avalancheKeys.avalanche.key, - PVM: avalancheKeys.avalanche.key, // Same as AVM for now + PVM: avalancheKeys.pvm?.key || avalancheKeys.avalanche.key, // Use P-Chain address if available, fallback to AVM BITCOIN: bitcoinAddress, - SVM: solanaKeys[0]?.key || '', + SVM: solanaKeys[0]?.key ? bs58.encode(new Uint8Array(Buffer.from(solanaKeys[0].key, 'hex'))) : '', CoreEth: '' // Not implemented yet } diff --git a/packages/core-mobile/app/services/ledger/LedgerService.ts b/packages/core-mobile/app/services/ledger/LedgerService.ts index 7c2e0f26f0..4d5490a123 100644 --- a/packages/core-mobile/app/services/ledger/LedgerService.ts +++ b/packages/core-mobile/app/services/ledger/LedgerService.ts @@ -35,6 +35,7 @@ export interface PublicKeyInfo { export enum LedgerAppType { AVALANCHE = 'Avalanche', SOLANA = 'Solana', + ETHEREUM = 'Ethereum', UNKNOWN = 'Unknown' } @@ -52,13 +53,16 @@ export class LedgerService { // Connect to Ledger device (transport only, no apps) async connect(deviceId: string): Promise { try { + Logger.info('Starting BLE connection attempt with deviceId:', deviceId) // Use a longer timeout for connection (30 seconds) this.transport = await TransportBLE.open(deviceId, 30000) Logger.info('BLE transport connected successfully') this.currentAppType = LedgerAppType.UNKNOWN // Start passive app detection + Logger.info('Starting app polling...') this.startAppPolling() + Logger.info('App polling started') } catch (error) { Logger.error('Failed to connect to Ledger', error) throw new Error( @@ -122,6 +126,8 @@ export class LedgerService { return LedgerAppType.AVALANCHE case 'solana': return LedgerAppType.SOLANA + case 'ethereum': + return LedgerAppType.ETHEREUM default: return LedgerAppType.UNKNOWN } @@ -173,7 +179,15 @@ export class LedgerService { if (!this.transport || this.transport.isDisconnected) { Logger.info('Transport is disconnected, attempting reconnection') - await this.connect(deviceId) + try { + await this.connect(deviceId) + Logger.info('Reconnection successful') + } catch (error) { + Logger.error('Reconnection failed:', error) + throw error // Re-throw to propagate the error + } + } else { + Logger.info('Transport is already connected') } } @@ -724,6 +738,25 @@ export class LedgerService { this.stopAppPolling() // Stop polling on disconnect } } + + // Get current transport (for wallet usage) + getTransport(): TransportBLE { + if (!this.transport) { + throw new Error('Transport not initialized. Call connect() first.') + } + return this.transport + } + + // Check if transport is available and connected + isConnected(): boolean { + return this.transport !== null && !this.transport.isDisconnected + } + + // Ensure connection is established for a specific device + async ensureConnection(deviceId: string): Promise { + await this.reconnectIfNeeded(deviceId) + return this.getTransport() + } } export default new LedgerService() From 110086a5e0acf832e88a506ba2c9affbd14dd77f Mon Sep 17 00:00:00 2001 From: B0Y3R-AVA Date: Tue, 12 Aug 2025 11:19:19 -0400 Subject: [PATCH 05/24] ledger provider, modifications to wallet, ui now using hook --- .../hooks/useLedgerBasePublickKeyFetcher.ts | 210 --------- .../features/ledger/hooks/useLedgerWallet.ts | 251 +++++++++++ .../ledger/confirmAddresses.tsx | 426 +++++++----------- .../accountSettings/ledger/connectWallet.tsx | 345 +++----------- .../core-mobile/app/new/routes/signup.tsx | 12 - 5 files changed, 486 insertions(+), 758 deletions(-) delete mode 100644 packages/core-mobile/app/new/features/ledger/hooks/useLedgerBasePublickKeyFetcher.ts diff --git a/packages/core-mobile/app/new/features/ledger/hooks/useLedgerBasePublickKeyFetcher.ts b/packages/core-mobile/app/new/features/ledger/hooks/useLedgerBasePublickKeyFetcher.ts deleted file mode 100644 index 580ee015cf..0000000000 --- a/packages/core-mobile/app/new/features/ledger/hooks/useLedgerBasePublickKeyFetcher.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { useState, useCallback, useEffect } from 'react' -import { LedgerService } from 'services/ledger/ledgerService' -import { NetworkVMType } from '@avalabs/core-chains-sdk' -import Logger from 'utils/Logger' - -export interface BasePublicKey { - path: string - key: string - chainCode: string - network: NetworkVMType -} - -export interface PublicKeyInfo { - key: string - derivationPath: string - curve: 'secp256k1' | 'ed25519' - network: NetworkVMType -} - -export interface UseLedgerBasePublickKeyFetcherProps { - deviceId?: string - derivationPathSpec?: 'BIP44' | 'LedgerLive' - accountIndex?: number - count?: number -} - -export interface UseLedgerBasePublickKeyFetcherReturn { - basePublicKeys: BasePublicKey[] - publicKeys: PublicKeyInfo[] - isLoading: boolean - error: string | null - connect: (deviceId: string) => Promise - fetchBasePublicKeys: () => Promise - fetchPublicKeys: () => Promise - disconnect: () => Promise - isConnected: boolean -} - -export const useLedgerBasePublickKeyFetcher = ({ - deviceId, - derivationPathSpec = 'BIP44', - accountIndex = 0, - count = 5 -}: UseLedgerBasePublickKeyFetcherProps = {}): UseLedgerBasePublickKeyFetcherReturn => { - const [ledgerService] = useState(() => new LedgerService()) - const [basePublicKeys, setBasePublicKeys] = useState([]) - const [publicKeys, setPublicKeys] = useState([]) - const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState(null) - const [isConnected, setIsConnected] = useState(false) - - const connect = useCallback(async (deviceId: string) => { - setIsLoading(true) - setError(null) - - try { - await ledgerService.connect(deviceId) - setIsConnected(true) - console.log('Successfully connected to Ledger device') - } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Unknown error' - setError(`Failed to connect to Ledger: ${errorMessage}`) - Logger.error('Ledger connection failed:', err) - setIsConnected(false) - } finally { - setIsLoading(false) - } - }, [ledgerService]) - - const fetchBasePublicKeys = useCallback(async () => { - if (!isConnected) { - setError('Device not connected') - return - } - - setIsLoading(true) - setError(null) - - try { - // Get extended public keys for BIP44 derivation - const extendedPublicKeys = await ledgerService.getExtendedPublicKeys() - - const baseKeys: BasePublicKey[] = [ - { - path: extendedPublicKeys.evm.path, - key: extendedPublicKeys.evm.key, - chainCode: extendedPublicKeys.evm.chainCode, - network: NetworkVMType.EVM - }, - { - path: extendedPublicKeys.avalanche.path, - key: extendedPublicKeys.avalanche.key, - chainCode: extendedPublicKeys.avalanche.chainCode, - network: NetworkVMType.AVM - } - ] - - setBasePublicKeys(baseKeys) - console.log('Successfully fetched base public keys') - } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Unknown error' - setError(`Failed to fetch base public keys: ${errorMessage}`) - Logger.error('Base public key fetching failed:', err) - } finally { - setIsLoading(false) - } - }, [isConnected, ledgerService]) - - const fetchPublicKeys = useCallback(async () => { - if (!isConnected) { - setError('Device not connected') - return - } - - setIsLoading(true) - setError(null) - - try { - // Get individual public keys for LedgerLive derivation - const keys = await ledgerService.getPublicKeys(accountIndex, count) - - const publicKeyInfos: PublicKeyInfo[] = keys.map(key => ({ - key: key.key, - derivationPath: key.derivationPath, - curve: key.curve, - network: getNetworkFromDerivationPath(key.derivationPath) - })) - - setPublicKeys(publicKeyInfos) - console.log('Successfully fetched public keys') - } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Unknown error' - setError(`Failed to fetch public keys: ${errorMessage}`) - Logger.error('Public key fetching failed:', err) - } finally { - setIsLoading(false) - } - }, [isConnected, accountIndex, count, ledgerService]) - - const disconnect = useCallback(async () => { - try { - await ledgerService.disconnect() - setIsConnected(false) - setBasePublicKeys([]) - setPublicKeys([]) - setError(null) - console.log('Successfully disconnected from Ledger device') - } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Unknown error' - setError(`Failed to disconnect: ${errorMessage}`) - Logger.error('Ledger disconnection failed:', err) - } - }, [ledgerService]) - - // Helper function to determine network from derivation path - const getNetworkFromDerivationPath = (derivationPath: string): NetworkVMType => { - if (derivationPath.includes("'/60'")) { - return NetworkVMType.EVM - } else if (derivationPath.includes("'/9000'")) { - return NetworkVMType.AVM - } else if (derivationPath.includes("'/0'")) { - return NetworkVMType.BITCOIN - } else if (derivationPath.includes("'/501'")) { - return NetworkVMType.SVM - } - return NetworkVMType.EVM // Default fallback - } - - // Auto-connect when deviceId changes - useEffect(() => { - if (deviceId && !isConnected) { - connect(deviceId) - } - }, [deviceId, connect, isConnected]) - - // Auto-fetch base public keys when connected and in BIP44 mode - useEffect(() => { - if (isConnected && derivationPathSpec === 'BIP44') { - fetchBasePublicKeys() - } - }, [isConnected, derivationPathSpec, fetchBasePublicKeys]) - - // Auto-fetch public keys when connected and in LedgerLive mode - useEffect(() => { - if (isConnected && derivationPathSpec === 'LedgerLive') { - fetchPublicKeys() - } - }, [isConnected, derivationPathSpec, fetchPublicKeys]) - - // Cleanup on unmount - useEffect(() => { - return () => { - if (isConnected) { - disconnect() - } - } - }, [isConnected, disconnect]) - - return { - basePublicKeys, - publicKeys, - isLoading, - error, - connect, - fetchBasePublicKeys, - fetchPublicKeys, - disconnect, - isConnected - } -} \ No newline at end of file diff --git a/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts b/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts index 6c3891f8fe..6f81fe7e56 100644 --- a/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts +++ b/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts @@ -2,11 +2,16 @@ import { useState, useCallback, useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import { Alert, Platform, PermissionsAndroid } from 'react-native' import TransportBLE from '@ledgerhq/react-native-hw-transport-ble' +<<<<<<< HEAD import Transport from '@ledgerhq/hw-transport' import AppSolana from '@ledgerhq/hw-app-solana' import bs58 from 'bs58' import LedgerService from 'services/ledger/LedgerService' import { LedgerAppType, LedgerDerivationPathType } from 'services/ledger/types' +======= +import AppSolana from '@ledgerhq/hw-app-solana' +import { LedgerService, LedgerAppType } from 'services/ledger/ledgerService' +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) import { ChainName } from 'services/network/consts' import { WalletType } from 'services/wallet/types' import { AppThunkDispatch } from 'store/types' @@ -18,6 +23,7 @@ import { CoreAccountType } from '@avalabs/types' import { showSnackbar } from 'new/common/utils/toast' import { uuid } from 'utils/uuid' import Logger from 'utils/Logger' +<<<<<<< HEAD import { Curve } from 'utils/publicKeys' import { SetupProgress, @@ -77,10 +83,62 @@ import { SOLANA_DERIVATION_PATH, LEDGER_TIMEOUTS } from '../consts' +======= + +export interface LedgerDevice { + id: string + name: string + rssi?: number +} + +export interface LedgerTransportState { + available: boolean + powered: boolean +} + +export interface LedgerKeys { + solanaKeys: any[] + avalancheKeys: { + evm: { key: string; address: string } + avalanche: { key: string; address: string } + pvm?: { key: string; address: string } + } | null + bitcoinAddress: string + xpAddress: string +} + +export interface UseLedgerWalletReturn { + // Device scanning and connection + devices: LedgerDevice[] + isScanning: boolean + isConnecting: boolean + transportState: LedgerTransportState + scanForDevices: () => Promise + connectToDevice: (deviceId: string) => Promise + disconnectDevice: () => Promise + + // Key retrieval + isLoading: boolean + getSolanaKeys: () => Promise + getAvalancheKeys: () => Promise + resetKeys: () => void + keys: LedgerKeys + + // Wallet creation + createLedgerWallet: (params: { + deviceId: string + deviceName?: string + }) => Promise // Returns the new wallet ID +} +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) export function useLedgerWallet(): UseLedgerWalletReturn { const dispatch = useDispatch() const allAccounts = useSelector(selectAccounts) +<<<<<<< HEAD +======= + const [ledgerService] = useState(() => new LedgerService()) +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) const [transportState, setTransportState] = useState({ available: false, powered: false @@ -89,25 +147,40 @@ export function useLedgerWallet(): UseLedgerWalletReturn { const [isScanning, setIsScanning] = useState(false) const [isConnecting, setIsConnecting] = useState(false) const [isLoading, setIsLoading] = useState(false) +<<<<<<< HEAD const [setupProgress, setSetupProgress] = useState(null) // Key states const [solanaKeys, setSolanaKeys] = useState([]) const [avalancheKeys, setAvalancheKeys] = useState(null) +======= + + // Key states + const [solanaKeys, setSolanaKeys] = useState([]) + const [avalancheKeys, setAvalancheKeys] = useState(null) +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) const [bitcoinAddress, setBitcoinAddress] = useState('') const [xpAddress, setXpAddress] = useState('') // Monitor BLE transport state useEffect(() => { const subscription = TransportBLE.observeState({ +<<<<<<< HEAD next: (event: { available: boolean }) => { +======= + next: event => { +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) setTransportState({ available: event.available, powered: false }) }, +<<<<<<< HEAD error: (error: Error) => { +======= + error: error => { +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) Alert.alert( 'BLE Error', `Failed to monitor BLE state: ${error.message}` @@ -130,7 +203,11 @@ export function useLedgerWallet(): UseLedgerWalletReturn { PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT, PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION +<<<<<<< HEAD ].filter(Boolean) +======= + ].filter(Boolean) as any[] +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) const granted = await PermissionsAndroid.requestMultiple(permissions) return Object.values(granted).every( @@ -144,7 +221,11 @@ export function useLedgerWallet(): UseLedgerWalletReturn { }, []) // Handle scan errors +<<<<<<< HEAD const handleScanError = useCallback((error: Error) => { +======= + const handleScanError = useCallback((error: any) => { +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) setIsScanning(false) if ( @@ -193,10 +274,14 @@ export function useLedgerWallet(): UseLedgerWalletReturn { try { const subscription = TransportBLE.listen({ +<<<<<<< HEAD next: (event: { type: string descriptor: { id: string; name?: string; rssi?: number } }) => { +======= + next: event => { +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) if (event.type === 'add') { const device: LedgerDevice = { id: event.descriptor.id, @@ -221,13 +306,20 @@ export function useLedgerWallet(): UseLedgerWalletReturn { setTimeout(() => { subscription.unsubscribe() setIsScanning(false) +<<<<<<< HEAD }, LEDGER_TIMEOUTS.SCAN_TIMEOUT) } catch (error) { handleScanError(error as Error) +======= + }, 10000) + } catch (error) { + handleScanError(error) +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) } }, [transportState.available, requestBluetoothPermissions, handleScanError]) // Connect to device +<<<<<<< HEAD const connectToDevice = useCallback(async (deviceId: string) => { setIsConnecting(true) try { @@ -240,16 +332,38 @@ export function useLedgerWallet(): UseLedgerWalletReturn { setIsConnecting(false) } }, []) +======= + const connectToDevice = useCallback( + async (deviceId: string) => { + setIsConnecting(true) + try { + await ledgerService.connect(deviceId) + Logger.info('Connected to Ledger device') + } catch (error) { + Logger.error('Failed to connect to device', error) + throw error + } finally { + setIsConnecting(false) + } + }, + [ledgerService] + ) +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) // Disconnect device const disconnectDevice = useCallback(async () => { try { +<<<<<<< HEAD await LedgerService.disconnect() +======= + await ledgerService.disconnect() +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) Logger.info('Disconnected from Ledger device') } catch (error) { Logger.error('Failed to disconnect', error) throw error } +<<<<<<< HEAD }, []) // Get Solana keys @@ -260,10 +374,17 @@ export function useLedgerWallet(): UseLedgerWalletReturn { return } +======= + }, [ledgerService]) + + // Get Solana keys + const getSolanaKeys = useCallback(async () => { +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) try { setIsLoading(true) Logger.info('Getting Solana keys with passive app detection') +<<<<<<< HEAD await LedgerService.waitForApp(LedgerAppType.SOLANA) // Get address directly from Solana app @@ -274,12 +395,28 @@ export function useLedgerWallet(): UseLedgerWalletReturn { // Convert the Buffer to base58 format (Solana address format) const solanaAddress = bs58.encode(new Uint8Array(result.address)) +======= + await ledgerService.waitForApp(LedgerAppType.SOLANA) + + // Get address directly from Solana app + const solanaApp = new AppSolana(ledgerService.getTransport()) + const derivationPath = `44'/501'/0'/0'/0` + const result = await solanaApp.getAddress(derivationPath, false) + + // result.address is already in base58 format + const solanaAddress = result.address.toString() +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) setSolanaKeys([ { key: solanaAddress, derivationPath, +<<<<<<< HEAD curve: Curve.ED25519 +======= + curve: 'ed25519', + publicKey: solanaAddress +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) } ]) Logger.info('Successfully got Solana address', solanaAddress) @@ -289,6 +426,7 @@ export function useLedgerWallet(): UseLedgerWalletReturn { } finally { setIsLoading(false) } +<<<<<<< HEAD }, [isLoading]) // Get Avalanche keys @@ -299,16 +437,30 @@ export function useLedgerWallet(): UseLedgerWalletReturn { return } +======= + }, [ledgerService]) + + // Get Avalanche keys + const getAvalancheKeys = useCallback(async () => { +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) try { setIsLoading(true) Logger.info('Getting Avalanche keys') +<<<<<<< HEAD const addresses = await LedgerService.getAllAddresses(0, 1) +======= + const addresses = await ledgerService.getAllAddresses(0, 1) +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) const evmAddress = addresses.find(addr => addr.network === ChainName.AVALANCHE_C_EVM) ?.address || '' +<<<<<<< HEAD const xChainAddress = +======= + const xpAddress = +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) addresses.find(addr => addr.network === ChainName.AVALANCHE_X) ?.address || '' const pvmAddress = @@ -320,12 +472,21 @@ export function useLedgerWallet(): UseLedgerWalletReturn { // Store the addresses directly from the device setAvalancheKeys({ +<<<<<<< HEAD evm: evmAddress, avalanche: xChainAddress, pvm: pvmAddress }) setBitcoinAddress(btcAddress) setXpAddress(xChainAddress) +======= + evm: { key: evmAddress, address: evmAddress }, + avalanche: { key: xpAddress, address: xpAddress }, + pvm: { key: pvmAddress, address: pvmAddress } + }) + setBitcoinAddress(btcAddress) + setXpAddress(xpAddress) +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) Logger.info('Successfully got Avalanche keys') } catch (error) { @@ -334,7 +495,11 @@ export function useLedgerWallet(): UseLedgerWalletReturn { } finally { setIsLoading(false) } +<<<<<<< HEAD }, [isLoading]) +======= + }, [ledgerService]) +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) const resetKeys = useCallback(() => { setSolanaKeys([]) @@ -343,6 +508,7 @@ export function useLedgerWallet(): UseLedgerWalletReturn { setXpAddress('') }, []) +<<<<<<< HEAD // New method: Get individual keys for Ledger Live (sequential device confirmations) const getLedgerLiveKeys = useCallback( async ( @@ -483,22 +649,42 @@ export function useLedgerWallet(): UseLedgerWalletReturn { Logger.info( `Creating ${derivationPathType} Ledger wallet with generated keys...` ) +======= + const createLedgerWallet = useCallback( + async ({ + deviceId, + deviceName = 'Ledger Device' + }: { + deviceId: string + deviceName?: string + }) => { + try { + setIsLoading(true) + Logger.info('Creating Ledger wallet with generated keys...') +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) if (!avalancheKeys || solanaKeys.length === 0 || !bitcoinAddress) { throw new Error('Missing required keys for wallet creation') } +<<<<<<< HEAD updateProgress('Generating wallet ID...') const newWalletId = uuid() updateProgress('Storing wallet data...') // Store the Ledger wallet with the specified derivation path type +======= + const newWalletId = uuid() + + // Store the Ledger wallet +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) await dispatch( storeWallet({ walletId: newWalletId, walletSecret: JSON.stringify({ deviceId, deviceName, +<<<<<<< HEAD derivationPath: DERIVATION_PATHS.BIP44.EVM, vmType: 'EVM', derivationPathSpec: derivationPathType, @@ -542,6 +728,41 @@ export function useLedgerWallet(): UseLedgerWalletReturn { derivationPathType === LedgerDerivationPathType.BIP44 ? WalletType.LEDGER : WalletType.LEDGER_LIVE +======= + derivationPath: "m/44'/60'/0'/0/0", + vmType: 'EVM', + derivationPathSpec: 'BIP44', + extendedPublicKeys: { + evm: avalancheKeys.evm.key, + avalanche: avalancheKeys.avalanche.key + }, + publicKeys: [ + { + key: avalancheKeys.evm.key, + derivationPath: "m/44'/60'/0'/0/0", + curve: 'secp256k1' + }, + { + key: avalancheKeys.avalanche.key, + derivationPath: "m/44'/9000'/0'/0/0", + curve: 'secp256k1' + }, + { + key: avalancheKeys.pvm?.key || avalancheKeys.avalanche.key, + derivationPath: "m/44'/9000'/0'/0/0", + curve: 'secp256k1' + }, + { + key: solanaKeys[0]?.key || '', + derivationPath: "m/44'/501'/0'/0'", + curve: 'ed25519' + } + ], + avalancheKeys, + solanaKeys + }), + type: WalletType.LEDGER +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) }) ).unwrap() @@ -549,11 +770,19 @@ export function useLedgerWallet(): UseLedgerWalletReturn { // Create addresses from the keys const addresses = { +<<<<<<< HEAD EVM: avalancheKeys.evm, AVM: avalancheKeys.avalanche, PVM: avalancheKeys.pvm || avalancheKeys.avalanche, BITCOIN: bitcoinAddress, SVM: solanaKeys[0]?.key || '', +======= + EVM: avalancheKeys.evm.address, + AVM: avalancheKeys.avalanche.address, + PVM: avalancheKeys.pvm?.address || avalancheKeys.avalanche.address, + BITCOIN: bitcoinAddress, + SVM: solanaKeys[0]?.publicKey || '', +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) CoreEth: '' } @@ -583,13 +812,20 @@ export function useLedgerWallet(): UseLedgerWalletReturn { throw error } finally { setIsLoading(false) +<<<<<<< HEAD setSetupProgress(null) +======= +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) } }, [avalancheKeys, solanaKeys, bitcoinAddress, dispatch, allAccounts] ) return { +<<<<<<< HEAD +======= + // Device scanning and connection +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) devices, isScanning, isConnecting, @@ -597,18 +833,33 @@ export function useLedgerWallet(): UseLedgerWalletReturn { scanForDevices, connectToDevice, disconnectDevice, +<<<<<<< HEAD isLoading, getSolanaKeys, getAvalancheKeys, getLedgerLiveKeys, resetKeys, +======= + + // Key retrieval + isLoading, + getSolanaKeys, + getAvalancheKeys, +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) keys: { solanaKeys, avalancheKeys, bitcoinAddress, xpAddress }, +<<<<<<< HEAD createLedgerWallet, setupProgress +======= + + // Wallet creation + createLedgerWallet, + resetKeys +>>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) } } diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx index 1c2fa1b0d6..2ab7465fdf 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx @@ -1,7 +1,6 @@ import React, { useState, useCallback, useEffect } from 'react' import { Alert } from 'react-native' import { useRouter, useLocalSearchParams } from 'expo-router' -import { useDispatch, useSelector } from 'react-redux' import { View, Text, @@ -12,19 +11,8 @@ import { CircularProgress } from '@avalabs/k2-alpine' import { LoadingState } from 'new/common/components/LoadingState' -import { LedgerService, LedgerAppType } from 'services/ledger/ledgerService' -import { WalletType } from 'services/wallet/types' -import { AppThunkDispatch } from 'store/types' -import { storeWallet } from 'store/wallet/thunks' -import { setActiveWallet } from 'store/wallet/slice' -import { setAccount, setActiveAccount, selectAccounts } from 'store/account' -import { Account } from 'store/account/types' -import { CoreAccountType } from '@avalabs/types' -import { showSnackbar } from 'new/common/utils/toast' -import { uuid } from 'utils/uuid' -import Logger from 'utils/Logger' import { ScrollScreen } from 'common/components/ScrollScreen' -import bs58 from 'bs58' +import { useLedgerWallet } from 'new/features/ledger/hooks/useLedgerWallet' export default function ConfirmAddresses() { const params = useLocalSearchParams<{ @@ -34,50 +22,116 @@ export default function ConfirmAddresses() { const [step, setStep] = useState< 'connecting' | 'solana' | 'avalanche' | 'complete' >('connecting') - const [isLoading, setIsLoading] = useState(false) - const [solanaKeys, setSolanaKeys] = useState([]) - const [avalancheKeys, setAvalancheKeys] = useState(null) - const [bitcoinAddress, setBitcoinAddress] = useState('') - const [xpAddress, setXpAddress] = useState('') - const [ledgerService] = useState(() => new LedgerService()) - const dispatch = useDispatch() const router = useRouter() - const allAccounts = useSelector(selectAccounts) const { theme: { colors } } = useTheme() - - const getSolanaKeys = useCallback(async () => { + const { + isLoading, + keys: { solanaKeys, avalancheKeys, bitcoinAddress, xpAddress }, + connectToDevice, + getSolanaKeys, + getAvalancheKeys, + createLedgerWallet + } = useLedgerWallet() + + // Track key retrieval states + const [solanaKeyState, setSolanaKeyState] = useState<'pending' | 'success' | 'error'>('pending') + const [avalancheKeyState, setAvalancheKeyState] = useState<'pending' | 'success' | 'error'>('pending') + + // Handle Solana key retrieval and app switching + const handleSolanaKeys = useCallback(async () => { try { - setIsLoading(true) - Logger.info('Getting Solana keys with passive app detection') - - // Wait for Solana app to be open (passive detection) - await ledgerService.waitForApp(LedgerAppType.SOLANA) - - // Get Solana keys - const keys = await ledgerService.getSolanaPublicKeys(0) - setSolanaKeys(keys) - Logger.info('Successfully got Solana keys', keys) - - // Prompt for app switch while staying on Solana step - promptForAvalancheSwitch() + setSolanaKeyState('pending') + + // Add a small delay to ensure app is ready + await new Promise(resolve => setTimeout(resolve, 1000)) + + try { + await getSolanaKeys() + setSolanaKeyState('success') + promptForAvalancheSwitch() + } catch (error) { + // Check if it's a specific Solana app error + if (error instanceof Error) { + if (error.message.includes('Solana app is not ready') || + error.message.includes('Solana app is not open')) { + setSolanaKeyState('error') + Alert.alert( + 'Solana App Required', + 'Please open the Solana app on your Ledger device, then tap "I\'ve Switched" when ready.', + [ + { text: 'Cancel', style: 'cancel' }, + { + text: "I've Switched", + onPress: () => { + setSolanaKeyState('pending') + handleSolanaKeys() + } + } + ] + ) + return + } + + if (error.message.includes('Operation was rejected')) { + setSolanaKeyState('error') + Alert.alert( + 'Operation Rejected', + 'The operation was rejected on your Ledger device. Please try again.', + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Retry', + onPress: () => { + setSolanaKeyState('pending') + handleSolanaKeys() + } + } + ] + ) + return + } + } + + // For other errors, show a generic error message + setSolanaKeyState('error') + Alert.alert( + 'Error', + 'Failed to get Solana keys. Please ensure the Solana app is open and try again.', + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Retry', + onPress: () => { + setSolanaKeyState('pending') + handleSolanaKeys() + } + } + ] + ) + } } catch (error) { - Logger.error('Failed to get Solana keys', error) + setSolanaKeyState('error') Alert.alert( - 'Solana App Required', - 'Please open the Solana app on your Ledger device, then tap "I\'ve Switched" when ready.', + 'Error', + 'An unexpected error occurred. Please try again.', [ { text: 'Cancel', style: 'cancel' }, - { text: "I've Switched", onPress: getSolanaKeys } + { + text: 'Retry', + onPress: () => { + setSolanaKeyState('pending') + handleSolanaKeys() + } + } ] ) - } finally { - setIsLoading(false) } - }, [ledgerService, promptForAvalancheSwitch]) + }, [getSolanaKeys, promptForAvalancheSwitch]) - const promptForAvalancheSwitch = useCallback(async () => { + // Prompt for Avalanche app switch + const promptForAvalancheSwitch = useCallback(() => { Alert.alert( 'Switch to Avalanche App', 'Please switch to the Avalanche app on your Ledger device, then tap "I\'ve Switched" to continue.', @@ -87,242 +141,74 @@ export default function ConfirmAddresses() { text: "I've Switched", onPress: async () => { try { - setIsLoading(true) - Logger.info('=== RECONNECTION STARTED ===') - Logger.info( - 'User confirmed Avalanche app switch, reconnecting...' - ) - - // Force disconnect and reconnect to refresh the connection - Logger.info('Disconnecting...') - await ledgerService.disconnect() - Logger.info('Disconnected successfully') - - Logger.info('Waiting 1 second...') - await new Promise(resolve => setTimeout(resolve, 1000)) // Wait 1 second - - Logger.info('Reconnecting...') - await ledgerService.connect(params.deviceId!) - Logger.info('Reconnected successfully') - - // Move to Avalanche step and get keys setStep('avalanche') - Logger.info('Calling getAvalancheKeys...') + setAvalancheKeyState('pending') await getAvalancheKeys() - Logger.info('=== RECONNECTION COMPLETED ===') + setAvalancheKeyState('success') + setStep('complete') } catch (error) { - Logger.error('Failed to reconnect for Avalanche:', error) + setAvalancheKeyState('error') Alert.alert( - 'Reconnection Failed', - 'Failed to reconnect to the device. Please try again.', + 'Avalanche App Required', + 'Please ensure the Avalanche app is open on your Ledger device, then tap "Retry".', [ { text: 'Cancel', style: 'cancel' }, - { text: 'Retry', onPress: promptForAvalancheSwitch } + { + text: 'Retry', + onPress: () => { + setAvalancheKeyState('pending') + promptForAvalancheSwitch() + } + } ] ) - } finally { - setIsLoading(false) } } } ] ) - }, [ledgerService, params.deviceId, getAvalancheKeys]) + }, [getAvalancheKeys]) - const getAvalancheKeys = useCallback(async () => { + // Initial connection and setup + const handleInitialSetup = useCallback(async () => { try { - setIsLoading(true) - Logger.info('=== getAvalancheKeys STARTED ===') - - // Get Avalanche addresses (getAllAddresses will handle app detection internally) - Logger.info('Calling getAllAddresses...') - const addresses = await ledgerService.getAllAddresses(0, 1) - Logger.info('Avalanche app detected successfully') - Logger.info('Successfully got Avalanche addresses:', addresses) - - // Extract addresses from the response - const evmAddress = addresses.find(addr => addr.network === 'Avalanche C-Chain/EVM')?.address || '' - const xpAddress = addresses.find(addr => addr.network === 'Avalanche X-Chain')?.address || '' - const pvmAddress = addresses.find(addr => addr.network === 'Avalanche P-Chain')?.address || '' - const btcAddress = addresses.find(addr => addr.network === 'Bitcoin')?.address || '' - - // Set the addresses - setAvalancheKeys({ - evm: { key: evmAddress }, - avalanche: { key: xpAddress }, - pvm: { key: pvmAddress } - }) - setBitcoinAddress(btcAddress) - setXpAddress(xpAddress) - - Logger.info('Successfully extracted addresses:', { - evm: evmAddress, - xp: xpAddress, - pvm: pvmAddress, - btc: btcAddress - }) - - // Successfully got all keys, move to complete step - Logger.info('Setting step to complete...') - setStep('complete') - Logger.info('Ledger setup completed successfully') - } catch (error) { - Logger.error('Failed to get Avalanche keys:', error) - Alert.alert( - 'Avalanche App Required', - 'Please ensure the Avalanche app is open on your Ledger device, then tap "Retry".', - [ - { text: 'Cancel', style: 'cancel' }, - { text: 'Retry', onPress: getAvalancheKeys } - ] - ) - } finally { - setIsLoading(false) - Logger.info('=== getAvalancheKeys FINISHED ===') - } - }, [ledgerService]) - - const connectToDevice = useCallback(async () => { - try { - setIsLoading(true) - await ledgerService.connect(params.deviceId) - Logger.info('Connected to Ledger device') + await connectToDevice(params.deviceId) setStep('solana') - // Start with Solana keys (passive detection will handle app switching) - await getSolanaKeys() + await handleSolanaKeys() } catch (error) { - Logger.error('Failed to connect to device', error) Alert.alert( 'Connection Failed', 'Failed to connect to Ledger device. Please ensure your device is unlocked and try again.', [{ text: 'OK' }] ) - } finally { - setIsLoading(false) } - }, [ledgerService, params.deviceId, getSolanaKeys]) + }, [connectToDevice, params.deviceId, handleSolanaKeys]) - const createLedgerWallet = useCallback(async () => { + // Handle wallet creation + const handleCreateWallet = useCallback(async () => { try { - setIsLoading(true) - Logger.info('Creating Ledger wallet with generated keys...') - - if (!avalancheKeys || solanaKeys.length === 0 || !bitcoinAddress) { - throw new Error('Missing required keys for wallet creation') - } - - const newWalletId = uuid() - - // Store the Ledger wallet - await dispatch( - storeWallet({ - walletId: newWalletId, - walletSecret: JSON.stringify({ - deviceId: params.deviceId, - deviceName: params.deviceName || 'Ledger Device', - derivationPath: "m/44'/60'/0'/0/0", // Standard EVM derivation path - vmType: 'EVM', // NetworkVMType.EVM - derivationPathSpec: 'BIP44', // Use BIP44 derivation - extendedPublicKeys: { - evm: avalancheKeys.evm.key, - avalanche: avalancheKeys.avalanche.key - }, - publicKeys: [ - { - key: avalancheKeys.evm.key, - derivationPath: "m/44'/60'/0'/0/0", - curve: 'secp256k1' - }, - { - key: avalancheKeys.avalanche.key, - derivationPath: "m/44'/9000'/0'/0/0", - curve: 'secp256k1' - }, - { - key: avalancheKeys.pvm?.key || avalancheKeys.avalanche.key, - derivationPath: "m/44'/9000'/0'/0/0", // P-Chain uses same path as AVM - curve: 'secp256k1' - }, - { - key: solanaKeys[0]?.key || '', - derivationPath: "m/44'/501'/0'/0'", - curve: 'ed25519' - } - ], - avalancheKeys, - solanaKeys - }), - type: WalletType.LEDGER - }) - ).unwrap() - - dispatch(setActiveWallet(newWalletId)) - - // Create addresses from the keys - const addresses = { - EVM: avalancheKeys.evm.key, - AVM: avalancheKeys.avalanche.key, - PVM: avalancheKeys.pvm?.key || avalancheKeys.avalanche.key, // Use P-Chain address if available, fallback to AVM - BITCOIN: bitcoinAddress, - SVM: solanaKeys[0]?.key ? bs58.encode(new Uint8Array(Buffer.from(solanaKeys[0].key, 'hex'))) : '', - CoreEth: '' // Not implemented yet - } - - const allAccountsCount = Object.keys(allAccounts).length - - const newAccountId = uuid() - const newAccount: Account = { - id: newAccountId, - walletId: newWalletId, - name: `Account ${allAccountsCount + 1}`, - type: CoreAccountType.PRIMARY, - index: 0, - addressC: addresses.EVM, - addressBTC: addresses.BITCOIN, - addressAVM: addresses.AVM, - addressPVM: addresses.PVM, - addressSVM: addresses.SVM, - addressCoreEth: addresses.CoreEth + const walletId = await createLedgerWallet({ + deviceId: params.deviceId, + deviceName: params.deviceName || 'Ledger Device' + }) + if (walletId) { + router.push('/accountSettings/manageAccounts') } - - dispatch(setAccount(newAccount)) - dispatch(setActiveAccount(newAccountId)) - - Logger.info('Ledger wallet created successfully:', newWalletId) - showSnackbar('Ledger wallet created successfully!') - - // Navigate to manage accounts - router.push('/accountSettings/manageAccounts' as any) } catch (error) { - Logger.error('Failed to create Ledger wallet:', error) - showSnackbar( - `Failed to create wallet: ${ - error instanceof Error ? error.message : 'Unknown error' - }` + Alert.alert( + 'Wallet Creation Failed', + error instanceof Error ? error.message : 'Unknown error' ) - } finally { - setIsLoading(false) } - }, [ - avalancheKeys, - solanaKeys, - bitcoinAddress, - dispatch, - params.deviceId, - params.deviceName, - allAccounts, - router - ]) + }, [createLedgerWallet, params.deviceId, params.deviceName, router]) useEffect(() => { if (params.deviceId) { - connectToDevice() + handleInitialSetup() } else { - Logger.error('No deviceId provided in params') Alert.alert('Error', 'No device ID provided') } - }, [connectToDevice, params.deviceId]) - + }, [handleInitialSetup, params.deviceId]) const renderStepTitle = () => { @@ -375,12 +261,20 @@ export default function ConfirmAddresses() { {isLoading ? ( - ) : ( + ) : solanaKeyState === 'error' ? ( + + ) : solanaKeyState === 'success' ? ( + ) : ( + )} - {isLoading ? 'Getting Solana Keys...' : 'Solana Keys Retrieved'} + {isLoading + ? 'Getting Solana Keys...' + : solanaKeyState === 'error' + ? 'Solana Keys Failed' + : solanaKeyState === 'success' + ? 'Solana Keys Retrieved' + : 'Waiting for Solana App...'} {isLoading ? 'Please open the Solana app on your Ledger device' - : 'Successfully retrieved Solana public keys'} + : solanaKeyState === 'error' + ? 'Failed to retrieve Solana keys. Please try again.' + : solanaKeyState === 'success' + ? 'Successfully retrieved Solana public keys' + : 'Please open the Solana app on your Ledger device'} @@ -414,12 +318,20 @@ export default function ConfirmAddresses() { {isLoading ? ( - ) : ( + ) : avalancheKeyState === 'error' ? ( + + ) : avalancheKeyState === 'success' ? ( + ) : ( + )} - {isLoading ? 'Getting Avalanche Keys...' : 'Avalanche Keys Retrieved'} + {isLoading + ? 'Getting Avalanche Keys...' + : avalancheKeyState === 'error' + ? 'Avalanche Keys Failed' + : avalancheKeyState === 'success' + ? 'Avalanche Keys Retrieved' + : 'Waiting for Avalanche App...'} {isLoading ? 'Please open the Avalanche app on your Ledger device' - : 'Successfully retrieved Avalanche public keys'} + : avalancheKeyState === 'error' + ? 'Failed to retrieve Avalanche keys. Please try again.' + : avalancheKeyState === 'success' + ? 'Successfully retrieved Avalanche public keys' + : 'Please open the Avalanche app on your Ledger device'} @@ -560,7 +482,7 @@ export default function ConfirmAddresses() { diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx index ad3f70cb8f..0bb6193542 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx @@ -1,278 +1,55 @@ -import React, { useEffect, useState, useCallback } from 'react' -import { - View, - Alert, - FlatList, - Platform, - PermissionsAndroid, - Linking -} from 'react-native' - -import TransportBLE from '@ledgerhq/react-native-hw-transport-ble' +import React from 'react' +import { View, Alert, FlatList, Linking, Platform } from 'react-native' import { Button, Card, Text as K2Text, useTheme } from '@avalabs/k2-alpine' import { useRouter } from 'expo-router' import { ScrollScreen } from 'common/components/ScrollScreen' -import { LedgerService } from 'services/ledger/ledgerService' - -interface Device { - id: string - name: string - rssi?: number -} - -interface TransportState { - available: boolean - powered: boolean -} +import { + useLedgerWallet, + LedgerDevice +} from 'new/features/ledger/hooks/useLedgerWallet' export default function ConnectWallet(): JSX.Element { const router = useRouter() const { theme: { colors } } = useTheme() - const [ledgerService] = useState(() => new LedgerService()) - const [transportState, setTransportState] = useState({ - available: false, - powered: false - }) - const [devices, setDevices] = useState([]) - const [isScanning, setIsScanning] = useState(false) - const [isConnecting, setIsConnecting] = useState(false) - const [connectedDevice, setConnectedDevice] = useState(null) - - // Monitor BLE transport state - useEffect(() => { - const subscription = TransportBLE.observeState({ - next: event => { - setTransportState({ - available: event.available, - powered: false - }) - }, - complete: () => { - // Handle completion - }, - error: error => { - Alert.alert( - 'BLE Error', - `Failed to monitor BLE state: ${error.message}` - ) - } - }) - - return () => { - subscription.unsubscribe() - } - }, []) - - // Request Bluetooth permissions - const requestBluetoothPermissions = useCallback(async () => { - if (Platform.OS === 'android') { - try { - const permissions = [ - PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, - PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT, - PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION - ].filter(Boolean) as any[] - - const granted = await PermissionsAndroid.requestMultiple(permissions) - - return Object.values(granted).every( - permission => permission === 'granted' - ) - } catch (err) { - return false + const { + devices, + isScanning, + isConnecting, + transportState, + scanForDevices, + connectToDevice + } = useLedgerWallet() + + // Handle successful connection + const handleDeviceConnection = async ( + deviceId: string, + deviceName: string + ): Promise => { + Alert.alert('Success', `Connected to ${deviceName}`, [ + { + text: 'Continue', + onPress: () => { + router.push({ + pathname: '/accountSettings/ledger/confirmAddresses', + params: { deviceId, deviceName } + }) + } } - } else if (Platform.OS === 'ios') { - // iOS permissions are handled automatically by the TransportBLE library - return true - } - return false - }, []) - - // Handle scan errors - const handleScanError = useCallback((error: any) => { - setIsScanning(false) - - // Handle specific authorization errors - if ( - error.message?.includes('not authorized') || - error.message?.includes('Origin: 101') - ) { - Alert.alert( - 'Bluetooth Permission Required', - 'Please enable Bluetooth permissions in your device settings to scan for Ledger devices.', - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Open Settings', - onPress: () => { - // You can add navigation to settings here if needed - } - } - ] - ) - } else { - Alert.alert('Scan Error', `Failed to scan for devices: ${error.message}`) - } - }, []) - - // Scan for Ledger devices - const scanForDevices = useCallback(async () => { - if (!transportState.available) { - Alert.alert( - 'Bluetooth Unavailable', - 'Please enable Bluetooth to scan for Ledger devices' - ) - return - } - - // Request permissions before scanning - const hasPermissions = await requestBluetoothPermissions() - if (!hasPermissions) { - Alert.alert( - 'Permission Required', - 'Bluetooth permissions are required to scan for Ledger devices. Please grant permissions in your device settings.' - ) - return - } - - setIsScanning(true) - setDevices([]) - - try { - const subscription = TransportBLE.listen({ - next: event => { - if (event.type === 'add') { - const device: Device = { - id: event.descriptor.id, - name: event.descriptor.name || 'Unknown Device', - rssi: event.descriptor.rssi - } - - setDevices(prev => { - // Avoid duplicates - const exists = prev.find(d => d.id === device.id) - if (!exists) { - return [...prev, device] - } - return prev - }) - } - }, - complete: () => { - setIsScanning(false) - }, - error: handleScanError - }) - - // Stop scanning after 10 seconds - setTimeout(() => { - subscription.unsubscribe() - setIsScanning(false) - }, 10000) - } catch (error) { - setIsScanning(false) - Alert.alert( - 'Scan Error', - `Failed to start scanning: ${ - error instanceof Error ? error.message : 'Unknown error' - }` - ) - } - }, [transportState.available, requestBluetoothPermissions, handleScanError]) + ]) + } // Open Bluetooth settings - const openBluetoothSettings = useCallback(() => { + const openBluetoothSettings = (): void => { if (Platform.OS === 'ios') { Linking.openURL('App-Prefs:Bluetooth') } else { Linking.openSettings() } - }, []) - - // Connect to a specific device using LedgerService - const connectToDevice = useCallback( - async (device: Device) => { - setIsConnecting(true) - - try { - // Use LedgerService to connect - await ledgerService.connect(device.id) + } - setConnectedDevice(device) - Alert.alert('Success', `Connected to ${device.name}`, [ - { - text: 'Continue', - onPress: () => { - // Navigate to confirm addresses screen with device ID - router.push({ - pathname: '/accountSettings/ledger/confirmAddresses' as any, - params: { deviceId: device.id } - }) - } - } - ]) - } catch (error) { - let errorMessage = 'Unknown connection error' - - if (error instanceof Error) { - if (error.message.includes('TurboModule')) { - errorMessage = - 'Connection failed due to compatibility issue. Please try restarting the app or updating to the latest version.' - } else if (error.message.includes('timeout')) { - errorMessage = - 'Connection timed out. Please make sure your Ledger device is unlocked and in pairing mode.' - } else if (error.message.includes('PeerRemovedPairing')) { - Alert.alert( - 'Pairing Removed', - 'The pairing with your Ledger device was removed. Would you like to open Bluetooth settings to re-pair?', - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Open Settings', - onPress: openBluetoothSettings - } - ] - ) - return // Don't show the generic error alert - } else { - errorMessage = error.message - } - } - - Alert.alert( - 'Connection Error', - `Failed to connect to ${device.name}: ${errorMessage}` - ) - } finally { - setIsConnecting(false) - } - }, - [openBluetoothSettings, router, ledgerService] - ) - - // Disconnect from current device - const disconnectDevice = useCallback(async () => { - try { - // Use LedgerService to disconnect - await ledgerService.disconnect() - setConnectedDevice(null) - Alert.alert( - 'Disconnected', - 'Successfully disconnected from Ledger device' - ) - } catch (error) { - Alert.alert( - 'Disconnect Error', - `Failed to disconnect: ${ - error instanceof Error ? error.message : 'Unknown error' - }` - ) - } - }, [ledgerService]) - - const renderDevice = ({ item }: { item: Device }) => ( + const renderDevice = ({ item }: { item: LedgerDevice }): JSX.Element => ( connectToDevice(item)} + onPress={async () => { + try { + await connectToDevice(item.id) + handleDeviceConnection(item.id, item.name) + } catch (error) { + if (error instanceof Error) { + if (error.message.includes('PeerRemovedPairing')) { + Alert.alert( + 'Pairing Removed', + 'The pairing with your Ledger device was removed. Would you like to open Bluetooth settings to re-pair?', + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Open Settings', + onPress: openBluetoothSettings + } + ] + ) + } else { + Alert.alert( + 'Connection Error', + `Failed to connect to ${item.name}: ${error.message}` + ) + } + } + } + }} disabled={isConnecting}> {isConnecting ? 'Connecting...' : 'Connect'} @@ -324,32 +127,6 @@ export default function ConnectWallet(): JSX.Element { - {/* Connected Device */} - {connectedDevice && ( - - - Connected Device - - - {connectedDevice.name} - - - - )} - {/* Scan Button */} - ) } From f37db80c9288f65ff818ceab28ed7eca25142b61 Mon Sep 17 00:00:00 2001 From: B0Y3R-AVA Date: Tue, 12 Aug 2025 11:49:12 -0400 Subject: [PATCH 06/24] docs --- docs/ledger-signing-findings.mdc | 135 +++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 docs/ledger-signing-findings.mdc diff --git a/docs/ledger-signing-findings.mdc b/docs/ledger-signing-findings.mdc new file mode 100644 index 0000000000..61906334d5 --- /dev/null +++ b/docs/ledger-signing-findings.mdc @@ -0,0 +1,135 @@ +# Ledger Transaction Signing Findings + +## Latest Success! (2024-03-19) + +### Working Solution +- Successfully got signature from Ledger device +- User was able to approve on device +- Signature format confirmed working + +### Working Transaction Format +```typescript +// Transaction data format: +{ + nonce: 0, + type: 0, // Legacy transaction type + chainId: 43114, + gasPrice: maxFeePerGas, // Use maxFeePerGas as gasPrice + gasLimit: 45427, + data: "0xa9059cbb...", // USDC.e transfer + from: "0x449b3fffe66378227dbbd05539b6542e5ca75a28", + to: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E" +} + +// RLP encoded: +0xf86b8085022ad57bb082b17394b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e80b844a9059cbb... +``` + +### Working Resolution Object +```typescript +{ + externalPlugin: [], + erc20Tokens: ['0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E'], + nfts: [], + plugin: [], + domains: [] // Required field +} +``` + +### Signature Format +```typescript +{ + v: "0150f8", + r: "20f4e602da6206d60ead3988e0415fb9119db0debe1d4d2c4910702cd32900fe", + s: "017447d453a1648c4813fa1685b718a70320651f7134573e1f06dcf5c077b6c3" +} +``` + +## Key Learnings +1. Must use legacy transaction format (type 0) instead of EIP-1559 +2. Resolution object requires domains field +3. Signature comes back as object with r, s, v properties +4. Need to concatenate signature parts with 0x prefix + +## Working Implementation Steps +1. Convert EIP-1559 transaction to legacy format +2. Use maxFeePerGas as gasPrice +3. Include domains array in resolution object +4. Handle signature result as object properties +5. Concatenate signature parts correctly + +## Previous Failed Attempts + +### 1. Using LedgerSigner from SDK +```typescript +const signer = await this.getEvmSigner(provider, accountIndex) +``` +Status: ❌ Failed +Reason: Direct SDK signer approach doesn't work with Ledger device + +### 2. Raw Transaction Data +```typescript +const unsignedTx = transaction.data?.toString() || '' +``` +Status: ❌ Failed +Reason: Data too short, missing proper transaction structure + +### 3. Ethers.js Transaction.from() with EIP-1559 +```typescript +const tx = { + type: 2, + chainId: transaction.chainId, + maxFeePerGas: transaction.maxFeePerGas, + maxPriorityFeePerGas: transaction.maxPriorityFeePerGas, + // ... +} +``` +Status: ❌ Failed +Reason: Ledger app doesn't support EIP-1559 format + +### 4. Manual Hex Concatenation +```typescript +const unsignedTx = [txType, txNonce, ...].join('') +``` +Status: ❌ Failed +Reason: Incorrect transaction format + +### 5. Using Ethereum App +```typescript +const ethApp = new AppEth(transport) +``` +Status: ❌ Failed +Reason: Wrong app for Avalanche C-Chain + +### 6. Using Avalanche App with signTransaction +```typescript +const avaxApp = new AppAvax(transport) +await avaxApp.signTransaction(...) +``` +Status: ❌ Failed +Reason: Wrong method name + +### 7. Using Avalanche App with signEVMTransaction and EIP-1559 +```typescript +const avaxApp = new AppAvax(transport) +await avaxApp.signEVMTransaction(...) +``` +Status: ❌ Failed +Reason: EIP-1559 format not supported + +### 8. Using Avalanche App with signEVMTransaction and Legacy Format +```typescript +const avaxApp = new AppAvax(transport) +const result = await avaxApp.signEVMTransaction(...) +return `0x${result.r}${result.s}${result.v}` +``` +Status: ✅ Success +Reason: Legacy format works with proper signature handling + +## Requirements for Valid Transaction +1. Must use legacy transaction format (type 0) +2. Must include all required fields +3. Must use correct signing method (signEVMTransaction) +4. Resolution object must include all fields including domains +5. Must handle chainId 43114 (Avalanche C-Chain) correctly +6. Must properly concatenate signature components \ No newline at end of file From e10770575848a65956afac506ee4c5ad96430801 Mon Sep 17 00:00:00 2001 From: B0Y3R-AVA Date: Tue, 19 Aug 2025 16:07:21 -0400 Subject: [PATCH 07/24] linting --- .../ledger/confirmAddresses.tsx | 69 ++++++++++--------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx index 2ab7465fdf..a02898efff 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx @@ -36,17 +36,21 @@ export default function ConfirmAddresses() { } = useLedgerWallet() // Track key retrieval states - const [solanaKeyState, setSolanaKeyState] = useState<'pending' | 'success' | 'error'>('pending') - const [avalancheKeyState, setAvalancheKeyState] = useState<'pending' | 'success' | 'error'>('pending') + const [solanaKeyState, setSolanaKeyState] = useState< + 'pending' | 'success' | 'error' + >('pending') + const [avalancheKeyState, setAvalancheKeyState] = useState< + 'pending' | 'success' | 'error' + >('pending') // Handle Solana key retrieval and app switching const handleSolanaKeys = useCallback(async () => { try { setSolanaKeyState('pending') - + // Add a small delay to ensure app is ready await new Promise(resolve => setTimeout(resolve, 1000)) - + try { await getSolanaKeys() setSolanaKeyState('success') @@ -54,16 +58,18 @@ export default function ConfirmAddresses() { } catch (error) { // Check if it's a specific Solana app error if (error instanceof Error) { - if (error.message.includes('Solana app is not ready') || - error.message.includes('Solana app is not open')) { + if ( + error.message.includes('Solana app is not ready') || + error.message.includes('Solana app is not open') + ) { setSolanaKeyState('error') Alert.alert( 'Solana App Required', 'Please open the Solana app on your Ledger device, then tap "I\'ve Switched" when ready.', [ { text: 'Cancel', style: 'cancel' }, - { - text: "I've Switched", + { + text: "I've Switched", onPress: () => { setSolanaKeyState('pending') handleSolanaKeys() @@ -73,7 +79,7 @@ export default function ConfirmAddresses() { ) return } - + if (error.message.includes('Operation was rejected')) { setSolanaKeyState('error') Alert.alert( @@ -81,8 +87,8 @@ export default function ConfirmAddresses() { 'The operation was rejected on your Ledger device. Please try again.', [ { text: 'Cancel', style: 'cancel' }, - { - text: 'Retry', + { + text: 'Retry', onPress: () => { setSolanaKeyState('pending') handleSolanaKeys() @@ -93,7 +99,7 @@ export default function ConfirmAddresses() { return } } - + // For other errors, show a generic error message setSolanaKeyState('error') Alert.alert( @@ -101,8 +107,8 @@ export default function ConfirmAddresses() { 'Failed to get Solana keys. Please ensure the Solana app is open and try again.', [ { text: 'Cancel', style: 'cancel' }, - { - text: 'Retry', + { + text: 'Retry', onPress: () => { setSolanaKeyState('pending') handleSolanaKeys() @@ -113,20 +119,16 @@ export default function ConfirmAddresses() { } } catch (error) { setSolanaKeyState('error') - Alert.alert( - 'Error', - 'An unexpected error occurred. Please try again.', - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Retry', - onPress: () => { - setSolanaKeyState('pending') - handleSolanaKeys() - } + Alert.alert('Error', 'An unexpected error occurred. Please try again.', [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Retry', + onPress: () => { + setSolanaKeyState('pending') + handleSolanaKeys() } - ] - ) + } + ]) } }, [getSolanaKeys, promptForAvalancheSwitch]) @@ -153,8 +155,8 @@ export default function ConfirmAddresses() { 'Please ensure the Avalanche app is open on your Ledger device, then tap "Retry".', [ { text: 'Cancel', style: 'cancel' }, - { - text: 'Retry', + { + text: 'Retry', onPress: () => { setAvalancheKeyState('pending') promptForAvalancheSwitch() @@ -210,7 +212,6 @@ export default function ConfirmAddresses() { } }, [handleInitialSetup, params.deviceId]) - const renderStepTitle = () => { switch (step) { case 'connecting': @@ -283,8 +284,8 @@ export default function ConfirmAddresses() { textAlign: 'center', color: '$textPrimary' }}> - {isLoading - ? 'Getting Solana Keys...' + {isLoading + ? 'Getting Solana Keys...' : solanaKeyState === 'error' ? 'Solana Keys Failed' : solanaKeyState === 'success' @@ -340,8 +341,8 @@ export default function ConfirmAddresses() { textAlign: 'center', color: '$textPrimary' }}> - {isLoading - ? 'Getting Avalanche Keys...' + {isLoading + ? 'Getting Avalanche Keys...' : avalancheKeyState === 'error' ? 'Avalanche Keys Failed' : avalancheKeyState === 'success' From 27e71760e66d010730f43e82f4a741494f59da96 Mon Sep 17 00:00:00 2001 From: B0Y3R-AVA Date: Wed, 10 Sep 2025 20:46:02 -0400 Subject: [PATCH 08/24] ledger live address derivation working, transactiosn on avalanche and solana proven with this setup --- .../components/DerivationPathEducation.tsx | 281 +++++++++ .../features/ledger/hooks/useLedgerWallet.ts | 209 ++++++- .../ledger/confirmAddresses.tsx | 548 +----------------- .../accountSettings/ledger/connectWallet.tsx | 188 +----- 4 files changed, 505 insertions(+), 721 deletions(-) create mode 100644 packages/core-mobile/app/new/features/ledger/components/DerivationPathEducation.tsx diff --git a/packages/core-mobile/app/new/features/ledger/components/DerivationPathEducation.tsx b/packages/core-mobile/app/new/features/ledger/components/DerivationPathEducation.tsx new file mode 100644 index 0000000000..b43850fa9b --- /dev/null +++ b/packages/core-mobile/app/new/features/ledger/components/DerivationPathEducation.tsx @@ -0,0 +1,281 @@ +import React, { useState } from 'react' +import { View, ScrollView, TouchableOpacity } from 'react-native' +import { Text, Button, useTheme } from '@avalabs/k2-alpine' +import { LedgerDerivationPathType } from 'services/wallet/LedgerWallet' + +interface EducationSection { + title: string + content: string + icon: string +} + +interface DerivationPathEducationProps { + onClose: () => void + onSelectRecommended?: () => void +} + +const educationSections: EducationSection[] = [ + { + title: 'What are derivation paths?', + content: 'Derivation paths are like addresses that tell your Ledger device which keys to generate. Different paths offer different security and convenience tradeoffs.', + icon: '🔑' + }, + { + title: 'BIP44 Standard', + content: 'BIP44 is the most common approach used by most wallets. It stores "extended keys" that allow creating new accounts without connecting your device each time.', + icon: '⚡' + }, + { + title: 'Ledger Live Approach', + content: 'Ledger Live uses individual keys for maximum security. Each account requires explicit device confirmation, but no extended keys are stored locally.', + icon: '🔒' + }, + { + title: 'Which should I choose?', + content: 'For most users, BIP44 offers the best balance of security and convenience. Choose Ledger Live only if you prioritize maximum security over convenience.', + icon: '🤔' + } +] + +const comparisonData = [ + { + feature: 'Setup Time', + bip44: '~15 seconds', + ledgerLive: '~45 seconds', + winner: 'bip44' + }, + { + feature: 'New Account Creation', + bip44: 'Instant (no device)', + ledgerLive: 'Requires device', + winner: 'bip44' + }, + { + feature: 'Security Level', + bip44: 'High (standard)', + ledgerLive: 'Maximum', + winner: 'ledgerLive' + }, + { + feature: 'Compatibility', + bip44: 'Universal', + ledgerLive: 'Ledger ecosystem', + winner: 'bip44' + }, + { + feature: 'User Experience', + bip44: 'Smooth', + ledgerLive: 'More confirmations', + winner: 'bip44' + } +] + +export const DerivationPathEducation: React.FC = ({ + onClose, + onSelectRecommended +}) => { + const { theme: { colors } } = useTheme() + const [activeSection, setActiveSection] = useState(null) + + const toggleSection = (index: number) => { + setActiveSection(activeSection === index ? null : index) + } + + return ( + + {/* Header */} + + + Understanding Setup Methods + + + + ✕ + + + + + + {/* Education sections */} + + {educationSections.map((section, index) => ( + toggleSection(index)} + > + + + {section.icon} + + + {section.title} + + + ▼ + + + + {activeSection === index && ( + + + {section.content} + + + )} + + ))} + + + {/* Comparison table */} + + + Side-by-Side Comparison + + + + {/* Header row */} + + Feature + BIP44 + Ledger Live + + + {/* Data rows */} + {comparisonData.map((row, index) => ( + + + {row.feature} + + + {row.bip44} + + + {row.ledgerLive} + + + ))} + + + + {/* Recommendation */} + + + 💡 Our Recommendation + + + For most users, BIP44 provides the best + experience with excellent security. It's faster to set up, easier to manage, + and compatible with all wallets. + + + Choose Ledger Live only if you're a + security expert who prioritizes maximum security over convenience. + + + + + {/* Action buttons */} + + + {onSelectRecommended && ( + + )} + + + ) +} diff --git a/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts b/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts index 6f81fe7e56..de2c0e37d6 100644 --- a/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts +++ b/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts @@ -10,8 +10,13 @@ import LedgerService from 'services/ledger/LedgerService' import { LedgerAppType, LedgerDerivationPathType } from 'services/ledger/types' ======= import AppSolana from '@ledgerhq/hw-app-solana' +import bs58 from 'bs58' import { LedgerService, LedgerAppType } from 'services/ledger/ledgerService' +<<<<<<< HEAD >>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) +======= +import { LedgerDerivationPathType } from 'services/wallet/LedgerWallet' +>>>>>>> 6a596f952 (ledger live address derivation working, transactiosn on avalanche and solana proven with this setup) import { ChainName } from 'services/network/consts' import { WalletType } from 'services/wallet/types' import { AppThunkDispatch } from 'store/types' @@ -107,6 +112,26 @@ export interface LedgerKeys { xpAddress: string } +export interface WalletCreationOptions { + deviceId: string + deviceName?: string + derivationPathType: LedgerDerivationPathType + accountCount?: number + individualKeys?: any[] // For Ledger Live - individual keys retrieved from device + progressCallback?: ( + step: string, + progress: number, + totalSteps: number + ) => void +} + +export interface SetupProgress { + currentStep: string + progress: number + totalSteps: number + estimatedTimeRemaining?: number +} + export interface UseLedgerWalletReturn { // Device scanning and connection devices: LedgerDevice[] @@ -121,14 +146,15 @@ export interface UseLedgerWalletReturn { isLoading: boolean getSolanaKeys: () => Promise getAvalancheKeys: () => Promise + getLedgerLiveKeys: (accountCount?: number, progressCallback?: (step: string, progress: number, totalSteps: number) => void) => Promise<{ avalancheKeys: any, individualKeys: any[] }> resetKeys: () => void keys: LedgerKeys // Wallet creation - createLedgerWallet: (params: { - deviceId: string - deviceName?: string - }) => Promise // Returns the new wallet ID + createLedgerWallet: (options: WalletCreationOptions) => Promise // Returns the new wallet ID + + // Setup progress + setupProgress: SetupProgress | null } >>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) @@ -147,6 +173,7 @@ export function useLedgerWallet(): UseLedgerWalletReturn { const [isScanning, setIsScanning] = useState(false) const [isConnecting, setIsConnecting] = useState(false) const [isLoading, setIsLoading] = useState(false) +<<<<<<< HEAD <<<<<<< HEAD const [setupProgress, setSetupProgress] = useState(null) @@ -155,6 +182,9 @@ export function useLedgerWallet(): UseLedgerWalletReturn { const [avalancheKeys, setAvalancheKeys] = useState(null) ======= +======= + const [setupProgress, setSetupProgress] = useState(null) +>>>>>>> 6a596f952 (ledger live address derivation working, transactiosn on avalanche and solana proven with this setup) // Key states const [solanaKeys, setSolanaKeys] = useState([]) @@ -403,9 +433,14 @@ export function useLedgerWallet(): UseLedgerWalletReturn { const derivationPath = `44'/501'/0'/0'/0` const result = await solanaApp.getAddress(derivationPath, false) +<<<<<<< HEAD // result.address is already in base58 format const solanaAddress = result.address.toString() >>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) +======= + // Convert the Buffer to base58 format (Solana address format) + const solanaAddress = bs58.encode(new Uint8Array(result.address)) +>>>>>>> 6a596f952 (ledger live address derivation working, transactiosn on avalanche and solana proven with this setup) setSolanaKeys([ { @@ -508,6 +543,7 @@ export function useLedgerWallet(): UseLedgerWalletReturn { setXpAddress('') }, []) +<<<<<<< HEAD <<<<<<< HEAD // New method: Get individual keys for Ledger Live (sequential device confirmations) const getLedgerLiveKeys = useCallback( @@ -650,23 +686,140 @@ export function useLedgerWallet(): UseLedgerWalletReturn { `Creating ${derivationPathType} Ledger wallet with generated keys...` ) ======= +======= + // New method: Get individual keys for Ledger Live (sequential device confirmations) + const getLedgerLiveKeys = useCallback(async ( + accountCount: number = 3, + progressCallback?: (step: string, progress: number, totalSteps: number) => void + ) => { + try { + setIsLoading(true) + Logger.info(`Starting Ledger Live key retrieval for ${accountCount} accounts`) + + const totalSteps = accountCount // One step per account (gets both EVM and AVM) + const individualKeys: any[] = [] + let avalancheKeysResult: any = null + + // Sequential address retrieval - each account requires device confirmation + for (let accountIndex = 0; accountIndex < accountCount; accountIndex++) { + const stepName = `Getting keys for account ${accountIndex + 1}...` + const progress = Math.round(((accountIndex + 1) / totalSteps) * 100) + progressCallback?.(stepName, progress, totalSteps) + + Logger.info(`Requesting addresses for account ${accountIndex} (Ledger Live style)`) + + // Get public keys for this specific account (1 at a time for device confirmation) + const publicKeys = await ledgerService.getPublicKeys(accountIndex, 1) + + // Also get addresses for display purposes + const addresses = await ledgerService.getAllAddresses(accountIndex, 1) + + // Extract the keys for this account + const evmPublicKey = publicKeys.find(key => key.derivationPath.includes("44'/60'")) + const avmPublicKey = publicKeys.find(key => key.derivationPath.includes("44'/9000'")) + + // Extract addresses for this account + const evmAddress = addresses.find(addr => addr.network === ChainName.AVALANCHE_C_EVM) + const xpAddress = addresses.find(addr => addr.network === ChainName.AVALANCHE_X) + + if (evmPublicKey) { + individualKeys.push({ + key: evmPublicKey.key, + derivationPath: `m/44'/60'/${accountIndex}'/0/0`, // Ledger Live path + curve: evmPublicKey.curve + }) + } + + if (avmPublicKey) { + individualKeys.push({ + key: avmPublicKey.key, + derivationPath: `m/44'/9000'/${accountIndex}'/0/0`, // Ledger Live path + curve: avmPublicKey.curve + }) + } + + // Store first account's keys as primary + if (accountIndex === 0) { + avalancheKeysResult = { + evm: { + key: evmPublicKey?.key || '', + address: evmAddress?.address || '' + }, + avalanche: { + key: avmPublicKey?.key || '', + address: xpAddress?.address || '' + } + } + } + } + + // Update state with the retrieved keys + if (avalancheKeysResult) { + setAvalancheKeys(avalancheKeysResult) + } + + Logger.info(`Successfully retrieved Ledger Live keys for ${accountCount} accounts`) + Logger.info('Individual keys count:', individualKeys.length) + + return { avalancheKeys: avalancheKeysResult, individualKeys } + + } catch (error) { + Logger.error('Failed to get Ledger Live keys:', error) + throw error + } finally { + setIsLoading(false) + } + }, [ledgerService]) + +>>>>>>> 6a596f952 (ledger live address derivation working, transactiosn on avalanche and solana proven with this setup) const createLedgerWallet = useCallback( async ({ deviceId, - deviceName = 'Ledger Device' - }: { - deviceId: string - deviceName?: string - }) => { + deviceName = 'Ledger Device', + derivationPathType = LedgerDerivationPathType.BIP44, + accountCount = derivationPathType === LedgerDerivationPathType.BIP44 + ? 3 + : 1, + individualKeys = [], + progressCallback + }: WalletCreationOptions) => { try { setIsLoading(true) +<<<<<<< HEAD Logger.info('Creating Ledger wallet with generated keys...') >>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) +======= + + // Initialize progress tracking + const totalSteps = + derivationPathType === LedgerDerivationPathType.BIP44 ? 3 : 6 + let currentStep = 1 + + const updateProgress = (stepName: string) => { + const progress = { + currentStep: stepName, + progress: Math.round((currentStep / totalSteps) * 100), + totalSteps, + estimatedTimeRemaining: + (totalSteps - currentStep) * + (derivationPathType === LedgerDerivationPathType.BIP44 ? 5 : 8) + } + setSetupProgress(progress) + progressCallback?.(stepName, progress.progress, totalSteps) + currentStep++ + } + + updateProgress('Validating keys...') + Logger.info( + `Creating ${derivationPathType} Ledger wallet with generated keys...` + ) +>>>>>>> 6a596f952 (ledger live address derivation working, transactiosn on avalanche and solana proven with this setup) if (!avalancheKeys || solanaKeys.length === 0 || !bitcoinAddress) { throw new Error('Missing required keys for wallet creation') } +<<<<<<< HEAD <<<<<<< HEAD updateProgress('Generating wallet ID...') const newWalletId = uuid() @@ -678,6 +831,13 @@ export function useLedgerWallet(): UseLedgerWalletReturn { // Store the Ledger wallet >>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) +======= + updateProgress('Generating wallet ID...') + const newWalletId = uuid() + + updateProgress('Storing wallet data...') + // Store the Ledger wallet with the specified derivation path type +>>>>>>> 6a596f952 (ledger live address derivation working, transactiosn on avalanche and solana proven with this setup) await dispatch( storeWallet({ walletId: newWalletId, @@ -731,12 +891,16 @@ export function useLedgerWallet(): UseLedgerWalletReturn { ======= derivationPath: "m/44'/60'/0'/0/0", vmType: 'EVM', - derivationPathSpec: 'BIP44', - extendedPublicKeys: { - evm: avalancheKeys.evm.key, - avalanche: avalancheKeys.avalanche.key - }, - publicKeys: [ + derivationPathSpec: derivationPathType, + ...(derivationPathType === LedgerDerivationPathType.BIP44 && { + extendedPublicKeys: { + evm: avalancheKeys.evm.key, + avalanche: avalancheKeys.avalanche.key + } + }), + publicKeys: derivationPathType === LedgerDerivationPathType.LedgerLive && individualKeys.length > 0 + ? individualKeys // Use individual keys for Ledger Live + : [ // Use existing keys for BIP44 { key: avalancheKeys.evm.key, derivationPath: "m/44'/60'/0'/0/0", @@ -812,10 +976,14 @@ export function useLedgerWallet(): UseLedgerWalletReturn { throw error } finally { setIsLoading(false) +<<<<<<< HEAD <<<<<<< HEAD setSetupProgress(null) ======= >>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) +======= + setSetupProgress(null) +>>>>>>> 6a596f952 (ledger live address derivation working, transactiosn on avalanche and solana proven with this setup) } }, [avalancheKeys, solanaKeys, bitcoinAddress, dispatch, allAccounts] @@ -845,7 +1013,12 @@ export function useLedgerWallet(): UseLedgerWalletReturn { isLoading, getSolanaKeys, getAvalancheKeys, +<<<<<<< HEAD >>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) +======= + getLedgerLiveKeys, + resetKeys, +>>>>>>> 6a596f952 (ledger live address derivation working, transactiosn on avalanche and solana proven with this setup) keys: { solanaKeys, avalancheKeys, @@ -859,7 +1032,13 @@ export function useLedgerWallet(): UseLedgerWalletReturn { // Wallet creation createLedgerWallet, +<<<<<<< HEAD resetKeys >>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) +======= + + // Setup progress + setupProgress +>>>>>>> 6a596f952 (ledger live address derivation working, transactiosn on avalanche and solana proven with this setup) } } diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx index a02898efff..13a174b6e0 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx @@ -1,532 +1,18 @@ -import React, { useState, useCallback, useEffect } from 'react' -import { Alert } from 'react-native' -import { useRouter, useLocalSearchParams } from 'expo-router' -import { - View, - Text, - Button, - Card, - Icons, - useTheme, - CircularProgress -} from '@avalabs/k2-alpine' -import { LoadingState } from 'new/common/components/LoadingState' -import { ScrollScreen } from 'common/components/ScrollScreen' -import { useLedgerWallet } from 'new/features/ledger/hooks/useLedgerWallet' - -export default function ConfirmAddresses() { - const params = useLocalSearchParams<{ - deviceId: string - deviceName: string - }>() - const [step, setStep] = useState< - 'connecting' | 'solana' | 'avalanche' | 'complete' - >('connecting') +import React from 'react' +import { useRouter } from 'expo-router' + +/** + * @deprecated This route is deprecated. Use enhancedSetup.tsx instead. + * This file redirects to the new enhanced Ledger setup flow. + */ +export default function ConfirmAddresses(): JSX.Element { const router = useRouter() - const { - theme: { colors } - } = useTheme() - const { - isLoading, - keys: { solanaKeys, avalancheKeys, bitcoinAddress, xpAddress }, - connectToDevice, - getSolanaKeys, - getAvalancheKeys, - createLedgerWallet - } = useLedgerWallet() - - // Track key retrieval states - const [solanaKeyState, setSolanaKeyState] = useState< - 'pending' | 'success' | 'error' - >('pending') - const [avalancheKeyState, setAvalancheKeyState] = useState< - 'pending' | 'success' | 'error' - >('pending') - - // Handle Solana key retrieval and app switching - const handleSolanaKeys = useCallback(async () => { - try { - setSolanaKeyState('pending') - - // Add a small delay to ensure app is ready - await new Promise(resolve => setTimeout(resolve, 1000)) - - try { - await getSolanaKeys() - setSolanaKeyState('success') - promptForAvalancheSwitch() - } catch (error) { - // Check if it's a specific Solana app error - if (error instanceof Error) { - if ( - error.message.includes('Solana app is not ready') || - error.message.includes('Solana app is not open') - ) { - setSolanaKeyState('error') - Alert.alert( - 'Solana App Required', - 'Please open the Solana app on your Ledger device, then tap "I\'ve Switched" when ready.', - [ - { text: 'Cancel', style: 'cancel' }, - { - text: "I've Switched", - onPress: () => { - setSolanaKeyState('pending') - handleSolanaKeys() - } - } - ] - ) - return - } - - if (error.message.includes('Operation was rejected')) { - setSolanaKeyState('error') - Alert.alert( - 'Operation Rejected', - 'The operation was rejected on your Ledger device. Please try again.', - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Retry', - onPress: () => { - setSolanaKeyState('pending') - handleSolanaKeys() - } - } - ] - ) - return - } - } - - // For other errors, show a generic error message - setSolanaKeyState('error') - Alert.alert( - 'Error', - 'Failed to get Solana keys. Please ensure the Solana app is open and try again.', - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Retry', - onPress: () => { - setSolanaKeyState('pending') - handleSolanaKeys() - } - } - ] - ) - } - } catch (error) { - setSolanaKeyState('error') - Alert.alert('Error', 'An unexpected error occurred. Please try again.', [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Retry', - onPress: () => { - setSolanaKeyState('pending') - handleSolanaKeys() - } - } - ]) - } - }, [getSolanaKeys, promptForAvalancheSwitch]) - - // Prompt for Avalanche app switch - const promptForAvalancheSwitch = useCallback(() => { - Alert.alert( - 'Switch to Avalanche App', - 'Please switch to the Avalanche app on your Ledger device, then tap "I\'ve Switched" to continue.', - [ - { text: 'Cancel', style: 'cancel' }, - { - text: "I've Switched", - onPress: async () => { - try { - setStep('avalanche') - setAvalancheKeyState('pending') - await getAvalancheKeys() - setAvalancheKeyState('success') - setStep('complete') - } catch (error) { - setAvalancheKeyState('error') - Alert.alert( - 'Avalanche App Required', - 'Please ensure the Avalanche app is open on your Ledger device, then tap "Retry".', - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Retry', - onPress: () => { - setAvalancheKeyState('pending') - promptForAvalancheSwitch() - } - } - ] - ) - } - } - } - ] - ) - }, [getAvalancheKeys]) - - // Initial connection and setup - const handleInitialSetup = useCallback(async () => { - try { - await connectToDevice(params.deviceId) - setStep('solana') - await handleSolanaKeys() - } catch (error) { - Alert.alert( - 'Connection Failed', - 'Failed to connect to Ledger device. Please ensure your device is unlocked and try again.', - [{ text: 'OK' }] - ) - } - }, [connectToDevice, params.deviceId, handleSolanaKeys]) - - // Handle wallet creation - const handleCreateWallet = useCallback(async () => { - try { - const walletId = await createLedgerWallet({ - deviceId: params.deviceId, - deviceName: params.deviceName || 'Ledger Device' - }) - if (walletId) { - router.push('/accountSettings/manageAccounts') - } - } catch (error) { - Alert.alert( - 'Wallet Creation Failed', - error instanceof Error ? error.message : 'Unknown error' - ) - } - }, [createLedgerWallet, params.deviceId, params.deviceName, router]) - - useEffect(() => { - if (params.deviceId) { - handleInitialSetup() - } else { - Alert.alert('Error', 'No device ID provided') - } - }, [handleInitialSetup, params.deviceId]) - - const renderStepTitle = () => { - switch (step) { - case 'connecting': - return 'Connecting to Ledger' - case 'solana': - return 'Solana Setup' - case 'avalanche': - return 'Avalanche Setup' - case 'complete': - return 'Setup Complete' - default: - return 'Confirm Addresses' - } - } - - const renderStepContent = () => { - switch (step) { - case 'connecting': - return ( - - - - Connecting to your Ledger device... - - - Please ensure your device is unlocked and nearby - - - ) - - case 'solana': - return ( - - - - {isLoading ? ( - - ) : solanaKeyState === 'error' ? ( - - ) : solanaKeyState === 'success' ? ( - - ) : ( - - )} - - {isLoading - ? 'Getting Solana Keys...' - : solanaKeyState === 'error' - ? 'Solana Keys Failed' - : solanaKeyState === 'success' - ? 'Solana Keys Retrieved' - : 'Waiting for Solana App...'} - - - {isLoading - ? 'Please open the Solana app on your Ledger device' - : solanaKeyState === 'error' - ? 'Failed to retrieve Solana keys. Please try again.' - : solanaKeyState === 'success' - ? 'Successfully retrieved Solana public keys' - : 'Please open the Solana app on your Ledger device'} - - - - - ) - - case 'avalanche': - return ( - - - - {isLoading ? ( - - ) : avalancheKeyState === 'error' ? ( - - ) : avalancheKeyState === 'success' ? ( - - ) : ( - - )} - - {isLoading - ? 'Getting Avalanche Keys...' - : avalancheKeyState === 'error' - ? 'Avalanche Keys Failed' - : avalancheKeyState === 'success' - ? 'Avalanche Keys Retrieved' - : 'Waiting for Avalanche App...'} - - - {isLoading - ? 'Please open the Avalanche app on your Ledger device' - : avalancheKeyState === 'error' - ? 'Failed to retrieve Avalanche keys. Please try again.' - : avalancheKeyState === 'success' - ? 'Successfully retrieved Avalanche public keys' - : 'Please open the Avalanche app on your Ledger device'} - - - - - ) - - case 'complete': - return ( - - - - - - All Keys Retrieved Successfully - - - Your Ledger wallet is ready to be created - - - - - - - Generated Addresses: - - - {avalancheKeys && avalancheKeys.evm.key && ( - - - Avalanche (EVM): - - - {avalancheKeys.evm.key} - - - )} - - {xpAddress && ( - - - Avalanche (X/P): - - - {xpAddress} - - - )} - - {bitcoinAddress && ( - - - Bitcoin: - - - {bitcoinAddress} - - - )} - - {solanaKeys.length > 0 && ( - - - Solana: - - - {solanaKeys[0].key.substring(0, 20)}... - - - )} - - - - - ) - - default: - return null - } - } - - return ( - - - - {renderStepTitle()} - - - {step !== 'connecting' && ( - - - - Step {step === 'solana' ? '1' : step === 'avalanche' ? '2' : '3'}{' '} - of 3 - - - )} - - - {renderStepContent()} - - ) -} + + // Redirect to enhanced setup - this route is deprecated + React.useEffect(() => { + router.replace('/accountSettings/ledger/enhancedSetup') + }, [router]) + + // Return null while redirecting + return <> +} \ No newline at end of file diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx index 0bb6193542..a88769cb23 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx @@ -1,180 +1,18 @@ import React from 'react' -import { View, Alert, FlatList, Linking, Platform } from 'react-native' -import { Button, Card, Text as K2Text, useTheme } from '@avalabs/k2-alpine' import { useRouter } from 'expo-router' -import { ScrollScreen } from 'common/components/ScrollScreen' -import { - useLedgerWallet, - LedgerDevice -} from 'new/features/ledger/hooks/useLedgerWallet' +/** + * @deprecated This route is deprecated. Use enhancedSetup.tsx instead. + * This file redirects to the new enhanced Ledger setup flow. + */ export default function ConnectWallet(): JSX.Element { const router = useRouter() - const { - theme: { colors } - } = useTheme() - const { - devices, - isScanning, - isConnecting, - transportState, - scanForDevices, - connectToDevice - } = useLedgerWallet() - - // Handle successful connection - const handleDeviceConnection = async ( - deviceId: string, - deviceName: string - ): Promise => { - Alert.alert('Success', `Connected to ${deviceName}`, [ - { - text: 'Continue', - onPress: () => { - router.push({ - pathname: '/accountSettings/ledger/confirmAddresses', - params: { deviceId, deviceName } - }) - } - } - ]) - } - - // Open Bluetooth settings - const openBluetoothSettings = (): void => { - if (Platform.OS === 'ios') { - Linking.openURL('App-Prefs:Bluetooth') - } else { - Linking.openSettings() - } - } - - const renderDevice = ({ item }: { item: LedgerDevice }): JSX.Element => ( - - - - - {item.name} - - - ID: {item.id} - - {item.rssi && ( - - Signal: {item.rssi} dBm - - )} - - - - - ) - - return ( - - {/* BLE Status */} - - - Bluetooth Status - - - Available: {transportState.available ? 'Yes' : 'No'} - - - Powered: {transportState.powered ? 'Yes' : 'No'} - - - - {/* Scan Button */} - - - {/* Device List */} - {devices.length > 0 && ( - - - Available Devices ({devices.length}) - - item.id} - showsVerticalScrollIndicator={false} - /> - - )} - - {/* Instructions */} - {devices.length === 0 && !isScanning && ( - - - Make sure your Ledger device is: - - - • Unlocked and on the home screen - - - • Has Bluetooth enabled - - - • Is within range of your device - - - )} - - ) -} + + // Redirect to enhanced setup - this route is deprecated + React.useEffect(() => { + router.replace('/accountSettings/ledger/enhancedSetup') + }, [router]) + + // Return null while redirecting + return <> +} \ No newline at end of file From 1892aa206b9b209844f2b70c65991636d49ef32f Mon Sep 17 00:00:00 2001 From: B0Y3R-AVA Date: Sun, 14 Sep 2025 19:21:51 -0400 Subject: [PATCH 09/24] ledger live wallet creation / demo ui flow much better --- .../ledger/components/EnhancedLedgerSetup.tsx | 25 ++ .../features/ledger/hooks/useLedgerWallet.ts | 237 ++++++++++-------- 2 files changed, 161 insertions(+), 101 deletions(-) diff --git a/packages/core-mobile/app/new/features/ledger/components/EnhancedLedgerSetup.tsx b/packages/core-mobile/app/new/features/ledger/components/EnhancedLedgerSetup.tsx index 854d2260c3..ad3b27481d 100644 --- a/packages/core-mobile/app/new/features/ledger/components/EnhancedLedgerSetup.tsx +++ b/packages/core-mobile/app/new/features/ledger/components/EnhancedLedgerSetup.tsx @@ -211,6 +211,31 @@ export const EnhancedLedgerSetup: React.FC = ({ /> ) + case 'device-connection': + return ( + + ) + + case 'app-connection': + return ( + setCurrentStep('setup-progress')} + onCancel={handleCancel} + getSolanaKeys={getSolanaKeys} + getAvalancheKeys={getAvalancheKeys} + deviceName={connectedDeviceName} + selectedDerivationPath={selectedDerivationPath} + /> + ) + case 'setup-progress': return setupProgress ? ( Promise getAvalancheKeys: () => Promise - getLedgerLiveKeys: (accountCount?: number, progressCallback?: (step: string, progress: number, totalSteps: number) => void) => Promise<{ avalancheKeys: any, individualKeys: any[] }> + getLedgerLiveKeys: ( + accountCount?: number, + progressCallback?: ( + step: string, + progress: number, + totalSteps: number + ) => void + ) => Promise<{ avalancheKeys: any; individualKeys: any[] }> resetKeys: () => void keys: LedgerKeys @@ -688,88 +695,112 @@ export function useLedgerWallet(): UseLedgerWalletReturn { ======= ======= // New method: Get individual keys for Ledger Live (sequential device confirmations) - const getLedgerLiveKeys = useCallback(async ( - accountCount: number = 3, - progressCallback?: (step: string, progress: number, totalSteps: number) => void - ) => { - try { - setIsLoading(true) - Logger.info(`Starting Ledger Live key retrieval for ${accountCount} accounts`) - - const totalSteps = accountCount // One step per account (gets both EVM and AVM) - const individualKeys: any[] = [] - let avalancheKeysResult: any = null - - // Sequential address retrieval - each account requires device confirmation - for (let accountIndex = 0; accountIndex < accountCount; accountIndex++) { - const stepName = `Getting keys for account ${accountIndex + 1}...` - const progress = Math.round(((accountIndex + 1) / totalSteps) * 100) - progressCallback?.(stepName, progress, totalSteps) - - Logger.info(`Requesting addresses for account ${accountIndex} (Ledger Live style)`) - - // Get public keys for this specific account (1 at a time for device confirmation) - const publicKeys = await ledgerService.getPublicKeys(accountIndex, 1) - - // Also get addresses for display purposes - const addresses = await ledgerService.getAllAddresses(accountIndex, 1) - - // Extract the keys for this account - const evmPublicKey = publicKeys.find(key => key.derivationPath.includes("44'/60'")) - const avmPublicKey = publicKeys.find(key => key.derivationPath.includes("44'/9000'")) - - // Extract addresses for this account - const evmAddress = addresses.find(addr => addr.network === ChainName.AVALANCHE_C_EVM) - const xpAddress = addresses.find(addr => addr.network === ChainName.AVALANCHE_X) - - if (evmPublicKey) { - individualKeys.push({ - key: evmPublicKey.key, - derivationPath: `m/44'/60'/${accountIndex}'/0/0`, // Ledger Live path - curve: evmPublicKey.curve - }) - } - - if (avmPublicKey) { - individualKeys.push({ - key: avmPublicKey.key, - derivationPath: `m/44'/9000'/${accountIndex}'/0/0`, // Ledger Live path - curve: avmPublicKey.curve - }) - } + const getLedgerLiveKeys = useCallback( + async ( + accountCount = 3, + progressCallback?: ( + step: string, + progress: number, + totalSteps: number + ) => void + ) => { + try { + setIsLoading(true) + Logger.info( + `Starting Ledger Live key retrieval for ${accountCount} accounts` + ) - // Store first account's keys as primary - if (accountIndex === 0) { - avalancheKeysResult = { - evm: { - key: evmPublicKey?.key || '', - address: evmAddress?.address || '' - }, - avalanche: { - key: avmPublicKey?.key || '', - address: xpAddress?.address || '' + const totalSteps = accountCount // One step per account (gets both EVM and AVM) + const individualKeys: any[] = [] + let avalancheKeysResult: any = null + + // Sequential address retrieval - each account requires device confirmation + for ( + let accountIndex = 0; + accountIndex < accountCount; + accountIndex++ + ) { + const stepName = `Getting keys for account ${accountIndex + 1}...` + const progress = Math.round(((accountIndex + 1) / totalSteps) * 100) + progressCallback?.(stepName, progress, totalSteps) + + Logger.info( + `Requesting addresses for account ${accountIndex} (Ledger Live style)` + ) + + // Get public keys for this specific account (1 at a time for device confirmation) + const publicKeys = await ledgerService.getPublicKeys(accountIndex, 1) + + // Also get addresses for display purposes + const addresses = await ledgerService.getAllAddresses(accountIndex, 1) + + // Extract the keys for this account + const evmPublicKey = publicKeys.find(key => + key.derivationPath.includes("44'/60'") + ) + const avmPublicKey = publicKeys.find(key => + key.derivationPath.includes("44'/9000'") + ) + + // Extract addresses for this account + const evmAddress = addresses.find( + addr => addr.network === ChainName.AVALANCHE_C_EVM + ) + const xpAddress = addresses.find( + addr => addr.network === ChainName.AVALANCHE_X + ) + + if (evmPublicKey) { + individualKeys.push({ + key: evmPublicKey.key, + derivationPath: `m/44'/60'/${accountIndex}'/0/0`, // Ledger Live path + curve: evmPublicKey.curve + }) + } + + if (avmPublicKey) { + individualKeys.push({ + key: avmPublicKey.key, + derivationPath: `m/44'/9000'/${accountIndex}'/0/0`, // Ledger Live path + curve: avmPublicKey.curve + }) + } + + // Store first account's keys as primary + if (accountIndex === 0) { + avalancheKeysResult = { + evm: { + key: evmPublicKey?.key || '', + address: evmAddress?.address || '' + }, + avalanche: { + key: avmPublicKey?.key || '', + address: xpAddress?.address || '' + } } } } - } - // Update state with the retrieved keys - if (avalancheKeysResult) { - setAvalancheKeys(avalancheKeysResult) + // Update state with the retrieved keys + if (avalancheKeysResult) { + setAvalancheKeys(avalancheKeysResult) + } + + Logger.info( + `Successfully retrieved Ledger Live keys for ${accountCount} accounts` + ) + Logger.info('Individual keys count:', individualKeys.length) + + return { avalancheKeys: avalancheKeysResult, individualKeys } + } catch (error) { + Logger.error('Failed to get Ledger Live keys:', error) + throw error + } finally { + setIsLoading(false) } - - Logger.info(`Successfully retrieved Ledger Live keys for ${accountCount} accounts`) - Logger.info('Individual keys count:', individualKeys.length) - - return { avalancheKeys: avalancheKeysResult, individualKeys } - - } catch (error) { - Logger.error('Failed to get Ledger Live keys:', error) - throw error - } finally { - setIsLoading(false) - } - }, [ledgerService]) + }, + [ledgerService] + ) >>>>>>> 6a596f952 (ledger live address derivation working, transactiosn on avalanche and solana proven with this setup) const createLedgerWallet = useCallback( @@ -898,30 +929,34 @@ export function useLedgerWallet(): UseLedgerWalletReturn { avalanche: avalancheKeys.avalanche.key } }), - publicKeys: derivationPathType === LedgerDerivationPathType.LedgerLive && individualKeys.length > 0 - ? individualKeys // Use individual keys for Ledger Live - : [ // Use existing keys for BIP44 - { - key: avalancheKeys.evm.key, - derivationPath: "m/44'/60'/0'/0/0", - curve: 'secp256k1' - }, - { - key: avalancheKeys.avalanche.key, - derivationPath: "m/44'/9000'/0'/0/0", - curve: 'secp256k1' - }, - { - key: avalancheKeys.pvm?.key || avalancheKeys.avalanche.key, - derivationPath: "m/44'/9000'/0'/0/0", - curve: 'secp256k1' - }, - { - key: solanaKeys[0]?.key || '', - derivationPath: "m/44'/501'/0'/0'", - curve: 'ed25519' - } - ], + publicKeys: + derivationPathType === LedgerDerivationPathType.LedgerLive && + individualKeys.length > 0 + ? individualKeys // Use individual keys for Ledger Live + : [ + // Use existing keys for BIP44 + { + key: avalancheKeys.evm.key, + derivationPath: "m/44'/60'/0'/0/0", + curve: 'secp256k1' + }, + { + key: avalancheKeys.avalanche.key, + derivationPath: "m/44'/9000'/0'/0/0", + curve: 'secp256k1' + }, + { + key: + avalancheKeys.pvm?.key || avalancheKeys.avalanche.key, + derivationPath: "m/44'/9000'/0'/0/0", + curve: 'secp256k1' + }, + { + key: solanaKeys[0]?.key || '', + derivationPath: "m/44'/501'/0'/0'", + curve: 'ed25519' + } + ], avalancheKeys, solanaKeys }), From 766ec037b648e7697fb7df9edf20f070ced1e371 Mon Sep 17 00:00:00 2001 From: B0Y3R-AVA Date: Mon, 15 Sep 2025 15:40:21 -0400 Subject: [PATCH 10/24] small fix for import wallet screen --- .../(modals)/accountSettings/importWallet.tsx | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/importWallet.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/importWallet.tsx index 958d324d19..a0e1c173e5 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/importWallet.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/importWallet.tsx @@ -113,25 +113,6 @@ const ImportWalletScreen = (): JSX.Element => { ), onPress: handleImportPrivateKey - }, - { - title: 'Import from Ledger', - subtitle: ( - - Access with an existing Ledger - - ), - leftIcon: ( - - ), - accessory: ( - - ), - onPress: handleImportLedger } ] From aeb7219f564033761c71215e19803077af3498ae Mon Sep 17 00:00:00 2001 From: B0Y3R-AVA Date: Wed, 17 Sep 2025 09:48:06 -0400 Subject: [PATCH 11/24] linting fixes --- .../components/DerivationPathEducation.tsx | 281 ------------------ .../features/ledger/hooks/useLedgerWallet.ts | 3 - .../ledger/confirmAddresses.tsx | 4 +- .../accountSettings/ledger/connectWallet.tsx | 4 +- .../app/services/ledger/LedgerService.ts | 159 +++------- 5 files changed, 44 insertions(+), 407 deletions(-) delete mode 100644 packages/core-mobile/app/new/features/ledger/components/DerivationPathEducation.tsx diff --git a/packages/core-mobile/app/new/features/ledger/components/DerivationPathEducation.tsx b/packages/core-mobile/app/new/features/ledger/components/DerivationPathEducation.tsx deleted file mode 100644 index b43850fa9b..0000000000 --- a/packages/core-mobile/app/new/features/ledger/components/DerivationPathEducation.tsx +++ /dev/null @@ -1,281 +0,0 @@ -import React, { useState } from 'react' -import { View, ScrollView, TouchableOpacity } from 'react-native' -import { Text, Button, useTheme } from '@avalabs/k2-alpine' -import { LedgerDerivationPathType } from 'services/wallet/LedgerWallet' - -interface EducationSection { - title: string - content: string - icon: string -} - -interface DerivationPathEducationProps { - onClose: () => void - onSelectRecommended?: () => void -} - -const educationSections: EducationSection[] = [ - { - title: 'What are derivation paths?', - content: 'Derivation paths are like addresses that tell your Ledger device which keys to generate. Different paths offer different security and convenience tradeoffs.', - icon: '🔑' - }, - { - title: 'BIP44 Standard', - content: 'BIP44 is the most common approach used by most wallets. It stores "extended keys" that allow creating new accounts without connecting your device each time.', - icon: '⚡' - }, - { - title: 'Ledger Live Approach', - content: 'Ledger Live uses individual keys for maximum security. Each account requires explicit device confirmation, but no extended keys are stored locally.', - icon: '🔒' - }, - { - title: 'Which should I choose?', - content: 'For most users, BIP44 offers the best balance of security and convenience. Choose Ledger Live only if you prioritize maximum security over convenience.', - icon: '🤔' - } -] - -const comparisonData = [ - { - feature: 'Setup Time', - bip44: '~15 seconds', - ledgerLive: '~45 seconds', - winner: 'bip44' - }, - { - feature: 'New Account Creation', - bip44: 'Instant (no device)', - ledgerLive: 'Requires device', - winner: 'bip44' - }, - { - feature: 'Security Level', - bip44: 'High (standard)', - ledgerLive: 'Maximum', - winner: 'ledgerLive' - }, - { - feature: 'Compatibility', - bip44: 'Universal', - ledgerLive: 'Ledger ecosystem', - winner: 'bip44' - }, - { - feature: 'User Experience', - bip44: 'Smooth', - ledgerLive: 'More confirmations', - winner: 'bip44' - } -] - -export const DerivationPathEducation: React.FC = ({ - onClose, - onSelectRecommended -}) => { - const { theme: { colors } } = useTheme() - const [activeSection, setActiveSection] = useState(null) - - const toggleSection = (index: number) => { - setActiveSection(activeSection === index ? null : index) - } - - return ( - - {/* Header */} - - - Understanding Setup Methods - - - - ✕ - - - - - - {/* Education sections */} - - {educationSections.map((section, index) => ( - toggleSection(index)} - > - - - {section.icon} - - - {section.title} - - - ▼ - - - - {activeSection === index && ( - - - {section.content} - - - )} - - ))} - - - {/* Comparison table */} - - - Side-by-Side Comparison - - - - {/* Header row */} - - Feature - BIP44 - Ledger Live - - - {/* Data rows */} - {comparisonData.map((row, index) => ( - - - {row.feature} - - - {row.bip44} - - - {row.ledgerLive} - - - ))} - - - - {/* Recommendation */} - - - 💡 Our Recommendation - - - For most users, BIP44 provides the best - experience with excellent security. It's faster to set up, easier to manage, - and compatible with all wallets. - - - Choose Ledger Live only if you're a - security expert who prioritizes maximum security over convenience. - - - - - {/* Action buttons */} - - - {onSelectRecommended && ( - - )} - - - ) -} diff --git a/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts b/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts index 3bf3acaada..5a9828ee0e 100644 --- a/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts +++ b/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts @@ -808,9 +808,6 @@ export function useLedgerWallet(): UseLedgerWalletReturn { deviceId, deviceName = 'Ledger Device', derivationPathType = LedgerDerivationPathType.BIP44, - accountCount = derivationPathType === LedgerDerivationPathType.BIP44 - ? 3 - : 1, individualKeys = [], progressCallback }: WalletCreationOptions) => { diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx index 13a174b6e0..730dca20b3 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx @@ -7,7 +7,7 @@ import { useRouter } from 'expo-router' */ export default function ConfirmAddresses(): JSX.Element { const router = useRouter() - + // Redirect to enhanced setup - this route is deprecated React.useEffect(() => { router.replace('/accountSettings/ledger/enhancedSetup') @@ -15,4 +15,4 @@ export default function ConfirmAddresses(): JSX.Element { // Return null while redirecting return <> -} \ No newline at end of file +} diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx index a88769cb23..c8978323c4 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx @@ -7,7 +7,7 @@ import { useRouter } from 'expo-router' */ export default function ConnectWallet(): JSX.Element { const router = useRouter() - + // Redirect to enhanced setup - this route is deprecated React.useEffect(() => { router.replace('/accountSettings/ledger/enhancedSetup') @@ -15,4 +15,4 @@ export default function ConnectWallet(): JSX.Element { // Return null while redirecting return <> -} \ No newline at end of file +} diff --git a/packages/core-mobile/app/services/ledger/LedgerService.ts b/packages/core-mobile/app/services/ledger/LedgerService.ts index 4d5490a123..d293dfcf40 100644 --- a/packages/core-mobile/app/services/ledger/LedgerService.ts +++ b/packages/core-mobile/app/services/ledger/LedgerService.ts @@ -45,9 +45,9 @@ export interface AppInfo { } export class LedgerService { - private transport: any = null + private transport: TransportBLE | null = null private currentAppType: LedgerAppType = LedgerAppType.UNKNOWN - private appPollingInterval: NodeJS.Timeout | null = null + private appPollingInterval: number | null = null private appPollingEnabled = false // Connect to Ledger device (transport only, no apps) @@ -144,8 +144,10 @@ export class LedgerService { Logger.info(`Waiting for ${appType} app (timeout: ${timeoutMs}ms)...`) while (Date.now() - startTime < timeoutMs) { - Logger.info(`Current app type: ${this.currentAppType}, waiting for: ${appType}`) - + Logger.info( + `Current app type: ${this.currentAppType}, waiting for: ${appType}` + ) + if (this.currentAppType === appType) { Logger.info(`${appType} app is ready`) return @@ -220,24 +222,27 @@ export class LedgerService { vmType: NetworkVMType.EVM }).replace('/0/0', '') Logger.info('EVM derivation path:', evmPath) - + const evmXpubResponse = await avalancheApp.getExtendedPubKey( evmPath, false ) Logger.info('EVM response return code:', evmXpubResponse.returnCode) - + // Check for error response if (evmXpubResponse.returnCode !== 0x9000) { - Logger.error('EVM extended public key error:', evmXpubResponse.errorMessage) + Logger.error( + 'EVM extended public key error:', + evmXpubResponse.errorMessage + ) throw new Error( `EVM extended public key error: ${ evmXpubResponse.errorMessage || 'Unknown error' }` ) } - + Logger.info('EVM extended public key retrieved successfully') // Get Avalanche extended public key (m/44'/9000'/0') @@ -247,24 +252,30 @@ export class LedgerService { vmType: NetworkVMType.AVM }).replace('/0/0', '') Logger.info('Avalanche derivation path:', avalanchePath) - + const avalancheXpubResponse = await avalancheApp.getExtendedPubKey( avalanchePath, false ) - Logger.info('Avalanche response return code:', avalancheXpubResponse.returnCode) - + Logger.info( + 'Avalanche response return code:', + avalancheXpubResponse.returnCode + ) + // Check for error response if (avalancheXpubResponse.returnCode !== 0x9000) { - Logger.error('Avalanche extended public key error:', avalancheXpubResponse.errorMessage) + Logger.error( + 'Avalanche extended public key error:', + avalancheXpubResponse.errorMessage + ) throw new Error( `Avalanche extended public key error: ${ avalancheXpubResponse.errorMessage || 'Unknown error' }` ) } - + Logger.info('Avalanche extended public key retrieved successfully') return { @@ -309,7 +320,7 @@ export class LedgerService { Logger.error('=== getExtendedPublicKeys FAILED ===', error) throw new Error(`Failed to get extended public keys: ${error}`) } - + Logger.info('=== getExtendedPublicKeys COMPLETED SUCCESSFULLY ===') } @@ -334,31 +345,33 @@ export class LedgerService { } // Get Solana public keys using SDK function (like extension) - async getSolanaPublicKeys(startIndex: number): Promise { + async getSolanaPublicKeys( + startIndex: number, + count: number + ): Promise { if (!this.transport) { throw new Error('Transport not initialized') } // Create a fresh AppSolana instance for each call (like the SDK does) const freshSolanaApp = new AppSolana(this.transport) - - // Use correct Solana derivation path format - const derivationPath = `44'/501'/0'/0'/${startIndex}` + const publicKeys: PublicKeyInfo[] = [] try { - // Simple direct call to get Solana address using fresh instance - const result = await freshSolanaApp.getAddress(derivationPath, false) - const publicKey = result.address + for (let i = startIndex; i < startIndex + count; i++) { + // Use correct Solana derivation path format + const derivationPath = `44'/501'/0'/0'/${i}` - console.log('HIT PUBLIC KEY', publicKey) + // Simple direct call to get Solana address using fresh instance + const result = await freshSolanaApp.getAddress(derivationPath, false) + const publicKey = result.address - const publicKeys: PublicKeyInfo[] = [ - { + publicKeys.push({ key: publicKey.toString('hex'), derivationPath, curve: 'ed25519' - } - ] + }) + } return publicKeys } catch (error) { @@ -390,8 +403,6 @@ export class LedgerService { this.transport ) - console.log('HIT PUBLIC KEY via SDK', publicKey) - const publicKeys: PublicKeyInfo[] = [ { key: publicKey.toString('hex'), @@ -416,96 +427,6 @@ export class LedgerService { } } - // Robust method with timeout and retry logic - async getSolanaPublicKeysRobust( - startIndex: number, - _count: number - ): Promise { - if (!this.transport) { - throw new Error('Transport not initialized') - } - - const maxRetries = 3 - const retryDelay = 1000 // 1 second - - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - Logger.info(`Solana attempt ${attempt}/${maxRetries}`) - - // Create a fresh transport connection for each attempt - const freshTransport = await TransportBLE.open( - this.transport.deviceId || 'unknown', - 15000 - ) - - // Create fresh app instance - const freshSolanaApp = new AppSolana(freshTransport) - - // Use derivation path - const derivationPath = `44'/501'/0'/0'/${startIndex}` - - // Call with timeout wrapper - const timeoutPromise = new Promise((_, reject) => { - setTimeout( - () => reject(new Error('Solana getAddress timeout')), - 10000 - ) - }) - - const getAddressPromise = freshSolanaApp.getAddress( - derivationPath, - false - ) - - const result = await Promise.race([getAddressPromise, timeoutPromise]) - const publicKey = result.address - - console.log('HIT PUBLIC KEY robust method', publicKey) - - // Close the fresh transport - await freshTransport.close() - - const publicKeys: PublicKeyInfo[] = [ - { - key: publicKey.toString('hex'), - derivationPath, - curve: 'ed25519' - } - ] - - return publicKeys - } catch (error) { - Logger.error(`Solana attempt ${attempt} failed:`, error) - - if (attempt === maxRetries) { - if (error instanceof Error) { - if (error.message.includes('6a80')) { - throw new Error( - 'Wrong app open. Please open the Solana app on your Ledger device.' - ) - } - if (error.message.includes('DisconnectedDevice')) { - throw new Error( - 'Ledger device disconnected. Please ensure the Solana app is open and try again.' - ) - } - throw new Error( - `Failed to get Solana address after ${maxRetries} attempts: ${error.message}` - ) - } - throw new Error( - `Failed to get Solana address after ${maxRetries} attempts` - ) - } - - // Wait before retry - await new Promise(resolve => setTimeout(resolve, retryDelay)) - } - } - - throw new Error('Unexpected error in getSolanaPublicKeysRobust') - } - // Get Solana addresses from public keys async getSolanaAddresses( startIndex: number, @@ -513,7 +434,7 @@ export class LedgerService { ): Promise { Logger.info('Starting getSolanaAddresses') try { - const publicKeys = await this.getSolanaPublicKeys(startIndex) + const publicKeys = await this.getSolanaPublicKeys(startIndex, count) Logger.info('Got Solana public keys, converting to addresses') return publicKeys.map((pk, index) => { From 24767262965ca01c48c2c7f7704d6cf8244ed53d Mon Sep 17 00:00:00 2001 From: B0Y3R-AVA Date: Wed, 17 Sep 2025 11:46:27 -0400 Subject: [PATCH 12/24] cleanup --- .../accountSettings/ledger/connectWallet.tsx | 1 + .../app/services/ledger/LedgerService.ts | 34 ++++++++++++------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx index c8978323c4..aa88356a9a 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx @@ -10,6 +10,7 @@ export default function ConnectWallet(): JSX.Element { // Redirect to enhanced setup - this route is deprecated React.useEffect(() => { + // @ts-ignore TODO: make routes typesafe router.replace('/accountSettings/ledger/enhancedSetup') }, [router]) diff --git a/packages/core-mobile/app/services/ledger/LedgerService.ts b/packages/core-mobile/app/services/ledger/LedgerService.ts index d293dfcf40..480d0c2401 100644 --- a/packages/core-mobile/app/services/ledger/LedgerService.ts +++ b/packages/core-mobile/app/services/ledger/LedgerService.ts @@ -39,6 +39,18 @@ export enum LedgerAppType { UNKNOWN = 'Unknown' } +export const LedgerReturnCode = { + SUCCESS: 0x9000, + USER_REJECTED: 0x6985, + APP_NOT_OPEN: 0x6a80, + DEVICE_LOCKED: 0x5515, + INVALID_PARAMETER: 0x6b00, + COMMAND_NOT_ALLOWED: 0x6986 +} as const + +export type LedgerReturnCodeType = + typeof LedgerReturnCode[keyof typeof LedgerReturnCode] + export interface AppInfo { applicationName: string version: string @@ -80,7 +92,7 @@ export class LedgerService { this.appPollingEnabled = true this.appPollingInterval = setInterval(async () => { try { - if (!this.transport || this.transport.isDisconnected) { + if (!this.transport || !this.transport.isConnected) { this.stopAppPolling() return } @@ -116,7 +128,7 @@ export class LedgerService { throw new Error('Transport not initialized') } - return await getLedgerAppInfo(this.transport) + return await getLedgerAppInfo(this.transport as any) } // Map app name to our enum @@ -179,7 +191,7 @@ export class LedgerService { private async reconnectIfNeeded(deviceId: string): Promise { Logger.info('Checking if reconnection is needed') - if (!this.transport || this.transport.isDisconnected) { + if (!this.transport || !this.transport.isConnected) { Logger.info('Transport is disconnected, attempting reconnection') try { await this.connect(deviceId) @@ -211,7 +223,7 @@ export class LedgerService { Logger.info('Avalanche app detected, creating app instance...') // Create Avalanche app instance - const avalancheApp = new AppAvalanche(this.transport) + const avalancheApp = new AppAvalanche(this.transport as any) Logger.info('Avalanche app instance created') try { @@ -231,7 +243,7 @@ export class LedgerService { Logger.info('EVM response return code:', evmXpubResponse.returnCode) // Check for error response - if (evmXpubResponse.returnCode !== 0x9000) { + if (evmXpubResponse.returnCode !== LedgerReturnCode.SUCCESS) { Logger.error( 'EVM extended public key error:', evmXpubResponse.errorMessage @@ -264,7 +276,7 @@ export class LedgerService { ) // Check for error response - if (avalancheXpubResponse.returnCode !== 0x9000) { + if (avalancheXpubResponse.returnCode !== LedgerReturnCode.SUCCESS) { Logger.error( 'Avalanche extended public key error:', avalancheXpubResponse.errorMessage @@ -320,8 +332,6 @@ export class LedgerService { Logger.error('=== getExtendedPublicKeys FAILED ===', error) throw new Error(`Failed to get extended public keys: ${error}`) } - - Logger.info('=== getExtendedPublicKeys COMPLETED SUCCESSFULLY ===') } // Check if Solana app is open @@ -400,7 +410,7 @@ export class LedgerService { // Use the SDK function directly (like the extension does) const publicKey = await getSolanaPublicKeyFromLedger( startIndex, - this.transport + this.transport as any ) const publicKeys: PublicKeyInfo[] = [ @@ -467,7 +477,7 @@ export class LedgerService { await this.waitForApp(LedgerAppType.AVALANCHE) // Create Avalanche app instance - const avalancheApp = new AppAvalanche(this.transport) + const avalancheApp = new AppAvalanche(this.transport as any) const publicKeys: PublicKeyInfo[] = [] @@ -541,7 +551,7 @@ export class LedgerService { await this.waitForApp(LedgerAppType.AVALANCHE) // Create Avalanche app instance - const avalancheApp = new AppAvalanche(this.transport) + const avalancheApp = new AppAvalanche(this.transport as any) const addresses: AddressInfo[] = [] @@ -670,7 +680,7 @@ export class LedgerService { // Check if transport is available and connected isConnected(): boolean { - return this.transport !== null && !this.transport.isDisconnected + return this.transport !== null && this.transport.isConnected } // Ensure connection is established for a specific device From 2cb96d87ce1444de596e888381f36abf5d67c609 Mon Sep 17 00:00:00 2001 From: B0Y3R-AVA Date: Wed, 17 Sep 2025 12:14:02 -0400 Subject: [PATCH 13/24] test fixes --- .../core-mobile/ios/AvaxWallet.xcodeproj/project.pbxproj | 8 -------- packages/core-mobile/ios/Podfile.lock | 1 - 2 files changed, 9 deletions(-) diff --git a/packages/core-mobile/ios/AvaxWallet.xcodeproj/project.pbxproj b/packages/core-mobile/ios/AvaxWallet.xcodeproj/project.pbxproj index d42d39d6f0..5d6f495a81 100644 --- a/packages/core-mobile/ios/AvaxWallet.xcodeproj/project.pbxproj +++ b/packages/core-mobile/ios/AvaxWallet.xcodeproj/project.pbxproj @@ -602,14 +602,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-common-AvaxWalletInternal/Pods-common-AvaxWalletInternal-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-common-AvaxWalletInternal/Pods-common-AvaxWalletInternal-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-common-AvaxWalletInternal/Pods-common-AvaxWalletInternal-frameworks.sh\"\n"; @@ -821,14 +817,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-common-AvaxWallet/Pods-common-AvaxWallet-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-common-AvaxWallet/Pods-common-AvaxWallet-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-common-AvaxWallet/Pods-common-AvaxWallet-resources.sh\"\n"; diff --git a/packages/core-mobile/ios/Podfile.lock b/packages/core-mobile/ios/Podfile.lock index 3dfa511c76..50c8fb8f38 100644 --- a/packages/core-mobile/ios/Podfile.lock +++ b/packages/core-mobile/ios/Podfile.lock @@ -62,7 +62,6 @@ PODS: - DatadogWebViewTracking (2.24.1): - DatadogInternal (= 2.24.1) - DGSwiftUtilities (0.47.0) - - DGSwiftUtilities (0.47.0) - DoubleConversion (1.1.6) - EXApplication (6.1.5): - ExpoModulesCore From 430160b207ff47576b7002b7a8002b68431eea50 Mon Sep 17 00:00:00 2001 From: B0Y3R-AVA Date: Wed, 17 Sep 2025 16:39:02 -0400 Subject: [PATCH 14/24] removed unused .mdc file --- docs/ledger-signing-findings.mdc | 135 ------------------------------- 1 file changed, 135 deletions(-) delete mode 100644 docs/ledger-signing-findings.mdc diff --git a/docs/ledger-signing-findings.mdc b/docs/ledger-signing-findings.mdc deleted file mode 100644 index 61906334d5..0000000000 --- a/docs/ledger-signing-findings.mdc +++ /dev/null @@ -1,135 +0,0 @@ -# Ledger Transaction Signing Findings - -## Latest Success! (2024-03-19) - -### Working Solution -- Successfully got signature from Ledger device -- User was able to approve on device -- Signature format confirmed working - -### Working Transaction Format -```typescript -// Transaction data format: -{ - nonce: 0, - type: 0, // Legacy transaction type - chainId: 43114, - gasPrice: maxFeePerGas, // Use maxFeePerGas as gasPrice - gasLimit: 45427, - data: "0xa9059cbb...", // USDC.e transfer - from: "0x449b3fffe66378227dbbd05539b6542e5ca75a28", - to: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E" -} - -// RLP encoded: -0xf86b8085022ad57bb082b17394b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e80b844a9059cbb... -``` - -### Working Resolution Object -```typescript -{ - externalPlugin: [], - erc20Tokens: ['0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E'], - nfts: [], - plugin: [], - domains: [] // Required field -} -``` - -### Signature Format -```typescript -{ - v: "0150f8", - r: "20f4e602da6206d60ead3988e0415fb9119db0debe1d4d2c4910702cd32900fe", - s: "017447d453a1648c4813fa1685b718a70320651f7134573e1f06dcf5c077b6c3" -} -``` - -## Key Learnings -1. Must use legacy transaction format (type 0) instead of EIP-1559 -2. Resolution object requires domains field -3. Signature comes back as object with r, s, v properties -4. Need to concatenate signature parts with 0x prefix - -## Working Implementation Steps -1. Convert EIP-1559 transaction to legacy format -2. Use maxFeePerGas as gasPrice -3. Include domains array in resolution object -4. Handle signature result as object properties -5. Concatenate signature parts correctly - -## Previous Failed Attempts - -### 1. Using LedgerSigner from SDK -```typescript -const signer = await this.getEvmSigner(provider, accountIndex) -``` -Status: ❌ Failed -Reason: Direct SDK signer approach doesn't work with Ledger device - -### 2. Raw Transaction Data -```typescript -const unsignedTx = transaction.data?.toString() || '' -``` -Status: ❌ Failed -Reason: Data too short, missing proper transaction structure - -### 3. Ethers.js Transaction.from() with EIP-1559 -```typescript -const tx = { - type: 2, - chainId: transaction.chainId, - maxFeePerGas: transaction.maxFeePerGas, - maxPriorityFeePerGas: transaction.maxPriorityFeePerGas, - // ... -} -``` -Status: ❌ Failed -Reason: Ledger app doesn't support EIP-1559 format - -### 4. Manual Hex Concatenation -```typescript -const unsignedTx = [txType, txNonce, ...].join('') -``` -Status: ❌ Failed -Reason: Incorrect transaction format - -### 5. Using Ethereum App -```typescript -const ethApp = new AppEth(transport) -``` -Status: ❌ Failed -Reason: Wrong app for Avalanche C-Chain - -### 6. Using Avalanche App with signTransaction -```typescript -const avaxApp = new AppAvax(transport) -await avaxApp.signTransaction(...) -``` -Status: ❌ Failed -Reason: Wrong method name - -### 7. Using Avalanche App with signEVMTransaction and EIP-1559 -```typescript -const avaxApp = new AppAvax(transport) -await avaxApp.signEVMTransaction(...) -``` -Status: ❌ Failed -Reason: EIP-1559 format not supported - -### 8. Using Avalanche App with signEVMTransaction and Legacy Format -```typescript -const avaxApp = new AppAvax(transport) -const result = await avaxApp.signEVMTransaction(...) -return `0x${result.r}${result.s}${result.v}` -``` -Status: ✅ Success -Reason: Legacy format works with proper signature handling - -## Requirements for Valid Transaction -1. Must use legacy transaction format (type 0) -2. Must include all required fields -3. Must use correct signing method (signEVMTransaction) -4. Resolution object must include all fields including domains -5. Must handle chainId 43114 (Avalanche C-Chain) correctly -6. Must properly concatenate signature components \ No newline at end of file From d9a584e863bb029e63c148f5b3d752244c3c9778 Mon Sep 17 00:00:00 2001 From: B0Y3R-AVA Date: Fri, 19 Sep 2025 17:35:27 -0400 Subject: [PATCH 15/24] moved steps into routes, modified ui for path selection and bluettoth connection --- .vscode/settings.json | 19 +- packages/core-mobile/.nvimlog | 0 .../components/DerivationPathSelector.tsx | 152 ++------------ .../ledger/components/ScanningAnimation.tsx | 131 ++++++++++++ .../new/features/ledger/components/index.ts | 2 + .../ledger/contexts/LedgerSetupContext.tsx | 140 +++++++++++++ .../app/new/features/ledger/index.ts | 2 + .../(modals)/accountSettings/_layout.tsx | 1 + .../(modals)/accountSettings/importWallet.tsx | 4 +- .../accountSettings/ledger/_layout.tsx | 36 ++++ .../accountSettings/ledger/appConnection.tsx | 48 +++++ .../accountSettings/ledger/complete.tsx | 50 +++++ .../ledger/confirmAddresses.tsx | 3 +- .../accountSettings/ledger/connectWallet.tsx | 2 +- .../ledger/deviceConnection.tsx | 193 ++++++++++++++++++ .../accountSettings/ledger/pathSelection.tsx | 32 +++ .../accountSettings/ledger/setupProgress.tsx | 141 +++++++++++++ .../k2-alpine/src/assets/icons/bluetooth.svg | 3 + .../src/assets/icons/ledger_logo.svg | 4 + packages/k2-alpine/src/theme/tokens/Icons.ts | 6 +- 20 files changed, 830 insertions(+), 139 deletions(-) create mode 100644 packages/core-mobile/.nvimlog create mode 100644 packages/core-mobile/app/new/features/ledger/components/ScanningAnimation.tsx create mode 100644 packages/core-mobile/app/new/features/ledger/contexts/LedgerSetupContext.tsx create mode 100644 packages/core-mobile/app/new/features/ledger/index.ts create mode 100644 packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/_layout.tsx create mode 100644 packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/appConnection.tsx create mode 100644 packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/complete.tsx create mode 100644 packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/deviceConnection.tsx create mode 100644 packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/pathSelection.tsx create mode 100644 packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/setupProgress.tsx create mode 100644 packages/k2-alpine/src/assets/icons/bluetooth.svg create mode 100644 packages/k2-alpine/src/assets/icons/ledger_logo.svg diff --git a/.vscode/settings.json b/.vscode/settings.json index 23e6b4c30d..a95f568ac3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,22 @@ "mode": "auto" } ], - "typescript.tsdk": "./packages/core-mobile/node_modules/typescript/lib" + "typescript.tsdk": "./packages/core-mobile/node_modules/typescript/lib", + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } } diff --git a/packages/core-mobile/.nvimlog b/packages/core-mobile/.nvimlog new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/core-mobile/app/new/features/ledger/components/DerivationPathSelector.tsx b/packages/core-mobile/app/new/features/ledger/components/DerivationPathSelector.tsx index 7863e863c3..d7377fdc11 100644 --- a/packages/core-mobile/app/new/features/ledger/components/DerivationPathSelector.tsx +++ b/packages/core-mobile/app/new/features/ledger/components/DerivationPathSelector.tsx @@ -1,6 +1,6 @@ -import React, { useState, useCallback } from 'react' -import { View } from 'react-native' -import { Text, Button, useTheme, GroupList, Icons } from '@avalabs/k2-alpine' +import React from 'react' +import { View, TouchableOpacity } from 'react-native' +import { Text, useTheme, Icons } from '@avalabs/k2-alpine' import { ScrollScreen } from 'common/components/ScrollScreen' import { LedgerDerivationPathType } from 'services/ledger/types' @@ -22,7 +22,7 @@ interface DerivationPathSelectorProps { const derivationPathOptions: DerivationPathOption[] = [ { type: LedgerDerivationPathType.BIP44, - title: 'BIP44 (Recommended)', + title: 'BIP44', subtitle: 'Standard approach for most users', benefits: [ 'Faster setup (~15 seconds)', @@ -127,137 +127,23 @@ export const DerivationPathSelector: React.FC = ({ return ( - - - - - {selectedOption && ( - - {/* Recommendation badge */} - {selectedOption.recommended && ( - - - RECOMMENDED - - - )} - - {/* Quick stats */} - - - - - Setup Time - - - {selectedOption.setupTime} - - - - - New Accounts - - - {selectedOption.newAccountRequirement} - - - - + contentContainerStyle={{ + padding: 16, + gap: 16, + paddingBottom: 100 + }}> + - {/* Benefits */} - - - ✓ Benefits - - {selectedOption.benefits.map((benefit, index) => ( - - - • - - - {benefit} - - - ))} - - - {/* Considerations */} - {selectedOption.warnings.length > 0 && ( - - - ⚠️ Considerations - - {selectedOption.warnings.map((warning, index) => ( - - - • - - - {warning} - - - ))} - - )} - - )} + {derivationPathOptions.map(option => ( + onSelect(option.type)} + /> + ))} ) } diff --git a/packages/core-mobile/app/new/features/ledger/components/ScanningAnimation.tsx b/packages/core-mobile/app/new/features/ledger/components/ScanningAnimation.tsx new file mode 100644 index 0000000000..2ee8c2b050 --- /dev/null +++ b/packages/core-mobile/app/new/features/ledger/components/ScanningAnimation.tsx @@ -0,0 +1,131 @@ +import React, { useEffect } from 'react' +import { View } from 'react-native' +import Animated, { + useSharedValue, + useAnimatedStyle, + withRepeat, + withTiming, + interpolate, + Easing +} from 'react-native-reanimated' +import { Icons, useTheme } from '@avalabs/k2-alpine' + +interface ScanningAnimationProps { + size?: number + iconSize?: number +} + +export const ScanningAnimation: React.FC = ({ + size = 200, + iconSize = 32 +}) => { + const { + theme: { colors } + } = useTheme() + const animationValue = useSharedValue(0) + + useEffect(() => { + animationValue.value = withRepeat( + withTiming(1, { + duration: 2000, + easing: Easing.out(Easing.quad) + }), + -1, + false + ) + }, [animationValue]) + + // Create animated styles for each circle + const createCircleStyle = (delay: number, maxScale: number) => { + return useAnimatedStyle(() => { + const progress = (animationValue.value + delay) % 1 + const scale = interpolate(progress, [0, 1], [0.3, maxScale]) + const opacity = interpolate(progress, [0, 0.7, 1], [0.8, 0.3, 0]) + + return { + transform: [{ scale }], + opacity + } + }) + } + + const circle1Style = createCircleStyle(0, 1.0) + const circle2Style = createCircleStyle(0.3, 1.3) + const circle3Style = createCircleStyle(0.6, 1.6) + + const circleSize = size + const borderWidth = 2 + + return ( + + {/* Outermost circle */} + + + {/* Middle circle */} + + + {/* Inner circle */} + + + {/* Center icon */} + + + + + ) +} diff --git a/packages/core-mobile/app/new/features/ledger/components/index.ts b/packages/core-mobile/app/new/features/ledger/components/index.ts index ac676ca023..cacfdc6eb4 100644 --- a/packages/core-mobile/app/new/features/ledger/components/index.ts +++ b/packages/core-mobile/app/new/features/ledger/components/index.ts @@ -1,3 +1,5 @@ export { DerivationPathSelector } from './DerivationPathSelector' export { LedgerSetupProgress } from './LedgerSetupProgress' export { EnhancedLedgerSetup } from './EnhancedLedgerSetup' +export { LedgerAppConnection } from './LedgerAppConnection' +export { ScanningAnimation } from './ScanningAnimation' diff --git a/packages/core-mobile/app/new/features/ledger/contexts/LedgerSetupContext.tsx b/packages/core-mobile/app/new/features/ledger/contexts/LedgerSetupContext.tsx new file mode 100644 index 0000000000..4c82b0a7ad --- /dev/null +++ b/packages/core-mobile/app/new/features/ledger/contexts/LedgerSetupContext.tsx @@ -0,0 +1,140 @@ +import React, { + createContext, + useContext, + useState, + useCallback, + useMemo, + ReactNode +} from 'react' +import { LedgerDerivationPathType } from 'services/wallet/LedgerWallet' +import { + WalletCreationOptions, + useLedgerWallet +} from '../hooks/useLedgerWallet' + +interface LedgerSetupState { + selectedDerivationPath: LedgerDerivationPathType | null + connectedDeviceId: string | null + connectedDeviceName: string + isCreatingWallet: boolean + hasStartedSetup: boolean +} + +interface LedgerSetupContextValue extends LedgerSetupState { + // State setters + setSelectedDerivationPath: (path: LedgerDerivationPathType) => void + setConnectedDevice: (deviceId: string, deviceName: string) => void + setIsCreatingWallet: (creating: boolean) => void + setHasStartedSetup: (started: boolean) => void + + // Ledger wallet hook values + devices: any[] + isScanning: boolean + isConnecting: boolean + transportState: unknown + scanForDevices: () => void + connectToDevice: (deviceId: string) => Promise + disconnectDevice: () => Promise + getSolanaKeys: () => Promise + getAvalancheKeys: () => Promise + createLedgerWallet: (options: WalletCreationOptions) => Promise + setupProgress: any + keys: any + + // Helper methods + resetSetup: () => void +} + +const LedgerSetupContext = createContext(null) + +interface LedgerSetupProviderProps { + children: ReactNode +} + +export const LedgerSetupProvider: React.FC = ({ + children +}) => { + const [state, setState] = useState({ + selectedDerivationPath: null, + connectedDeviceId: null, + connectedDeviceName: 'Ledger Device', + isCreatingWallet: false, + hasStartedSetup: false + }) + + // Use the existing ledger wallet hook + const ledgerWallet = useLedgerWallet() + + const setSelectedDerivationPath = useCallback( + (path: LedgerDerivationPathType) => { + setState(prev => ({ ...prev, selectedDerivationPath: path })) + }, + [] + ) + + const setConnectedDevice = useCallback( + (deviceId: string, deviceName: string) => { + setState(prev => ({ + ...prev, + connectedDeviceId: deviceId, + connectedDeviceName: deviceName + })) + }, + [] + ) + + const setIsCreatingWallet = useCallback((creating: boolean) => { + setState(prev => ({ ...prev, isCreatingWallet: creating })) + }, []) + + const setHasStartedSetup = useCallback((started: boolean) => { + setState(prev => ({ ...prev, hasStartedSetup: started })) + }, []) + + const resetSetup = useCallback(() => { + setState({ + selectedDerivationPath: null, + connectedDeviceId: null, + connectedDeviceName: 'Ledger Device', + isCreatingWallet: false, + hasStartedSetup: false + }) + }, []) + + const contextValue: LedgerSetupContextValue = useMemo( + () => ({ + ...state, + ...ledgerWallet, + setSelectedDerivationPath, + setConnectedDevice, + setIsCreatingWallet, + setHasStartedSetup, + resetSetup + }), + [ + state, + ledgerWallet, + setSelectedDerivationPath, + setConnectedDevice, + setIsCreatingWallet, + setHasStartedSetup, + resetSetup + ] + ) + + return ( + + {children} + + ) +} + +export const useLedgerSetupContext = (): LedgerSetupContextValue => { + const context = useContext(LedgerSetupContext) + if (!context) { + throw new Error( + 'useLedgerSetupContext must be used within a LedgerSetupProvider' + ) + } + return context +} diff --git a/packages/core-mobile/app/new/features/ledger/index.ts b/packages/core-mobile/app/new/features/ledger/index.ts new file mode 100644 index 0000000000..18023eec97 --- /dev/null +++ b/packages/core-mobile/app/new/features/ledger/index.ts @@ -0,0 +1,2 @@ +export * from './components' +export * from './contexts/LedgerSetupContext' diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/_layout.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/_layout.tsx index 9be54fb03a..14730cfecd 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/_layout.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/_layout.tsx @@ -28,6 +28,7 @@ export default function AccountSettingsLayout(): JSX.Element { + diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/importWallet.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/importWallet.tsx index a0e1c173e5..d57a979592 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/importWallet.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/importWallet.tsx @@ -72,7 +72,7 @@ const ImportWalletScreen = (): JSX.Element => { const handleImportLedger = (): void => { // @ts-ignore TODO: make routes typesafe - navigate({ pathname: '/accountSettings/ledger/enhancedSetup' }) + navigate({ pathname: '/accountSettings/ledger' }) } const baseData = [ @@ -125,7 +125,7 @@ const ImportWalletScreen = (): JSX.Element => { ), leftIcon: ( - + + + + + + + + + ) +} diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/appConnection.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/appConnection.tsx new file mode 100644 index 0000000000..86cebed141 --- /dev/null +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/appConnection.tsx @@ -0,0 +1,48 @@ +import React, { useCallback, useEffect } from 'react' +import { useRouter } from 'expo-router' +import { LedgerAppConnection } from 'new/features/ledger/components/LedgerAppConnection' +import { useLedgerSetupContext } from 'new/features/ledger/contexts/LedgerSetupContext' + +export default function AppConnectionScreen(): JSX.Element { + const { push, back } = useRouter() + + const { + getSolanaKeys, + getAvalancheKeys, + connectedDeviceName, + selectedDerivationPath, + keys, + resetSetup, + disconnectDevice + } = useLedgerSetupContext() + + // Check if keys are available and auto-progress to setup + useEffect(() => { + if (keys.avalancheKeys && keys.solanaKeys.length > 0) { + // @ts-ignore TODO: make routes typesafe + push('/accountSettings/ledger/setupProgress') + } + }, [keys.avalancheKeys, keys.solanaKeys, push]) + + const handleComplete = useCallback(() => { + // @ts-ignore TODO: make routes typesafe + push('/accountSettings/ledger/setupProgress') + }, [push]) + + const handleCancel = useCallback(async () => { + await disconnectDevice() + resetSetup() + back() + }, [disconnectDevice, resetSetup, back]) + + return ( + + ) +} diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/complete.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/complete.tsx new file mode 100644 index 0000000000..746fce5308 --- /dev/null +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/complete.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import { View } from 'react-native' +import { useRouter } from 'expo-router' +import { Text, Button, useTheme } from '@avalabs/k2-alpine' +import { useLedgerSetupContext } from 'new/features/ledger/contexts/LedgerSetupContext' + +export default function CompleteScreen(): JSX.Element { + const { push } = useRouter() + const { + theme: { colors } + } = useTheme() + + const { resetSetup } = useLedgerSetupContext() + + const handleComplete = (): void => { + resetSetup() + // Navigate to account management after successful wallet creation + // @ts-ignore TODO: make routes typesafe + push('/accountSettings/manageAccounts') + } + + return ( + + + 🎉 Wallet Created Successfully! + + + Your Ledger wallet has been set up and is ready to use. + + + + ) +} diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx index 730dca20b3..b8a43c1ef7 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx @@ -10,7 +10,8 @@ export default function ConfirmAddresses(): JSX.Element { // Redirect to enhanced setup - this route is deprecated React.useEffect(() => { - router.replace('/accountSettings/ledger/enhancedSetup') + // @ts-ignore TODO: make routes typesafe + router.replace('/accountSettings/ledger') }, [router]) // Return null while redirecting diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx index aa88356a9a..c68f73617e 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx @@ -11,7 +11,7 @@ export default function ConnectWallet(): JSX.Element { // Redirect to enhanced setup - this route is deprecated React.useEffect(() => { // @ts-ignore TODO: make routes typesafe - router.replace('/accountSettings/ledger/enhancedSetup') + router.replace('/accountSettings/ledger') }, [router]) // Return null while redirecting diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/deviceConnection.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/deviceConnection.tsx new file mode 100644 index 0000000000..6ce808c0db --- /dev/null +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/deviceConnection.tsx @@ -0,0 +1,193 @@ +import React, { useCallback } from 'react' +import { View, Alert } from 'react-native' +import { useRouter } from 'expo-router' +import { Text, Button, useTheme, GroupList, Icons } from '@avalabs/k2-alpine' +import { ScrollScreen } from 'common/components/ScrollScreen' +import { useLedgerSetupContext } from 'new/features/ledger/contexts/LedgerSetupContext' +import { ScanningAnimation } from 'new/features/ledger/components/ScanningAnimation' + +interface LedgerDevice { + id: string + name: string +} + +export default function DeviceConnectionScreen(): JSX.Element { + const { push, back } = useRouter() + const { + theme: { colors } + } = useTheme() + + const { + devices, + isScanning, + isConnecting, + scanForDevices, + connectToDevice, + setConnectedDevice, + resetSetup + } = useLedgerSetupContext() + + // Handle device connection + const handleDeviceConnection = useCallback( + async (deviceId: string, deviceName: string) => { + try { + await connectToDevice(deviceId) + setConnectedDevice(deviceId, deviceName) + + // Navigate to app connection step + // @ts-ignore TODO: make routes typesafe + push('/accountSettings/ledger/appConnection') + } catch (error) { + Alert.alert( + 'Connection Failed', + 'Failed to connect to Ledger device. Please try again.', + [{ text: 'OK' }] + ) + } + }, + [connectToDevice, setConnectedDevice, push] + ) + + const handleCancel = useCallback(() => { + resetSetup() + back() + }, [resetSetup, back]) + + const renderFooter = useCallback(() => { + return ( + + + + + + ) + }, [isScanning, isConnecting, scanForDevices, handleCancel]) + + const deviceListData = devices.map((device: LedgerDevice) => ({ + title: device.name || 'Ledger Device', + subtitle: ( + + Found over Bluetooth + + ), + leftIcon: ( + + ), + accessory: ( + + ), + onPress: () => handleDeviceConnection(device.id, device.name) + })) + + return ( + + {isScanning && ( + + + + + {/* Text positioned 34px below the center icon */} + + + Looking for devices... + + + Make sure your Ledger device is unlocked and the Avalanche app + is open + + + + + )} + + {!isScanning && devices.length === 0 && ( + + + + Get your Ledger ready + + + Make sure your Ledger device is unlocked + + + )} + + {devices.length > 0 && ( + + + Available Devices + + + + )} + + ) +} diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/pathSelection.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/pathSelection.tsx new file mode 100644 index 0000000000..9647d07b71 --- /dev/null +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/pathSelection.tsx @@ -0,0 +1,32 @@ +import React, { useCallback } from 'react' +import { useRouter } from 'expo-router' +import { DerivationPathSelector } from 'new/features/ledger/components/DerivationPathSelector' +import { useLedgerSetupContext } from 'new/features/ledger/contexts/LedgerSetupContext' +import { LedgerDerivationPathType } from 'services/wallet/LedgerWallet' + +export default function PathSelectionScreen(): JSX.Element { + const { push, back } = useRouter() + const { setSelectedDerivationPath, resetSetup } = useLedgerSetupContext() + + const handleDerivationPathSelect = useCallback( + (derivationPathType: LedgerDerivationPathType) => { + setSelectedDerivationPath(derivationPathType) + // Navigate to device connection step + // @ts-ignore TODO: make routes typesafe + push('/accountSettings/ledger/deviceConnection') + }, + [setSelectedDerivationPath, push] + ) + + const handleCancel = useCallback(() => { + resetSetup() + back() + }, [resetSetup, back]) + + return ( + + ) +} diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/setupProgress.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/setupProgress.tsx new file mode 100644 index 0000000000..05dd5f4550 --- /dev/null +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/setupProgress.tsx @@ -0,0 +1,141 @@ +import React, { useCallback, useEffect } from 'react' +import { View, Alert } from 'react-native' +import { useRouter } from 'expo-router' +import { Text, useTheme } from '@avalabs/k2-alpine' +import { LedgerDerivationPathType } from 'services/wallet/LedgerWallet' +import { LedgerSetupProgress } from 'new/features/ledger/components/LedgerSetupProgress' +import { useLedgerSetupContext } from 'new/features/ledger/contexts/LedgerSetupContext' +import { WalletCreationOptions } from 'new/features/ledger/hooks/useLedgerWallet' + +export default function SetupProgressScreen(): JSX.Element { + const { push, back } = useRouter() + const { + theme: { colors } + } = useTheme() + + const { + connectedDeviceId, + connectedDeviceName, + selectedDerivationPath, + isCreatingWallet, + hasStartedSetup, + setIsCreatingWallet, + setHasStartedSetup, + createLedgerWallet, + setupProgress, + resetSetup, + disconnectDevice + } = useLedgerSetupContext() + + // Start wallet setup when entering this screen + const handleStartSetup = useCallback(async () => { + if (!connectedDeviceId || !selectedDerivationPath || isCreatingWallet) { + return + } + + try { + setIsCreatingWallet(true) + + const walletCreationOptions: WalletCreationOptions = { + deviceId: connectedDeviceId, + deviceName: connectedDeviceName, + derivationPathType: selectedDerivationPath, + accountCount: 3, // Standard 3 accounts for both BIP44 and Ledger Live + progressCallback: (_step, _progress, _totalSteps) => { + // Progress callback for UI updates + } + } + + await createLedgerWallet(walletCreationOptions) + + // Navigate to completion screen + // @ts-ignore TODO: make routes typesafe + push('/accountSettings/ledger/complete') + } catch (error) { + // Wallet creation failed + Alert.alert( + 'Setup Failed', + 'Failed to create Ledger wallet. Please try again.', + [ + { + text: 'Try Again', + onPress: () => { + setIsCreatingWallet(false) + // @ts-ignore TODO: make routes typesafe + back() // Go back to app connection + } + }, + { + text: 'Cancel', + onPress: async () => { + setIsCreatingWallet(false) + await disconnectDevice() + resetSetup() + // Navigate back to import wallet screen + // @ts-ignore TODO: make routes typesafe + push('/accountSettings/importWallet') + } + } + ] + ) + } finally { + setIsCreatingWallet(false) + } + }, [ + connectedDeviceId, + connectedDeviceName, + selectedDerivationPath, + isCreatingWallet, + setIsCreatingWallet, + createLedgerWallet, + push, + back, + disconnectDevice, + resetSetup + ]) + + // Auto-start wallet creation when entering this screen + useEffect(() => { + if ( + selectedDerivationPath && + connectedDeviceId && + !isCreatingWallet && + !hasStartedSetup + ) { + console.log('Starting wallet setup...') + setHasStartedSetup(true) + handleStartSetup() + } + }, [selectedDerivationPath, connectedDeviceId, isCreatingWallet, hasStartedSetup, setHasStartedSetup, handleStartSetup]) + + const handleCancel = useCallback(async () => { + await disconnectDevice() + resetSetup() + // @ts-ignore TODO: make routes typesafe + push('/accountSettings/importWallet') + }, [disconnectDevice, resetSetup, push]) + + if (!setupProgress) { + return ( + + Initializing setup... + + ) + } + + return ( + + ) +} diff --git a/packages/k2-alpine/src/assets/icons/bluetooth.svg b/packages/k2-alpine/src/assets/icons/bluetooth.svg new file mode 100644 index 0000000000..b16b6407ef --- /dev/null +++ b/packages/k2-alpine/src/assets/icons/bluetooth.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/k2-alpine/src/assets/icons/ledger_logo.svg b/packages/k2-alpine/src/assets/icons/ledger_logo.svg new file mode 100644 index 0000000000..2a83f6ba96 --- /dev/null +++ b/packages/k2-alpine/src/assets/icons/ledger_logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/k2-alpine/src/theme/tokens/Icons.ts b/packages/k2-alpine/src/theme/tokens/Icons.ts index 15b07845e0..36ba0b61ce 100644 --- a/packages/k2-alpine/src/theme/tokens/Icons.ts +++ b/packages/k2-alpine/src/theme/tokens/Icons.ts @@ -97,6 +97,7 @@ import IconAch from '../../assets/icons/ach.svg' import IconDownload from '../../assets/icons/download.svg' import IconEncrypted from '../../assets/icons/shield.svg' import IconSwapProviderAuto from '../../assets/icons/swap_auto.svg' +import IconLedger from '../../assets/icons/ledger_logo.svg' // Transaction types import IconTxTypeAdd from '../../assets/icons/tx-type-add.svg' import IconTxTypeAdvanceTime from '../../assets/icons/advance-time.svg' @@ -111,6 +112,7 @@ import IconTxTypeSubnet from '../../assets/icons/transaction-subnet.svg' import IconTxTypeUnwrap from '../../assets/icons/unwrap.svg' import IconTxTypeUnknown from '../../assets/icons/unknown.svg' import IconPsychiatry from '../../assets/icons/psychiatry.svg' +import IconBluetooth from '../../assets/icons/bluetooth.svg' // token logos import AAVE from '../../assets/tokenLogos/AAVE.svg' @@ -332,7 +334,9 @@ export const Icons = { ShopeePay: IconShopeePay, Ach: IconAch, Download: IconDownload, - SwapProviderAuto: IconSwapProviderAuto + SwapProviderAuto: IconSwapProviderAuto, + Ledger: IconLedger, + Bluetooth: IconBluetooth }, RecoveryMethod: { Passkey: IconPasskey, From 0a097a65d9237103439bf05508a524c08e666bde Mon Sep 17 00:00:00 2001 From: B0Y3R-AVA Date: Wed, 24 Sep 2025 17:11:17 -0400 Subject: [PATCH 16/24] wrapped up design --- .../app/assets/lotties/connect-waves.json | 1 + .../components/AnimatedIconWithText.tsx | 109 ++++++++++++++++++ .../components/DerivationPathSelector.tsx | 6 +- .../ledger/components/EnhancedLedgerSetup.tsx | 58 ++++------ .../ledger/deviceConnection.tsx | 100 ++++------------ .../src/assets/icons/ledger_logo.svg | 5 +- 6 files changed, 164 insertions(+), 115 deletions(-) create mode 100644 packages/core-mobile/app/assets/lotties/connect-waves.json create mode 100644 packages/core-mobile/app/new/features/ledger/components/AnimatedIconWithText.tsx diff --git a/packages/core-mobile/app/assets/lotties/connect-waves.json b/packages/core-mobile/app/assets/lotties/connect-waves.json new file mode 100644 index 0000000000..e4920fb849 --- /dev/null +++ b/packages/core-mobile/app/assets/lotties/connect-waves.json @@ -0,0 +1 @@ +{"v":"5.7.5","fr":100,"ip":0,"op":300,"w":180,"h":180,"nm":"Comp 1","ddd":0,"metadata":{"backgroundColor":{"r":255,"g":255,"b":255}},"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Bluetooth-logo.svg 1","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":0,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"hd":true,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","nm":"SVG","it":[{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[14.144503,18.121092],[18.520503,13.745092],[14.140603,9.367192],[14.144503,18.121092],[14.144503,18.121092]],"i":[[0,0],[-1.458666666666666,1.4586666666666677],[1.4599666666666682,1.4593000000000007],[-0.001300000000000523,-2.9179666666666666],[0,0]],"o":[[1.458666666666666,-1.458666666666666],[-1.4599666666666664,-1.4593000000000007],[0.001300000000000523,2.9179666666666666],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[14.140603,36.172892],[18.520503,31.793992],[14.144503,27.417992],[14.140603,36.172892],[14.140603,36.172892]],"i":[[0,0],[-1.4599666666666664,1.4596333333333327],[1.458666666666666,1.458666666666666],[0.001300000000000523,-2.918300000000002],[0,0]],"o":[[1.4599666666666682,-1.4596333333333362],[-1.458666666666666,-1.458666666666666],[-0.001300000000000523,2.9182999999999986],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[23.999003000000002,13.713892],[14.943403,22.769492],[23.999003000000002,31.826192000000002],[10.286103,45.539092],[10.286103,27.460892],[2.743163,35.003892],[0,32.260791999999995],[9.465823,22.769492],[0,13.278291999999999],[2.743163,10.535191999999999],[10.286103,18.079092],[10.286103,0],[23.999003000000002,13.713892],[23.999003000000002,13.713892]],"i":[[0,0],[3.018533333333334,-3.018533333333334],[-3.018533333333334,-3.018900000000002],[4.570966666666667,-4.5709666666666635],[0,6.026066666666665],[2.5143133333333347,-2.514333333333333],[0.9143876666666669,0.9143666666666661],[-3.1552743333333337,3.1637666666666675],[3.1552743333333333,3.163733333333335],[-0.9143876666666668,0.9143666666666661],[-2.514313333333334,-2.514633333333334],[0,6.026364],[-4.570966666666667,-4.571297333333334],[0,0]],"o":[[-3.018533333333334,3.0185333333333357],[3.018533333333334,3.018900000000002],[-4.570966666666667,4.57096666666666],[0,-6.026066666666665],[-2.514313333333334,2.514333333333333],[-0.9143876666666668,-0.9143666666666661],[3.1552743333333333,-3.163766666666664],[-3.155274333333333,-3.163733333333333],[0.9143876666666665,-0.9143666666666661],[2.514313333333333,2.5146333333333324],[0,-6.026364000000001],[4.570966666666667,4.571297333333333],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[0.1568627450980392,0.1568627450980392,0.1803921568627451],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[12.403797149658203,23.570756912231445],"ix":2},"a":{"a":0,"k":[11.99950122833252,22.769546508789062],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[90,90],"ix":2},"a":{"a":0,"k":[12.403796672821045,23.57075595855713],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Wave-1 1:0-stroke-mask","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"td":1,"ao":0,"parent":2,"shapes":[{"ty":"gr","nm":"Wave-1 1","it":[{"ty":"el","d":1,"s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"tr","p":{"a":0,"k":[90,90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0}],"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Wave-1 1:0","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"tt":1,"ao":0,"parent":2,"shapes":[{"ty":"gr","nm":"Wave-1 1","it":[{"ty":"el","d":1,"s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"st","c":{"a":0,"k":[0.1568627450980392,0.1568627450980392,0.1803921568627451],"ix":2},"o":{"a":0,"k":0,"ix":2},"w":{"a":0,"k":4,"ix":2},"lc":1,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[90,90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Wave-1 4:0-stroke-mask","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"td":1,"ao":0,"parent":5,"shapes":[{"ty":"gr","nm":"Wave-1 4","it":[{"ty":"el","d":1,"s":{"a":1,"k":[{"t":85,"s":[70,70],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":175,"s":[175,175],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"tr","p":{"a":0,"k":[88,90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0}],"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Wave-1 4:0","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"tt":1,"ao":0,"parent":5,"shapes":[{"ty":"gr","nm":"Wave-1 4","it":[{"ty":"el","d":1,"s":{"a":1,"k":[{"t":85,"s":[70,70],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":175,"s":[175,175],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"st","c":{"a":0,"k":[0.1568627450980392,0.1568627450980392,0.1803921568627451],"ix":2},"o":{"a":1,"k":[{"t":85,"s":[0],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":100,"s":[20],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":175,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"w":{"a":0,"k":4,"ix":2},"lc":1,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[88,90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Wave-1 3:0-stroke-mask","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"td":1,"ao":0,"parent":8,"shapes":[{"ty":"gr","nm":"Wave-1 3","it":[{"ty":"el","d":1,"s":{"a":1,"k":[{"t":60,"s":[70,70],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":150,"s":[175,175],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"tr","p":{"a":0,"k":[90,90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0}],"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Wave-1 3:0","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"tt":1,"ao":0,"parent":8,"shapes":[{"ty":"gr","nm":"Wave-1 3","it":[{"ty":"el","d":1,"s":{"a":1,"k":[{"t":60,"s":[70,70],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":150,"s":[175,175],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"st","c":{"a":0,"k":[0.1568627450980392,0.1568627450980392,0.1803921568627451],"ix":2},"o":{"a":1,"k":[{"t":60,"s":[0],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":75,"s":[20],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":150,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"w":{"a":0,"k":4,"ix":2},"lc":1,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[90,90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Wave-1 2:0-stroke-mask","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"td":1,"ao":0,"parent":11,"shapes":[{"ty":"gr","nm":"Wave-1 2","it":[{"ty":"el","d":1,"s":{"a":1,"k":[{"t":35,"s":[70,70],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":125,"s":[175,175],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"tr","p":{"a":0,"k":[90,90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0}],"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"Wave-1 2:0","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"tt":1,"ao":0,"parent":11,"shapes":[{"ty":"gr","nm":"Wave-1 2","it":[{"ty":"el","d":1,"s":{"a":1,"k":[{"t":35,"s":[70,70],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":125,"s":[175,175],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"st","c":{"a":0,"k":[0.1568627450980392,0.1568627450980392,0.1803921568627451],"ix":2},"o":{"a":1,"k":[{"t":35,"s":[0],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":50,"s":[20],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":125,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"w":{"a":0,"k":4,"ix":2},"lc":1,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[90,90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"Wave-1:0-stroke-mask","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"td":1,"ao":0,"parent":14,"shapes":[{"ty":"gr","nm":"Wave-1","it":[{"ty":"el","d":1,"s":{"a":1,"k":[{"t":10,"s":[70,70],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":100,"s":[175,175],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"tr","p":{"a":0,"k":[90,90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0}],"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"Wave-1:0","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"tt":1,"ao":0,"parent":14,"shapes":[{"ty":"gr","nm":"Wave-1","it":[{"ty":"el","d":1,"s":{"a":1,"k":[{"t":10,"s":[70,70],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":100,"s":[175,175],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"st","c":{"a":0,"k":[0.1568627450980392,0.1568627450980392,0.1803921568627451],"ix":2},"o":{"a":1,"k":[{"t":10,"s":[0],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":25,"s":[20],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":100,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"w":{"a":0,"k":4,"ix":2},"lc":1,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[90,90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":301,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/packages/core-mobile/app/new/features/ledger/components/AnimatedIconWithText.tsx b/packages/core-mobile/app/new/features/ledger/components/AnimatedIconWithText.tsx new file mode 100644 index 0000000000..2bdd350e14 --- /dev/null +++ b/packages/core-mobile/app/new/features/ledger/components/AnimatedIconWithText.tsx @@ -0,0 +1,109 @@ +import React from 'react' +import { View } from 'react-native' +import LottieView from 'lottie-react-native' +import { Text, useTheme } from '@avalabs/k2-alpine' + +interface AnimatedIconWithTextProps { + /** The icon component to display */ + icon: React.ReactNode + /** The main title text */ + title: string + /** The subtitle/description text */ + subtitle: string + /** Whether to show the animation behind the icon */ + showAnimation?: boolean + /** Custom animation source (defaults to connect-waves.json) */ + animationSource?: any + /** Custom animation size (defaults to 220x220) */ + animationSize?: { width: number; height: number } + /** Custom icon positioning offset for animation centering */ + animationOffset?: { top: number; left: number } +} + +export const AnimatedIconWithText: React.FC = ({ + icon, + title, + subtitle, + showAnimation = false, + animationSource = require('assets/lotties/connect-waves.json'), + animationSize = { width: 220, height: 220 } +}) => { + const { + theme: { colors } + } = useTheme() + + // Calculate dynamic positioning based on animation size + const iconContainerHeight = 44 // Assuming standard icon size + const animationRadius = animationSize.width / 2 + const iconRadius = iconContainerHeight / 2 + + // Calculate animation offset to center it around the icon + const dynamicAnimationOffset = { + top: -(animationRadius - iconRadius), + left: -(animationRadius - iconRadius) + } + + // Calculate consistent text position regardless of animation state + const baseTopPosition = 160 // Base centering position + const textOverlapPosition = baseTopPosition + iconContainerHeight + 16 // Keep text close to icon for both states + + return ( + + + {showAnimation && ( + + )} + {icon} + + + + {title} + + + {subtitle} + + + + ) +} diff --git a/packages/core-mobile/app/new/features/ledger/components/DerivationPathSelector.tsx b/packages/core-mobile/app/new/features/ledger/components/DerivationPathSelector.tsx index d7377fdc11..b405e9d2a5 100644 --- a/packages/core-mobile/app/new/features/ledger/components/DerivationPathSelector.tsx +++ b/packages/core-mobile/app/new/features/ledger/components/DerivationPathSelector.tsx @@ -23,9 +23,9 @@ const derivationPathOptions: DerivationPathOption[] = [ { type: LedgerDerivationPathType.BIP44, title: 'BIP44', - subtitle: 'Standard approach for most users', + subtitle: 'Recommended for most users', benefits: [ - 'Faster setup (~15 seconds)', + 'Faster setup, about 15 seconds', 'Create new accounts without device', 'Industry standard approach', 'Better for multiple accounts' @@ -135,7 +135,7 @@ export const DerivationPathSelector: React.FC = ({ gap: 16, paddingBottom: 100 }}> - + {derivationPathOptions.map(option => ( {isScanning && ( - - - - Searching for Ledger devices... - - + + } + title="Looking for devices..." + subtitle="Make sure your Ledger device is unlocked and the Avalanche app is open" + showAnimation={true} + /> )} {!isScanning && devices.length === 0 && ( - - - - No devices found. Make sure your Ledger is connected and unlocked. - - + + } + title="No devices found" + subtitle="Make sure your Ledger is connected and unlocked." + showAnimation={false} + /> )} {devices.length > 0 && ( diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/deviceConnection.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/deviceConnection.tsx index 6ce808c0db..22dfa546aa 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/deviceConnection.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/deviceConnection.tsx @@ -4,7 +4,7 @@ import { useRouter } from 'expo-router' import { Text, Button, useTheme, GroupList, Icons } from '@avalabs/k2-alpine' import { ScrollScreen } from 'common/components/ScrollScreen' import { useLedgerSetupContext } from 'new/features/ledger/contexts/LedgerSetupContext' -import { ScanningAnimation } from 'new/features/ledger/components/ScanningAnimation' +import { AnimatedIconWithText } from 'features/ledger/components/AnimatedIconWithText' interface LedgerDevice { id: string @@ -104,87 +104,37 @@ export default function DeviceConnectionScreen(): JSX.Element { renderFooter={renderFooter} contentContainerStyle={{ padding: 16, flex: 1 }}> {isScanning && ( - - - - - {/* Text positioned 34px below the center icon */} - - - Looking for devices... - - - Make sure your Ledger device is unlocked and the Avalanche app - is open - - - - + + } + title="Looking for devices..." + subtitle="Make sure your Ledger device is unlocked and the Avalanche app is open" + showAnimation={true} + /> )} {!isScanning && devices.length === 0 && ( - - - - Get your Ledger ready - - - Make sure your Ledger device is unlocked - - + + } + title="Get your Ledger ready" + subtitle="Make sure your Ledger device is unlocked" + showAnimation={false} + /> )} {devices.length > 0 && ( - - Available Devices - )} diff --git a/packages/k2-alpine/src/assets/icons/ledger_logo.svg b/packages/k2-alpine/src/assets/icons/ledger_logo.svg index 2a83f6ba96..c8f53582a5 100644 --- a/packages/k2-alpine/src/assets/icons/ledger_logo.svg +++ b/packages/k2-alpine/src/assets/icons/ledger_logo.svg @@ -1,4 +1,3 @@ - - - + + From d21759b0cd41bdd9170c0aca6256f5e593f70b82 Mon Sep 17 00:00:00 2001 From: James Boyer Date: Wed, 5 Nov 2025 16:33:23 -0500 Subject: [PATCH 17/24] fixes for bluetooth connect flows --- .../app/assets/lotties/connect-waves.json | 1193 ++++++++++++++++- .../components/AnimatedIconWithText.tsx | 17 +- .../components/DerivationPathSelector.tsx | 251 +++- .../ledger/components/EnhancedLedgerSetup.tsx | 62 +- .../ledger/components/ScanningAnimation.tsx | 131 -- .../new/features/ledger/components/index.ts | 2 +- .../ledger/contexts/LedgerSetupContext.tsx | 2 +- .../features/ledger/hooks/useLedgerWallet.ts | 462 ------- .../accountSettings/ledger/_layout.tsx | 4 +- .../ledger/deviceConnection.tsx | 94 +- .../(modals)/accountSettings/ledger/index.tsx | 19 + .../accountSettings/ledger/pathSelection.tsx | 2 +- packages/core-mobile/index.js | 1 - .../k2-alpine/src/assets/icons/bluetooth.svg | 2 +- 14 files changed, 1497 insertions(+), 745 deletions(-) delete mode 100644 packages/core-mobile/app/new/features/ledger/components/ScanningAnimation.tsx create mode 100644 packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/index.tsx diff --git a/packages/core-mobile/app/assets/lotties/connect-waves.json b/packages/core-mobile/app/assets/lotties/connect-waves.json index e4920fb849..4f768f2a8d 100644 --- a/packages/core-mobile/app/assets/lotties/connect-waves.json +++ b/packages/core-mobile/app/assets/lotties/connect-waves.json @@ -1 +1,1192 @@ -{"v":"5.7.5","fr":100,"ip":0,"op":300,"w":180,"h":180,"nm":"Comp 1","ddd":0,"metadata":{"backgroundColor":{"r":255,"g":255,"b":255}},"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Bluetooth-logo.svg 1","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":0,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"hd":true,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","nm":"SVG","it":[{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[14.144503,18.121092],[18.520503,13.745092],[14.140603,9.367192],[14.144503,18.121092],[14.144503,18.121092]],"i":[[0,0],[-1.458666666666666,1.4586666666666677],[1.4599666666666682,1.4593000000000007],[-0.001300000000000523,-2.9179666666666666],[0,0]],"o":[[1.458666666666666,-1.458666666666666],[-1.4599666666666664,-1.4593000000000007],[0.001300000000000523,2.9179666666666666],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[14.140603,36.172892],[18.520503,31.793992],[14.144503,27.417992],[14.140603,36.172892],[14.140603,36.172892]],"i":[[0,0],[-1.4599666666666664,1.4596333333333327],[1.458666666666666,1.458666666666666],[0.001300000000000523,-2.918300000000002],[0,0]],"o":[[1.4599666666666682,-1.4596333333333362],[-1.458666666666666,-1.458666666666666],[-0.001300000000000523,2.9182999999999986],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[23.999003000000002,13.713892],[14.943403,22.769492],[23.999003000000002,31.826192000000002],[10.286103,45.539092],[10.286103,27.460892],[2.743163,35.003892],[0,32.260791999999995],[9.465823,22.769492],[0,13.278291999999999],[2.743163,10.535191999999999],[10.286103,18.079092],[10.286103,0],[23.999003000000002,13.713892],[23.999003000000002,13.713892]],"i":[[0,0],[3.018533333333334,-3.018533333333334],[-3.018533333333334,-3.018900000000002],[4.570966666666667,-4.5709666666666635],[0,6.026066666666665],[2.5143133333333347,-2.514333333333333],[0.9143876666666669,0.9143666666666661],[-3.1552743333333337,3.1637666666666675],[3.1552743333333333,3.163733333333335],[-0.9143876666666668,0.9143666666666661],[-2.514313333333334,-2.514633333333334],[0,6.026364],[-4.570966666666667,-4.571297333333334],[0,0]],"o":[[-3.018533333333334,3.0185333333333357],[3.018533333333334,3.018900000000002],[-4.570966666666667,4.57096666666666],[0,-6.026066666666665],[-2.514313333333334,2.514333333333333],[-0.9143876666666668,-0.9143666666666661],[3.1552743333333333,-3.163766666666664],[-3.155274333333333,-3.163733333333333],[0.9143876666666665,-0.9143666666666661],[2.514313333333333,2.5146333333333324],[0,-6.026364000000001],[4.570966666666667,4.571297333333333],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[0.1568627450980392,0.1568627450980392,0.1803921568627451],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[12.403797149658203,23.570756912231445],"ix":2},"a":{"a":0,"k":[11.99950122833252,22.769546508789062],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[90,90],"ix":2},"a":{"a":0,"k":[12.403796672821045,23.57075595855713],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Wave-1 1:0-stroke-mask","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"td":1,"ao":0,"parent":2,"shapes":[{"ty":"gr","nm":"Wave-1 1","it":[{"ty":"el","d":1,"s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"tr","p":{"a":0,"k":[90,90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0}],"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Wave-1 1:0","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"tt":1,"ao":0,"parent":2,"shapes":[{"ty":"gr","nm":"Wave-1 1","it":[{"ty":"el","d":1,"s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"st","c":{"a":0,"k":[0.1568627450980392,0.1568627450980392,0.1803921568627451],"ix":2},"o":{"a":0,"k":0,"ix":2},"w":{"a":0,"k":4,"ix":2},"lc":1,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[90,90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Wave-1 4:0-stroke-mask","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"td":1,"ao":0,"parent":5,"shapes":[{"ty":"gr","nm":"Wave-1 4","it":[{"ty":"el","d":1,"s":{"a":1,"k":[{"t":85,"s":[70,70],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":175,"s":[175,175],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"tr","p":{"a":0,"k":[88,90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0}],"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Wave-1 4:0","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"tt":1,"ao":0,"parent":5,"shapes":[{"ty":"gr","nm":"Wave-1 4","it":[{"ty":"el","d":1,"s":{"a":1,"k":[{"t":85,"s":[70,70],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":175,"s":[175,175],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"st","c":{"a":0,"k":[0.1568627450980392,0.1568627450980392,0.1803921568627451],"ix":2},"o":{"a":1,"k":[{"t":85,"s":[0],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":100,"s":[20],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":175,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"w":{"a":0,"k":4,"ix":2},"lc":1,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[88,90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Wave-1 3:0-stroke-mask","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"td":1,"ao":0,"parent":8,"shapes":[{"ty":"gr","nm":"Wave-1 3","it":[{"ty":"el","d":1,"s":{"a":1,"k":[{"t":60,"s":[70,70],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":150,"s":[175,175],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"tr","p":{"a":0,"k":[90,90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0}],"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Wave-1 3:0","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"tt":1,"ao":0,"parent":8,"shapes":[{"ty":"gr","nm":"Wave-1 3","it":[{"ty":"el","d":1,"s":{"a":1,"k":[{"t":60,"s":[70,70],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":150,"s":[175,175],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"st","c":{"a":0,"k":[0.1568627450980392,0.1568627450980392,0.1803921568627451],"ix":2},"o":{"a":1,"k":[{"t":60,"s":[0],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":75,"s":[20],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":150,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"w":{"a":0,"k":4,"ix":2},"lc":1,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[90,90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Wave-1 2:0-stroke-mask","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"td":1,"ao":0,"parent":11,"shapes":[{"ty":"gr","nm":"Wave-1 2","it":[{"ty":"el","d":1,"s":{"a":1,"k":[{"t":35,"s":[70,70],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":125,"s":[175,175],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"tr","p":{"a":0,"k":[90,90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0}],"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"Wave-1 2:0","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"tt":1,"ao":0,"parent":11,"shapes":[{"ty":"gr","nm":"Wave-1 2","it":[{"ty":"el","d":1,"s":{"a":1,"k":[{"t":35,"s":[70,70],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":125,"s":[175,175],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"st","c":{"a":0,"k":[0.1568627450980392,0.1568627450980392,0.1803921568627451],"ix":2},"o":{"a":1,"k":[{"t":35,"s":[0],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":50,"s":[20],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":125,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"w":{"a":0,"k":4,"ix":2},"lc":1,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[90,90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"Wave-1:0-stroke-mask","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"td":1,"ao":0,"parent":14,"shapes":[{"ty":"gr","nm":"Wave-1","it":[{"ty":"el","d":1,"s":{"a":1,"k":[{"t":10,"s":[70,70],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":100,"s":[175,175],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"tr","p":{"a":0,"k":[90,90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0}],"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"Wave-1:0","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"tt":1,"ao":0,"parent":14,"shapes":[{"ty":"gr","nm":"Wave-1","it":[{"ty":"el","d":1,"s":{"a":1,"k":[{"t":10,"s":[70,70],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":100,"s":[175,175],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"st","c":{"a":0,"k":[0.1568627450980392,0.1568627450980392,0.1803921568627451],"ix":2},"o":{"a":1,"k":[{"t":10,"s":[0],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":25,"s":[20],"o":{"x":[0],"y":[0]},"i":{"x":[0.58],"y":[1]}},{"t":100,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"w":{"a":0,"k":4,"ix":2},"lc":1,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[90,90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":301,"st":0,"bm":0}],"markers":[]} \ No newline at end of file +{ + "v": "5.7.5", + "fr": 100, + "ip": 0, + "op": 300, + "w": 180, + "h": 180, + "nm": "Comp 1", + "ddd": 0, + "metadata": { "backgroundColor": { "r": 255, "g": 255, "b": 255 } }, + "assets": [], + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "Bluetooth-logo.svg 1", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "ao": 0, + "hd": true, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ty": "gr", + "it": [ + { + "ty": "gr", + "nm": "SVG", + "it": [ + { + "ty": "gr", + "nm": "Path", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [14.144503, 18.121092], + [18.520503, 13.745092], + [14.140603, 9.367192], + [14.144503, 18.121092], + [14.144503, 18.121092] + ], + "i": [ + [0, 0], + [-1.458666666666666, 1.4586666666666677], + [1.4599666666666682, 1.4593000000000007], + [-0.001300000000000523, -2.9179666666666666], + [0, 0] + ], + "o": [ + [1.458666666666666, -1.458666666666666], + [-1.4599666666666664, -1.4593000000000007], + [0.001300000000000523, 2.9179666666666666], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "nm": "Path", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [14.140603, 36.172892], + [18.520503, 31.793992], + [14.144503, 27.417992], + [14.140603, 36.172892], + [14.140603, 36.172892] + ], + "i": [ + [0, 0], + [-1.4599666666666664, 1.4596333333333327], + [1.458666666666666, 1.458666666666666], + [0.001300000000000523, -2.918300000000002], + [0, 0] + ], + "o": [ + [1.4599666666666682, -1.4596333333333362], + [-1.458666666666666, -1.458666666666666], + [-0.001300000000000523, 2.9182999999999986], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "nm": "Path", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [23.999003000000002, 13.713892], + [14.943403, 22.769492], + [23.999003000000002, 31.826192000000002], + [10.286103, 45.539092], + [10.286103, 27.460892], + [2.743163, 35.003892], + [0, 32.260791999999995], + [9.465823, 22.769492], + [0, 13.278291999999999], + [2.743163, 10.535191999999999], + [10.286103, 18.079092], + [10.286103, 0], + [23.999003000000002, 13.713892], + [23.999003000000002, 13.713892] + ], + "i": [ + [0, 0], + [3.018533333333334, -3.018533333333334], + [-3.018533333333334, -3.018900000000002], + [4.570966666666667, -4.5709666666666635], + [0, 6.026066666666665], + [2.5143133333333347, -2.514333333333333], + [0.9143876666666669, 0.9143666666666661], + [-3.1552743333333337, 3.1637666666666675], + [3.1552743333333333, 3.163733333333335], + [-0.9143876666666668, 0.9143666666666661], + [-2.514313333333334, -2.514633333333334], + [0, 6.026364], + [-4.570966666666667, -4.571297333333334], + [0, 0] + ], + "o": [ + [-3.018533333333334, 3.0185333333333357], + [3.018533333333334, 3.018900000000002], + [-4.570966666666667, 4.57096666666666], + [0, -6.026066666666665], + [-2.514313333333334, 2.514333333333333], + [-0.9143876666666668, -0.9143666666666661], + [3.1552743333333333, -3.163766666666664], + [-3.155274333333333, -3.163733333333333], + [0.9143876666666665, -0.9143666666666661], + [2.514313333333333, 2.5146333333333324], + [0, -6.026364000000001], + [4.570966666666667, 4.571297333333333], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.1568627450980392, 0.1568627450980392, + 0.1803921568627451 + ], + "ix": 2 + }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "r": 1, + "bm": 0 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [12.403797149658203, 23.570756912231445], + "ix": 2 + }, + "a": { + "a": 0, + "k": [11.99950122833252, 22.769546508789062], + "ix": 2 + }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "tr", + "p": { "a": 0, "k": [90, 90], "ix": 2 }, + "a": { + "a": 0, + "k": [12.403796672821045, 23.57075595855713], + "ix": 2 + }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + } + ], + "ip": 0, + "op": 301, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 2, + "ty": 3, + "nm": "", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "ao": 0, + "ip": 0, + "op": 301, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 3, + "ty": 4, + "nm": "Wave-1 1:0-stroke-mask", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "td": 1, + "ao": 0, + "parent": 2, + "shapes": [ + { + "ty": "gr", + "nm": "Wave-1 1", + "it": [ + { + "ty": "el", + "d": 1, + "s": { "a": 0, "k": [70, 70], "ix": 2 }, + "p": { "a": 0, "k": [0, 0], "ix": 2 } + }, + { + "ty": "tr", + "p": { "a": 0, "k": [90, 90], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0, 0, 0], "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "r": 1, + "bm": 0 + } + ], + "ip": 0, + "op": 301, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 4, + "ty": 4, + "nm": "Wave-1 1:0", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "tt": 1, + "ao": 0, + "parent": 2, + "shapes": [ + { + "ty": "gr", + "nm": "Wave-1 1", + "it": [ + { + "ty": "el", + "d": 1, + "s": { "a": 0, "k": [70, 70], "ix": 2 }, + "p": { "a": 0, "k": [0, 0], "ix": 2 } + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0.1568627450980392, 0.1568627450980392, 0.1803921568627451 + ], + "ix": 2 + }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "w": { "a": 0, "k": 4, "ix": 2 }, + "lc": 1, + "lj": 1, + "ml": 4 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [90, 90], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + } + ], + "ip": 0, + "op": 301, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 5, + "ty": 3, + "nm": "", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "ao": 0, + "ip": 0, + "op": 301, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 6, + "ty": 4, + "nm": "Wave-1 4:0-stroke-mask", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "td": 1, + "ao": 0, + "parent": 5, + "shapes": [ + { + "ty": "gr", + "nm": "Wave-1 4", + "it": [ + { + "ty": "el", + "d": 1, + "s": { + "a": 1, + "k": [ + { + "t": 85, + "s": [70, 70], + "o": { "x": [0], "y": [0] }, + "i": { "x": [0.58], "y": [1] } + }, + { + "t": 175, + "s": [175, 175], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "p": { "a": 0, "k": [0, 0], "ix": 2 } + }, + { + "ty": "tr", + "p": { "a": 0, "k": [88, 90], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0, 0, 0], "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "r": 1, + "bm": 0 + } + ], + "ip": 0, + "op": 301, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 7, + "ty": 4, + "nm": "Wave-1 4:0", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "tt": 1, + "ao": 0, + "parent": 5, + "shapes": [ + { + "ty": "gr", + "nm": "Wave-1 4", + "it": [ + { + "ty": "el", + "d": 1, + "s": { + "a": 1, + "k": [ + { + "t": 85, + "s": [70, 70], + "o": { "x": [0], "y": [0] }, + "i": { "x": [0.58], "y": [1] } + }, + { + "t": 175, + "s": [175, 175], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "p": { "a": 0, "k": [0, 0], "ix": 2 } + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0.1568627450980392, 0.1568627450980392, 0.1803921568627451 + ], + "ix": 2 + }, + "o": { + "a": 1, + "k": [ + { + "t": 85, + "s": [0], + "o": { "x": [0], "y": [0] }, + "i": { "x": [0.58], "y": [1] } + }, + { + "t": 100, + "s": [20], + "o": { "x": [0], "y": [0] }, + "i": { "x": [0.58], "y": [1] } + }, + { + "t": 175, + "s": [0], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "w": { "a": 0, "k": 4, "ix": 2 }, + "lc": 1, + "lj": 1, + "ml": 4 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [88, 90], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + } + ], + "ip": 0, + "op": 301, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 8, + "ty": 3, + "nm": "", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "ao": 0, + "ip": 0, + "op": 301, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 9, + "ty": 4, + "nm": "Wave-1 3:0-stroke-mask", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "td": 1, + "ao": 0, + "parent": 8, + "shapes": [ + { + "ty": "gr", + "nm": "Wave-1 3", + "it": [ + { + "ty": "el", + "d": 1, + "s": { + "a": 1, + "k": [ + { + "t": 60, + "s": [70, 70], + "o": { "x": [0], "y": [0] }, + "i": { "x": [0.58], "y": [1] } + }, + { + "t": 150, + "s": [175, 175], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "p": { "a": 0, "k": [0, 0], "ix": 2 } + }, + { + "ty": "tr", + "p": { "a": 0, "k": [90, 90], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0, 0, 0], "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "r": 1, + "bm": 0 + } + ], + "ip": 0, + "op": 301, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 10, + "ty": 4, + "nm": "Wave-1 3:0", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "tt": 1, + "ao": 0, + "parent": 8, + "shapes": [ + { + "ty": "gr", + "nm": "Wave-1 3", + "it": [ + { + "ty": "el", + "d": 1, + "s": { + "a": 1, + "k": [ + { + "t": 60, + "s": [70, 70], + "o": { "x": [0], "y": [0] }, + "i": { "x": [0.58], "y": [1] } + }, + { + "t": 150, + "s": [175, 175], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "p": { "a": 0, "k": [0, 0], "ix": 2 } + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0.1568627450980392, 0.1568627450980392, 0.1803921568627451 + ], + "ix": 2 + }, + "o": { + "a": 1, + "k": [ + { + "t": 60, + "s": [0], + "o": { "x": [0], "y": [0] }, + "i": { "x": [0.58], "y": [1] } + }, + { + "t": 75, + "s": [20], + "o": { "x": [0], "y": [0] }, + "i": { "x": [0.58], "y": [1] } + }, + { + "t": 150, + "s": [0], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "w": { "a": 0, "k": 4, "ix": 2 }, + "lc": 1, + "lj": 1, + "ml": 4 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [90, 90], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + } + ], + "ip": 0, + "op": 301, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 11, + "ty": 3, + "nm": "", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "ao": 0, + "ip": 0, + "op": 301, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 12, + "ty": 4, + "nm": "Wave-1 2:0-stroke-mask", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "td": 1, + "ao": 0, + "parent": 11, + "shapes": [ + { + "ty": "gr", + "nm": "Wave-1 2", + "it": [ + { + "ty": "el", + "d": 1, + "s": { + "a": 1, + "k": [ + { + "t": 35, + "s": [70, 70], + "o": { "x": [0], "y": [0] }, + "i": { "x": [0.58], "y": [1] } + }, + { + "t": 125, + "s": [175, 175], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "p": { "a": 0, "k": [0, 0], "ix": 2 } + }, + { + "ty": "tr", + "p": { "a": 0, "k": [90, 90], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0, 0, 0], "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "r": 1, + "bm": 0 + } + ], + "ip": 0, + "op": 301, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 13, + "ty": 4, + "nm": "Wave-1 2:0", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "tt": 1, + "ao": 0, + "parent": 11, + "shapes": [ + { + "ty": "gr", + "nm": "Wave-1 2", + "it": [ + { + "ty": "el", + "d": 1, + "s": { + "a": 1, + "k": [ + { + "t": 35, + "s": [70, 70], + "o": { "x": [0], "y": [0] }, + "i": { "x": [0.58], "y": [1] } + }, + { + "t": 125, + "s": [175, 175], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "p": { "a": 0, "k": [0, 0], "ix": 2 } + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0.1568627450980392, 0.1568627450980392, 0.1803921568627451 + ], + "ix": 2 + }, + "o": { + "a": 1, + "k": [ + { + "t": 35, + "s": [0], + "o": { "x": [0], "y": [0] }, + "i": { "x": [0.58], "y": [1] } + }, + { + "t": 50, + "s": [20], + "o": { "x": [0], "y": [0] }, + "i": { "x": [0.58], "y": [1] } + }, + { + "t": 125, + "s": [0], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "w": { "a": 0, "k": 4, "ix": 2 }, + "lc": 1, + "lj": 1, + "ml": 4 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [90, 90], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + } + ], + "ip": 0, + "op": 301, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 14, + "ty": 3, + "nm": "", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "ao": 0, + "ip": 0, + "op": 301, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 15, + "ty": 4, + "nm": "Wave-1:0-stroke-mask", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "td": 1, + "ao": 0, + "parent": 14, + "shapes": [ + { + "ty": "gr", + "nm": "Wave-1", + "it": [ + { + "ty": "el", + "d": 1, + "s": { + "a": 1, + "k": [ + { + "t": 10, + "s": [70, 70], + "o": { "x": [0], "y": [0] }, + "i": { "x": [0.58], "y": [1] } + }, + { + "t": 100, + "s": [175, 175], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "p": { "a": 0, "k": [0, 0], "ix": 2 } + }, + { + "ty": "tr", + "p": { "a": 0, "k": [90, 90], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0, 0, 0], "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "r": 1, + "bm": 0 + } + ], + "ip": 0, + "op": 301, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 16, + "ty": 4, + "nm": "Wave-1:0", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "tt": 1, + "ao": 0, + "parent": 14, + "shapes": [ + { + "ty": "gr", + "nm": "Wave-1", + "it": [ + { + "ty": "el", + "d": 1, + "s": { + "a": 1, + "k": [ + { + "t": 10, + "s": [70, 70], + "o": { "x": [0], "y": [0] }, + "i": { "x": [0.58], "y": [1] } + }, + { + "t": 100, + "s": [175, 175], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "p": { "a": 0, "k": [0, 0], "ix": 2 } + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 0.1568627450980392, 0.1568627450980392, 0.1803921568627451 + ], + "ix": 2 + }, + "o": { + "a": 1, + "k": [ + { + "t": 10, + "s": [0], + "o": { "x": [0], "y": [0] }, + "i": { "x": [0.58], "y": [1] } + }, + { + "t": 25, + "s": [20], + "o": { "x": [0], "y": [0] }, + "i": { "x": [0.58], "y": [1] } + }, + { + "t": 100, + "s": [0], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "w": { "a": 0, "k": 4, "ix": 2 }, + "lc": 1, + "lj": 1, + "ml": 4 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [90, 90], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + } + ], + "ip": 0, + "op": 301, + "st": 0, + "bm": 0 + } + ], + "markers": [] +} diff --git a/packages/core-mobile/app/new/features/ledger/components/AnimatedIconWithText.tsx b/packages/core-mobile/app/new/features/ledger/components/AnimatedIconWithText.tsx index 2bdd350e14..2e152a4c27 100644 --- a/packages/core-mobile/app/new/features/ledger/components/AnimatedIconWithText.tsx +++ b/packages/core-mobile/app/new/features/ledger/components/AnimatedIconWithText.tsx @@ -3,6 +3,9 @@ import { View } from 'react-native' import LottieView from 'lottie-react-native' import { Text, useTheme } from '@avalabs/k2-alpine' +// Import animation at the top level +const connectWavesAnimation = require('assets/lotties/connect-waves.json') + interface AnimatedIconWithTextProps { /** The icon component to display */ icon: React.ReactNode @@ -18,6 +21,8 @@ interface AnimatedIconWithTextProps { animationSize?: { width: number; height: number } /** Custom icon positioning offset for animation centering */ animationOffset?: { top: number; left: number } + /** Custom color for the animation (defaults to theme textPrimary) */ + animationColor?: string } export const AnimatedIconWithText: React.FC = ({ @@ -25,8 +30,9 @@ export const AnimatedIconWithText: React.FC = ({ title, subtitle, showAnimation = false, - animationSource = require('assets/lotties/connect-waves.json'), - animationSize = { width: 220, height: 220 } + animationSource = connectWavesAnimation, + animationSize = { width: 220, height: 220 }, + animationColor }) => { const { theme: { colors } @@ -66,6 +72,13 @@ export const AnimatedIconWithText: React.FC = ({ source={animationSource} autoPlay loop + resizeMode="contain" + colorFilters={[ + { + keypath: '*', // Apply to all layers + color: animationColor || colors.$textPrimary // Use custom color or theme default + } + ]} style={{ position: 'absolute', width: animationSize.width, diff --git a/packages/core-mobile/app/new/features/ledger/components/DerivationPathSelector.tsx b/packages/core-mobile/app/new/features/ledger/components/DerivationPathSelector.tsx index b405e9d2a5..9431b673bb 100644 --- a/packages/core-mobile/app/new/features/ledger/components/DerivationPathSelector.tsx +++ b/packages/core-mobile/app/new/features/ledger/components/DerivationPathSelector.tsx @@ -17,6 +17,186 @@ interface DerivationPathOption { interface DerivationPathSelectorProps { onSelect: (derivationPathType: LedgerDerivationPathType) => void + onCancel?: () => void +} + +interface ListItemProps { + text: string + type: 'benefit' | 'consideration' + isFirst?: boolean +} + +const ListItem: React.FC = ({ text, type, isFirst = false }) => { + const { + theme: { colors } + } = useTheme() + + const iconProps = + type === 'benefit' + ? { + Icon: Icons.Custom.CheckSmall, + color: colors.$textSuccess + } + : { + Icon: Icons.Custom.RedExclamation, + color: colors.$textDanger, + width: 16, + height: 12 + } + + return ( + + + + {text} + + + ) +} + +interface OptionCardProps { + option: DerivationPathOption + onPress?: () => void +} + +const OptionCard: React.FC = ({ option, onPress }) => { + const { + theme: { colors } + } = useTheme() + + return ( + + + + + + + {option.title} + + + {option.subtitle} + + + + {onPress && ( + + )} + + + {/* Divider that spans from text start to card end */} + + + + + Benefits + + {option.benefits.map((benefit, index) => ( + + ))} + + + {option.warnings.length > 0 && ( + + + Considerations + + {option.warnings.map((warning, index) => ( + + ))} + + )} + + ) } const derivationPathOptions: DerivationPathOption[] = [ @@ -54,77 +234,6 @@ const derivationPathOptions: DerivationPathOption[] = [ export const DerivationPathSelector: React.FC = ({ onSelect }) => { - const { - theme: { colors } - } = useTheme() - const [selectedType, setSelectedType] = - useState(null) - - const selectedOption = derivationPathOptions.find( - option => option.type === selectedType - ) - - const renderFooter = useCallback(() => { - return ( - - - - ) - }, [selectedType, onSelect]) - - const groupListData = derivationPathOptions.map(option => ({ - title: option.title, - subtitle: ( - - {option.subtitle} - - ), - leftIcon: ( - - - - ), - accessory: - selectedType === option.type ? ( - - ) : ( - - ), - onPress: () => setSelectedType(option.type) - })) - return ( = ({ theme: { colors } } = useTheme() const [currentStep, setCurrentStep] = useState('path-selection') - const [selectedDerivationPath, setSelectedDerivationPath] = - useState(null) - const [connectedDeviceId, setConnectedDeviceId] = useState( - null - ) - const [connectedDeviceName, setConnectedDeviceName] = - useState('Ledger Device') - const [isCreatingWallet, setIsCreatingWallet] = useState(false) const { + // Context state + selectedDerivationPath, + connectedDeviceId, + connectedDeviceName, + isCreatingWallet, + setSelectedDerivationPath, + setConnectedDevice, + setIsCreatingWallet, + // Ledger wallet functionality devices, isScanning, isConnecting, @@ -54,8 +55,9 @@ export const EnhancedLedgerSetup: React.FC = ({ getAvalancheKeys, createLedgerWallet, setupProgress, - keys - } = useLedgerWallet() + keys, + resetSetup + } = useLedgerSetupContext() // Check if keys are available and auto-progress to setup useEffect(() => { @@ -73,8 +75,7 @@ export const EnhancedLedgerSetup: React.FC = ({ async (deviceId: string, deviceName: string) => { try { await connectToDevice(deviceId) - setConnectedDeviceId(deviceId) - setConnectedDeviceName(deviceName) + setConnectedDevice(deviceId, deviceName) // Move to app connection step instead of getting keys immediately setCurrentStep('app-connection') @@ -86,7 +87,7 @@ export const EnhancedLedgerSetup: React.FC = ({ ) } }, - [connectToDevice] + [connectToDevice, setConnectedDevice] ) // Start wallet setup (called from setup-progress step) @@ -140,6 +141,7 @@ export const EnhancedLedgerSetup: React.FC = ({ selectedDerivationPath, isCreatingWallet, createLedgerWallet, + setIsCreatingWallet, onComplete, onCancel ]) @@ -167,15 +169,16 @@ export const EnhancedLedgerSetup: React.FC = ({ setSelectedDerivationPath(derivationPathType) setCurrentStep('device-connection') }, - [] + [setSelectedDerivationPath] ) const handleCancel = useCallback(async () => { if (connectedDeviceId) { await disconnectDevice() } + resetSetup() onCancel?.() - }, [connectedDeviceId, disconnectDevice, onCancel]) + }, [connectedDeviceId, disconnectDevice, resetSetup, onCancel]) const renderCurrentStep = (): React.ReactNode => { switch (currentStep) { @@ -211,31 +214,6 @@ export const EnhancedLedgerSetup: React.FC = ({ /> ) - case 'device-connection': - return ( - - ) - - case 'app-connection': - return ( - setCurrentStep('setup-progress')} - onCancel={handleCancel} - getSolanaKeys={getSolanaKeys} - getAvalancheKeys={getAvalancheKeys} - deviceName={connectedDeviceName} - selectedDerivationPath={selectedDerivationPath} - /> - ) - case 'setup-progress': return setupProgress ? ( = ({ - size = 200, - iconSize = 32 -}) => { - const { - theme: { colors } - } = useTheme() - const animationValue = useSharedValue(0) - - useEffect(() => { - animationValue.value = withRepeat( - withTiming(1, { - duration: 2000, - easing: Easing.out(Easing.quad) - }), - -1, - false - ) - }, [animationValue]) - - // Create animated styles for each circle - const createCircleStyle = (delay: number, maxScale: number) => { - return useAnimatedStyle(() => { - const progress = (animationValue.value + delay) % 1 - const scale = interpolate(progress, [0, 1], [0.3, maxScale]) - const opacity = interpolate(progress, [0, 0.7, 1], [0.8, 0.3, 0]) - - return { - transform: [{ scale }], - opacity - } - }) - } - - const circle1Style = createCircleStyle(0, 1.0) - const circle2Style = createCircleStyle(0.3, 1.3) - const circle3Style = createCircleStyle(0.6, 1.6) - - const circleSize = size - const borderWidth = 2 - - return ( - - {/* Outermost circle */} - - - {/* Middle circle */} - - - {/* Inner circle */} - - - {/* Center icon */} - - - - - ) -} diff --git a/packages/core-mobile/app/new/features/ledger/components/index.ts b/packages/core-mobile/app/new/features/ledger/components/index.ts index cacfdc6eb4..bca1a150d4 100644 --- a/packages/core-mobile/app/new/features/ledger/components/index.ts +++ b/packages/core-mobile/app/new/features/ledger/components/index.ts @@ -2,4 +2,4 @@ export { DerivationPathSelector } from './DerivationPathSelector' export { LedgerSetupProgress } from './LedgerSetupProgress' export { EnhancedLedgerSetup } from './EnhancedLedgerSetup' export { LedgerAppConnection } from './LedgerAppConnection' -export { ScanningAnimation } from './ScanningAnimation' +export { AnimatedIconWithText } from './AnimatedIconWithText' diff --git a/packages/core-mobile/app/new/features/ledger/contexts/LedgerSetupContext.tsx b/packages/core-mobile/app/new/features/ledger/contexts/LedgerSetupContext.tsx index 4c82b0a7ad..83f515a3b5 100644 --- a/packages/core-mobile/app/new/features/ledger/contexts/LedgerSetupContext.tsx +++ b/packages/core-mobile/app/new/features/ledger/contexts/LedgerSetupContext.tsx @@ -6,7 +6,7 @@ import React, { useMemo, ReactNode } from 'react' -import { LedgerDerivationPathType } from 'services/wallet/LedgerWallet' +import { LedgerDerivationPathType } from 'services/ledger/types' import { WalletCreationOptions, useLedgerWallet diff --git a/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts b/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts index 5a9828ee0e..6c3891f8fe 100644 --- a/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts +++ b/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts @@ -2,21 +2,11 @@ import { useState, useCallback, useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import { Alert, Platform, PermissionsAndroid } from 'react-native' import TransportBLE from '@ledgerhq/react-native-hw-transport-ble' -<<<<<<< HEAD import Transport from '@ledgerhq/hw-transport' import AppSolana from '@ledgerhq/hw-app-solana' import bs58 from 'bs58' import LedgerService from 'services/ledger/LedgerService' import { LedgerAppType, LedgerDerivationPathType } from 'services/ledger/types' -======= -import AppSolana from '@ledgerhq/hw-app-solana' -import bs58 from 'bs58' -import { LedgerService, LedgerAppType } from 'services/ledger/ledgerService' -<<<<<<< HEAD ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) -======= -import { LedgerDerivationPathType } from 'services/wallet/LedgerWallet' ->>>>>>> 6a596f952 (ledger live address derivation working, transactiosn on avalanche and solana proven with this setup) import { ChainName } from 'services/network/consts' import { WalletType } from 'services/wallet/types' import { AppThunkDispatch } from 'store/types' @@ -28,7 +18,6 @@ import { CoreAccountType } from '@avalabs/types' import { showSnackbar } from 'new/common/utils/toast' import { uuid } from 'utils/uuid' import Logger from 'utils/Logger' -<<<<<<< HEAD import { Curve } from 'utils/publicKeys' import { SetupProgress, @@ -88,90 +77,10 @@ import { SOLANA_DERIVATION_PATH, LEDGER_TIMEOUTS } from '../consts' -======= - -export interface LedgerDevice { - id: string - name: string - rssi?: number -} - -export interface LedgerTransportState { - available: boolean - powered: boolean -} - -export interface LedgerKeys { - solanaKeys: any[] - avalancheKeys: { - evm: { key: string; address: string } - avalanche: { key: string; address: string } - pvm?: { key: string; address: string } - } | null - bitcoinAddress: string - xpAddress: string -} - -export interface WalletCreationOptions { - deviceId: string - deviceName?: string - derivationPathType: LedgerDerivationPathType - accountCount?: number - individualKeys?: any[] // For Ledger Live - individual keys retrieved from device - progressCallback?: ( - step: string, - progress: number, - totalSteps: number - ) => void -} - -export interface SetupProgress { - currentStep: string - progress: number - totalSteps: number - estimatedTimeRemaining?: number -} - -export interface UseLedgerWalletReturn { - // Device scanning and connection - devices: LedgerDevice[] - isScanning: boolean - isConnecting: boolean - transportState: LedgerTransportState - scanForDevices: () => Promise - connectToDevice: (deviceId: string) => Promise - disconnectDevice: () => Promise - - // Key retrieval - isLoading: boolean - getSolanaKeys: () => Promise - getAvalancheKeys: () => Promise - getLedgerLiveKeys: ( - accountCount?: number, - progressCallback?: ( - step: string, - progress: number, - totalSteps: number - ) => void - ) => Promise<{ avalancheKeys: any; individualKeys: any[] }> - resetKeys: () => void - keys: LedgerKeys - - // Wallet creation - createLedgerWallet: (options: WalletCreationOptions) => Promise // Returns the new wallet ID - - // Setup progress - setupProgress: SetupProgress | null -} ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) export function useLedgerWallet(): UseLedgerWalletReturn { const dispatch = useDispatch() const allAccounts = useSelector(selectAccounts) -<<<<<<< HEAD -======= - const [ledgerService] = useState(() => new LedgerService()) ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) const [transportState, setTransportState] = useState({ available: false, powered: false @@ -180,44 +89,25 @@ export function useLedgerWallet(): UseLedgerWalletReturn { const [isScanning, setIsScanning] = useState(false) const [isConnecting, setIsConnecting] = useState(false) const [isLoading, setIsLoading] = useState(false) -<<<<<<< HEAD -<<<<<<< HEAD const [setupProgress, setSetupProgress] = useState(null) // Key states const [solanaKeys, setSolanaKeys] = useState([]) const [avalancheKeys, setAvalancheKeys] = useState(null) -======= -======= - const [setupProgress, setSetupProgress] = useState(null) ->>>>>>> 6a596f952 (ledger live address derivation working, transactiosn on avalanche and solana proven with this setup) - - // Key states - const [solanaKeys, setSolanaKeys] = useState([]) - const [avalancheKeys, setAvalancheKeys] = useState(null) ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) const [bitcoinAddress, setBitcoinAddress] = useState('') const [xpAddress, setXpAddress] = useState('') // Monitor BLE transport state useEffect(() => { const subscription = TransportBLE.observeState({ -<<<<<<< HEAD next: (event: { available: boolean }) => { -======= - next: event => { ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) setTransportState({ available: event.available, powered: false }) }, -<<<<<<< HEAD error: (error: Error) => { -======= - error: error => { ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) Alert.alert( 'BLE Error', `Failed to monitor BLE state: ${error.message}` @@ -240,11 +130,7 @@ export function useLedgerWallet(): UseLedgerWalletReturn { PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT, PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION -<<<<<<< HEAD ].filter(Boolean) -======= - ].filter(Boolean) as any[] ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) const granted = await PermissionsAndroid.requestMultiple(permissions) return Object.values(granted).every( @@ -258,11 +144,7 @@ export function useLedgerWallet(): UseLedgerWalletReturn { }, []) // Handle scan errors -<<<<<<< HEAD const handleScanError = useCallback((error: Error) => { -======= - const handleScanError = useCallback((error: any) => { ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) setIsScanning(false) if ( @@ -311,14 +193,10 @@ export function useLedgerWallet(): UseLedgerWalletReturn { try { const subscription = TransportBLE.listen({ -<<<<<<< HEAD next: (event: { type: string descriptor: { id: string; name?: string; rssi?: number } }) => { -======= - next: event => { ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) if (event.type === 'add') { const device: LedgerDevice = { id: event.descriptor.id, @@ -343,20 +221,13 @@ export function useLedgerWallet(): UseLedgerWalletReturn { setTimeout(() => { subscription.unsubscribe() setIsScanning(false) -<<<<<<< HEAD }, LEDGER_TIMEOUTS.SCAN_TIMEOUT) } catch (error) { handleScanError(error as Error) -======= - }, 10000) - } catch (error) { - handleScanError(error) ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) } }, [transportState.available, requestBluetoothPermissions, handleScanError]) // Connect to device -<<<<<<< HEAD const connectToDevice = useCallback(async (deviceId: string) => { setIsConnecting(true) try { @@ -369,38 +240,16 @@ export function useLedgerWallet(): UseLedgerWalletReturn { setIsConnecting(false) } }, []) -======= - const connectToDevice = useCallback( - async (deviceId: string) => { - setIsConnecting(true) - try { - await ledgerService.connect(deviceId) - Logger.info('Connected to Ledger device') - } catch (error) { - Logger.error('Failed to connect to device', error) - throw error - } finally { - setIsConnecting(false) - } - }, - [ledgerService] - ) ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) // Disconnect device const disconnectDevice = useCallback(async () => { try { -<<<<<<< HEAD await LedgerService.disconnect() -======= - await ledgerService.disconnect() ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) Logger.info('Disconnected from Ledger device') } catch (error) { Logger.error('Failed to disconnect', error) throw error } -<<<<<<< HEAD }, []) // Get Solana keys @@ -411,17 +260,10 @@ export function useLedgerWallet(): UseLedgerWalletReturn { return } -======= - }, [ledgerService]) - - // Get Solana keys - const getSolanaKeys = useCallback(async () => { ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) try { setIsLoading(true) Logger.info('Getting Solana keys with passive app detection') -<<<<<<< HEAD await LedgerService.waitForApp(LedgerAppType.SOLANA) // Get address directly from Solana app @@ -432,33 +274,12 @@ export function useLedgerWallet(): UseLedgerWalletReturn { // Convert the Buffer to base58 format (Solana address format) const solanaAddress = bs58.encode(new Uint8Array(result.address)) -======= - await ledgerService.waitForApp(LedgerAppType.SOLANA) - - // Get address directly from Solana app - const solanaApp = new AppSolana(ledgerService.getTransport()) - const derivationPath = `44'/501'/0'/0'/0` - const result = await solanaApp.getAddress(derivationPath, false) - -<<<<<<< HEAD - // result.address is already in base58 format - const solanaAddress = result.address.toString() ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) -======= - // Convert the Buffer to base58 format (Solana address format) - const solanaAddress = bs58.encode(new Uint8Array(result.address)) ->>>>>>> 6a596f952 (ledger live address derivation working, transactiosn on avalanche and solana proven with this setup) setSolanaKeys([ { key: solanaAddress, derivationPath, -<<<<<<< HEAD curve: Curve.ED25519 -======= - curve: 'ed25519', - publicKey: solanaAddress ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) } ]) Logger.info('Successfully got Solana address', solanaAddress) @@ -468,7 +289,6 @@ export function useLedgerWallet(): UseLedgerWalletReturn { } finally { setIsLoading(false) } -<<<<<<< HEAD }, [isLoading]) // Get Avalanche keys @@ -479,30 +299,16 @@ export function useLedgerWallet(): UseLedgerWalletReturn { return } -======= - }, [ledgerService]) - - // Get Avalanche keys - const getAvalancheKeys = useCallback(async () => { ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) try { setIsLoading(true) Logger.info('Getting Avalanche keys') -<<<<<<< HEAD const addresses = await LedgerService.getAllAddresses(0, 1) -======= - const addresses = await ledgerService.getAllAddresses(0, 1) ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) const evmAddress = addresses.find(addr => addr.network === ChainName.AVALANCHE_C_EVM) ?.address || '' -<<<<<<< HEAD const xChainAddress = -======= - const xpAddress = ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) addresses.find(addr => addr.network === ChainName.AVALANCHE_X) ?.address || '' const pvmAddress = @@ -514,21 +320,12 @@ export function useLedgerWallet(): UseLedgerWalletReturn { // Store the addresses directly from the device setAvalancheKeys({ -<<<<<<< HEAD evm: evmAddress, avalanche: xChainAddress, pvm: pvmAddress }) setBitcoinAddress(btcAddress) setXpAddress(xChainAddress) -======= - evm: { key: evmAddress, address: evmAddress }, - avalanche: { key: xpAddress, address: xpAddress }, - pvm: { key: pvmAddress, address: pvmAddress } - }) - setBitcoinAddress(btcAddress) - setXpAddress(xpAddress) ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) Logger.info('Successfully got Avalanche keys') } catch (error) { @@ -537,11 +334,7 @@ export function useLedgerWallet(): UseLedgerWalletReturn { } finally { setIsLoading(false) } -<<<<<<< HEAD }, [isLoading]) -======= - }, [ledgerService]) ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) const resetKeys = useCallback(() => { setSolanaKeys([]) @@ -550,8 +343,6 @@ export function useLedgerWallet(): UseLedgerWalletReturn { setXpAddress('') }, []) -<<<<<<< HEAD -<<<<<<< HEAD // New method: Get individual keys for Ledger Live (sequential device confirmations) const getLedgerLiveKeys = useCallback( async ( @@ -692,187 +483,22 @@ export function useLedgerWallet(): UseLedgerWalletReturn { Logger.info( `Creating ${derivationPathType} Ledger wallet with generated keys...` ) -======= -======= - // New method: Get individual keys for Ledger Live (sequential device confirmations) - const getLedgerLiveKeys = useCallback( - async ( - accountCount = 3, - progressCallback?: ( - step: string, - progress: number, - totalSteps: number - ) => void - ) => { - try { - setIsLoading(true) - Logger.info( - `Starting Ledger Live key retrieval for ${accountCount} accounts` - ) - - const totalSteps = accountCount // One step per account (gets both EVM and AVM) - const individualKeys: any[] = [] - let avalancheKeysResult: any = null - - // Sequential address retrieval - each account requires device confirmation - for ( - let accountIndex = 0; - accountIndex < accountCount; - accountIndex++ - ) { - const stepName = `Getting keys for account ${accountIndex + 1}...` - const progress = Math.round(((accountIndex + 1) / totalSteps) * 100) - progressCallback?.(stepName, progress, totalSteps) - - Logger.info( - `Requesting addresses for account ${accountIndex} (Ledger Live style)` - ) - - // Get public keys for this specific account (1 at a time for device confirmation) - const publicKeys = await ledgerService.getPublicKeys(accountIndex, 1) - - // Also get addresses for display purposes - const addresses = await ledgerService.getAllAddresses(accountIndex, 1) - - // Extract the keys for this account - const evmPublicKey = publicKeys.find(key => - key.derivationPath.includes("44'/60'") - ) - const avmPublicKey = publicKeys.find(key => - key.derivationPath.includes("44'/9000'") - ) - - // Extract addresses for this account - const evmAddress = addresses.find( - addr => addr.network === ChainName.AVALANCHE_C_EVM - ) - const xpAddress = addresses.find( - addr => addr.network === ChainName.AVALANCHE_X - ) - - if (evmPublicKey) { - individualKeys.push({ - key: evmPublicKey.key, - derivationPath: `m/44'/60'/${accountIndex}'/0/0`, // Ledger Live path - curve: evmPublicKey.curve - }) - } - - if (avmPublicKey) { - individualKeys.push({ - key: avmPublicKey.key, - derivationPath: `m/44'/9000'/${accountIndex}'/0/0`, // Ledger Live path - curve: avmPublicKey.curve - }) - } - - // Store first account's keys as primary - if (accountIndex === 0) { - avalancheKeysResult = { - evm: { - key: evmPublicKey?.key || '', - address: evmAddress?.address || '' - }, - avalanche: { - key: avmPublicKey?.key || '', - address: xpAddress?.address || '' - } - } - } - } - - // Update state with the retrieved keys - if (avalancheKeysResult) { - setAvalancheKeys(avalancheKeysResult) - } - - Logger.info( - `Successfully retrieved Ledger Live keys for ${accountCount} accounts` - ) - Logger.info('Individual keys count:', individualKeys.length) - - return { avalancheKeys: avalancheKeysResult, individualKeys } - } catch (error) { - Logger.error('Failed to get Ledger Live keys:', error) - throw error - } finally { - setIsLoading(false) - } - }, - [ledgerService] - ) - ->>>>>>> 6a596f952 (ledger live address derivation working, transactiosn on avalanche and solana proven with this setup) - const createLedgerWallet = useCallback( - async ({ - deviceId, - deviceName = 'Ledger Device', - derivationPathType = LedgerDerivationPathType.BIP44, - individualKeys = [], - progressCallback - }: WalletCreationOptions) => { - try { - setIsLoading(true) -<<<<<<< HEAD - Logger.info('Creating Ledger wallet with generated keys...') ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) -======= - - // Initialize progress tracking - const totalSteps = - derivationPathType === LedgerDerivationPathType.BIP44 ? 3 : 6 - let currentStep = 1 - - const updateProgress = (stepName: string) => { - const progress = { - currentStep: stepName, - progress: Math.round((currentStep / totalSteps) * 100), - totalSteps, - estimatedTimeRemaining: - (totalSteps - currentStep) * - (derivationPathType === LedgerDerivationPathType.BIP44 ? 5 : 8) - } - setSetupProgress(progress) - progressCallback?.(stepName, progress.progress, totalSteps) - currentStep++ - } - - updateProgress('Validating keys...') - Logger.info( - `Creating ${derivationPathType} Ledger wallet with generated keys...` - ) ->>>>>>> 6a596f952 (ledger live address derivation working, transactiosn on avalanche and solana proven with this setup) if (!avalancheKeys || solanaKeys.length === 0 || !bitcoinAddress) { throw new Error('Missing required keys for wallet creation') } -<<<<<<< HEAD -<<<<<<< HEAD - updateProgress('Generating wallet ID...') - const newWalletId = uuid() - - updateProgress('Storing wallet data...') - // Store the Ledger wallet with the specified derivation path type -======= - const newWalletId = uuid() - - // Store the Ledger wallet ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) -======= updateProgress('Generating wallet ID...') const newWalletId = uuid() updateProgress('Storing wallet data...') // Store the Ledger wallet with the specified derivation path type ->>>>>>> 6a596f952 (ledger live address derivation working, transactiosn on avalanche and solana proven with this setup) await dispatch( storeWallet({ walletId: newWalletId, walletSecret: JSON.stringify({ deviceId, deviceName, -<<<<<<< HEAD derivationPath: DERIVATION_PATHS.BIP44.EVM, vmType: 'EVM', derivationPathSpec: derivationPathType, @@ -916,49 +542,6 @@ export function useLedgerWallet(): UseLedgerWalletReturn { derivationPathType === LedgerDerivationPathType.BIP44 ? WalletType.LEDGER : WalletType.LEDGER_LIVE -======= - derivationPath: "m/44'/60'/0'/0/0", - vmType: 'EVM', - derivationPathSpec: derivationPathType, - ...(derivationPathType === LedgerDerivationPathType.BIP44 && { - extendedPublicKeys: { - evm: avalancheKeys.evm.key, - avalanche: avalancheKeys.avalanche.key - } - }), - publicKeys: - derivationPathType === LedgerDerivationPathType.LedgerLive && - individualKeys.length > 0 - ? individualKeys // Use individual keys for Ledger Live - : [ - // Use existing keys for BIP44 - { - key: avalancheKeys.evm.key, - derivationPath: "m/44'/60'/0'/0/0", - curve: 'secp256k1' - }, - { - key: avalancheKeys.avalanche.key, - derivationPath: "m/44'/9000'/0'/0/0", - curve: 'secp256k1' - }, - { - key: - avalancheKeys.pvm?.key || avalancheKeys.avalanche.key, - derivationPath: "m/44'/9000'/0'/0/0", - curve: 'secp256k1' - }, - { - key: solanaKeys[0]?.key || '', - derivationPath: "m/44'/501'/0'/0'", - curve: 'ed25519' - } - ], - avalancheKeys, - solanaKeys - }), - type: WalletType.LEDGER ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) }) ).unwrap() @@ -966,19 +549,11 @@ export function useLedgerWallet(): UseLedgerWalletReturn { // Create addresses from the keys const addresses = { -<<<<<<< HEAD EVM: avalancheKeys.evm, AVM: avalancheKeys.avalanche, PVM: avalancheKeys.pvm || avalancheKeys.avalanche, BITCOIN: bitcoinAddress, SVM: solanaKeys[0]?.key || '', -======= - EVM: avalancheKeys.evm.address, - AVM: avalancheKeys.avalanche.address, - PVM: avalancheKeys.pvm?.address || avalancheKeys.avalanche.address, - BITCOIN: bitcoinAddress, - SVM: solanaKeys[0]?.publicKey || '', ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) CoreEth: '' } @@ -1008,24 +583,13 @@ export function useLedgerWallet(): UseLedgerWalletReturn { throw error } finally { setIsLoading(false) -<<<<<<< HEAD -<<<<<<< HEAD - setSetupProgress(null) -======= ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) -======= setSetupProgress(null) ->>>>>>> 6a596f952 (ledger live address derivation working, transactiosn on avalanche and solana proven with this setup) } }, [avalancheKeys, solanaKeys, bitcoinAddress, dispatch, allAccounts] ) return { -<<<<<<< HEAD -======= - // Device scanning and connection ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) devices, isScanning, isConnecting, @@ -1033,44 +597,18 @@ export function useLedgerWallet(): UseLedgerWalletReturn { scanForDevices, connectToDevice, disconnectDevice, -<<<<<<< HEAD - isLoading, - getSolanaKeys, - getAvalancheKeys, - getLedgerLiveKeys, - resetKeys, -======= - - // Key retrieval isLoading, getSolanaKeys, getAvalancheKeys, -<<<<<<< HEAD ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) -======= getLedgerLiveKeys, resetKeys, ->>>>>>> 6a596f952 (ledger live address derivation working, transactiosn on avalanche and solana proven with this setup) keys: { solanaKeys, avalancheKeys, bitcoinAddress, xpAddress }, -<<<<<<< HEAD createLedgerWallet, setupProgress -======= - - // Wallet creation - createLedgerWallet, -<<<<<<< HEAD - resetKeys ->>>>>>> 0070815a2 (ledger provider, modifications to wallet, ui now using hook) -======= - - // Setup progress - setupProgress ->>>>>>> 6a596f952 (ledger live address derivation working, transactiosn on avalanche and solana proven with this setup) } } diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/_layout.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/_layout.tsx index 3d3e67a7b4..8359d765eb 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/_layout.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/_layout.tsx @@ -14,7 +14,8 @@ export default function LedgerSetupLayout(): JSX.Element { ...modalStackNavigatorScreenOptions, headerShown: false }} - initialRouteName="pathSelection"> + initialRouteName="index"> + @@ -30,6 +31,7 @@ export default function LedgerSetupLayout(): JSX.Element { gestureEnabled: false // Prevent going back after completion }} /> + ) diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/deviceConnection.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/deviceConnection.tsx index 22dfa546aa..dfe5b7cb89 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/deviceConnection.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/deviceConnection.tsx @@ -1,10 +1,10 @@ import React, { useCallback } from 'react' -import { View, Alert } from 'react-native' +import { View, Alert, ActivityIndicator } from 'react-native' import { useRouter } from 'expo-router' import { Text, Button, useTheme, GroupList, Icons } from '@avalabs/k2-alpine' import { ScrollScreen } from 'common/components/ScrollScreen' import { useLedgerSetupContext } from 'new/features/ledger/contexts/LedgerSetupContext' -import { AnimatedIconWithText } from 'features/ledger/components/AnimatedIconWithText' +import { AnimatedIconWithText } from 'new/features/ledger/components/AnimatedIconWithText' interface LedgerDevice { id: string @@ -53,41 +53,35 @@ export default function DeviceConnectionScreen(): JSX.Element { back() }, [resetSetup, back]) - const renderFooter = useCallback(() => { - return ( - - - - - - ) - }, [isScanning, isConnecting, scanForDevices, handleCancel]) - const deviceListData = devices.map((device: LedgerDevice) => ({ title: device.name || 'Ledger Device', subtitle: ( - + Found over Bluetooth ), leftIcon: ( - + + + ), accessory: ( diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/index.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/index.tsx new file mode 100644 index 0000000000..815879afff --- /dev/null +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/index.tsx @@ -0,0 +1,19 @@ +import React, { useEffect } from 'react' +import { useRouter } from 'expo-router' + +/** + * Index route for Ledger setup - redirects to path selection + * This ensures that navigating to /accountSettings/ledger shows the path selection screen + */ +export default function LedgerIndex(): JSX.Element { + const router = useRouter() + + useEffect(() => { + // Redirect to path selection screen immediately + // @ts-ignore TODO: make routes typesafe + router.replace('/accountSettings/ledger/pathSelection') + }, [router]) + + // Return null while redirecting + return <> +} diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/pathSelection.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/pathSelection.tsx index 9647d07b71..18c1681a49 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/pathSelection.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/pathSelection.tsx @@ -2,7 +2,7 @@ import React, { useCallback } from 'react' import { useRouter } from 'expo-router' import { DerivationPathSelector } from 'new/features/ledger/components/DerivationPathSelector' import { useLedgerSetupContext } from 'new/features/ledger/contexts/LedgerSetupContext' -import { LedgerDerivationPathType } from 'services/wallet/LedgerWallet' +import { LedgerDerivationPathType } from 'services/ledger/types' export default function PathSelectionScreen(): JSX.Element { const { push, back } = useRouter() diff --git a/packages/core-mobile/index.js b/packages/core-mobile/index.js index 38a27a1f0a..d8db055924 100644 --- a/packages/core-mobile/index.js +++ b/packages/core-mobile/index.js @@ -13,7 +13,6 @@ import DevDebuggingConfig from 'utils/debugging/DevDebuggingConfig' import SentryService from 'services/sentry/SentryService' import NewApp from 'new/ContextApp' import { expo } from './app.json' -import { server } from './tests/msw/native/server' if (__DEV__) { require('./ReactotronConfig') diff --git a/packages/k2-alpine/src/assets/icons/bluetooth.svg b/packages/k2-alpine/src/assets/icons/bluetooth.svg index b16b6407ef..b3563f99bf 100644 --- a/packages/k2-alpine/src/assets/icons/bluetooth.svg +++ b/packages/k2-alpine/src/assets/icons/bluetooth.svg @@ -1,3 +1,3 @@ - + From c2f82bba7cca1e15accae2f683efdba41455500c Mon Sep 17 00:00:00 2001 From: James Boyer Date: Thu, 6 Nov 2025 10:47:11 -0500 Subject: [PATCH 18/24] linting fixes --- .../accountSettings/ledger/setupProgress.tsx | 141 ------------------ packages/core-mobile/index.js | 1 + 2 files changed, 1 insertion(+), 141 deletions(-) delete mode 100644 packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/setupProgress.tsx diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/setupProgress.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/setupProgress.tsx deleted file mode 100644 index 05dd5f4550..0000000000 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/setupProgress.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import React, { useCallback, useEffect } from 'react' -import { View, Alert } from 'react-native' -import { useRouter } from 'expo-router' -import { Text, useTheme } from '@avalabs/k2-alpine' -import { LedgerDerivationPathType } from 'services/wallet/LedgerWallet' -import { LedgerSetupProgress } from 'new/features/ledger/components/LedgerSetupProgress' -import { useLedgerSetupContext } from 'new/features/ledger/contexts/LedgerSetupContext' -import { WalletCreationOptions } from 'new/features/ledger/hooks/useLedgerWallet' - -export default function SetupProgressScreen(): JSX.Element { - const { push, back } = useRouter() - const { - theme: { colors } - } = useTheme() - - const { - connectedDeviceId, - connectedDeviceName, - selectedDerivationPath, - isCreatingWallet, - hasStartedSetup, - setIsCreatingWallet, - setHasStartedSetup, - createLedgerWallet, - setupProgress, - resetSetup, - disconnectDevice - } = useLedgerSetupContext() - - // Start wallet setup when entering this screen - const handleStartSetup = useCallback(async () => { - if (!connectedDeviceId || !selectedDerivationPath || isCreatingWallet) { - return - } - - try { - setIsCreatingWallet(true) - - const walletCreationOptions: WalletCreationOptions = { - deviceId: connectedDeviceId, - deviceName: connectedDeviceName, - derivationPathType: selectedDerivationPath, - accountCount: 3, // Standard 3 accounts for both BIP44 and Ledger Live - progressCallback: (_step, _progress, _totalSteps) => { - // Progress callback for UI updates - } - } - - await createLedgerWallet(walletCreationOptions) - - // Navigate to completion screen - // @ts-ignore TODO: make routes typesafe - push('/accountSettings/ledger/complete') - } catch (error) { - // Wallet creation failed - Alert.alert( - 'Setup Failed', - 'Failed to create Ledger wallet. Please try again.', - [ - { - text: 'Try Again', - onPress: () => { - setIsCreatingWallet(false) - // @ts-ignore TODO: make routes typesafe - back() // Go back to app connection - } - }, - { - text: 'Cancel', - onPress: async () => { - setIsCreatingWallet(false) - await disconnectDevice() - resetSetup() - // Navigate back to import wallet screen - // @ts-ignore TODO: make routes typesafe - push('/accountSettings/importWallet') - } - } - ] - ) - } finally { - setIsCreatingWallet(false) - } - }, [ - connectedDeviceId, - connectedDeviceName, - selectedDerivationPath, - isCreatingWallet, - setIsCreatingWallet, - createLedgerWallet, - push, - back, - disconnectDevice, - resetSetup - ]) - - // Auto-start wallet creation when entering this screen - useEffect(() => { - if ( - selectedDerivationPath && - connectedDeviceId && - !isCreatingWallet && - !hasStartedSetup - ) { - console.log('Starting wallet setup...') - setHasStartedSetup(true) - handleStartSetup() - } - }, [selectedDerivationPath, connectedDeviceId, isCreatingWallet, hasStartedSetup, setHasStartedSetup, handleStartSetup]) - - const handleCancel = useCallback(async () => { - await disconnectDevice() - resetSetup() - // @ts-ignore TODO: make routes typesafe - push('/accountSettings/importWallet') - }, [disconnectDevice, resetSetup, push]) - - if (!setupProgress) { - return ( - - Initializing setup... - - ) - } - - return ( - - ) -} diff --git a/packages/core-mobile/index.js b/packages/core-mobile/index.js index d8db055924..38a27a1f0a 100644 --- a/packages/core-mobile/index.js +++ b/packages/core-mobile/index.js @@ -13,6 +13,7 @@ import DevDebuggingConfig from 'utils/debugging/DevDebuggingConfig' import SentryService from 'services/sentry/SentryService' import NewApp from 'new/ContextApp' import { expo } from './app.json' +import { server } from './tests/msw/native/server' if (__DEV__) { require('./ReactotronConfig') From 894be27efb97b0c26b513f5f3f059a6a7332adb9 Mon Sep 17 00:00:00 2001 From: James Boyer Date: Thu, 6 Nov 2025 12:06:30 -0500 Subject: [PATCH 19/24] removed changes to settings.json --- .vscode/settings.json | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index a95f568ac3..23e6b4c30d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,22 +4,5 @@ "mode": "auto" } ], - "typescript.tsdk": "./packages/core-mobile/node_modules/typescript/lib", - "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[javascriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[typescriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[json]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - } + "typescript.tsdk": "./packages/core-mobile/node_modules/typescript/lib" } From 74bd792e9097a99256a899cbaacfd0de8a12dc05 Mon Sep 17 00:00:00 2001 From: James Boyer Date: Thu, 6 Nov 2025 12:24:03 -0500 Subject: [PATCH 20/24] typescript errors --- .../app/new/features/ledger/hooks/useLedgerWallet.ts | 3 +++ packages/core-mobile/app/services/ledger/LedgerService.ts | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts b/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts index 6c3891f8fe..13e9b08ae2 100644 --- a/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts +++ b/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts @@ -27,6 +27,9 @@ import { LedgerKeys } from 'services/ledger/types' +// Re-export types for consumers +export type { WalletCreationOptions, SetupProgress, LedgerDevice, LedgerKeys } + export interface UseLedgerWalletReturn { // Connection state devices: LedgerDevice[] diff --git a/packages/core-mobile/app/services/ledger/LedgerService.ts b/packages/core-mobile/app/services/ledger/LedgerService.ts index 480d0c2401..04cabc464a 100644 --- a/packages/core-mobile/app/services/ledger/LedgerService.ts +++ b/packages/core-mobile/app/services/ledger/LedgerService.ts @@ -1,4 +1,5 @@ import TransportBLE from '@ledgerhq/react-native-hw-transport-ble' +import Transport from '@ledgerhq/hw-transport' import AppAvalanche from '@avalabs/hw-app-avalanche' import AppSolana from '@ledgerhq/hw-app-solana' import { NetworkVMType } from '@avalabs/core-chains-sdk' @@ -342,7 +343,8 @@ export class LedgerService { try { // Create fresh Solana app instance - const solanaApp = new AppSolana(this.transport) + const transport = await this.getTransport() + const solanaApp = new AppSolana(transport as Transport) // Try to get a simple address to check if app is open // Use a standard Solana derivation path const testPath = "m/44'/501'/0'" @@ -364,7 +366,8 @@ export class LedgerService { } // Create a fresh AppSolana instance for each call (like the SDK does) - const freshSolanaApp = new AppSolana(this.transport) + const transport = await this.getTransport() + const freshSolanaApp = new AppSolana(transport as Transport) const publicKeys: PublicKeyInfo[] = [] try { From c162e1d69001148087ac58090fb00a2c038095bd Mon Sep 17 00:00:00 2001 From: James Boyer Date: Thu, 6 Nov 2025 13:52:44 -0500 Subject: [PATCH 21/24] clean up --- .../ledger/components/EnhancedLedgerSetup.tsx | 384 ------------------ .../ledger/components/LedgerSetupProgress.tsx | 203 --------- .../new/features/ledger/components/index.ts | 2 - .../(modals)/accountSettings/importWallet.tsx | 2 +- .../accountSettings/ledger/_layout.tsx | 10 +- .../accountSettings/ledger/appConnection.tsx | 6 +- .../ledger/confirmAddresses.tsx | 19 - .../accountSettings/ledger/connectWallet.tsx | 19 - .../accountSettings/ledger/enhancedSetup.tsx | 21 - .../(modals)/accountSettings/ledger/index.tsx | 19 - 10 files changed, 5 insertions(+), 680 deletions(-) delete mode 100644 packages/core-mobile/app/new/features/ledger/components/EnhancedLedgerSetup.tsx delete mode 100644 packages/core-mobile/app/new/features/ledger/components/LedgerSetupProgress.tsx delete mode 100644 packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx delete mode 100644 packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx delete mode 100644 packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/enhancedSetup.tsx delete mode 100644 packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/index.tsx diff --git a/packages/core-mobile/app/new/features/ledger/components/EnhancedLedgerSetup.tsx b/packages/core-mobile/app/new/features/ledger/components/EnhancedLedgerSetup.tsx deleted file mode 100644 index 9b78aa37db..0000000000 --- a/packages/core-mobile/app/new/features/ledger/components/EnhancedLedgerSetup.tsx +++ /dev/null @@ -1,384 +0,0 @@ -import React, { useState, useCallback, useEffect } from 'react' -import { View, Alert } from 'react-native' -import { Text, Button, useTheme, GroupList, Icons } from '@avalabs/k2-alpine' -import { ScrollScreen } from 'common/components/ScrollScreen' -import { - LedgerDerivationPathType, - WalletCreationOptions -} from 'services/ledger/types' -import { useLedgerSetupContext } from '../contexts/LedgerSetupContext' -import { DerivationPathSelector } from './DerivationPathSelector' -import { LedgerSetupProgress } from './LedgerSetupProgress' -import { LedgerAppConnection } from './LedgerAppConnection' -import { AnimatedIconWithText } from './AnimatedIconWithText' - -type SetupStep = - | 'path-selection' - | 'education' - | 'device-connection' - | 'app-connection' - | 'setup-progress' - | 'complete' - -interface EnhancedLedgerSetupProps { - onComplete: (walletId: string) => void - onCancel: () => void -} - -export const EnhancedLedgerSetup: React.FC = ({ - onComplete, - onCancel -}) => { - const { - theme: { colors } - } = useTheme() - const [currentStep, setCurrentStep] = useState('path-selection') - - const { - // Context state - selectedDerivationPath, - connectedDeviceId, - connectedDeviceName, - isCreatingWallet, - setSelectedDerivationPath, - setConnectedDevice, - setIsCreatingWallet, - // Ledger wallet functionality - devices, - isScanning, - isConnecting, - transportState, - scanForDevices, - connectToDevice, - disconnectDevice, - getSolanaKeys, - getAvalancheKeys, - createLedgerWallet, - setupProgress, - keys, - resetSetup - } = useLedgerSetupContext() - - // Check if keys are available and auto-progress to setup - useEffect(() => { - if ( - keys.avalancheKeys && - keys.solanaKeys.length > 0 && - currentStep === 'app-connection' - ) { - setCurrentStep('setup-progress') - } - }, [keys.avalancheKeys, keys.solanaKeys, currentStep]) - - // Handle device connection - const handleDeviceConnection = useCallback( - async (deviceId: string, deviceName: string) => { - try { - await connectToDevice(deviceId) - setConnectedDevice(deviceId, deviceName) - - // Move to app connection step instead of getting keys immediately - setCurrentStep('app-connection') - } catch (error) { - Alert.alert( - 'Connection Failed', - 'Failed to connect to Ledger device. Please try again.', - [{ text: 'OK' }] - ) - } - }, - [connectToDevice, setConnectedDevice] - ) - - // Start wallet setup (called from setup-progress step) - const handleStartSetup = useCallback(async () => { - if (!connectedDeviceId || !selectedDerivationPath || isCreatingWallet) { - return - } - - try { - setIsCreatingWallet(true) - // We already have all the keys we need from the app connection step - // No need to retrieve keys again - just create the wallet with what we have - const walletCreationOptions: WalletCreationOptions = { - deviceId: connectedDeviceId, - deviceName: connectedDeviceName, - derivationPathType: selectedDerivationPath, - accountCount: 3 // Standard 3 accounts for both BIP44 and Ledger Live - } - - const walletId = await createLedgerWallet(walletCreationOptions) - setCurrentStep('complete') - onComplete(walletId) - } catch (error) { - // Wallet creation failed - Alert.alert( - 'Setup Failed', - 'Failed to create Ledger wallet. Please try again.', - [ - { - text: 'Try Again', - onPress: () => { - setIsCreatingWallet(false) - setCurrentStep('app-connection') - } - }, - { - text: 'Cancel', - onPress: () => { - setIsCreatingWallet(false) - onCancel?.() - } - } - ] - ) - } finally { - setIsCreatingWallet(false) - } - }, [ - connectedDeviceId, - connectedDeviceName, - selectedDerivationPath, - isCreatingWallet, - createLedgerWallet, - setIsCreatingWallet, - onComplete, - onCancel - ]) - - // Auto-start wallet creation when entering setup-progress step - useEffect(() => { - if ( - currentStep === 'setup-progress' && - selectedDerivationPath && - connectedDeviceId && - !isCreatingWallet - ) { - handleStartSetup() - } - }, [ - currentStep, - selectedDerivationPath, - connectedDeviceId, - isCreatingWallet, - handleStartSetup - ]) - - const handleDerivationPathSelect = useCallback( - (derivationPathType: LedgerDerivationPathType) => { - setSelectedDerivationPath(derivationPathType) - setCurrentStep('device-connection') - }, - [setSelectedDerivationPath] - ) - - const handleCancel = useCallback(async () => { - if (connectedDeviceId) { - await disconnectDevice() - } - resetSetup() - onCancel?.() - }, [connectedDeviceId, disconnectDevice, resetSetup, onCancel]) - - const renderCurrentStep = (): React.ReactNode => { - switch (currentStep) { - case 'path-selection': - return ( - - - - ) - - case 'device-connection': - return ( - - ) - - case 'app-connection': - return ( - setCurrentStep('setup-progress')} - onCancel={handleCancel} - getSolanaKeys={getSolanaKeys} - getAvalancheKeys={getAvalancheKeys} - deviceName={connectedDeviceName} - selectedDerivationPath={selectedDerivationPath} - /> - ) - - case 'setup-progress': - return setupProgress ? ( - - ) : ( - - Initializing setup... - - ) - - case 'complete': - return ( - - - 🎉 Wallet Created Successfully! - - - Your Ledger wallet has been set up and is ready to use. - - - - ) - - default: - return null - } - } - - return ( - - {renderCurrentStep()} - - ) -} - -interface LedgerDevice { - id: string - name: string -} - -// Enhanced device connection component -const DeviceConnectionStep: React.FC<{ - devices: LedgerDevice[] - isScanning: boolean - isConnecting: boolean - transportState: unknown - onScan: () => void - onConnect: (deviceId: string, deviceName: string) => void - onCancel: () => void -}> = ({ devices, isScanning, isConnecting, onScan, onConnect, onCancel }) => { - const { - theme: { colors } - } = useTheme() - - const renderFooter = useCallback(() => { - return ( - - - - - - ) - }, [isScanning, isConnecting, onScan, onCancel]) - - const deviceListData = devices.map(device => ({ - title: device.name || 'Ledger Device', - subtitle: ( - - {device.id ? `ID: ${device.id.slice(0, 8)}...` : 'Ready to connect'} - - ), - leftIcon: ( - - ), - accessory: ( - - ), - onPress: () => onConnect(device.id, device.name) - })) - - return ( - - {isScanning && ( - - } - title="Looking for devices..." - subtitle="Make sure your Ledger device is unlocked and the Avalanche app is open" - showAnimation={true} - /> - )} - - {!isScanning && devices.length === 0 && ( - - } - title="No devices found" - subtitle="Make sure your Ledger is connected and unlocked." - showAnimation={false} - /> - )} - - {devices.length > 0 && ( - - - Available Devices - - - - )} - - ) -} diff --git a/packages/core-mobile/app/new/features/ledger/components/LedgerSetupProgress.tsx b/packages/core-mobile/app/new/features/ledger/components/LedgerSetupProgress.tsx deleted file mode 100644 index 41a91dad2a..0000000000 --- a/packages/core-mobile/app/new/features/ledger/components/LedgerSetupProgress.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import React from 'react' -import { View, ActivityIndicator, TouchableOpacity } from 'react-native' -import { Text, useTheme } from '@avalabs/k2-alpine' -import { LedgerDerivationPathType } from 'services/ledger/types' -import { SetupProgress } from 'services/ledger/types' - -interface LedgerSetupProgressProps { - progress: SetupProgress - derivationPathType: LedgerDerivationPathType - onCancel?: () => void -} - -const formatTime = (seconds: number): string => { - if (seconds < 60) { - return `${seconds}s` - } - const minutes = Math.floor(seconds / 60) - const remainingSeconds = seconds % 60 - return remainingSeconds > 0 - ? `${minutes}m ${remainingSeconds}s` - : `${minutes}m` -} - -const getSetupInstructions = ( - derivationPathType: LedgerDerivationPathType -): string[] => { - if (derivationPathType === LedgerDerivationPathType.BIP44) { - return [ - 'Please confirm each derivation path on your Ledger device', - 'This will take approximately 15 seconds', - 'Keep your device connected during setup' - ] - } else { - return [ - 'Please confirm each derivation path on your Ledger device', - 'This will take approximately 45 seconds', - 'Each account requires individual confirmation', - 'Keep your device connected during setup' - ] - } -} - -export const LedgerSetupProgress: React.FC = ({ - progress, - derivationPathType, - onCancel -}) => { - const { - theme: { colors } - } = useTheme() - const instructions = getSetupInstructions(derivationPathType) - - return ( - - {/* Header */} - - - - - - - Setting up your Ledger wallet - - - - {derivationPathType === LedgerDerivationPathType.BIP44 - ? 'BIP44 Setup' - : 'Ledger Live Setup'} - - - - {/* Progress section */} - - {/* Current step */} - - {progress.currentStep} - - - {/* Progress bar */} - - - - - - - {/* Progress stats */} - - - Step {Math.ceil((progress.progress / 100) * progress.totalSteps)} of{' '} - {progress.totalSteps} - - - {progress.estimatedTimeRemaining !== undefined && - progress.estimatedTimeRemaining > 0 && ( - - ~{formatTime(progress.estimatedTimeRemaining)} remaining - - )} - - - - {/* Instructions */} - - - 📱 Instructions - - {instructions.map((instruction, index) => ( - - • {instruction} - - ))} - - - {/* Device status indicators */} - - - - Device connected - - - - {/* Cancel button */} - {onCancel && ( - - - - Cancel Setup - - - - )} - - ) -} diff --git a/packages/core-mobile/app/new/features/ledger/components/index.ts b/packages/core-mobile/app/new/features/ledger/components/index.ts index bca1a150d4..a35676bd0e 100644 --- a/packages/core-mobile/app/new/features/ledger/components/index.ts +++ b/packages/core-mobile/app/new/features/ledger/components/index.ts @@ -1,5 +1,3 @@ export { DerivationPathSelector } from './DerivationPathSelector' -export { LedgerSetupProgress } from './LedgerSetupProgress' -export { EnhancedLedgerSetup } from './EnhancedLedgerSetup' export { LedgerAppConnection } from './LedgerAppConnection' export { AnimatedIconWithText } from './AnimatedIconWithText' diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/importWallet.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/importWallet.tsx index d57a979592..3a6cf40da6 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/importWallet.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/importWallet.tsx @@ -72,7 +72,7 @@ const ImportWalletScreen = (): JSX.Element => { const handleImportLedger = (): void => { // @ts-ignore TODO: make routes typesafe - navigate({ pathname: '/accountSettings/ledger' }) + navigate({ pathname: '/accountSettings/ledger/pathSelection' }) } const baseData = [ diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/_layout.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/_layout.tsx index 8359d765eb..c906ea423c 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/_layout.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/_layout.tsx @@ -14,24 +14,16 @@ export default function LedgerSetupLayout(): JSX.Element { ...modalStackNavigatorScreenOptions, headerShown: false }} - initialRouteName="index"> - + initialRouteName="pathSelection"> - - ) diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/appConnection.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/appConnection.tsx index 86cebed141..c24d1f7066 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/appConnection.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/appConnection.tsx @@ -16,17 +16,17 @@ export default function AppConnectionScreen(): JSX.Element { disconnectDevice } = useLedgerSetupContext() - // Check if keys are available and auto-progress to setup + // Check if keys are available and auto-progress to complete useEffect(() => { if (keys.avalancheKeys && keys.solanaKeys.length > 0) { // @ts-ignore TODO: make routes typesafe - push('/accountSettings/ledger/setupProgress') + push('/accountSettings/ledger/complete') } }, [keys.avalancheKeys, keys.solanaKeys, push]) const handleComplete = useCallback(() => { // @ts-ignore TODO: make routes typesafe - push('/accountSettings/ledger/setupProgress') + push('/accountSettings/ledger/complete') }, [push]) const handleCancel = useCallback(async () => { diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx deleted file mode 100644 index b8a43c1ef7..0000000000 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/confirmAddresses.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' -import { useRouter } from 'expo-router' - -/** - * @deprecated This route is deprecated. Use enhancedSetup.tsx instead. - * This file redirects to the new enhanced Ledger setup flow. - */ -export default function ConfirmAddresses(): JSX.Element { - const router = useRouter() - - // Redirect to enhanced setup - this route is deprecated - React.useEffect(() => { - // @ts-ignore TODO: make routes typesafe - router.replace('/accountSettings/ledger') - }, [router]) - - // Return null while redirecting - return <> -} diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx deleted file mode 100644 index c68f73617e..0000000000 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/connectWallet.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' -import { useRouter } from 'expo-router' - -/** - * @deprecated This route is deprecated. Use enhancedSetup.tsx instead. - * This file redirects to the new enhanced Ledger setup flow. - */ -export default function ConnectWallet(): JSX.Element { - const router = useRouter() - - // Redirect to enhanced setup - this route is deprecated - React.useEffect(() => { - // @ts-ignore TODO: make routes typesafe - router.replace('/accountSettings/ledger') - }, [router]) - - // Return null while redirecting - return <> -} diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/enhancedSetup.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/enhancedSetup.tsx deleted file mode 100644 index 3f52c1286b..0000000000 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/enhancedSetup.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react' -import { useRouter } from 'expo-router' -import { EnhancedLedgerSetup } from 'new/features/ledger/components' - -export default function EnhancedLedgerSetupScreen(): JSX.Element { - const router = useRouter() - - const handleComplete = (): void => { - // Navigate to account management after successful wallet creation - // @ts-ignore TODO: make routes typesafe - router.push('/accountSettings/manageAccounts') - } - - const handleCancel = (): void => { - router.back() - } - - return ( - - ) -} diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/index.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/index.tsx deleted file mode 100644 index 815879afff..0000000000 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/accountSettings/ledger/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React, { useEffect } from 'react' -import { useRouter } from 'expo-router' - -/** - * Index route for Ledger setup - redirects to path selection - * This ensures that navigating to /accountSettings/ledger shows the path selection screen - */ -export default function LedgerIndex(): JSX.Element { - const router = useRouter() - - useEffect(() => { - // Redirect to path selection screen immediately - // @ts-ignore TODO: make routes typesafe - router.replace('/accountSettings/ledger/pathSelection') - }, [router]) - - // Return null while redirecting - return <> -} From 871fb2ec9ac8f7ee61e814deddd5d2eb458a8d1e Mon Sep 17 00:00:00 2001 From: James Boyer Date: Thu, 6 Nov 2025 14:03:01 -0500 Subject: [PATCH 22/24] more clean up --- packages/core-mobile/.nvimlog | 0 .../android/app/src/main/AndroidManifest.xml | 8 --- .../app/services/ledger/LedgerService.ts | 55 ++++--------------- .../core-mobile/ios/AvaxWallet/Info.plist | 5 -- 4 files changed, 12 insertions(+), 56 deletions(-) delete mode 100644 packages/core-mobile/.nvimlog diff --git a/packages/core-mobile/.nvimlog b/packages/core-mobile/.nvimlog deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/core-mobile/android/app/src/main/AndroidManifest.xml b/packages/core-mobile/android/app/src/main/AndroidManifest.xml index ba85a7659d..37afe82b0f 100644 --- a/packages/core-mobile/android/app/src/main/AndroidManifest.xml +++ b/packages/core-mobile/android/app/src/main/AndroidManifest.xml @@ -20,14 +20,6 @@ - - - - - - - - diff --git a/packages/core-mobile/app/services/ledger/LedgerService.ts b/packages/core-mobile/app/services/ledger/LedgerService.ts index 04cabc464a..5e077b3920 100644 --- a/packages/core-mobile/app/services/ledger/LedgerService.ts +++ b/packages/core-mobile/app/services/ledger/LedgerService.ts @@ -13,44 +13,13 @@ import { import { networks } from 'bitcoinjs-lib' import Logger from 'utils/Logger' import bs58 from 'bs58' - -export interface AddressInfo { - id: string - address: string - derivationPath: string - network: string -} - -export interface ExtendedPublicKey { - path: string - key: string - chainCode: string -} - -export interface PublicKeyInfo { - key: string - derivationPath: string - curve: 'secp256k1' | 'ed25519' -} - -export enum LedgerAppType { - AVALANCHE = 'Avalanche', - SOLANA = 'Solana', - ETHEREUM = 'Ethereum', - UNKNOWN = 'Unknown' -} - -export const LedgerReturnCode = { - SUCCESS: 0x9000, - USER_REJECTED: 0x6985, - APP_NOT_OPEN: 0x6a80, - DEVICE_LOCKED: 0x5515, - INVALID_PARAMETER: 0x6b00, - COMMAND_NOT_ALLOWED: 0x6986 -} as const - -export type LedgerReturnCodeType = - typeof LedgerReturnCode[keyof typeof LedgerReturnCode] +import { + LedgerAppType, + ExtendedPublicKey, + LedgerReturnCode, + PublicKeyInfo, + AddressInfo +} from './types' export interface AppInfo { applicationName: string @@ -129,7 +98,7 @@ export class LedgerService { throw new Error('Transport not initialized') } - return await getLedgerAppInfo(this.transport as any) + return await getLedgerAppInfo(this.transport as Transport) } // Map app name to our enum @@ -224,7 +193,7 @@ export class LedgerService { Logger.info('Avalanche app detected, creating app instance...') // Create Avalanche app instance - const avalancheApp = new AppAvalanche(this.transport as any) + const avalancheApp = new AppAvalanche(this.transport as Transport) Logger.info('Avalanche app instance created') try { @@ -413,7 +382,7 @@ export class LedgerService { // Use the SDK function directly (like the extension does) const publicKey = await getSolanaPublicKeyFromLedger( startIndex, - this.transport as any + this.transport as Transport ) const publicKeys: PublicKeyInfo[] = [ @@ -480,7 +449,7 @@ export class LedgerService { await this.waitForApp(LedgerAppType.AVALANCHE) // Create Avalanche app instance - const avalancheApp = new AppAvalanche(this.transport as any) + const avalancheApp = new AppAvalanche(this.transport as Transport) const publicKeys: PublicKeyInfo[] = [] @@ -554,7 +523,7 @@ export class LedgerService { await this.waitForApp(LedgerAppType.AVALANCHE) // Create Avalanche app instance - const avalancheApp = new AppAvalanche(this.transport as any) + const avalancheApp = new AppAvalanche(this.transport as Transport) const addresses: AddressInfo[] = [] diff --git a/packages/core-mobile/ios/AvaxWallet/Info.plist b/packages/core-mobile/ios/AvaxWallet/Info.plist index 887093f846..833328afa0 100644 --- a/packages/core-mobile/ios/AvaxWallet/Info.plist +++ b/packages/core-mobile/ios/AvaxWallet/Info.plist @@ -51,11 +51,6 @@ $(CURRENT_PROJECT_VERSION) ITSAppUsesNonExemptEncryption - LSApplicationQueriesSchemes - - twitter - mailto - LSMinimumSystemVersion 13.3.0 LSRequiresIPhoneOS From c4d9ffc018c215b1901ef5a1bf652c3fff362bdf Mon Sep 17 00:00:00 2001 From: James Boyer Date: Thu, 6 Nov 2025 14:10:48 -0500 Subject: [PATCH 23/24] fixed ledger service after rebase --- .../app/services/ledger/LedgerService.ts | 117 +++++++++--------- 1 file changed, 60 insertions(+), 57 deletions(-) diff --git a/packages/core-mobile/app/services/ledger/LedgerService.ts b/packages/core-mobile/app/services/ledger/LedgerService.ts index 5e077b3920..e3345668b7 100644 --- a/packages/core-mobile/app/services/ledger/LedgerService.ts +++ b/packages/core-mobile/app/services/ledger/LedgerService.ts @@ -14,30 +14,47 @@ import { networks } from 'bitcoinjs-lib' import Logger from 'utils/Logger' import bs58 from 'bs58' import { - LedgerAppType, + LEDGER_TIMEOUTS, + getSolanaDerivationPath +} from 'new/features/ledger/consts' +import { assertNotNull } from 'utils/assertions' +import { + AddressInfo, ExtendedPublicKey, - LedgerReturnCode, PublicKeyInfo, - AddressInfo + LedgerAppType, + LedgerReturnCode, + AppInfo } from './types' -export interface AppInfo { - applicationName: string - version: string -} - export class LedgerService { - private transport: TransportBLE | null = null + #transport: TransportBLE | null = null private currentAppType: LedgerAppType = LedgerAppType.UNKNOWN private appPollingInterval: number | null = null private appPollingEnabled = false + // Transport getter/setter with automatic error handling + private get transport(): TransportBLE { + assertNotNull( + this.#transport, + 'Ledger transport is not initialized. Please connect to a device first.' + ) + return this.#transport + } + + private set transport(transport: TransportBLE) { + this.#transport = transport + } + // Connect to Ledger device (transport only, no apps) async connect(deviceId: string): Promise { try { Logger.info('Starting BLE connection attempt with deviceId:', deviceId) - // Use a longer timeout for connection (30 seconds) - this.transport = await TransportBLE.open(deviceId, 30000) + // Use a longer timeout for connection + this.transport = await TransportBLE.open( + deviceId, + LEDGER_TIMEOUTS.CONNECTION_TIMEOUT + ) Logger.info('BLE transport connected successfully') this.currentAppType = LedgerAppType.UNKNOWN @@ -62,7 +79,7 @@ export class LedgerService { this.appPollingEnabled = true this.appPollingInterval = setInterval(async () => { try { - if (!this.transport || !this.transport.isConnected) { + if (!this.#transport || !this.#transport.isConnected) { this.stopAppPolling() return } @@ -80,7 +97,7 @@ export class LedgerService { Logger.error('Error polling app info', error) // Don't stop polling on error, just log it } - }, 2000) // Poll every 2 seconds like the extension + }, LEDGER_TIMEOUTS.APP_POLLING_INTERVAL) // Poll every 2 seconds like the extension } // Stop passive app detection polling @@ -94,10 +111,6 @@ export class LedgerService { // Get current app info from device private async getCurrentAppInfo(): Promise { - if (!this.transport) { - throw new Error('Transport not initialized') - } - return await getLedgerAppInfo(this.transport as Transport) } @@ -121,7 +134,10 @@ export class LedgerService { } // Wait for specific app to be open (passive approach) - async waitForApp(appType: LedgerAppType, timeoutMs = 30000): Promise { + async waitForApp( + appType: LedgerAppType, + timeoutMs = LEDGER_TIMEOUTS.APP_WAIT_TIMEOUT + ): Promise { const startTime = Date.now() Logger.info(`Waiting for ${appType} app (timeout: ${timeoutMs}ms)...`) @@ -135,8 +151,10 @@ export class LedgerService { return } - // Wait 1 second before next check - await new Promise(resolve => setTimeout(resolve, 1000)) + // Wait before next check + await new Promise(resolve => + setTimeout(resolve, LEDGER_TIMEOUTS.APP_CHECK_DELAY) + ) } Logger.error(`Timeout waiting for ${appType} app after ${timeoutMs}ms`) @@ -161,7 +179,7 @@ export class LedgerService { private async reconnectIfNeeded(deviceId: string): Promise { Logger.info('Checking if reconnection is needed') - if (!this.transport || !this.transport.isConnected) { + if (!this.#transport || !this.#transport.isConnected) { Logger.info('Transport is disconnected, attempting reconnection') try { await this.connect(deviceId) @@ -180,10 +198,6 @@ export class LedgerService { evm: ExtendedPublicKey avalanche: ExtendedPublicKey }> { - if (!this.transport) { - throw new Error('Transport not initialized') - } - Logger.info('=== getExtendedPublicKeys STARTED ===') Logger.info('Current app type:', this.currentAppType) @@ -306,7 +320,7 @@ export class LedgerService { // Check if Solana app is open async checkSolanaApp(): Promise { - if (!this.transport) { + if (!this.#transport) { return false } @@ -325,15 +339,19 @@ export class LedgerService { } } + // Get Solana address for a specific derivation path + async getSolanaAddress(derivationPath: string): Promise<{ address: Buffer }> { + await this.waitForApp(LedgerAppType.SOLANA) + const transport = await this.getTransport() + const solanaApp = new AppSolana(transport as Transport) + return await solanaApp.getAddress(derivationPath, false) + } + // Get Solana public keys using SDK function (like extension) async getSolanaPublicKeys( startIndex: number, count: number ): Promise { - if (!this.transport) { - throw new Error('Transport not initialized') - } - // Create a fresh AppSolana instance for each call (like the SDK does) const transport = await this.getTransport() const freshSolanaApp = new AppSolana(transport as Transport) @@ -342,7 +360,7 @@ export class LedgerService { try { for (let i = startIndex; i < startIndex + count; i++) { // Use correct Solana derivation path format - const derivationPath = `44'/501'/0'/0'/${i}` + const derivationPath = getSolanaDerivationPath(i) // Simple direct call to get Solana address using fresh instance const result = await freshSolanaApp.getAddress(derivationPath, false) @@ -374,10 +392,6 @@ export class LedgerService { startIndex: number, _count: number ): Promise { - if (!this.transport) { - throw new Error('Transport not initialized') - } - try { // Use the SDK function directly (like the extension does) const publicKey = await getSolanaPublicKeyFromLedger( @@ -388,7 +402,7 @@ export class LedgerService { const publicKeys: PublicKeyInfo[] = [ { key: publicKey.toString('hex'), - derivationPath: `44'/501'/0'/0'/${startIndex}`, + derivationPath: getSolanaDerivationPath(startIndex), curve: 'ed25519' } ] @@ -441,10 +455,6 @@ export class LedgerService { startIndex: number, count: number ): Promise { - if (!this.transport) { - throw new Error('Transport not initialized') - } - // Connect to Avalanche app await this.waitForApp(LedgerAppType.AVALANCHE) @@ -515,10 +525,6 @@ export class LedgerService { startIndex: number, count: number ): Promise { - if (!this.transport) { - throw new Error('Transport not initialized') - } - // Connect to Avalanche app await this.waitForApp(LedgerAppType.AVALANCHE) @@ -634,31 +640,28 @@ export class LedgerService { // Disconnect from Ledger device async disconnect(): Promise { - if (this.transport) { - await this.transport.close() - this.transport = null + if (this.#transport) { + await this.#transport.close() + this.#transport = null this.currentAppType = LedgerAppType.UNKNOWN this.stopAppPolling() // Stop polling on disconnect } } - // Get current transport (for wallet usage) - getTransport(): TransportBLE { - if (!this.transport) { - throw new Error('Transport not initialized. Call connect() first.') - } - return this.transport - } - // Check if transport is available and connected isConnected(): boolean { - return this.transport !== null && this.transport.isConnected + return this.#transport !== null && this.#transport.isConnected } // Ensure connection is established for a specific device async ensureConnection(deviceId: string): Promise { await this.reconnectIfNeeded(deviceId) - return this.getTransport() + return this.transport + } + + // Get the current transport (for compatibility with existing code) + async getTransport(): Promise { + return this.transport } } From 6ef7f0c0d1a774a2ad25cdb6f8383ef49c726510 Mon Sep 17 00:00:00 2001 From: James Boyer Date: Thu, 6 Nov 2025 14:14:45 -0500 Subject: [PATCH 24/24] fixed imports --- .../new/features/ledger/contexts/LedgerSetupContext.tsx | 8 ++++---- .../app/new/features/ledger/hooks/useLedgerWallet.ts | 3 --- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/core-mobile/app/new/features/ledger/contexts/LedgerSetupContext.tsx b/packages/core-mobile/app/new/features/ledger/contexts/LedgerSetupContext.tsx index 83f515a3b5..2aa126805e 100644 --- a/packages/core-mobile/app/new/features/ledger/contexts/LedgerSetupContext.tsx +++ b/packages/core-mobile/app/new/features/ledger/contexts/LedgerSetupContext.tsx @@ -6,11 +6,11 @@ import React, { useMemo, ReactNode } from 'react' -import { LedgerDerivationPathType } from 'services/ledger/types' import { - WalletCreationOptions, - useLedgerWallet -} from '../hooks/useLedgerWallet' + LedgerDerivationPathType, + WalletCreationOptions +} from 'services/ledger/types' +import { useLedgerWallet } from '../hooks/useLedgerWallet' interface LedgerSetupState { selectedDerivationPath: LedgerDerivationPathType | null diff --git a/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts b/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts index 13e9b08ae2..6c3891f8fe 100644 --- a/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts +++ b/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts @@ -27,9 +27,6 @@ import { LedgerKeys } from 'services/ledger/types' -// Re-export types for consumers -export type { WalletCreationOptions, SetupProgress, LedgerDevice, LedgerKeys } - export interface UseLedgerWalletReturn { // Connection state devices: LedgerDevice[]