diff --git a/packages/core-mobile/app/new/common/components/ProgressDots.tsx b/packages/core-mobile/app/new/common/components/ProgressDots.tsx new file mode 100644 index 0000000000..19103ce1b0 --- /dev/null +++ b/packages/core-mobile/app/new/common/components/ProgressDots.tsx @@ -0,0 +1,84 @@ +import React from 'react' +import { View } from 'react-native' +import { useTheme } from '@avalabs/k2-alpine' +import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated' + +interface ProgressDotsProps { + /** Total number of steps */ + totalSteps: number + /** Current active step (0-indexed) */ + currentStep: number + /** Size of each dot */ + dotSize?: number + /** Gap between dots */ + gap?: number + /** Test ID for testing */ + testID?: string +} + +interface DotProps { + isActive: boolean + dotSize: number + colors: { + $textPrimary: string + } +} + +const AnimatedDot: React.FC = ({ isActive, dotSize, colors }) => { + const animatedStyle = useAnimatedStyle(() => { + return { + width: withTiming(isActive ? dotSize * 2 : dotSize, { + duration: 200 + }), + opacity: withTiming(isActive ? 1 : 0.4, { + duration: 200 + }) + } + }) + + return ( + + ) +} + +export const ProgressDots: React.FC = ({ + totalSteps, + currentStep, + dotSize = 6, + gap = 6, + testID +}) => { + const { + theme: { colors } + } = useTheme() + + return ( + + {Array.from({ length: totalSteps }).map((_, index) => ( + + ))} + + ) +} diff --git a/packages/core-mobile/app/new/common/components/ScrollScreen.tsx b/packages/core-mobile/app/new/common/components/ScrollScreen.tsx index c9e96742ce..365f573888 100644 --- a/packages/core-mobile/app/new/common/components/ScrollScreen.tsx +++ b/packages/core-mobile/app/new/common/components/ScrollScreen.tsx @@ -10,10 +10,17 @@ import React, { useCallback, useEffect, useLayoutEffect, + useMemo, useRef, useState } from 'react' -import { LayoutRectangle, StyleProp, View, ViewStyle } from 'react-native' +import { + LayoutRectangle, + StyleProp, + View, + ViewStyle, + Platform +} from 'react-native' import { ScrollView } from 'react-native-gesture-handler' import { KeyboardAwareScrollView, @@ -66,6 +73,8 @@ interface ScrollScreenProps extends KeyboardAwareScrollViewProps { disableStickyFooter?: boolean /** Title to be displayed in the navigation header */ navigationTitle?: string + /** Custom component to render in the navigation header title area (replaces navigationTitle) */ + renderNavigationHeader?: () => React.ReactNode /** Custom header component to be rendered */ renderHeader?: () => React.ReactNode /** Custom footer component to be rendered at the bottom of the screen */ @@ -80,6 +89,8 @@ interface ScrollScreenProps extends KeyboardAwareScrollViewProps { headerStyle?: StyleProp /** Whether this screen should hide the header background */ hideHeaderBackground?: boolean + /** Custom component to render centered in the header area, vertically level with back arrow */ + renderHeaderCenterComponent?: () => React.ReactNode /** TestID for the screen */ testID?: string } @@ -96,10 +107,12 @@ export const ScrollScreen = ({ hasParent, isModal, navigationTitle, + renderNavigationHeader, shouldAvoidKeyboard, disableStickyFooter, showNavigationHeaderTitle = true, hideHeaderBackground, + renderHeaderCenterComponent, headerStyle, testID, renderHeader, @@ -118,9 +131,16 @@ export const ScrollScreen = ({ const footerHeight = useSharedValue(0) const footerRef = useRef(null) + const navigationHeader = useMemo(() => { + if (renderNavigationHeader) { + return renderNavigationHeader() + } + return + }, [renderNavigationHeader, navigationTitle, title]) + const { onScroll, scrollY, targetHiddenProgress } = useFadingHeaderNavigation( { - header: , + header: navigationHeader, targetLayout: headerLayout, shouldHeaderHaveGrabber: isModal, hasParent, @@ -173,7 +193,8 @@ export const ScrollScreen = ({ style={[ headerStyle, { - gap: 8 + gap: 8, + paddingHorizontal: 16 } ]}> {title ? ( @@ -312,6 +333,40 @@ export const ScrollScreen = ({ ) }, [headerHeight, animatedBorderStyle]) + const renderHeaderCenterOverlay = useCallback(() => { + if (!renderHeaderCenterComponent) { + return null + } + + // try insetsbottom + 16 for android + // be sure to check 3 button bottom and manual test on android with native buttons on the bottom of the screen + const paddingValue = + Platform.OS === 'ios' + ? isModal + ? 15 // iOS modal - positive padding to move dots down + : 10 // iOS regular - positive padding + : isModal + ? 50 // Android modal + : 45 // Android regular + + return ( + + {renderHeaderCenterComponent()} + + ) + }, [renderHeaderCenterComponent, headerHeight, isModal]) + // 90% of our screens reuse this component but only some need keyboard avoiding // If you have an input on the screen, you need to enable this prop if (shouldAvoidKeyboard) { @@ -345,6 +400,7 @@ export const ScrollScreen = ({ {renderFooterContent()} {renderHeaderBackground()} + {renderHeaderCenterOverlay()} ) } @@ -374,6 +430,7 @@ export const ScrollScreen = ({ {renderFooterContent()} {renderHeaderBackground()} + {renderHeaderCenterOverlay()} ) } diff --git a/packages/core-mobile/app/new/features/accountSettings/components/accountAddresses.tsx b/packages/core-mobile/app/new/features/accountSettings/components/accountAddresses.tsx index d6b0c249c6..681e955ac5 100644 --- a/packages/core-mobile/app/new/features/accountSettings/components/accountAddresses.tsx +++ b/packages/core-mobile/app/new/features/accountSettings/components/accountAddresses.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react' +import React, { useMemo, useCallback } from 'react' import { GroupList, useTheme, @@ -26,59 +26,64 @@ export const AccountAddresses = ({ } = useTheme() const { networks } = useCombinedPrimaryNetworks() - const onCopyAddress = (value: string, message: string): void => { + const onCopyAddress = useCallback((value: string, message: string): void => { copyToClipboard(value, message) - } + }, []) const data = useMemo(() => { - return networks.map(network => { - const address = (() => { - switch (network.vmName) { - case NetworkVMType.AVM: - case NetworkVMType.PVM: - return account.addressPVM.replace(/^[XP]-/, '') - case NetworkVMType.BITCOIN: - return account.addressBTC - case NetworkVMType.EVM: - return account.addressC - case NetworkVMType.SVM: - return account.addressSVM - default: - return undefined + return networks + .map(network => { + const address = (() => { + switch (network.vmName) { + case NetworkVMType.AVM: + case NetworkVMType.PVM: + return account.addressPVM.replace(/^[XP]-/, '') + case NetworkVMType.BITCOIN: + return account.addressBTC + case NetworkVMType.EVM: + return account.addressC + case NetworkVMType.SVM: + return account.addressSVM + default: + return undefined + } + })() + + // Only return data if we have a valid address + if (!address) { + return null } - })() - return { - subtitle: address - ? truncateAddress(address, TRUNCATE_ADDRESS_LENGTH) - : '', - title: network.chainName, - leftIcon: ( - - ), - value: ( - - address && - onCopyAddress(address, `${network.chainName} address copied`) - } - /> - ) - } - }) + return { + subtitle: truncateAddress(address, TRUNCATE_ADDRESS_LENGTH), + title: network.chainName, + leftIcon: ( + + ), + value: ( + + onCopyAddress(address, `${network.chainName} address copied`) + } + /> + ) + } + }) + .filter((item): item is NonNullable => item !== null) // Type-safe filter }, [ account.addressBTC, account.addressC, account.addressPVM, account.addressSVM, colors.$surfaceSecondary, - networks + networks, + onCopyAddress ]) return ( 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 f532dd8780..5ad1e70888 100644 --- a/packages/core-mobile/app/new/features/ledger/components/AnimatedIconWithText.tsx +++ b/packages/core-mobile/app/new/features/ledger/components/AnimatedIconWithText.tsx @@ -52,20 +52,14 @@ export const AnimatedIconWithText: React.FC = ({ 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 ( @@ -92,33 +86,24 @@ export const AnimatedIconWithText: React.FC = ({ )} {icon} - - - {title} - - - {subtitle} - - + {title} + + + {subtitle} + ) } diff --git a/packages/core-mobile/app/new/features/ledger/components/LedgerAppConnection.tsx b/packages/core-mobile/app/new/features/ledger/components/LedgerAppConnection.tsx index c97216a58f..382d5abbf3 100644 --- a/packages/core-mobile/app/new/features/ledger/components/LedgerAppConnection.tsx +++ b/packages/core-mobile/app/new/features/ledger/components/LedgerAppConnection.tsx @@ -1,38 +1,84 @@ -import React, { useState, useCallback, useEffect } from 'react' -import { View } from 'react-native' -import { Text, Button, useTheme, Icons } from '@avalabs/k2-alpine' -import { ScrollScreen } from 'common/components/ScrollScreen' +import React, { useState, useCallback, useEffect, useMemo } from 'react' +import { View, Alert, ActivityIndicator } from 'react-native' +import { Text, Button, useTheme, Icons, GroupList } from '@avalabs/k2-alpine' import { LoadingState } from 'common/components/LoadingState' import { LedgerDerivationPathType } from 'services/ledger/types' +import { showSnackbar } from 'common/utils/toast' +import { truncateAddress } from '@avalabs/core-utils-sdk' +import { TRUNCATE_ADDRESS_LENGTH } from 'common/consts/text' +import { NetworkLogoWithChain } from 'common/components/NetworkLogoWithChain' +import { isXPChain } from 'utils/network/isAvalancheNetwork' +import bs58 from 'bs58' +import { + AVALANCHE_MAINNET_NETWORK, + NETWORK_SOLANA +} from 'services/network/consts' +import { BITCOIN_NETWORK, AVALANCHE_XP_NETWORK } from '@avalabs/core-chains-sdk' +import { ChainName } from 'services/network/consts' +import LedgerService from 'services/ledger/LedgerService' +import Logger from 'utils/Logger' +import { LedgerDeviceList } from './LedgerDeviceList' +import { AnimatedIconWithText } from './AnimatedIconWithText' enum AppConnectionStep { AVALANCHE_CONNECT = 'avalanche-connect', AVALANCHE_LOADING = 'avalanche-loading', - AVALANCHE_SUCCESS = 'avalanche-success', SOLANA_CONNECT = 'solana-connect', SOLANA_LOADING = 'solana-loading', - SOLANA_SUCCESS = 'solana-success', COMPLETE = 'complete' } +interface StepConfig { + icon: React.ReactNode + title: string + subtitle: string + primaryButton?: { + text: string + onPress: () => void + } + secondaryButton?: { + text: string + onPress: () => void + } + showAnimation?: boolean + isLoading?: boolean +} + interface LedgerAppConnectionProps { - onComplete: () => void + onComplete: (keys: LocalKeyState) => void onCancel: () => void - getSolanaKeys: () => Promise - getAvalancheKeys: () => Promise deviceName: string selectedDerivationPath: LedgerDerivationPathType | null isCreatingWallet?: boolean + connectedDeviceId?: string | null + connectedDeviceName?: string + onStepChange?: (step: number) => void +} + +interface LocalKeyState { + solanaKeys: Array<{ + key: string + derivationPath: string + curve: string + }> + avalancheKeys: { + evm: string + avalanche: string + pvm: string + } | null + bitcoinAddress: string + xpAddress: string } export const LedgerAppConnection: React.FC = ({ onComplete, onCancel, - getSolanaKeys, - getAvalancheKeys, deviceName, - selectedDerivationPath, - isCreatingWallet = false + selectedDerivationPath: _selectedDerivationPath, + isCreatingWallet = false, + connectedDeviceId, + connectedDeviceName, + onStepChange }) => { const { theme: { colors } @@ -41,389 +87,485 @@ export const LedgerAppConnection: React.FC = ({ const [currentStep, setCurrentStep] = useState( AppConnectionStep.AVALANCHE_CONNECT ) - const [error, setError] = useState(null) + + // Local key state - managed only in this component + const [keys, setKeys] = useState({ + solanaKeys: [], + avalancheKeys: null, + bitcoinAddress: '', + xpAddress: '' + }) // Auto-progress through steps useEffect(() => { - let timeoutId: ReturnType + if (currentStep === AppConnectionStep.COMPLETE && !isCreatingWallet) { + const timeoutId = setTimeout(() => { + onComplete(keys) + }, 1500) - switch (currentStep) { - case AppConnectionStep.AVALANCHE_SUCCESS: - timeoutId = setTimeout(() => { - setCurrentStep(AppConnectionStep.SOLANA_CONNECT) - }, 2000) - break - case AppConnectionStep.SOLANA_SUCCESS: - timeoutId = setTimeout(() => { - setCurrentStep(AppConnectionStep.COMPLETE) - }, 2000) - break - case AppConnectionStep.COMPLETE: - // Don't auto-navigate if wallet is being created - if (!isCreatingWallet) { - timeoutId = setTimeout(() => { - onComplete() - }, 1500) - } - break - } - - return () => { - if (timeoutId) { + return () => { clearTimeout(timeoutId) } } - }, [currentStep, onComplete, isCreatingWallet]) + }, [currentStep, onComplete, isCreatingWallet, keys]) const handleConnectAvalanche = useCallback(async () => { try { - setError(null) setCurrentStep(AppConnectionStep.AVALANCHE_LOADING) - await getAvalancheKeys() - setCurrentStep(AppConnectionStep.AVALANCHE_SUCCESS) + // Get keys from service + const avalancheKeys = await LedgerService.getAvalancheKeys() + const { bitcoinAddress, xpAddress } = + await LedgerService.getBitcoinAndXPAddresses() + + // Update local state + setKeys(prev => ({ + ...prev, + avalancheKeys, + bitcoinAddress, + xpAddress + })) + + // Show success toast notification + showSnackbar('Avalanche app connected') + // if get avalanche keys succeeds move forward to solana connect + setCurrentStep(AppConnectionStep.SOLANA_CONNECT) } catch (err) { - setError( - 'Failed to connect to Avalanche app. Please make sure the Avalanche app is open on your Ledger.' - ) + Logger.error('Failed to connect to Avalanche app', err) setCurrentStep(AppConnectionStep.AVALANCHE_CONNECT) + Alert.alert( + 'Connection Failed', + 'Failed to connect to Avalanche app. Please make sure the Avalanche app is open on your Ledger.', + [{ text: 'OK' }] + ) } - }, [getAvalancheKeys]) + }, []) const handleConnectSolana = useCallback(async () => { try { - setError(null) setCurrentStep(AppConnectionStep.SOLANA_LOADING) - await getSolanaKeys() - setCurrentStep(AppConnectionStep.SOLANA_SUCCESS) + // Get keys from service + const solanaKeys = await LedgerService.getSolanaKeys() + + // Update local state + setKeys(prev => ({ + ...prev, + solanaKeys + })) + + // Show success toast notification + showSnackbar('Solana app connected') + + // Skip success step and go directly to complete + setCurrentStep(AppConnectionStep.COMPLETE) } catch (err) { - setError( - 'Failed to connect to Solana app. Please make sure the Solana app is open on your Ledger.' - ) + Logger.error('Failed to connect to Solana app', err) setCurrentStep(AppConnectionStep.SOLANA_CONNECT) + Alert.alert( + 'Connection Failed', + 'Failed to connect to Solana app. Please make sure the Solana app is installed and open on your Ledger.', + [{ text: 'OK' }] + ) } - }, [getSolanaKeys]) + }, []) - const renderStepContent = (): React.ReactNode => { - switch (currentStep) { + const handleSkipSolana = useCallback(() => { + // Skip Solana and proceed to complete step + setCurrentStep(AppConnectionStep.COMPLETE) + }, []) + + // Generate address list data for the complete step + const addressListData = useMemo(() => { + const addresses = [] + + // C-Chain/EVM address (derived from avalanche keys) + if (keys.avalancheKeys?.evm) { + addresses.push({ + title: AVALANCHE_MAINNET_NETWORK.chainName, + subtitle: truncateAddress( + keys.avalancheKeys.evm, + TRUNCATE_ADDRESS_LENGTH + ), + value: ( + + ), + leftIcon: ( + + ) + }) + } + + // X/P Chain address + if (keys.xpAddress) { + const xpNetwork = { + ...AVALANCHE_XP_NETWORK, + chainName: ChainName.AVALANCHE_XP + } + addresses.push({ + title: xpNetwork.chainName, + subtitle: truncateAddress( + keys.xpAddress.replace(/^[XP]-/, ''), + TRUNCATE_ADDRESS_LENGTH + ), + value: ( + + ), + leftIcon: ( + + ) + }) + } + + // Bitcoin address + if (keys.bitcoinAddress) { + const bitcoinNetwork = { + ...BITCOIN_NETWORK, + chainName: ChainName.BITCOIN + } + addresses.push({ + title: bitcoinNetwork.chainName, + subtitle: truncateAddress(keys.bitcoinAddress, TRUNCATE_ADDRESS_LENGTH), + value: ( + + ), + leftIcon: ( + + ) + }) + } + + // Solana address + if (keys.solanaKeys.length > 0 && keys.solanaKeys[0]?.key) { + // Convert public key to Solana address (Base58 encoding) + const solanaAddress = bs58.encode( + Uint8Array.from(Buffer.from(keys.solanaKeys[0].key, 'hex')) + ) + + addresses.push({ + title: NETWORK_SOLANA.chainName, + subtitle: truncateAddress(solanaAddress, TRUNCATE_ADDRESS_LENGTH), + value: ( + + ), + leftIcon: ( + + ) + }) + } + + // Always add the "Storing wallet data" row at the end + addresses.push({ + title: 'Storing wallet data', + value: + }) + + return addresses + }, [keys, colors]) + + // Step configurations + const getStepConfig = (step: AppConnectionStep): StepConfig | null => { + switch (step) { case AppConnectionStep.AVALANCHE_CONNECT: - return ( - - - - Connect to Avalanche App - - - Open the Avalanche app on your {deviceName} and press continue - when ready. - + return { + icon: ( + + ), + title: 'Connect to Avalanche App', + subtitle: `Open the Avalanche app on your ${deviceName}, then press Continue when ready.`, + primaryButton: { + text: 'Continue', + onPress: handleConnectAvalanche + }, + showAnimation: false + } - {error && ( - + ), + title: 'Connecting to Avalanche app', + subtitle: `Please keep your Avalanche app open on your ${deviceName}, We're retrieving your Avalanche addresses...`, + showAnimation: true, + isLoading: true + } + + case AppConnectionStep.SOLANA_CONNECT: + return { + icon: ( + + ), + title: 'Connect to Solana App', + subtitle: `Close the Avalanche app and open the Solana app on your ${deviceName}, then press Continue when ready.`, + primaryButton: { + text: 'Continue', + onPress: handleConnectSolana + }, + secondaryButton: { + text: 'Skip Solana', + onPress: handleSkipSolana + }, + showAnimation: false + } + + case AppConnectionStep.SOLANA_LOADING: + return { + icon: ( + + ), + title: 'Connecting to Solana', + subtitle: `Please keep your Solana app open on your ${deviceName}, We're retrieving your Solana address...`, + showAnimation: true, + isLoading: true + } + + default: + return null + } + } + + const renderStepContent = (): React.ReactNode => { + // Handle COMPLETE step separately as it has unique layout + if (currentStep === AppConnectionStep.COMPLETE) { + return ( + + {/* Header with refresh icon and title */} + + + + + + {`Your Ledger wallet \nis being set up`} + + + - - {error} - - - )} + {`The BIP44 setup is in progress and should \n take about 15 seconds. Keep your device \n connected during setup.`} + + + - + + + + - ) + + ) + } - case AppConnectionStep.AVALANCHE_LOADING: - return ( - - - - Connecting to Avalanche - - - Please confirm the connection on your {deviceName}. We're - retrieving your Avalanche addresses... - - - ) + // Use template for all other steps + const config = getStepConfig(currentStep) + if (!config) { + return null + } - case AppConnectionStep.AVALANCHE_SUCCESS: - return ( - - - - - - Avalanche Connected! - - - Successfully retrieved your Avalanche addresses. Now let's connect - to Solana... - + // Render step using inline template logic + if (config.isLoading) { + return ( + + + - ) - - case AppConnectionStep.SOLANA_CONNECT: - return ( - - - - Connect to Solana App - - - Close the Avalanche app and open the Solana app on your{' '} - {deviceName}, then press continue. - - {error && ( + + - - {error} - + - )} - - - + {config.secondaryButton && ( + + )} + - ) + + ) + } - case AppConnectionStep.SOLANA_LOADING: - return ( - - - - Connecting to Solana - + // Non-loading step + return ( + + + + {config.icon} - Please confirm the connection on your {deviceName}. We're - retrieving your Solana addresses... - - - ) - - case AppConnectionStep.SOLANA_SUCCESS: - return ( - - - - - - Solana Connected! + {config.title} - Successfully retrieved your Solana addresses. Setting up your - wallet... + {config.subtitle} - ) + - case AppConnectionStep.COMPLETE: - return ( - - {isCreatingWallet ? ( - <> - - - Creating Wallet... - - - Setting up your Ledger wallet. This may take a moment... - - - ) : ( - <> - - - - - Apps Connected! - - - Both Avalanche and Solana apps are connected. Proceeding to - wallet setup... - - + + + {config.primaryButton && ( + )} - - ) - default: - return null - } + {config.secondaryButton && ( + + )} + + + + ) } - const getProgressText = (): string => { - const pathType = - selectedDerivationPath === LedgerDerivationPathType.LedgerLive - ? 'Ledger Live' - : 'BIP44' - + const progressDotsCurrentStep = useMemo(() => { switch (currentStep) { case AppConnectionStep.AVALANCHE_CONNECT: case AppConnectionStep.AVALANCHE_LOADING: - case AppConnectionStep.AVALANCHE_SUCCESS: - return `Step 1 of 2: Avalanche (${pathType})` + return 0 + case AppConnectionStep.SOLANA_CONNECT: case AppConnectionStep.SOLANA_LOADING: - case AppConnectionStep.SOLANA_SUCCESS: - return `Step 2 of 2: Solana (${pathType})` + return 1 + case AppConnectionStep.COMPLETE: - return `Complete (${pathType})` + return 2 + default: - return '' + return 0 } - } + }, [currentStep]) + + // Notify parent of step changes + useEffect(() => { + onStepChange?.(progressDotsCurrentStep) + }, [progressDotsCurrentStep, onStepChange]) + + // Create device object for display + const connectedDevice = connectedDeviceId + ? [{ id: connectedDeviceId, name: connectedDeviceName || deviceName }] + : [] return ( - - + + {/* Show connected device */} + {connectedDevice.length > 0 && ( + + + + )} + + {renderStepContent()} - + ) } diff --git a/packages/core-mobile/app/new/features/ledger/components/LedgerDeviceList.tsx b/packages/core-mobile/app/new/features/ledger/components/LedgerDeviceList.tsx new file mode 100644 index 0000000000..35afef67f8 --- /dev/null +++ b/packages/core-mobile/app/new/features/ledger/components/LedgerDeviceList.tsx @@ -0,0 +1,103 @@ +import React, { useMemo } from 'react' +import { View } from 'react-native' +import { + Text, + Button, + useTheme, + GroupList, + Icons, + GroupListItem +} from '@avalabs/k2-alpine' + +interface LedgerDevice { + id: string + name: string +} + +interface LedgerDeviceListProps { + devices: LedgerDevice[] + onDevicePress?: (deviceId: string, deviceName: string) => void + isConnecting?: boolean + itemHeight?: number + subtitleText?: string + buttonText?: string + buttonLoadingText?: string + testID?: string +} + +export const LedgerDeviceList: React.FC = ({ + devices, + onDevicePress, + isConnecting = false, + itemHeight = 56, + subtitleText = 'Found over Bluetooth', + buttonText = 'Connect', + buttonLoadingText = 'Connecting...', + testID +}) => { + const { + theme: { colors } + } = useTheme() + + const deviceListData: GroupListItem[] = useMemo( + () => + devices.map((device: LedgerDevice) => ({ + title: device.name || 'Ledger Device', + subtitle: ( + + {subtitleText} + + ), + leftIcon: ( + + + + ), + ...(onDevicePress && { + accessory: ( + + ), + onPress: () => onDevicePress(device.id, device.name) + }) + })), + [ + devices, + colors.$textSecondary, + colors.$surfaceSecondary, + colors.$textPrimary, + onDevicePress, + isConnecting, + buttonText, + buttonLoadingText, + subtitleText + ] + ) + + if (devices.length === 0) { + return null + } + + return ( + + ) +} diff --git a/packages/core-mobile/app/new/features/ledger/consts.ts b/packages/core-mobile/app/new/features/ledger/consts.ts index 0534ace65f..b9d039c9f3 100644 --- a/packages/core-mobile/app/new/features/ledger/consts.ts +++ b/packages/core-mobile/app/new/features/ledger/consts.ts @@ -5,7 +5,7 @@ export const DERIVATION_PATHS = { EVM: "m/44'/60'/0'/0/0", AVALANCHE: "m/44'/9000'/0'/0/0", PVM: "m/44'/9000'/0'/0/0", // Same as Avalanche for now - SOLANA: "m/44'/501'/0'/0'", + SOLANA: "m/44'/501'/0'/0/0", BITCOIN: "m/44'/0'/0'/0/0" }, @@ -14,7 +14,7 @@ export const DERIVATION_PATHS = { EVM: (accountIndex: number) => `m/44'/60'/${accountIndex}'/0/0`, AVALANCHE: (accountIndex: number) => `m/44'/9000'/${accountIndex}'/0/0`, PVM: (accountIndex: number) => `m/44'/9000'/${accountIndex}'/0/0`, - SOLANA: (accountIndex: number) => `m/44'/501'/${accountIndex}'/0'`, + SOLANA: (accountIndex: number) => `m/44'/501'/${accountIndex}'/0/0`, BITCOIN: (accountIndex: number) => `m/44'/0'/${accountIndex}'/0/0` }, @@ -25,11 +25,8 @@ export const DERIVATION_PATHS = { } } as const -// Raw derivation paths for Solana (without m/ prefix) -export const SOLANA_DERIVATION_PATH = "44'/501'/0'/0'/0" - // Solana derivation path prefix for generating indexed paths -export const SOLANA_DERIVATION_PATH_PREFIX = "44'/501'/0'/0'" +export const SOLANA_DERIVATION_PATH_PREFIX = "44'/501'/0'/0" // Deprecated Avalanche public key path prefix export const DEPRECATED_AVALANCHE_DERIVATION_PATH_PREFIX = "m/44'/9000'/0'" 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 4fafde6678..a83e953339 100644 --- a/packages/core-mobile/app/new/features/ledger/contexts/LedgerSetupContext.tsx +++ b/packages/core-mobile/app/new/features/ledger/contexts/LedgerSetupContext.tsx @@ -9,14 +9,9 @@ import React, { import { LedgerDerivationPathType, WalletCreationOptions, - LedgerDevice, - SetupProgress, LedgerTransportState } from 'services/ledger/types' -import { - useLedgerWallet, - UseLedgerWalletReturn -} from '../hooks/useLedgerWallet' +import { useLedgerWallet } from '../hooks/useLedgerWallet' interface LedgerSetupContextValue { // State values @@ -33,18 +28,16 @@ interface LedgerSetupContextValue { setHasStartedSetup: (started: boolean) => void // Ledger wallet hook values - devices: LedgerDevice[] - isScanning: boolean isConnecting: boolean transportState: LedgerTransportState - scanForDevices: () => void connectToDevice: (deviceId: string) => Promise disconnectDevice: () => Promise - getSolanaKeys: () => Promise - getAvalancheKeys: () => Promise - createLedgerWallet: (options: WalletCreationOptions) => Promise - setupProgress: SetupProgress | null - keys: UseLedgerWalletReturn['keys'] + createLedgerWallet: ( + options: WalletCreationOptions & { + avalancheKeys?: { evm: string; avalanche: string; pvm: string } + solanaKeys?: Array<{ key: string; derivationPath: string; curve: string }> + } + ) => Promise // Helper methods resetSetup: () => void @@ -71,18 +64,11 @@ export const LedgerSetupProvider: React.FC = ({ // Use the existing ledger wallet hook const { - devices, - isScanning, isConnecting, transportState, - scanForDevices, connectToDevice, disconnectDevice, - getSolanaKeys, - getAvalancheKeys, - createLedgerWallet, - setupProgress, - keys + createLedgerWallet } = useLedgerWallet() const handleSetConnectedDevice = useCallback( @@ -108,18 +94,11 @@ export const LedgerSetupProvider: React.FC = ({ connectedDeviceName, isCreatingWallet, hasStartedSetup, - devices, - isScanning, isConnecting, transportState, - scanForDevices, connectToDevice, disconnectDevice, - getSolanaKeys, - getAvalancheKeys, createLedgerWallet, - setupProgress, - keys, setSelectedDerivationPath, setConnectedDevice: handleSetConnectedDevice, setIsCreatingWallet, @@ -132,18 +111,11 @@ export const LedgerSetupProvider: React.FC = ({ connectedDeviceName, isCreatingWallet, hasStartedSetup, - devices, - isScanning, isConnecting, transportState, - scanForDevices, connectToDevice, disconnectDevice, - getSolanaKeys, - getAvalancheKeys, createLedgerWallet, - setupProgress, - keys, handleSetConnectedDevice, resetSetup ] 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 290f121180..dcbfccc19f 100644 --- a/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts +++ b/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts @@ -1,82 +1,41 @@ import { useState, useCallback, useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { Alert, Platform, PermissionsAndroid } from 'react-native' +import { Alert } from 'react-native' import TransportBLE from '@ledgerhq/react-native-hw-transport-ble' -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 { ChainName } from 'services/network/consts' +import { LedgerDerivationPathType } from 'services/ledger/types' 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 { Curve } from 'utils/publicKeys' import { - SetupProgress, WalletCreationOptions, - LedgerDevice, - LedgerTransportState, - LedgerKeys + LedgerTransportState } from 'services/ledger/types' +import AccountsService from 'services/account/AccountsService' +import { DERIVATION_PATHS } from '../consts' export interface UseLedgerWalletReturn { // Connection state - devices: LedgerDevice[] - isScanning: boolean isConnecting: boolean isLoading: boolean - setupProgress: SetupProgress | null transportState: LedgerTransportState - // Key states and methods - keys: { - solanaKeys: Array<{ - key: string - derivationPath: string - curve: Curve - }> - avalancheKeys: { - evm: string - avalanche: string - pvm: string - } | null - bitcoinAddress: string - xpAddress: string - } - // Methods - scanForDevices: () => Promise connectToDevice: (deviceId: string) => Promise disconnectDevice: () => Promise - getSolanaKeys: () => Promise - getAvalancheKeys: () => Promise - getLedgerLiveKeys: ( - accountCount?: number, - progressCallback?: ( - step: string, - progress: number, - totalSteps: number - ) => void - ) => Promise<{ - avalancheKeys: { evm: string; avalanche: string; pvm: string } | null - individualKeys: Array<{ key: string; derivationPath: string; curve: Curve }> - }> - resetKeys: () => void - createLedgerWallet: (options: WalletCreationOptions) => Promise + createLedgerWallet: ( + options: WalletCreationOptions & { + avalancheKeys?: { evm: string; avalanche: string; pvm: string } + solanaKeys?: Array<{ key: string; derivationPath: string; curve: string }> + } + ) => Promise } -import { - DERIVATION_PATHS, - SOLANA_DERIVATION_PATH, - LEDGER_TIMEOUTS -} from '../consts' export function useLedgerWallet(): UseLedgerWalletReturn { const dispatch = useDispatch() @@ -85,18 +44,8 @@ export function useLedgerWallet(): UseLedgerWalletReturn { available: false, powered: false }) - const [devices, setDevices] = useState([]) - const [isScanning, setIsScanning] = useState(false) const [isConnecting, setIsConnecting] = useState(false) const [isLoading, setIsLoading] = useState(false) - const [setupProgress, setSetupProgress] = useState(null) - - // Key states - const [solanaKeys, setSolanaKeys] = useState([]) - const [avalancheKeys, setAvalancheKeys] = - useState(null) - const [bitcoinAddress, setBitcoinAddress] = useState('') - const [xpAddress, setXpAddress] = useState('') // Monitor BLE transport state useEffect(() => { @@ -122,111 +71,6 @@ export function useLedgerWallet(): UseLedgerWalletReturn { } }, []) - // 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) - - const granted = await PermissionsAndroid.requestMultiple(permissions) - return Object.values(granted).every( - permission => permission === 'granted' - ) - } catch (err) { - return false - } - } - return true - }, []) - - // Handle scan errors - const handleScanError = useCallback((error: Error) => { - setIsScanning(false) - - 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: () => { - // Handle settings navigation 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 - } - - const hasPermissions = await requestBluetoothPermissions() - if (!hasPermissions) { - Alert.alert( - 'Permission Required', - 'Bluetooth permissions are required to scan for Ledger devices.' - ) - return - } - - setIsScanning(true) - setDevices([]) - - try { - const subscription = TransportBLE.listen({ - next: (event: { - type: string - descriptor: { id: string; name?: string; rssi?: number } - }) => { - if (event.type === 'add') { - const device: LedgerDevice = { - id: event.descriptor.id, - name: event.descriptor.name || 'Unknown Device', - rssi: event.descriptor.rssi - } - - setDevices(prev => { - const exists = prev.find(d => d.id === device.id) - if (!exists) { - return [...prev, device] - } - return prev - }) - } - }, - error: handleScanError, - // eslint-disable-next-line @typescript-eslint/no-empty-function - complete: () => {} - }) - - setTimeout(() => { - subscription.unsubscribe() - setIsScanning(false) - }, LEDGER_TIMEOUTS.SCAN_TIMEOUT) - } catch (error) { - handleScanError(error as Error) - } - }, [transportState.available, requestBluetoothPermissions, handleScanError]) - // Connect to device const connectToDevice = useCallback(async (deviceId: string) => { setIsConnecting(true) @@ -252,234 +96,21 @@ export function useLedgerWallet(): UseLedgerWalletReturn { } }, []) - // Get Solana keys - const getSolanaKeys = useCallback(async () => { - // Prevent multiple simultaneous calls - if (isLoading) { - Logger.info('Solana key retrieval already in progress, skipping') - return - } - - try { - setIsLoading(true) - Logger.info('Getting Solana keys with passive app detection') - - await LedgerService.waitForApp(LedgerAppType.SOLANA) - - // Get address directly from Solana app - const transport = await LedgerService.getTransport() - const solanaApp = new AppSolana(transport as Transport) - const derivationPath = SOLANA_DERIVATION_PATH - const result = await solanaApp.getAddress(derivationPath, false) - - // Convert the Buffer to base58 format (Solana address format) - const solanaAddress = bs58.encode(new Uint8Array(result.address)) - - setSolanaKeys([ - { - key: solanaAddress, - derivationPath, - curve: Curve.ED25519 - } - ]) - Logger.info('Successfully got Solana address', solanaAddress) - } catch (error) { - Logger.error('Failed to get Solana keys', error) - throw error - } finally { - setIsLoading(false) - } - }, [isLoading]) - - // Get Avalanche keys - const getAvalancheKeys = useCallback(async () => { - // Prevent multiple simultaneous calls - if (isLoading) { - Logger.info('Avalanche key retrieval already in progress, skipping') - return - } - - try { - setIsLoading(true) - Logger.info('Getting Avalanche keys') - - const addresses = await LedgerService.getAllAddresses(0, 1) - - const evmAddress = - addresses.find(addr => addr.network === ChainName.AVALANCHE_C_EVM) - ?.address || '' - const xChainAddress = - addresses.find(addr => addr.network === ChainName.AVALANCHE_X) - ?.address || '' - const pvmAddress = - addresses.find(addr => addr.network === ChainName.AVALANCHE_P) - ?.address || '' - const btcAddress = - addresses.find(addr => addr.network === ChainName.BITCOIN)?.address || - '' - - // Store the addresses directly from the device - setAvalancheKeys({ - evm: evmAddress, - avalanche: xChainAddress, - pvm: pvmAddress - }) - setBitcoinAddress(btcAddress) - setXpAddress(xChainAddress) - - Logger.info('Successfully got Avalanche keys') - } catch (error) { - Logger.error('Failed to get Avalanche keys', error) - throw error - } finally { - setIsLoading(false) - } - }, [isLoading]) - - const resetKeys = useCallback(() => { - setSolanaKeys([]) - setAvalancheKeys(null) - setBitcoinAddress('') - setXpAddress('') - }, []) - - // 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: Array<{ - key: string - derivationPath: string - curve: Curve - }> = [] - let avalancheKeysResult: LedgerKeys['avalancheKeys'] = 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 xChainAddr = addresses.find( - addr => addr.network === ChainName.AVALANCHE_X - ) - - if (evmPublicKey) { - individualKeys.push({ - key: evmPublicKey.key, - derivationPath: DERIVATION_PATHS.LEDGER_LIVE.EVM(accountIndex), - curve: evmPublicKey.curve as Curve - }) - } - - if (avmPublicKey) { - individualKeys.push({ - key: avmPublicKey.key, - derivationPath: - DERIVATION_PATHS.LEDGER_LIVE.AVALANCHE(accountIndex), - curve: avmPublicKey.curve as Curve - }) - } - - // Store first account's keys as primary - if (accountIndex === 0) { - avalancheKeysResult = { - evm: evmAddress?.address || '', - avalanche: xChainAddr?.address || '', - pvm: '' // Will be set when PVM addresses are implemented - } - } - } - - // 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) - } - }, - [] - ) - const createLedgerWallet = useCallback( async ({ deviceId, deviceName = 'Ledger Device', derivationPathType = LedgerDerivationPathType.BIP44, - individualKeys = [] - }: WalletCreationOptions) => { + individualKeys = [], + avalancheKeys, + solanaKeys = [] + }: WalletCreationOptions & { + avalancheKeys?: { evm: string; avalanche: string; pvm: string } + solanaKeys?: Array<{ key: string; derivationPath: string; curve: string }> + }) => { try { setIsLoading(true) - // Initialize progress tracking - const totalSteps = - derivationPathType === LedgerDerivationPathType.BIP44 ? 3 : 6 - let currentStep = 1 - - const updateProgress = (stepName: string): void => { - const progress = { - currentStep: stepName, - progress: Math.round((currentStep / totalSteps) * 100), - totalSteps, - estimatedTimeRemaining: - (totalSteps - currentStep) * - (derivationPathType === LedgerDerivationPathType.BIP44 ? 5 : 8) - } - setSetupProgress(progress) - currentStep++ - } - - updateProgress('Validating keys...') Logger.info( `Creating ${derivationPathType} Ledger wallet with generated keys...` ) @@ -487,18 +118,60 @@ export function useLedgerWallet(): UseLedgerWalletReturn { if (!avalancheKeys) { throw new Error('Missing Avalanche keys for wallet creation') } - if (solanaKeys.length === 0) { - throw new Error('Missing Solana keys for wallet creation') - } + // Solana keys are optional - wallet can be created with only Avalanche keys - updateProgress('Generating wallet ID...') const newWalletId = uuid() - updateProgress('Storing wallet data...') + // Fix key formatting - remove double 0x prefixes that cause VM module errors + const formattedAvalancheKeys = { + evm: avalancheKeys.evm?.startsWith('0x0x') + ? avalancheKeys.evm.slice(2) // Remove first 0x to fix double prefix + : avalancheKeys.evm, + avalanche: avalancheKeys.avalanche, + pvm: avalancheKeys.pvm || avalancheKeys.avalanche + } + + // Also fix the public keys array to ensure no double prefixes in storage + const formattedPublicKeys = individualKeys.map(key => ({ + ...key, + key: key.key?.startsWith('0x0x') ? key.key.slice(2) : key.key + })) + + // Create the public keys array for BIP44 + const publicKeysToStore = [ + // Use formatted keys for BIP44 + { + key: formattedAvalancheKeys.evm, // Use formatted key + derivationPath: DERIVATION_PATHS.BIP44.EVM, + curve: Curve.SECP256K1 + }, + { + key: formattedAvalancheKeys.avalanche, + derivationPath: DERIVATION_PATHS.BIP44.AVALANCHE, + curve: Curve.SECP256K1 + }, + { + key: formattedAvalancheKeys.pvm, + derivationPath: DERIVATION_PATHS.BIP44.PVM, + curve: Curve.SECP256K1 + }, + // Only include Solana key if it exists + ...(solanaKeys.length > 0 && solanaKeys[0]?.key + ? [ + { + key: solanaKeys[0].key, // Solana addresses don't use 0x prefix + derivationPath: solanaKeys[0].derivationPath, // Use the same path from getSolanaKeys + curve: Curve.ED25519 + } + ] + : []) + ] + // Store the Ledger wallet with the specified derivation path type await dispatch( storeWallet({ walletId: newWalletId, + walletName: `Ledger ${deviceName}`, walletSecret: JSON.stringify({ deviceId, deviceName, @@ -507,38 +180,16 @@ export function useLedgerWallet(): UseLedgerWalletReturn { derivationPathSpec: derivationPathType, ...(derivationPathType === LedgerDerivationPathType.BIP44 && { extendedPublicKeys: { - evm: avalancheKeys.evm, - avalanche: avalancheKeys.avalanche + evm: formattedAvalancheKeys.evm, // Use formatted key + avalanche: formattedAvalancheKeys.avalanche } }), publicKeys: derivationPathType === LedgerDerivationPathType.LedgerLive && individualKeys.length > 0 - ? individualKeys // Use individual keys for Ledger Live - : [ - // Use existing keys for BIP44 - { - key: avalancheKeys.evm, - derivationPath: DERIVATION_PATHS.BIP44.EVM, - curve: Curve.SECP256K1 - }, - { - key: avalancheKeys.avalanche, - derivationPath: DERIVATION_PATHS.BIP44.AVALANCHE, - curve: Curve.SECP256K1 - }, - { - key: avalancheKeys.pvm || avalancheKeys.avalanche, - derivationPath: DERIVATION_PATHS.BIP44.PVM, - curve: Curve.SECP256K1 - }, - { - key: solanaKeys[0]?.key || '', - derivationPath: DERIVATION_PATHS.BIP44.SOLANA, - curve: Curve.ED25519 - } - ], - avalancheKeys, + ? formattedPublicKeys // Use formatted individual keys for Ledger Live + : publicKeysToStore, // Use the public keys we just created + avalancheKeys: formattedAvalancheKeys, // Use formatted keys solanaKeys }), type: @@ -550,33 +201,19 @@ export function useLedgerWallet(): UseLedgerWalletReturn { dispatch(setActiveWallet(newWalletId)) - // Create addresses from the keys - const addresses = { - EVM: avalancheKeys.evm, - AVM: avalancheKeys.avalanche, - PVM: avalancheKeys.pvm || avalancheKeys.avalanche, - BITCOIN: bitcoinAddress, - SVM: solanaKeys[0]?.key || '', - CoreEth: '' - } - - const newAccountId = uuid() - const newAccount: Account = { - id: newAccountId, - walletId: newWalletId, - name: `Account ${Object.keys(allAccounts).length + 1}`, - type: CoreAccountType.PRIMARY, + const newAccount = await AccountsService.createNextAccount({ index: 0, - addressC: addresses.EVM, - addressBTC: addresses.BITCOIN, - addressAVM: addresses.AVM, - addressPVM: addresses.PVM, - addressSVM: addresses.SVM, - addressCoreEth: addresses.CoreEth - } + walletType: + derivationPathType === LedgerDerivationPathType.BIP44 + ? WalletType.LEDGER + : WalletType.LEDGER_LIVE, + isTestnet: false, // TODO: Get from settings + walletId: newWalletId, + name: `Account ${Object.keys(allAccounts).length + 1}` + }) dispatch(setAccount(newAccount)) - dispatch(setActiveAccount(newAccountId)) + dispatch(setActiveAccount(newAccount.id)) Logger.info('Ledger wallet created successfully:', newWalletId) showSnackbar('Ledger wallet created successfully!') @@ -586,32 +223,17 @@ export function useLedgerWallet(): UseLedgerWalletReturn { throw error } finally { setIsLoading(false) - setSetupProgress(null) } }, - [avalancheKeys, solanaKeys, bitcoinAddress, dispatch, allAccounts] + [dispatch, allAccounts] ) return { - devices, - isScanning, isConnecting, transportState, - scanForDevices, connectToDevice, disconnectDevice, isLoading, - getSolanaKeys, - getAvalancheKeys, - getLedgerLiveKeys, - resetKeys, - keys: { - solanaKeys, - avalancheKeys, - bitcoinAddress, - xpAddress - }, - createLedgerWallet, - setupProgress + createLedgerWallet } } diff --git a/packages/core-mobile/app/new/features/ledger/screens/AppConnectionScreen.tsx b/packages/core-mobile/app/new/features/ledger/screens/AppConnectionScreen.tsx index 57debd2c2e..8871687a45 100644 --- a/packages/core-mobile/app/new/features/ledger/screens/AppConnectionScreen.tsx +++ b/packages/core-mobile/app/new/features/ledger/screens/AppConnectionScreen.tsx @@ -1,31 +1,35 @@ -import React, { useCallback, useEffect, useState } from 'react' +import React, { useCallback, useState } from 'react' import { useRouter } from 'expo-router' import { Alert } from 'react-native' +import { ScrollScreen } from 'common/components/ScrollScreen' +import { ProgressDots } from 'common/components/ProgressDots' 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 [isCreatingWallet, setIsCreatingWallet] = useState(false) + const [currentStep, setCurrentStep] = useState(0) const { - getSolanaKeys, - getAvalancheKeys, connectedDeviceId, connectedDeviceName, selectedDerivationPath, - keys, resetSetup, disconnectDevice, createLedgerWallet } = useLedgerSetupContext() - // Check if keys are available and create wallet, then navigate to complete - useEffect(() => { - const createWalletAndNavigate = async () => { + const handleComplete = useCallback( + async (keys: { + solanaKeys: Array<{ key: string; derivationPath: string; curve: string }> + avalancheKeys: { evm: string; avalanche: string; pvm: string } | null + bitcoinAddress: string + xpAddress: string + }) => { + // If wallet hasn't been created yet, create it now if ( keys.avalancheKeys && - keys.solanaKeys.length > 0 && connectedDeviceId && selectedDerivationPath && !isCreatingWallet @@ -36,10 +40,11 @@ export default function AppConnectionScreen(): JSX.Element { await createLedgerWallet({ deviceId: connectedDeviceId, deviceName: connectedDeviceName, - derivationPathType: selectedDerivationPath + derivationPathType: selectedDerivationPath, + avalancheKeys: keys.avalancheKeys, + solanaKeys: keys.solanaKeys }) - // Navigate to complete screen after wallet creation // @ts-ignore TODO: make routes typesafe push('/accountSettings/ledger/complete') } catch (error) { @@ -52,65 +57,20 @@ export default function AppConnectionScreen(): JSX.Element { ) setIsCreatingWallet(false) } - } - } - - createWalletAndNavigate() - }, [ - keys.avalancheKeys, - keys.solanaKeys, - connectedDeviceId, - connectedDeviceName, - selectedDerivationPath, - createLedgerWallet, - push, - isCreatingWallet - ]) - - const handleComplete = useCallback(async () => { - // If wallet hasn't been created yet, create it now - if ( - keys.avalancheKeys && - keys.solanaKeys.length > 0 && - connectedDeviceId && - selectedDerivationPath && - !isCreatingWallet - ) { - setIsCreatingWallet(true) - - try { - await createLedgerWallet({ - deviceId: connectedDeviceId, - deviceName: connectedDeviceName, - derivationPathType: selectedDerivationPath - }) - + } else { // @ts-ignore TODO: make routes typesafe push('/accountSettings/ledger/complete') - } catch (error) { - Alert.alert( - 'Wallet Creation Failed', - error instanceof Error - ? error.message - : 'Failed to create Ledger wallet. Please try again.', - [{ text: 'OK' }] - ) - setIsCreatingWallet(false) } - } else { - // @ts-ignore TODO: make routes typesafe - push('/accountSettings/ledger/complete') - } - }, [ - keys.avalancheKeys, - keys.solanaKeys, - connectedDeviceId, - connectedDeviceName, - selectedDerivationPath, - createLedgerWallet, - push, - isCreatingWallet - ]) + }, + [ + connectedDeviceId, + connectedDeviceName, + selectedDerivationPath, + createLedgerWallet, + push, + isCreatingWallet + ] + ) const handleCancel = useCallback(async () => { await disconnectDevice() @@ -118,15 +78,28 @@ export default function AppConnectionScreen(): JSX.Element { back() }, [disconnectDevice, resetSetup, back]) + const renderHeaderCenterComponent = useCallback(() => { + return + }, [currentStep]) + return ( - + + + ) } diff --git a/packages/core-mobile/app/new/features/ledger/screens/CompleteScreen.tsx b/packages/core-mobile/app/new/features/ledger/screens/CompleteScreen.tsx index bc3247c698..09dce458aa 100644 --- a/packages/core-mobile/app/new/features/ledger/screens/CompleteScreen.tsx +++ b/packages/core-mobile/app/new/features/ledger/screens/CompleteScreen.tsx @@ -1,11 +1,13 @@ import React from 'react' import { View } from 'react-native' -import { useRouter } from 'expo-router' -import { Text, Button, useTheme } from '@avalabs/k2-alpine' +import { useNavigation } from '@react-navigation/native' +import { CommonActions } from '@react-navigation/native' +import { Text, Button, useTheme, Icons } from '@avalabs/k2-alpine' +import { ScrollScreen } from 'common/components/ScrollScreen' import { useLedgerSetupContext } from 'new/features/ledger/contexts/LedgerSetupContext' export default function CompleteScreen(): JSX.Element { - const { push } = useRouter() + const navigation = useNavigation() const { theme: { colors } } = useTheme() @@ -14,37 +16,64 @@ export default function CompleteScreen(): JSX.Element { const handleComplete = (): void => { resetSetup() - // Navigate to account management after successful wallet creation - // @ts-ignore TODO: make routes typesafe - push('/accountSettings/manageAccounts') + // Reset the accountSettings stack to have index as first screen and manageAccounts as second (active) + navigation.dispatch( + CommonActions.reset({ + index: 1, // manageAccounts will be the active screen + routes: [ + { name: 'index' }, // accountSettings index screen (first in stack) + { name: 'manageAccounts' } // manageAccounts screen (second in stack, active) + ] + }) + ) } return ( - - - 🎉 Wallet created successfully! - - + - Your Ledger wallet has been set up and is ready to use. - - - + + + Ledger wallet{'\n'}successfully added + + + You can now start buying, swapping, sending, receiving crypto and + collectibles via the app with your Ledger wallet + + + + + + + ) } diff --git a/packages/core-mobile/app/new/features/ledger/screens/DeviceConnectionScreen.tsx b/packages/core-mobile/app/new/features/ledger/screens/DeviceConnectionScreen.tsx index dd8c0c4b58..2232caaea0 100644 --- a/packages/core-mobile/app/new/features/ledger/screens/DeviceConnectionScreen.tsx +++ b/packages/core-mobile/app/new/features/ledger/screens/DeviceConnectionScreen.tsx @@ -1,15 +1,20 @@ -import React, { useCallback } from 'react' -import { View, Alert, ActivityIndicator } from 'react-native' +import React, { useCallback, useState, useEffect } from 'react' +import { + View, + Alert, + ActivityIndicator, + Platform, + PermissionsAndroid +} from 'react-native' import { useRouter } from 'expo-router' -import { Text, Button, useTheme, GroupList, Icons } from '@avalabs/k2-alpine' +import { Button, useTheme, Icons } from '@avalabs/k2-alpine' import { ScrollScreen } from 'common/components/ScrollScreen' import { useLedgerSetupContext } from 'new/features/ledger/contexts/LedgerSetupContext' import { AnimatedIconWithText } from 'new/features/ledger/components/AnimatedIconWithText' - -interface LedgerDevice { - id: string - name: string -} +import { LedgerDeviceList } from 'new/features/ledger/components/LedgerDeviceList' +import LedgerService from 'services/ledger/LedgerService' +import { LedgerDevice } from 'services/ledger/types' +import TransportBLE from '@ledgerhq/react-native-hw-transport-ble' export default function DeviceConnectionScreen(): JSX.Element { const { push, back } = useRouter() @@ -17,15 +22,111 @@ export default function DeviceConnectionScreen(): JSX.Element { theme: { colors } } = useTheme() - const { - devices, - isScanning, - isConnecting, - scanForDevices, - connectToDevice, - setConnectedDevice, - resetSetup - } = useLedgerSetupContext() + const { isConnecting, connectToDevice, setConnectedDevice, resetSetup } = + useLedgerSetupContext() + + // Local device management + const [devices, setDevices] = useState([]) + const [isScanning, setIsScanning] = useState(false) + const [transportState, setTransportState] = useState({ available: false }) + + // Monitor BLE transport state + useEffect(() => { + const subscription = TransportBLE.observeState({ + next: (event: { available: boolean }) => { + setTransportState({ available: event.available }) + }, + error: (error: Error) => { + Alert.alert( + 'BLE Error', + `Failed to monitor BLE state: ${error.message}` + ) + }, + complete: () => { + // BLE scan complete + } + }) + + return () => { + subscription.unsubscribe() + } + }, []) + + // Set up device listener for LedgerService + useEffect(() => { + const deviceListener = (newDevices: LedgerDevice[]): void => { + setDevices(newDevices) + } + + const syncScanningState = (): void => { + setIsScanning(LedgerService.getIsScanning()) + } + + LedgerService.addDeviceListener(deviceListener) + + // Sync scanning state periodically + const scanStateInterval = setInterval(syncScanningState, 1000) + syncScanningState() // Initial sync + + return () => { + LedgerService.removeDeviceListener(deviceListener) + clearInterval(scanStateInterval) + LedgerService.stopDeviceScanning() // Clean up scanning when screen unmounts + } + }, []) + + // Request Bluetooth permissions + const requestBluetoothPermissions = + useCallback(async (): Promise => { + if (Platform.OS !== 'android') { + return true + } + + try { + const granted = await PermissionsAndroid.requestMultiple([ + PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, + PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT, + PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION + ]) + + return Object.values(granted).every( + permission => permission === PermissionsAndroid.RESULTS.GRANTED + ) + } catch (error) { + return false + } + }, []) + + // Scan for devices + const scanForDevices = useCallback(async () => { + if (!transportState.available) { + Alert.alert( + 'Bluetooth Unavailable', + 'Please enable Bluetooth to scan for Ledger devices' + ) + return + } + + const hasPermissions = await requestBluetoothPermissions() + if (!hasPermissions) { + Alert.alert( + 'Permission Required', + 'Bluetooth permissions are required to scan for Ledger devices.' + ) + return + } + + try { + await LedgerService.startDeviceScanning() + } catch (error) { + Alert.alert( + 'Scan Error', + `Failed to scan for devices: ${ + error instanceof Error ? error.message : 'Unknown error' + }` + ) + } + }, [transportState.available, requestBluetoothPermissions]) // Handle device connection const handleDeviceConnection = useCallback( @@ -53,44 +154,6 @@ export default function DeviceConnectionScreen(): JSX.Element { back() }, [resetSetup, back]) - const deviceListData = devices.map((device: LedgerDevice) => ({ - title: device.name || 'Ledger Device', - subtitle: ( - - Found over Bluetooth - - ), - leftIcon: ( - - - - ), - accessory: ( - - ), - onPress: () => handleDeviceConnection(device.id, device.name) - })) - const renderFooter = useCallback(() => { return ( @@ -130,48 +193,48 @@ export default function DeviceConnectionScreen(): JSX.Element { title={`Connect \nYour Ledger`} isModal renderFooter={renderFooter} - contentContainerStyle={{ padding: 16, flex: 1 }}> - {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="Get your Ledger ready" - subtitle="Make sure your Ledger device is unlocked and ready to connect" - showAnimation={false} - /> - )} - - {devices.length > 0 && ( + contentContainerStyle={{ flex: 1 }}> + - + + } + title={ + isScanning ? 'Looking for devices...' : 'Get your Ledger ready' + } + subtitle="Make sure your Ledger device is unlocked and the Avalanche app is open" + showAnimation={isScanning} + /> - )} + + {/* Device list appears when devices are found */} + {devices.length > 0 && ( + + + + )} + ) } diff --git a/packages/core-mobile/app/services/account/AccountsService.tsx b/packages/core-mobile/app/services/account/AccountsService.tsx index f9d31775a9..74ebf9d6ae 100644 --- a/packages/core-mobile/app/services/account/AccountsService.tsx +++ b/packages/core-mobile/app/services/account/AccountsService.tsx @@ -170,7 +170,7 @@ class AccountsService { isTestnet } as Network - return ModuleManager.deriveAddresses({ + return await ModuleManager.deriveAddresses({ walletId, walletType, accountIndex, diff --git a/packages/core-mobile/app/services/balance/BalanceService.ts b/packages/core-mobile/app/services/balance/BalanceService.ts index ed10ccecd8..cbded7de4a 100644 --- a/packages/core-mobile/app/services/balance/BalanceService.ts +++ b/packages/core-mobile/app/services/balance/BalanceService.ts @@ -1,4 +1,5 @@ import { Network } from '@avalabs/core-chains-sdk' + import { SPAN_STATUS_ERROR } from '@sentry/core' import { Account } from 'store/account/types' import { getAddressByNetwork } from 'store/account/utils' @@ -133,7 +134,6 @@ export class BalanceService { ) const settled = await Promise.allSettled(perAddressPromises) - balancesResponse = settled.reduce((acc, r) => { if (r.status === 'fulfilled') { const { address, res } = r.value @@ -222,7 +222,6 @@ export class BalanceService { code: SPAN_STATUS_ERROR, message: err instanceof Error ? err.message : 'unknown error' }) - Logger.error( `[BalanceService][getBalancesForAccounts] failed for network ${network.chainId}`, err @@ -237,7 +236,6 @@ export class BalanceService { // Mark all accounts errored for this network for (const account of accounts) { const address = getAddressByNetwork(account, network) - errorPartial[account.id] = { accountId: account.id, chainId: network.chainId, @@ -246,10 +244,10 @@ export class BalanceService { dataAccurate: false, error: err as Error } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion finalResults[account.id]!.push(errorPartial[account.id]!) } + // Still notify UI for progressive updates onBalanceLoaded?.(network.chainId, errorPartial) } finally { diff --git a/packages/core-mobile/app/services/ledger/LedgerService.ts b/packages/core-mobile/app/services/ledger/LedgerService.ts index e3345668b7..92af67f4cd 100644 --- a/packages/core-mobile/app/services/ledger/LedgerService.ts +++ b/packages/core-mobile/app/services/ledger/LedgerService.ts @@ -17,6 +17,7 @@ import { LEDGER_TIMEOUTS, getSolanaDerivationPath } from 'new/features/ledger/consts' +import { getSolanaDerivationPath as getSolanaDerivationPathSDK } from '@avalabs/core-wallets-sdk' import { assertNotNull } from 'utils/assertions' import { AddressInfo, @@ -24,7 +25,8 @@ import { PublicKeyInfo, LedgerAppType, LedgerReturnCode, - AppInfo + AppInfo, + LedgerDevice } from './types' export class LedgerService { @@ -33,6 +35,13 @@ export class LedgerService { private appPollingInterval: number | null = null private appPollingEnabled = false + // Device scanning state + private scanSubscription: { unsubscribe: () => void } | null = null + private scanInterval: ReturnType | null = null + private deviceListeners: Set<(devices: LedgerDevice[]) => void> = new Set() + private currentDevices: LedgerDevice[] = [] + private isScanning = false + // Transport getter/setter with automatic error handling private get transport(): TransportBLE { assertNotNull( @@ -109,6 +118,142 @@ export class LedgerService { this.appPollingEnabled = false } + // Device scanning methods + async startDeviceScanning(): Promise { + if (this.isScanning) { + Logger.info('Device scanning already in progress') + return + } + + Logger.info('Starting device scanning...') + this.isScanning = true + this.currentDevices = [] + + // Track devices found in current scan for intelligent removal + const devicesFoundInCurrentScan = new Set() + + try { + this.scanSubscription = TransportBLE.listen({ + next: (event: { + type: string + descriptor: { id: string; name?: string; rssi?: number } + }) => { + if (event.type === 'add') { + const device: LedgerDevice = { + id: event.descriptor.id, + name: event.descriptor.name || 'Unknown Device', + rssi: event.descriptor.rssi + } + + // Track this device as found in current scan + devicesFoundInCurrentScan.add(device.id) + + // Update device list + const existingIndex = this.currentDevices.findIndex( + d => d.id === device.id + ) + if (existingIndex === -1) { + this.currentDevices = [...this.currentDevices, device] + } else { + // Update existing device with latest info (e.g., RSSI) + this.currentDevices = this.currentDevices.map(d => + d.id === device.id ? device : d + ) + } + + // Notify all listeners + this.notifyDeviceListeners() + } + }, + error: (error: Error) => { + Logger.error('Device scanning error:', error) + this.stopDeviceScanning() + }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + complete: () => {} + }) + + // Periodically remove devices not seen recently (every 3 seconds during scan) + this.scanInterval = setInterval(() => { + this.currentDevices = this.currentDevices.filter(device => + devicesFoundInCurrentScan.has(device.id) + ) + this.notifyDeviceListeners() + + // Reset the tracking set for the next interval + devicesFoundInCurrentScan.clear() + }, 3000) + + // Stop scanning after timeout + setTimeout(() => { + this.stopDeviceScanning() + + // Final cleanup: Remove devices not found in current scan + this.currentDevices = this.currentDevices.filter(device => + devicesFoundInCurrentScan.has(device.id) + ) + this.notifyDeviceListeners() + }, LEDGER_TIMEOUTS.SCAN_TIMEOUT) + } catch (error) { + Logger.error('Failed to start device scanning:', error) + this.stopDeviceScanning() + throw error + } + } + + stopDeviceScanning(): void { + if (!this.isScanning) return + + Logger.info('Stopping device scanning...') + + if (this.scanSubscription) { + this.scanSubscription.unsubscribe() + this.scanSubscription = null + } + + if (this.scanInterval) { + clearInterval(this.scanInterval) + this.scanInterval = null + } + + this.isScanning = false + } + + addDeviceListener(callback: (devices: LedgerDevice[]) => void): void { + this.deviceListeners.add(callback) + // Immediately notify with current devices + callback(this.currentDevices) + } + + removeDeviceListener(callback: (devices: LedgerDevice[]) => void): void { + this.deviceListeners.delete(callback) + } + + private notifyDeviceListeners(): void { + this.deviceListeners.forEach(callback => { + try { + callback([...this.currentDevices]) + } catch (error) { + Logger.error('Error in device listener callback:', error) + } + }) + } + + getIsScanning(): boolean { + return this.isScanning + } + + getCurrentDevices(): LedgerDevice[] { + return [...this.currentDevices] + } + + removeDevice(deviceId: string): void { + this.currentDevices = this.currentDevices.filter( + device => device.id !== deviceId + ) + this.notifyDeviceListeners() + } + // Get current app info from device private async getCurrentAppInfo(): Promise { return await getLedgerAppInfo(this.transport as Transport) @@ -663,6 +808,103 @@ export class LedgerService { async getTransport(): Promise { return this.transport } + + // ============================================================================ + // KEY RETRIEVAL METHODS + // ============================================================================ + + /** + * Get Solana keys from the connected Ledger device + * @returns Array of Solana keys with derivation paths + */ + async getSolanaKeys(): Promise< + Array<{ + key: string + derivationPath: string + curve: string + }> + > { + Logger.info('Getting Solana keys with passive app detection') + await this.waitForApp(LedgerAppType.SOLANA) + + // Get address directly from Solana app + const transport = await this.getTransport() + const solanaApp = new AppSolana(transport as Transport) + + // Use the same derivation path approach as Extension (from @avalabs/core-wallets-sdk) + const derivationPath = getSolanaDerivationPathSDK(0) + + // Convert to the format expected by Ledger (without m/ prefix) + const ledgerDerivationPath = derivationPath.replace('m/', '') + + const result = await solanaApp.getAddress(ledgerDerivationPath, false) + + // Get the raw public key (what the SVM module expects) + const solanaPublicKey = Buffer.isBuffer(result.address) + ? result.address.toString('hex') + : Buffer.from(result.address).toString('hex') + + return [ + { + key: solanaPublicKey, // Store the raw public key, not the address + derivationPath, // Use the SVM module's derivation path for compatibility + curve: 'ED25519' + } + ] + } + + /** + * Get Avalanche keys from the connected Ledger device + * @returns Avalanche keys for EVM, Avalanche, and PVM chains + */ + async getAvalancheKeys(): Promise<{ + evm: string + avalanche: string + pvm: string + }> { + Logger.info('Getting Avalanche keys') + + // Get public keys instead of addresses to avoid double 0x prefix issues + const publicKeys = await this.getPublicKeys(0, 1) + + // Find the public keys for each chain + const evmPublicKey = + publicKeys.find(key => key.derivationPath.includes("44'/60'"))?.key || '' + const avmPublicKey = + publicKeys.find(key => key.derivationPath.includes("44'/9000'"))?.key || + '' + + // Store the public keys (not addresses) to avoid VM module issues + return { + evm: evmPublicKey, // Use public key instead of address + avalanche: avmPublicKey, // Use public key instead of address + pvm: avmPublicKey // Use same key for PVM + } + } + + /** + * Get Bitcoin and XP addresses from Avalanche keys + * @param avalancheKeys The avalanche keys to derive addresses from + * @returns Bitcoin and XP addresses + */ + async getBitcoinAndXPAddresses(): Promise<{ + bitcoinAddress: string + xpAddress: string + }> { + const addresses = await this.getAllAddresses(0, 1) + + // Get addresses for display + const xChainAddress = + addresses.find(addr => addr.network === ChainName.AVALANCHE_X)?.address || + '' + const btcAddress = + addresses.find(addr => addr.network === ChainName.BITCOIN)?.address || '' + + return { + bitcoinAddress: btcAddress, + xpAddress: xChainAddress + } + } } export default new LedgerService() diff --git a/packages/core-mobile/app/services/wallet/WalletFactory.ts b/packages/core-mobile/app/services/wallet/WalletFactory.ts index 6a06c3d56e..c8bcb5aa88 100644 --- a/packages/core-mobile/app/services/wallet/WalletFactory.ts +++ b/packages/core-mobile/app/services/wallet/WalletFactory.ts @@ -65,6 +65,7 @@ class WalletFactory { } const ledgerData = JSON.parse(walletSecret.value) + return new LedgerWallet(ledgerData) } default: diff --git a/packages/core-mobile/app/store/wallet/thunks.ts b/packages/core-mobile/app/store/wallet/thunks.ts index c4b61c21fb..6d35ef6782 100644 --- a/packages/core-mobile/app/store/wallet/thunks.ts +++ b/packages/core-mobile/app/store/wallet/thunks.ts @@ -30,7 +30,10 @@ export const storeWallet = createAsyncThunk< ThunkApi >( `${reducerName}/storeWallet`, - async ({ walletId, walletSecret, type }: StoreWalletParams, thunkApi) => { + async ( + { walletId, walletSecret, type, walletName }: StoreWalletParams, + thunkApi + ) => { const success = await BiometricsSDK.storeWalletSecret( walletId, walletSecret @@ -45,7 +48,7 @@ export const storeWallet = createAsyncThunk< return { id: walletId, - name: generateWalletName(walletCount + 1), + name: walletName || generateWalletName(walletCount + 1), type } } diff --git a/packages/core-mobile/app/store/wallet/types.ts b/packages/core-mobile/app/store/wallet/types.ts index fdb0608168..c8c4046349 100644 --- a/packages/core-mobile/app/store/wallet/types.ts +++ b/packages/core-mobile/app/store/wallet/types.ts @@ -15,6 +15,7 @@ export interface WalletsState { export interface StoreWalletParams { walletId: WalletId + walletName?: string walletSecret: string type: WalletType } diff --git a/packages/k2-alpine/src/assets/icons/avalanche_logo.svg b/packages/k2-alpine/src/assets/icons/avalanche_logo.svg new file mode 100644 index 0000000000..d88ab37a71 --- /dev/null +++ b/packages/k2-alpine/src/assets/icons/avalanche_logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/k2-alpine/src/assets/icons/solana_logo.svg b/packages/k2-alpine/src/assets/icons/solana_logo.svg new file mode 100644 index 0000000000..2a1cffc171 --- /dev/null +++ b/packages/k2-alpine/src/assets/icons/solana_logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/k2-alpine/src/theme/tokens/Icons.ts b/packages/k2-alpine/src/theme/tokens/Icons.ts index 36ba0b61ce..56aed37003 100644 --- a/packages/k2-alpine/src/theme/tokens/Icons.ts +++ b/packages/k2-alpine/src/theme/tokens/Icons.ts @@ -98,6 +98,9 @@ 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' +import AvalancheLogo from '../../assets/icons/avalanche_logo.svg' +import SolanaLogo from '../../assets/icons/solana_logo.svg' + // Transaction types import IconTxTypeAdd from '../../assets/icons/tx-type-add.svg' import IconTxTypeAdvanceTime from '../../assets/icons/advance-time.svg' @@ -336,7 +339,9 @@ export const Icons = { Download: IconDownload, SwapProviderAuto: IconSwapProviderAuto, Ledger: IconLedger, - Bluetooth: IconBluetooth + Bluetooth: IconBluetooth, + Avalanche: AvalancheLogo, + Solana: SolanaLogo }, RecoveryMethod: { Passkey: IconPasskey,