diff --git a/packages/core-mobile/app/new/common/components/WalletCard.tsx b/packages/core-mobile/app/new/common/components/WalletCard.tsx
index f8bf395618..3ae03ac54e 100644
--- a/packages/core-mobile/app/new/common/components/WalletCard.tsx
+++ b/packages/core-mobile/app/new/common/components/WalletCard.tsx
@@ -12,7 +12,7 @@ import { useManageWallet } from 'common/hooks/useManageWallet'
import { AccountDisplayData, WalletDisplayData } from 'common/types'
import { AccountListItem } from 'features/wallets/components/AccountListItem'
import { WalletBalance } from 'features/wallets/components/WalletBalance'
-import React, { useCallback, useState } from 'react'
+import React, { useCallback, useMemo, useState } from 'react'
import {
FlatList,
LayoutChangeEvent,
@@ -22,9 +22,6 @@ import {
} from 'react-native'
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
import { WalletType } from 'services/wallet/types'
-import { LedgerAppType } from 'services/ledger/types'
-import { LedgerConnectionCaption } from 'features/accountSettings/components/LedgerConnectionCaption'
-import { useObserveLedgerState } from 'common/hooks/useObserveLedgerState'
import { DropdownMenu } from './DropdownMenu'
import { WalletIcon } from './WalletIcon'
@@ -58,10 +55,12 @@ const WalletCard = ({
isAddingAccount
} = useManageWallet()
- const { isAppOpened, isLedger } = useObserveLedgerState(
- wallet.id,
- LedgerAppType.AVALANCHE
- )
+ const isLedger = useMemo(() => {
+ return (
+ wallet?.type === WalletType.LEDGER_LIVE ||
+ wallet?.type === WalletType.LEDGER
+ )
+ }, [wallet?.type])
const renderExpansionIcon = useCallback(() => {
return (
@@ -84,7 +83,7 @@ const WalletCard = ({
/>
)
},
- [isRefreshing]
+ [isRefreshing, wallet.name]
)
const renderEmpty = useCallback(() => {
@@ -153,34 +152,27 @@ const WalletCard = ({
/>
{wallet.type !== WalletType.PRIVATE_KEY ? (
-
- {((isLedger && isAppOpened) || !isLedger) && (
-
- )
- }
- type="secondary"
- disabled={isAddingAccount}
- onPress={() => handleAddAccountToWallet(wallet)}>
- {isAddingAccount ? (
-
- ) : (
- 'Add account'
- )}
-
- )}
- {isLedger && !isAppOpened && (
-
+
+ )
+ }
+ type="secondary"
+ disabled={isAddingAccount}
+ testID={`add_account_btn__${wallet.name}`}
+ onPress={() => handleAddAccountToWallet(wallet)}>
+ {isAddingAccount ? (
+
+ ) : (
+ 'Add account'
)}
-
+
) : (
<>>
)}
@@ -277,7 +269,7 @@ const WalletCard = ({
groups={[
{
key: 'wallet-actions',
- items: getDropdownItems(wallet, isLedger && isAppOpened)
+ items: getDropdownItems(wallet, isLedger)
}
]}
onPressAction={(event: { nativeEvent: { event: string } }) =>
diff --git a/packages/core-mobile/app/new/common/hooks/useDeleteWallet.ts b/packages/core-mobile/app/new/common/hooks/useDeleteWallet.ts
index 6c0559133c..297d2384ee 100644
--- a/packages/core-mobile/app/new/common/hooks/useDeleteWallet.ts
+++ b/packages/core-mobile/app/new/common/hooks/useDeleteWallet.ts
@@ -8,9 +8,9 @@ import { resetLoginAttempt } from 'store/security'
export const useDeleteWallet = (): {
deleteWallet: () => void
} => {
- const { resetLedgerWalletMap } = useLedgerWalletMap()
const { deleteRecentAccounts } = useRecentAccounts()
const dispatch = useDispatch()
+ const { resetLedgerWalletMap } = useLedgerWalletMap()
const deleteWallet = useCallback(() => {
dispatch(onLogOut())
diff --git a/packages/core-mobile/app/new/common/hooks/useManageWallet.ts b/packages/core-mobile/app/new/common/hooks/useManageWallet.ts
index 9406572cbd..52e74a589e 100644
--- a/packages/core-mobile/app/new/common/hooks/useManageWallet.ts
+++ b/packages/core-mobile/app/new/common/hooks/useManageWallet.ts
@@ -4,7 +4,7 @@ import {
dismissAlertWithTextInput,
showAlertWithTextInput
} from 'common/utils/alertWithTextInput'
-import { useLedgerWalletMap } from 'features/ledger/store'
+import { useRouter } from 'expo-router'
import { showSnackbar } from 'new/common/utils/toast'
import { useCallback, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
@@ -22,6 +22,7 @@ import {
import { removeWallet } from 'store/wallet/thunks'
import { Wallet } from 'store/wallet/types'
import Logger from 'utils/Logger'
+import { useLedgerWalletMap } from 'features/ledger/store'
export const useManageWallet = (): {
handleAddAccount: (wallet: Wallet) => void
@@ -30,6 +31,7 @@ export const useManageWallet = (): {
isAddingAccount: boolean
} => {
const { removeLedgerWallet } = useLedgerWalletMap()
+ const { navigate } = useRouter()
const [isAddingAccount, setIsAddingAccount] = useState(false)
const dispatch = useDispatch()
const walletsCount = useSelector(selectWalletsCount)
@@ -115,7 +117,7 @@ export const useManageWallet = (): {
]
})
},
- [dispatch, walletsCount, removeLedgerWallet]
+ [dispatch, removeLedgerWallet, walletsCount]
)
const handleAddAccount = useCallback(
@@ -123,6 +125,24 @@ export const useManageWallet = (): {
if (isAddingAccount) return
try {
+ if (
+ wallet.type === WalletType.LEDGER ||
+ wallet.type === WalletType.LEDGER_LIVE
+ ) {
+ setIsAddingAccount(true)
+ navigate({
+ // @ts-ignore TODO: make routes typesafe
+ pathname: '/addAccountAppConnection',
+ params: { walletId: wallet.id }
+ })
+ // Reset the flag after navigation to allow future attempts
+ // The modal dismissal will naturally reset this state
+ setTimeout(() => {
+ setIsAddingAccount(false)
+ }, 1000)
+ return
+ }
+
AnalyticsService.capture('AccountSelectorAddAccount', {
accountNumber: Object.keys(accounts).length + 1
})
@@ -142,7 +162,7 @@ export const useManageWallet = (): {
setIsAddingAccount(false)
}
},
- [isAddingAccount, accounts, dispatch]
+ [isAddingAccount, accounts, dispatch, navigate]
)
const canRemoveWallet = useCallback(
diff --git a/packages/core-mobile/app/new/common/hooks/useObserveLedgerState.ts b/packages/core-mobile/app/new/common/hooks/useObserveLedgerState.ts
deleted file mode 100644
index 4089d4e48f..0000000000
--- a/packages/core-mobile/app/new/common/hooks/useObserveLedgerState.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { useLedgerWallet } from 'features/ledger/hooks/useLedgerWallet'
-import { useLedgerWalletMap } from 'features/ledger/store'
-import { LedgerAppType } from 'services/ledger/types'
-import { useEffect, useMemo, useState } from 'react'
-import LedgerService from 'services/ledger/LedgerService'
-import { useSelector } from 'react-redux'
-import { selectWalletById } from 'store/wallet/slice'
-import { WalletType } from 'services/wallet/types'
-
-export const useObserveLedgerState = (
- walletId: string,
- appType: LedgerAppType
-): {
- isAppOpened: boolean
- isLedger: boolean
-} => {
- const { transportState } = useLedgerWallet()
- const { ledgerWalletMap } = useLedgerWalletMap()
- const wallet = useSelector(selectWalletById(walletId))
- const [isAppOpened, setIsAppOpened] = useState(false)
-
- const deviceForWallet = useMemo(() => {
- return ledgerWalletMap[walletId]
- }, [ledgerWalletMap, walletId])
-
- const [isConnected, setIsConnected] = useState(false)
-
- const isLedger = useMemo(() => {
- return (
- wallet?.type === WalletType.LEDGER_LIVE ||
- wallet?.type === WalletType.LEDGER
- )
- }, [wallet?.type])
-
- useEffect(() => {
- if (isConnected === false) {
- setIsAppOpened(false)
- return
- }
-
- const intervalId = setInterval(() => {
- LedgerService.checkApp(appType)
- .then(isOpened => {
- setIsAppOpened(isOpened)
- })
- .catch(() => {
- setIsAppOpened(false)
- })
- }, 2000)
-
- return () => clearInterval(intervalId)
- }, [appType, isConnected, transportState.available])
-
- useEffect(() => {
- async function checkAppIsOpened(): Promise {
- if (isLedger && deviceForWallet?.deviceId && transportState.available) {
- setIsConnected(false)
- try {
- await LedgerService.ensureConnection(deviceForWallet.deviceId)
- setIsConnected(true)
- } catch (error) {
- setIsConnected(false)
- }
- } else {
- setIsConnected(false)
- }
- }
- checkAppIsOpened()
- }, [deviceForWallet?.deviceId, isLedger, transportState.available])
-
- return { isAppOpened, isLedger }
-}
diff --git a/packages/core-mobile/app/new/features/accountSettings/components/LedgerConnectionCaption.tsx b/packages/core-mobile/app/new/features/accountSettings/components/LedgerConnectionCaption.tsx
deleted file mode 100644
index 3a863e2771..0000000000
--- a/packages/core-mobile/app/new/features/accountSettings/components/LedgerConnectionCaption.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Text, useTheme } from '@avalabs/k2-alpine'
-import React from 'react'
-import { LedgerAppType } from 'services/ledger/types'
-
-export const LedgerConnectionCaption = ({
- appType
-}: {
- appType: LedgerAppType
-}): React.JSX.Element => {
- const {
- theme: { colors }
- } = useTheme()
- return (
-
- {`Connect your Ledger device and open the ${appType} app to add an account. Make sure you connect the device that you used to create this wallet.`}
-
- )
-}
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 c042a31b9b..2872efb738 100644
--- a/packages/core-mobile/app/new/features/accountSettings/components/accountAddresses.tsx
+++ b/packages/core-mobile/app/new/features/accountSettings/components/accountAddresses.tsx
@@ -21,8 +21,6 @@ import { WalletType } from 'services/wallet/types'
import { useSelector } from 'react-redux'
import { selectIsSolanaSupportBlocked } from 'store/posthog'
import { useRouter } from 'expo-router'
-import { LedgerAppType } from 'services/ledger/types'
-import { LedgerConnectionCaption } from './LedgerConnectionCaption'
export const AccountAddresses = ({
account
@@ -94,7 +92,7 @@ export const AccountAddresses = ({
value:
isLedger &&
network.vmName === NetworkVMType.SVM &&
- (address === undefined || address === '') &&
+ isMissingSolanaAddress &&
!isSolanaSupportBlocked ? (
) : (
@@ -114,6 +112,7 @@ export const AccountAddresses = ({
isSolanaSupportBlocked,
colors.$surfaceSecondary,
isLedger,
+ isMissingSolanaAddress,
account.id,
account.addressPVM,
account.addressBTC,
@@ -140,9 +139,6 @@ export const AccountAddresses = ({
lineHeight: 22
}}
/>
- {isMissingSolanaAddress && !isSolanaSupportBlocked && (
-
- )}
)
}
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 8e47e17772..463dd94de3 100644
--- a/packages/core-mobile/app/new/features/ledger/components/LedgerAppConnection.tsx
+++ b/packages/core-mobile/app/new/features/ledger/components/LedgerAppConnection.tsx
@@ -1,8 +1,7 @@
-import React, { useMemo } from 'react'
+import React, { useCallback, useMemo } from 'react'
import { View, ActivityIndicator } from 'react-native'
import { Text, useTheme, Icons, GroupList } from '@avalabs/k2-alpine'
import { LoadingState } from 'common/components/LoadingState'
-import { LedgerDerivationPathType, LedgerKeys } from 'services/ledger/types'
import { truncateAddress } from '@avalabs/core-utils-sdk'
import { TRUNCATE_ADDRESS_LENGTH } from 'common/consts/text'
import { NetworkLogoWithChain } from 'common/components/NetworkLogoWithChain'
@@ -16,6 +15,8 @@ import { ChainName } from 'services/network/consts'
import { stripAddressPrefix } from 'common/utils/stripAddressPrefix'
import { selectIsSolanaSupportBlocked } from 'store/posthog'
import { useSelector } from 'react-redux'
+import { selectIsDeveloperMode } from 'store/settings/advanced'
+import { LedgerKeysByNetwork } from 'services/ledger/types'
import { LedgerDeviceList } from './LedgerDeviceList'
import { AnimatedIconWithText } from './AnimatedIconWithText'
@@ -36,20 +37,17 @@ interface StepConfig {
}
interface LedgerAppConnectionProps {
- deviceName: string
- selectedDerivationPath: LedgerDerivationPathType | null
- isCreatingWallet?: boolean
+ completeStepTitle?: string
connectedDeviceId?: string | null
connectedDeviceName?: string
- keys?: LedgerKeys
+ keys?: LedgerKeysByNetwork
appConnectionStep: AppConnectionStep
skipSolana?: boolean
onlySolana?: boolean
}
export const LedgerAppConnection: React.FC = ({
- deviceName,
- selectedDerivationPath: _selectedDerivationPath,
+ completeStepTitle = `Your Ledger wallet\nis being set up`,
connectedDeviceId,
connectedDeviceName,
keys,
@@ -60,23 +58,27 @@ export const LedgerAppConnection: React.FC = ({
const {
theme: { colors }
} = useTheme()
+ const isDeveloperMode = useSelector(selectIsDeveloperMode)
const isSolanaSupportBlocked = useSelector(selectIsSolanaSupportBlocked)
+ const deviceName = connectedDeviceName || 'Ledger Device'
+ const keysByNetwork = isDeveloperMode ? keys?.testnet : keys?.mainnet
const hasAllKeys = useMemo(() => {
return (
- !!keys?.avalancheKeys &&
- keys?.bitcoinAddress !== '' &&
- keys?.xpAddress !== '' &&
+ !!keysByNetwork?.avalancheKeys?.addresses.pvm &&
+ !!keysByNetwork?.avalancheKeys?.addresses.avm &&
+ !!keysByNetwork?.avalancheKeys?.addresses.evm &&
+ !!keysByNetwork?.avalancheKeys?.addresses.btc &&
+ // Solana keys are optional, so we don't require them to consider the wallet "ready"
+
(isSolanaSupportBlocked ||
skipSolana ||
- (keys?.solanaKeys && keys.solanaKeys.length > 0))
+ (keysByNetwork?.solanaKeys && keysByNetwork.solanaKeys.length > 0))
)
}, [
isSolanaSupportBlocked,
- keys?.avalancheKeys,
- keys?.bitcoinAddress,
- keys?.solanaKeys,
- keys?.xpAddress,
+ keysByNetwork?.avalancheKeys?.addresses,
+ keysByNetwork?.solanaKeys,
skipSolana
])
@@ -85,11 +87,11 @@ export const LedgerAppConnection: React.FC = ({
const addresses = []
// C-Chain/EVM address (derived from avalanche keys)
- if (keys?.avalancheKeys?.addresses.evm) {
+ if (keysByNetwork?.avalancheKeys?.addresses.evm) {
addresses.push({
title: AVALANCHE_MAINNET_NETWORK.chainName,
subtitle: truncateAddress(
- keys.avalancheKeys.addresses.evm,
+ keysByNetwork.avalancheKeys.addresses.evm,
TRUNCATE_ADDRESS_LENGTH
),
value: (
@@ -111,7 +113,7 @@ export const LedgerAppConnection: React.FC = ({
}
// X/P Chain address
- if (keys?.xpAddress) {
+ if (keysByNetwork?.avalancheKeys?.addresses.pvm) {
const xpNetwork = {
...AVALANCHE_XP_NETWORK,
chainName: ChainName.AVALANCHE_XP
@@ -119,7 +121,7 @@ export const LedgerAppConnection: React.FC = ({
addresses.push({
title: xpNetwork.chainName,
subtitle: truncateAddress(
- stripAddressPrefix(keys.xpAddress),
+ stripAddressPrefix(keysByNetwork.avalancheKeys.addresses.pvm),
TRUNCATE_ADDRESS_LENGTH
),
value: (
@@ -141,14 +143,17 @@ export const LedgerAppConnection: React.FC = ({
}
// Bitcoin address
- if (keys?.bitcoinAddress) {
+ if (keysByNetwork?.avalancheKeys?.addresses.btc) {
const bitcoinNetwork = {
...BITCOIN_NETWORK,
chainName: ChainName.BITCOIN
}
addresses.push({
title: bitcoinNetwork.chainName,
- subtitle: truncateAddress(keys.bitcoinAddress, TRUNCATE_ADDRESS_LENGTH),
+ subtitle: truncateAddress(
+ keysByNetwork.avalancheKeys.addresses.btc,
+ TRUNCATE_ADDRESS_LENGTH
+ ),
value: (
= ({
// Solana address
if (
- keys?.solanaKeys &&
- keys?.solanaKeys.length > 0 &&
- keys?.solanaKeys[0]?.key
+ keysByNetwork?.solanaKeys &&
+ keysByNetwork.solanaKeys.length > 0 &&
+ keysByNetwork.solanaKeys[0]?.key
) {
// The key is already a Solana address (Base58 encoded) from LedgerService
- const solanaAddress = keys.solanaKeys[0].key
+ const solanaAddress = keysByNetwork.solanaKeys[0].key
addresses.push({
title: NETWORK_SOLANA.chainName,
@@ -213,84 +218,88 @@ export const LedgerAppConnection: React.FC = ({
return addresses
}, [
- keys?.avalancheKeys?.addresses.evm,
- keys?.xpAddress,
- keys?.bitcoinAddress,
- keys?.solanaKeys,
+ keysByNetwork?.avalancheKeys?.addresses.evm,
+ keysByNetwork?.avalancheKeys?.addresses.pvm,
+ keysByNetwork?.avalancheKeys?.addresses.btc,
+ keysByNetwork?.solanaKeys,
hasAllKeys,
colors.$textSuccess,
colors.$surfaceSecondary
])
// Step configurations
- const getStepConfig = (step: AppConnectionStep): StepConfig | null => {
- switch (step) {
- case AppConnectionStep.AVALANCHE_CONNECT:
- return {
- icon: (
-
- ),
- title: 'Connect to Avalanche App',
- subtitle: `Open the Avalanche app on your ${deviceName}, then press Continue when ready.`,
- showAnimation: false
- }
+ const getStepConfig = useCallback(
+ (step: AppConnectionStep): StepConfig | null => {
+ switch (step) {
+ case AppConnectionStep.AVALANCHE_CONNECT:
+ return {
+ icon: (
+
+ ),
+ title: 'Connect to Avalanche App',
+ subtitle: `Open the Avalanche app on your ${deviceName}, then press Continue when ready.`,
+ showAnimation: false,
+ isLoading: !connectedDeviceId
+ }
- case AppConnectionStep.AVALANCHE_LOADING:
- return {
- icon: (
-
- ),
- 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.AVALANCHE_LOADING:
+ return {
+ icon: (
+
+ ),
+ 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: onlySolana
- ? `Open the Solana app on your ${deviceName}, then press Continue when ready.`
- : `Close the Avalanche app and open the Solana app on your ${deviceName}, then press Continue when ready.`,
- showAnimation: false
- }
+ case AppConnectionStep.SOLANA_CONNECT:
+ return {
+ icon: (
+
+ ),
+ title: 'Connect to Solana App',
+ subtitle: onlySolana
+ ? `Open the Solana app on your ${deviceName}, then press Continue when ready.`
+ : `Close the Avalanche app and open the Solana app on your ${deviceName}, then press Continue when ready.`,
+ 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
- }
+ 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
- }
- }
+ default:
+ return null
+ }
+ },
+ [colors.$textPrimary, connectedDeviceId, deviceName, onlySolana]
+ )
- const renderStepContent = (): React.ReactNode => {
+ const renderStepContent = useCallback((): React.ReactNode => {
// Handle COMPLETE step separately as it has unique layout
if (currentStep === AppConnectionStep.COMPLETE) {
return (
@@ -308,7 +317,7 @@ export const LedgerAppConnection: React.FC = ({
- {`Your Ledger wallet \nis being set up`}
+ {completeStepTitle}
@@ -399,11 +408,18 @@ export const LedgerAppConnection: React.FC = ({
)
- }
+ }, [
+ addressListData,
+ colors.$textPrimary,
+ colors.$textSecondary,
+ completeStepTitle,
+ currentStep,
+ getStepConfig
+ ])
// Create device object for display
const connectedDevice = connectedDeviceId
- ? [{ id: connectedDeviceId, name: connectedDeviceName || deviceName }]
+ ? [{ id: connectedDeviceId, name: deviceName }]
: []
return (
diff --git a/packages/core-mobile/app/new/features/ledger/consts.ts b/packages/core-mobile/app/new/features/ledger/consts.ts
index 0bc52ad817..476dfe1775 100644
--- a/packages/core-mobile/app/new/features/ledger/consts.ts
+++ b/packages/core-mobile/app/new/features/ledger/consts.ts
@@ -1,30 +1,65 @@
// Ledger derivation path constants
+export enum DerivationPathKey {
+ EVM = 'EVM',
+ AVALANCHE = 'AVALANCHE',
+ SOLANA = 'SOLANA'
+}
+
export const DERIVATION_PATHS = {
// BIP44 Standard paths
BIP44: {
- EVM: "m/44'/60'/0'/0/0",
- AVALANCHE: "m/44'/9000'/0'/0/0",
- SOLANA: "m/44'/501'/0'/0",
- BITCOIN: "m/44'/0'/0'/0/0"
+ [DerivationPathKey.EVM]: (accountIndex: number, addressIndex: number) =>
+ `m/44'/60'/${accountIndex}'/0/${addressIndex}`,
+ [DerivationPathKey.AVALANCHE]: (
+ accountIndex: number,
+ addressIndex: number
+ ) => `m/44'/9000'/${accountIndex}'/0/${addressIndex}`,
+ [DerivationPathKey.SOLANA]: (accountIndex: number, addressIndex: number) =>
+ `44'/501'/${accountIndex}'/0/${addressIndex}`
},
// Ledger Live paths (account-based)
LEDGER_LIVE: {
- EVM: (accountIndex: number) => `m/44'/60'/${accountIndex}'/0/0`,
- AVALANCHE: (accountIndex: number) => `m/44'/9000'/${accountIndex}'/0/0`,
- SOLANA: (accountIndex: number) => `44'/501'/${accountIndex}'/0`,
- BITCOIN: (accountIndex: number) => `m/44'/0'/${accountIndex}'/0/0`
+ [DerivationPathKey.EVM]: (accountIndex: number) =>
+ `m/44'/60'/${accountIndex}'/0/0`,
+ [DerivationPathKey.AVALANCHE]: (accountIndex: number) =>
+ `m/44'/9000'/${accountIndex}'/0/0`,
+ [DerivationPathKey.SOLANA]: (accountIndex: number) =>
+ `44'/501'/${accountIndex}'/0`
},
// Extended public key paths (without final /0/0)
EXTENDED: {
- EVM: "m/44'/60'/0'",
- AVALANCHE: "m/44'/9000'/0'",
- BITCOIN: "m/44'/0'/0'",
- SOLANA: "m/44'/501'/0'"
+ [DerivationPathKey.EVM]: "m/44'/60'/0'",
+ [DerivationPathKey.AVALANCHE]: "m/44'/9000'/0'",
+ [DerivationPathKey.SOLANA]: "m/44'/501'/0'"
}
} as const
+/**
+ * Generate a Ledger derivation path based on the specified key and indices
+ * @param key - The type of derivation path to generate (EVM, Avalanche, Solana)
+ * @param accountIndex - The account index to generate the path for
+ * @param addressIndex - The address index to generate the path for (default: 0)
+ * @returns The complete derivation path string
+ */
+export const getLedgerDerivationPath = (
+ key: DerivationPathKey,
+ accountIndex: number,
+ addressIndex = 0
+): string => {
+ switch (key) {
+ case DerivationPathKey.EVM:
+ return DERIVATION_PATHS.BIP44.EVM(accountIndex, addressIndex)
+ case DerivationPathKey.AVALANCHE:
+ return DERIVATION_PATHS.BIP44.AVALANCHE(accountIndex, addressIndex)
+ case DerivationPathKey.SOLANA:
+ return DERIVATION_PATHS.BIP44.SOLANA(accountIndex, addressIndex)
+ default:
+ throw new Error(`Unsupported derivation path key: ${key}`)
+ }
+}
+
/**
* Generate a Solana derivation path for a specific account index
* @param accountIndex - The account index to generate the path for
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 341d41a2fe..962c2809ee 100644
--- a/packages/core-mobile/app/new/features/ledger/contexts/LedgerSetupContext.tsx
+++ b/packages/core-mobile/app/new/features/ledger/contexts/LedgerSetupContext.tsx
@@ -8,37 +8,26 @@ import React, {
} from 'react'
import {
LedgerDerivationPathType,
- WalletCreationOptions,
- LedgerTransportState,
- LedgerKeys,
- WalletUpdateOptions
+ LedgerTransportState
} from 'services/ledger/types'
import { useLedgerWallet } from '../hooks/useLedgerWallet'
-import { useLedgerWalletMap } from '../store'
interface LedgerSetupContextValue {
// State values
selectedDerivationPath: LedgerDerivationPathType | null
connectedDeviceId: string | null
connectedDeviceName: string
- isCreatingWallet: boolean
- hasStartedSetup: boolean
+ isUpdatingWallet: boolean
// State setters
setSelectedDerivationPath: (path: LedgerDerivationPathType) => void
- setConnectedDevice: (deviceId: string, deviceName: string) => void
- setIsCreatingWallet: (creating: boolean) => void
- setHasStartedSetup: (started: boolean) => void
+ setIsUpdatingWallet: (creating: boolean) => void
// Ledger wallet hook values
isConnecting: boolean
transportState: LedgerTransportState
- connectToDevice: (deviceId: string) => Promise
+ connectToDevice: (deviceId: string, deviceName?: string) => Promise
disconnectDevice: () => Promise
- createLedgerWallet: (
- options: WalletCreationOptions & LedgerKeys
- ) => Promise<{ walletId: string; accountId: string }>
- updateSolanaForLedgerWallet: (options: WalletUpdateOptions) => Promise
// Helper methods
resetSetup: () => void
}
@@ -59,34 +48,10 @@ export const LedgerSetupProvider: React.FC = ({
)
const [connectedDeviceName, setConnectedDeviceName] =
useState('Ledger Device')
- const [isCreatingWallet, setIsCreatingWallet] = useState(false)
- const [hasStartedSetup, setHasStartedSetup] = useState(false)
+ const [isUpdatingWallet, setIsUpdatingWallet] = useState(false)
- const {
- isConnecting,
- transportState,
- connectToDevice,
- disconnectDevice,
- createLedgerWallet: _createLedgerWallet,
- updateSolanaForLedgerWallet
- } = useLedgerWallet()
-
- const { setLedgerWalletMap } = useLedgerWalletMap()
-
- const createLedgerWallet = useCallback(
- async (
- options: WalletCreationOptions
- ): Promise<{ walletId: string; accountId: string }> => {
- const { walletId, accountId } = await _createLedgerWallet(options)
- setLedgerWalletMap(
- walletId,
- options.deviceId,
- options.deviceName || 'Ledger Device'
- )
- return { walletId, accountId }
- },
- [_createLedgerWallet, setLedgerWalletMap]
- )
+ const { isConnecting, transportState, connectToDevice, disconnectDevice } =
+ useLedgerWallet()
const handleSetConnectedDevice = useCallback(
(deviceId: string, deviceName: string) => {
@@ -100,42 +65,45 @@ export const LedgerSetupProvider: React.FC = ({
setSelectedDerivationPath(null)
setConnectedDeviceId(null)
setConnectedDeviceName('Ledger Device')
- setIsCreatingWallet(false)
- setHasStartedSetup(false)
+ setIsUpdatingWallet(false)
}, [])
+ const handleConnectToDevice = useCallback(
+ async (deviceId: string, deviceName?: string) => {
+ await connectToDevice(deviceId)
+ handleSetConnectedDevice(deviceId, deviceName || 'Ledger Device')
+ },
+ [connectToDevice, handleSetConnectedDevice]
+ )
+
+ const handleDisconnectDevice = useCallback(async () => {
+ await disconnectDevice()
+ resetSetup()
+ }, [disconnectDevice, resetSetup])
+
const contextValue: LedgerSetupContextValue = useMemo(
() => ({
selectedDerivationPath,
connectedDeviceId,
connectedDeviceName,
- isCreatingWallet,
- hasStartedSetup,
+ isUpdatingWallet,
isConnecting,
transportState,
- connectToDevice,
- disconnectDevice,
- createLedgerWallet,
- updateSolanaForLedgerWallet,
+ connectToDevice: handleConnectToDevice,
+ disconnectDevice: handleDisconnectDevice,
setSelectedDerivationPath,
- setConnectedDevice: handleSetConnectedDevice,
- setIsCreatingWallet,
- setHasStartedSetup,
+ setIsUpdatingWallet,
resetSetup
}),
[
selectedDerivationPath,
connectedDeviceId,
connectedDeviceName,
- isCreatingWallet,
- hasStartedSetup,
+ isUpdatingWallet,
isConnecting,
transportState,
- connectToDevice,
- disconnectDevice,
- createLedgerWallet,
- updateSolanaForLedgerWallet,
- handleSetConnectedDevice,
+ handleConnectToDevice,
+ handleDisconnectDevice,
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 cb25a2eb41..e171ef8fad 100644
--- a/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts
+++ b/packages/core-mobile/app/new/features/ledger/hooks/useLedgerWallet.ts
@@ -9,8 +9,10 @@ import {
LedgerDerivationPathType,
LedgerKeys,
LedgerTransportState,
+ PublicKeyInfo,
WalletCreationOptions,
- WalletUpdateOptions
+ WalletUpdateOptions,
+ WalletUpdateSolanaOptions
} from 'services/ledger/types'
import { WalletType } from 'services/wallet/types'
import { PrimaryAccount, setAccount, setActiveAccountId } from 'store/account'
@@ -18,12 +20,13 @@ import { AppThunkDispatch } from 'store/types'
import { setActiveWallet } from 'store/wallet/slice'
import { storeWallet } from 'store/wallet/thunks'
import Logger from 'utils/Logger'
-import { Curve } from 'utils/publicKeys'
import { uuid } from 'utils/uuid'
import { CoreAccountType } from '@avalabs/types'
import BiometricsSDK from 'utils/BiometricsSDK'
-import { DERIVATION_PATHS } from '../consts'
+import { Curve } from 'utils/publicKeys'
import { LedgerWalletSecretSchema } from '../utils'
+import { useLedgerWalletMap } from '../store'
+import { DerivationPathKey, getLedgerDerivationPath } from '../consts'
export interface UseLedgerWalletReturn {
// Connection state
@@ -37,10 +40,16 @@ export interface UseLedgerWalletReturn {
createLedgerWallet: (
options: WalletCreationOptions & LedgerKeys
) => Promise<{ walletId: string; accountId: string }>
- updateSolanaForLedgerWallet: (options: WalletUpdateOptions) => Promise
+ updateSolanaForLedgerWallet: (
+ options: WalletUpdateSolanaOptions
+ ) => Promise
+ createLedgerAccount: (
+ options: WalletUpdateOptions & LedgerKeys
+ ) => Promise<{ walletId: string; accountId: string }>
}
export function useLedgerWallet(): UseLedgerWalletReturn {
+ const { setLedgerWalletMap } = useLedgerWalletMap()
const dispatch = useDispatch()
const isLedgerBlocked = useSelector(selectIsLedgerSupportBlocked)
const [transportState, setTransportState] = useState({
@@ -82,7 +91,7 @@ export function useLedgerWallet(): UseLedgerWalletReturn {
const connectToDevice = useCallback(async (deviceId: string) => {
setIsConnecting(true)
try {
- await LedgerService.connect(deviceId)
+ await LedgerService.ensureConnection(deviceId)
Logger.info('Connected to Ledger device')
} catch (error) {
Logger.error('Failed to connect to device', error)
@@ -108,10 +117,8 @@ export function useLedgerWallet(): UseLedgerWalletReturn {
deviceId,
deviceName = 'Ledger Device',
derivationPathType = LedgerDerivationPathType.BIP44,
- individualKeys = [],
avalancheKeys,
- solanaKeys = [],
- bitcoinAddress
+ solanaKeys = []
}: WalletCreationOptions & LedgerKeys) => {
try {
setIsLoading(true)
@@ -123,6 +130,7 @@ export function useLedgerWallet(): UseLedgerWalletReturn {
if (!avalancheKeys) {
throw new Error('Missing Avalanche keys for wallet creation')
}
+
// Solana keys are optional - wallet can be created with only Avalanche keys
const newWalletId = uuid()
@@ -130,51 +138,14 @@ export function useLedgerWallet(): UseLedgerWalletReturn {
// Use addresses for display and xpubs for wallet functionality
const { addresses, xpubs } = avalancheKeys
- // Fix address formatting - remove double 0x prefixes that cause VM module errors
- const formattedAddresses = {
- evm: addresses.evm?.startsWith('0x0x')
- ? addresses.evm.slice(2) // Remove first 0x to fix double prefix
- : addresses.evm,
- avm: addresses.avm,
- pvm: addresses.pvm
- }
-
- // 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 addresses for BIP44
- {
- key: formattedAddresses.evm, // Use formatted address
- derivationPath: DERIVATION_PATHS.BIP44.EVM,
- curve: Curve.SECP256K1
- },
- {
- key: formattedAddresses.avm,
- derivationPath: DERIVATION_PATHS.BIP44.AVALANCHE,
- curve: Curve.SECP256K1
- },
- {
- key: formattedAddresses.pvm,
- derivationPath: DERIVATION_PATHS.BIP44.AVALANCHE,
- 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
- }
- ]
- : [])
- ]
+ const formattedAddresses = getFormattedAddresses(addresses)
+ // Create the public keys array
+ const publicKeysToStore = getPublicKeysForAccount(
+ formattedAddresses,
+ solanaKeys,
+ 0 // For wallet creation, we are only adding the first account (index 0)
+ )
// Store the Ledger wallet with the specified derivation path type
// For BIP44, store xpub in per-account format for future account additions
await dispatch(
@@ -195,13 +166,7 @@ export function useLedgerWallet(): UseLedgerWalletReturn {
}
}
}),
- publicKeys:
- derivationPathType === LedgerDerivationPathType.LedgerLive &&
- individualKeys.length > 0
- ? formattedPublicKeys // Use formatted individual keys for Ledger Live
- : publicKeysToStore, // Use the public keys we just created
- avalancheKeys: formattedAddresses, // Use formatted addresses for display
- solanaKeys
+ publicKeys: publicKeysToStore
}),
type:
derivationPathType === LedgerDerivationPathType.BIP44
@@ -210,6 +175,12 @@ export function useLedgerWallet(): UseLedgerWalletReturn {
})
).unwrap()
+ setLedgerWalletMap(
+ newWalletId,
+ { id: deviceId, name: deviceName || 'Ledger Device' },
+ derivationPathType
+ )
+
dispatch(setActiveWallet(newWalletId))
// For the first account (index 0), use the addresses we retrieved during setup
@@ -222,11 +193,11 @@ export function useLedgerWallet(): UseLedgerWalletReturn {
type: CoreAccountType.PRIMARY,
index: 0,
addressC: formattedAddresses.evm,
- addressBTC: bitcoinAddress || '',
+ addressBTC: formattedAddresses.btc,
addressAVM: formattedAddresses.avm,
addressPVM: formattedAddresses.pvm,
addressSVM: solanaKeys[0]?.key || '',
- addressCoreEth: ''
+ addressCoreEth: formattedAddresses.coreEth
}
dispatch(setAccount(newAccount))
@@ -242,18 +213,136 @@ export function useLedgerWallet(): UseLedgerWalletReturn {
setIsLoading(false)
}
},
- [dispatch]
+ [dispatch, setLedgerWalletMap]
)
- const updateSolanaForLedgerWallet = useCallback(
+ const createLedgerAccount = useCallback(
async ({
deviceId,
+ deviceName,
+ derivationPathType,
walletId,
walletName,
walletType,
+ accountIndexToUse,
+ avalancheKeys,
+ solanaKeys = []
+ }: WalletUpdateOptions & LedgerKeys) => {
+ try {
+ setIsLoading(true)
+
+ if (!avalancheKeys) {
+ throw new Error('Missing Avalanche keys for account creation')
+ }
+
+ // Use addresses for display and xpubs for wallet functionality
+ const { addresses, xpubs } = avalancheKeys
+
+ const formattedAddresses = getFormattedAddresses(addresses)
+
+ // Create the public keys array
+ const publicKeysToUpdate = getPublicKeysForAccount(
+ formattedAddresses,
+ solanaKeys,
+ accountIndexToUse
+ )
+
+ const walletSecretResult = await BiometricsSDK.loadWalletSecret(
+ walletId
+ )
+
+ if (
+ walletSecretResult.success === false ||
+ walletSecretResult.value === undefined
+ ) {
+ throw new Error('Failed to load existing wallet secret for update')
+ }
+
+ const parsedWalletSecret = LedgerWalletSecretSchema.parse(
+ JSON.parse(walletSecretResult.value)
+ )
+
+ if (deviceId !== parsedWalletSecret.deviceId) {
+ throw new Error(
+ 'Device ID mismatch between connected wallet and stored wallet'
+ )
+ }
+
+ // Destructure to explicitly omit extendedPublicKeys from spread
+ // This ensures LedgerLive wallets don't preserve invalid extendedPublicKeys
+ const { extendedPublicKeys, publicKeys, ...baseWalletSecret } =
+ parsedWalletSecret
+
+ // Update the Ledger wallet extended public keys for new account
+ await dispatch(
+ storeWallet({
+ walletId,
+ name: walletName,
+ type: walletType,
+ walletSecret: JSON.stringify({
+ ...baseWalletSecret,
+ // For BIP44, update the extended public keys for account index
+ ...(baseWalletSecret.derivationPathSpec ===
+ LedgerDerivationPathType.BIP44 && {
+ extendedPublicKeys: {
+ ...extendedPublicKeys,
+ [accountIndexToUse]: {
+ evm: xpubs.evm, // Update with new xpub from getAvalancheKeys
+ avalanche: xpubs.avalanche // Update with new xpub from getAvalancheKeys
+ }
+ }
+ }),
+ publicKeys: [...publicKeys, ...publicKeysToUpdate] // Append new account public keys to existing array
+ })
+ })
+ ).unwrap()
+
+ setLedgerWalletMap(
+ walletId,
+ { id: deviceId, name: deviceName || 'Ledger Device' },
+ derivationPathType
+ )
+
+ const newAccountId = uuid()
+ const updatedAccount: PrimaryAccount = {
+ id: newAccountId,
+ walletId,
+ name: `Account ${accountIndexToUse + 1}`,
+ type: CoreAccountType.PRIMARY,
+ index: accountIndexToUse,
+ addressC: formattedAddresses.evm,
+ addressCoreEth: formattedAddresses.coreEth,
+ addressAVM: formattedAddresses.avm,
+ addressPVM: formattedAddresses.pvm,
+ addressBTC: formattedAddresses.btc,
+ addressSVM: solanaKeys[0]?.key || ''
+ }
+
+ dispatch(setAccount(updatedAccount))
+ dispatch(setActiveAccountId(newAccountId))
+
+ Logger.info('Account created successfully')
+ showSnackbar('Account created successfully!')
+ return { walletId, accountId: newAccountId }
+ } catch (error) {
+ Logger.error('Failed to create account:', error)
+ throw error
+ } finally {
+ setIsLoading(false)
+ }
+ },
+ [dispatch, setLedgerWalletMap]
+ )
+
+ const updateSolanaForLedgerWallet = useCallback(
+ async ({
+ deviceId,
+ walletId,
account,
+ walletName,
+ walletType,
solanaKeys = []
- }: WalletUpdateOptions) => {
+ }: WalletUpdateSolanaOptions) => {
try {
setIsLoading(true)
@@ -282,34 +371,32 @@ export function useLedgerWallet(): UseLedgerWalletReturn {
)
}
- // Create the public keys array for BIP44
- const publicKeysToStore = [
- // Use formatted addresses for BIP44
- ...parsedWalletSecret.publicKeys,
- // Only include Solana key if it exists
- {
- key: solanaKeys[0].key, // Solana addresses don't use 0x prefix
- derivationPath: solanaKeys[0].derivationPath, // Use the same path from getSolanaKeys
- curve: Curve.ED25519
- }
- ]
+ const { publicKeys, ...baseWalletSecret } = parsedWalletSecret
- // Store the Ledger wallet with the specified derivation path type
+ // Update the Ledger wallet extended public keys for new account
await dispatch(
storeWallet({
walletId,
name: walletName,
type: walletType,
walletSecret: JSON.stringify({
- ...parsedWalletSecret,
- publicKeys: publicKeysToStore,
- solanaKeys
+ ...baseWalletSecret,
+ publicKeys: [
+ ...publicKeys,
+ ...(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
+ }
+ ]
+ : [])
+ ] // Append new account public keys to existing array
})
})
).unwrap()
- // For the first account (index 0), use the addresses we retrieved during setup
- // This avoids the complex derivation logic that returns empty addresses
const updatedAccount: PrimaryAccount = {
...account,
addressSVM: solanaKeys[0]?.key
@@ -336,6 +423,84 @@ export function useLedgerWallet(): UseLedgerWalletReturn {
disconnectDevice,
isLoading,
createLedgerWallet,
- updateSolanaForLedgerWallet
+ updateSolanaForLedgerWallet,
+ createLedgerAccount
}
}
+
+// Fix address formatting - remove double 0x prefixes that cause VM module errors
+const getFormattedAddresses = (address: {
+ evm: string
+ avm: string
+ pvm: string
+ btc: string
+ coreEth: string
+}): {
+ evm: string
+ avm: string
+ pvm: string
+ btc: string
+ coreEth: string
+} => {
+ return {
+ evm: address.evm?.startsWith('0x0x')
+ ? address.evm.slice(2) // Remove first 0x to fix double prefix
+ : address.evm,
+ avm: address.avm,
+ pvm: address.pvm,
+ btc: address.btc,
+ coreEth: address.coreEth?.startsWith('0x0x')
+ ? address.coreEth.slice(2) // Remove first 0x to fix double prefix
+ : address.coreEth
+ }
+}
+
+const getPublicKeysForAccount = (
+ address: {
+ evm: string
+ avm: string
+ pvm: string
+ btc: string
+ coreEth: string
+ },
+ solanaKeys: PublicKeyInfo[],
+ accountIndex = 0
+): PublicKeyInfo[] => {
+ return [
+ // Use formatted addresses
+ {
+ key: address.evm, // Use formatted address
+ derivationPath: getLedgerDerivationPath(
+ DerivationPathKey.EVM,
+ accountIndex
+ ),
+ curve: Curve.SECP256K1
+ },
+ {
+ key: address.avm,
+ derivationPath: getLedgerDerivationPath(
+ DerivationPathKey.AVALANCHE,
+ accountIndex
+ ),
+ curve: Curve.SECP256K1
+ },
+ {
+ key: address.pvm,
+ derivationPath: getLedgerDerivationPath(
+ DerivationPathKey.AVALANCHE,
+ accountIndex
+ ),
+ 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
+ }
+ ]
+ : [])
+ ]
+}
diff --git a/packages/core-mobile/app/new/features/ledger/hooks/useSetLedgerAddress.ts b/packages/core-mobile/app/new/features/ledger/hooks/useSetLedgerAddress.ts
new file mode 100644
index 0000000000..661f4c5b88
--- /dev/null
+++ b/packages/core-mobile/app/new/features/ledger/hooks/useSetLedgerAddress.ts
@@ -0,0 +1,63 @@
+import { useCallback } from 'react'
+import { useDispatch } from 'react-redux'
+import { LedgerKeysByNetwork } from 'services/ledger/types'
+import { setLedgerAddresses } from 'store/account'
+
+export const useSetLedgerAddress = (): {
+ setLedgerAddress: ({
+ walletId,
+ accountId,
+ accountIndex,
+ keys
+ }: {
+ walletId: string
+ accountId: string
+ accountIndex: number
+ keys: LedgerKeysByNetwork
+ }) => Promise
+} => {
+ const dispatch = useDispatch()
+
+ const setLedgerAddress = useCallback(
+ async ({
+ walletId,
+ accountId,
+ accountIndex,
+ keys
+ }: {
+ walletId: string
+ accountId: string
+ accountIndex: number
+ keys: LedgerKeysByNetwork
+ }) => {
+ const mainnet = {
+ addressBTC: keys.mainnet.avalancheKeys?.addresses.btc ?? '',
+ addressAVM: keys.mainnet.avalancheKeys?.addresses.avm || '',
+ addressPVM: keys.mainnet.avalancheKeys?.addresses.pvm || '',
+ addressCoreEth: keys.mainnet.avalancheKeys?.addresses.coreEth || ''
+ }
+
+ const testnet = {
+ addressBTC: keys.testnet.avalancheKeys?.addresses.btc ?? '',
+ addressAVM: keys.testnet.avalancheKeys?.addresses.avm || '',
+ addressPVM: keys.testnet.avalancheKeys?.addresses.pvm || '',
+ addressCoreEth: keys.testnet.avalancheKeys?.addresses.coreEth || ''
+ }
+
+ dispatch(
+ setLedgerAddresses({
+ [accountId]: {
+ mainnet,
+ testnet,
+ walletId,
+ index: accountIndex,
+ id: accountId
+ }
+ })
+ )
+ },
+ [dispatch]
+ )
+
+ return { setLedgerAddress }
+}
diff --git a/packages/core-mobile/app/new/features/ledger/screens/AppConnectionAddAccountScreen.tsx b/packages/core-mobile/app/new/features/ledger/screens/AppConnectionAddAccountScreen.tsx
new file mode 100644
index 0000000000..ed9ab484d3
--- /dev/null
+++ b/packages/core-mobile/app/new/features/ledger/screens/AppConnectionAddAccountScreen.tsx
@@ -0,0 +1,129 @@
+import React, { useCallback, useMemo } from 'react'
+import { useSelector } from 'react-redux'
+import { useLocalSearchParams, useRouter } from 'expo-router'
+import { LedgerKeysByNetwork } from 'services/ledger/types'
+import Logger from 'utils/Logger'
+import { selectIsDeveloperMode } from 'store/settings/advanced'
+import LedgerService from 'services/ledger/LedgerService'
+import { selectWalletById } from 'store/wallet/slice'
+import { RootState } from 'store/types'
+import { selectAccountsByWalletId } from 'store/account'
+import { showSnackbar } from 'common/utils/toast'
+import { useLedgerWallet } from '../hooks/useLedgerWallet'
+import { useSetLedgerAddress } from '../hooks/useSetLedgerAddress'
+import { useLedgerSetupContext } from '../contexts/LedgerSetupContext'
+import { useLedgerWalletMap } from '../store'
+import AppConnectionScreen from './AppConnectionScreen'
+
+export const AppConnectionAddAccountScreen = (): JSX.Element => {
+ const { walletId } = useLocalSearchParams<{ walletId: string }>()
+ const { dismiss } = useRouter()
+ const { createLedgerAccount } = useLedgerWallet()
+ const { setLedgerAddress } = useSetLedgerAddress()
+ const isDeveloperMode = useSelector(selectIsDeveloperMode)
+ const wallet = useSelector(selectWalletById(walletId))
+ const accounts = useSelector((state: RootState) =>
+ selectAccountsByWalletId(state, walletId)
+ )
+ const { getLedgerInfoByWalletId } = useLedgerWalletMap()
+
+ const { device, derivationPathType } = useMemo(() => {
+ return getLedgerInfoByWalletId(walletId)
+ }, [getLedgerInfoByWalletId, walletId])
+
+ const {
+ resetSetup,
+ disconnectDevice,
+ isUpdatingWallet,
+ setIsUpdatingWallet
+ } = useLedgerSetupContext()
+
+ const handleComplete = useCallback(
+ async (keys: LedgerKeysByNetwork) => {
+ const keysByNetwork = isDeveloperMode ? keys.testnet : keys.mainnet
+ if (
+ keysByNetwork.avalancheKeys &&
+ device &&
+ wallet &&
+ accounts?.length > 0 &&
+ derivationPathType &&
+ !isUpdatingWallet
+ ) {
+ Logger.info('All conditions met, creating account...')
+ setIsUpdatingWallet(true)
+
+ try {
+ const { accountId } = await createLedgerAccount({
+ walletId: wallet.id,
+ walletName: wallet.name,
+ walletType: wallet.type,
+ accountIndexToUse: accounts?.length ?? 0,
+ deviceId: device.id,
+ deviceName: device.name,
+ derivationPathType,
+ avalancheKeys: keysByNetwork.avalancheKeys,
+ solanaKeys: keysByNetwork.solanaKeys
+ })
+
+ await setLedgerAddress({
+ accountIndex: accounts?.length ?? 0,
+ walletId,
+ accountId,
+ keys
+ })
+
+ Logger.info('Account created successfully, dismissing modals')
+ // Stop polling since we no longer need app detection
+ LedgerService.stopAppPolling()
+ showSnackbar('Account added successfully')
+ } catch (error) {
+ Logger.error('Account creation failed', error)
+ showSnackbar('Unable to add account')
+ } finally {
+ setIsUpdatingWallet(false)
+ resetSetup()
+ dismiss()
+ }
+ } else {
+ showSnackbar('Unable to add account')
+ Logger.info(
+ 'Account creation conditions not met, skipping account creation',
+ {
+ hasAvalancheKeys: !!keysByNetwork.avalancheKeys,
+ hasConnectedDeviceId: !!device?.id,
+ hasSelectedDerivationPath: !!derivationPathType,
+ isUpdatingWallet
+ }
+ )
+ resetSetup()
+ dismiss()
+ }
+ },
+ [
+ device,
+ wallet,
+ accounts?.length,
+ derivationPathType,
+ isUpdatingWallet,
+ setIsUpdatingWallet,
+ createLedgerAccount,
+ setLedgerAddress,
+ walletId,
+ isDeveloperMode,
+ resetSetup,
+ dismiss
+ ]
+ )
+
+ return (
+
+ )
+}
diff --git a/packages/core-mobile/app/new/features/ledger/screens/AppConnectionOnboardingScreen.tsx b/packages/core-mobile/app/new/features/ledger/screens/AppConnectionOnboardingScreen.tsx
new file mode 100644
index 0000000000..6ee7aa1e0d
--- /dev/null
+++ b/packages/core-mobile/app/new/features/ledger/screens/AppConnectionOnboardingScreen.tsx
@@ -0,0 +1,123 @@
+import React, { useCallback } from 'react'
+import Logger from 'utils/Logger'
+import { useSelector } from 'react-redux'
+import { selectIsDeveloperMode } from 'store/settings/advanced'
+import { Alert } from 'react-native'
+import LedgerService from 'services/ledger/LedgerService'
+import { LedgerKeysByNetwork } from 'services/ledger/types'
+import { useRouter } from 'expo-router'
+import { useLedgerWallet } from '../hooks/useLedgerWallet'
+import { useLedgerSetupContext } from '../contexts/LedgerSetupContext'
+import { useSetLedgerAddress } from '../hooks/useSetLedgerAddress'
+import AppConnectionScreen from './AppConnectionScreen'
+
+export const AppConnectionOnboardingScreen = (): JSX.Element => {
+ const { createLedgerWallet } = useLedgerWallet()
+ const { push } = useRouter()
+ const { setLedgerAddress } = useSetLedgerAddress()
+ const isDeveloperMode = useSelector(selectIsDeveloperMode)
+
+ const {
+ connectedDeviceId,
+ connectedDeviceName,
+ selectedDerivationPath,
+ disconnectDevice,
+ isUpdatingWallet,
+ setIsUpdatingWallet
+ } = useLedgerSetupContext()
+
+ const handleComplete = useCallback(
+ async (keys: LedgerKeysByNetwork) => {
+ const keysByNetwork = isDeveloperMode ? keys.testnet : keys.mainnet
+ Logger.info('handleComplete called', {
+ hasAvalancheKeys: !!keysByNetwork.avalancheKeys,
+ hasConnectedDeviceId: !!connectedDeviceId,
+ hasSelectedDerivationPath: !!selectedDerivationPath,
+ isUpdatingWallet,
+ solanaKeysCount: keysByNetwork.solanaKeys?.length ?? 0
+ })
+
+ // If wallet hasn't been created yet, create it now
+ if (
+ keysByNetwork.avalancheKeys &&
+ connectedDeviceId &&
+ selectedDerivationPath &&
+ !isUpdatingWallet
+ ) {
+ Logger.info('All conditions met, creating wallet...')
+ setIsUpdatingWallet(true)
+
+ try {
+ const { walletId, accountId } = await createLedgerWallet({
+ deviceId: connectedDeviceId,
+ deviceName: connectedDeviceName,
+ derivationPathType: selectedDerivationPath,
+ avalancheKeys: keysByNetwork.avalancheKeys,
+ solanaKeys: keysByNetwork.solanaKeys
+ })
+
+ await setLedgerAddress({
+ accountIndex: 0,
+ walletId,
+ accountId,
+ keys
+ })
+
+ Logger.info(
+ 'Wallet created successfully, navigating to complete screen'
+ )
+ // Stop polling since we no longer need app detection
+ LedgerService.stopAppPolling()
+ // @ts-ignore TODO: make routes typesafe
+ push('/accountSettings/ledger/complete')
+ } catch (error) {
+ Logger.error('Wallet creation failed', error)
+ Alert.alert(
+ 'Wallet Creation Failed',
+ error instanceof Error
+ ? error.message
+ : 'Failed to create Ledger wallet. Please try again.',
+ [{ text: 'OK' }]
+ )
+ } finally {
+ setIsUpdatingWallet(false)
+ }
+ } else {
+ Logger.info(
+ 'Wallet creation conditions not met, skipping wallet creation',
+ {
+ hasAvalancheKeys: !!keysByNetwork.avalancheKeys,
+ hasConnectedDeviceId: !!connectedDeviceId,
+ hasSelectedDerivationPath: !!selectedDerivationPath,
+ isUpdatingWallet
+ }
+ )
+ // @ts-ignore TODO: make routes typesafe
+ push('/accountSettings/ledger/complete')
+ }
+ },
+ [
+ connectedDeviceId,
+ selectedDerivationPath,
+ isUpdatingWallet,
+ setIsUpdatingWallet,
+ createLedgerWallet,
+ connectedDeviceName,
+ setLedgerAddress,
+ isDeveloperMode,
+ push
+ ]
+ )
+
+ return (
+
+ )
+}
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 31b09f6273..fc42c08d8f 100644
--- a/packages/core-mobile/app/new/features/ledger/screens/AppConnectionScreen.tsx
+++ b/packages/core-mobile/app/new/features/ledger/screens/AppConnectionScreen.tsx
@@ -1,4 +1,10 @@
-import React, { useCallback, useState, useEffect, useMemo } from 'react'
+import React, {
+ useCallback,
+ useState,
+ useEffect,
+ useMemo,
+ ReactNode
+} from 'react'
import { useRouter } from 'expo-router'
import { Alert, Platform, View } from 'react-native'
import { ScrollScreen } from 'common/components/ScrollScreen'
@@ -7,192 +13,60 @@ import {
AppConnectionStep,
LedgerAppConnection
} from 'new/features/ledger/components/LedgerAppConnection'
-import { useLedgerSetupContext } from 'new/features/ledger/contexts/LedgerSetupContext'
import Logger from 'utils/Logger'
import LedgerService from 'services/ledger/LedgerService'
-import { Button, ButtonType } from '@avalabs/k2-alpine'
+import { ActivityIndicator, Button, ButtonType } from '@avalabs/k2-alpine'
import { useHeaderHeight } from '@react-navigation/elements'
-import { LedgerAppType, LedgerKeys } from 'services/ledger/types'
+import { LedgerKeysByNetwork } from 'services/ledger/types'
import { selectIsSolanaSupportBlocked } from 'store/posthog'
-import { useDispatch, useSelector } from 'react-redux'
+import { useSelector } from 'react-redux'
import { showSnackbar } from 'common/utils/toast'
import { selectIsDeveloperMode } from 'store/settings/advanced'
-import { setLedgerAddresses } from 'store/account'
-export default function AppConnectionScreen(): JSX.Element {
- const { push, back } = useRouter()
- const [isCreatingWallet, setIsCreatingWallet] = useState(false)
+export default function AppConnectionScreen({
+ completeStepTitle,
+ isUpdatingWallet,
+ handleComplete,
+ deviceId,
+ deviceName = 'Ledger Device',
+ disconnectDevice,
+ accountIndex
+}: {
+ completeStepTitle: string
+ isUpdatingWallet: boolean
+ deviceId?: string | null
+ deviceName?: string
+ disconnectDevice: () => Promise
+ handleComplete: (keys: LedgerKeysByNetwork) => Promise
+ accountIndex: number
+}): JSX.Element {
+ const { back } = useRouter()
const headerHeight = useHeaderHeight()
const isDeveloperMode = useSelector(selectIsDeveloperMode)
const isSolanaSupportBlocked = useSelector(selectIsSolanaSupportBlocked)
- const dispatch = useDispatch()
+
+ const hasDeviceId = !!deviceId
// Local key state - managed only in this component
- const [keys, setKeys] = useState({
- solanaKeys: [],
- avalancheKeys: undefined,
- bitcoinAddress: '',
- xpAddress: ''
+ const [keys, setKeys] = useState({
+ mainnet: {
+ solanaKeys: [],
+ avalancheKeys: undefined
+ },
+ testnet: {
+ solanaKeys: [],
+ avalancheKeys: undefined
+ }
})
const [currentAppConnectionStep, setAppConnectionStep] =
useState(AppConnectionStep.AVALANCHE_CONNECT)
const [skipSolana, setSkipSolana] = useState(false)
- const {
- connectedDeviceId,
- connectedDeviceName,
- selectedDerivationPath,
- resetSetup,
- disconnectDevice,
- createLedgerWallet
- } = useLedgerSetupContext()
-
- const getOppositeKeys = useCallback(async () => {
- try {
- const avalancheKeys = await LedgerService.getAvalancheKeys(
- 0,
- !isDeveloperMode
- )
- const { bitcoinAddress } = await LedgerService.getBitcoinAndXPAddresses(
- 0,
- !isDeveloperMode
- )
-
- return {
- addressBTC: bitcoinAddress,
- addressAVM: avalancheKeys.addresses.avm,
- addressPVM: avalancheKeys.addresses.pvm,
- addressCoreEth: avalancheKeys.addresses.coreEth
- }
- } catch (err) {
- Logger.error('Failed to get opposite keys', err)
- return {
- addressBTC: '',
- addressAVM: '',
- addressPVM: '',
- addressCoreEth: ''
- }
- }
- }, [isDeveloperMode])
-
- // eslint-disable-next-line sonarjs/cognitive-complexity
- const handleComplete = useCallback(async () => {
- Logger.info('handleComplete called', {
- hasAvalancheKeys: !!keys.avalancheKeys,
- hasConnectedDeviceId: !!connectedDeviceId,
- hasSelectedDerivationPath: !!selectedDerivationPath,
- isCreatingWallet,
- solanaKeysCount: keys.solanaKeys?.length ?? 0
- })
-
- // If wallet hasn't been created yet, create it now
- if (
- keys.avalancheKeys &&
- connectedDeviceId &&
- selectedDerivationPath &&
- !isCreatingWallet
- ) {
- Logger.info('All conditions met, creating wallet...')
- setIsCreatingWallet(true)
-
- try {
- const { walletId, accountId } = await createLedgerWallet({
- deviceId: connectedDeviceId,
- deviceName: connectedDeviceName,
- derivationPathType: selectedDerivationPath,
- avalancheKeys: keys.avalancheKeys,
- solanaKeys: keys.solanaKeys,
- bitcoinAddress: keys.bitcoinAddress
- })
-
- // TODO: implement await/retry logic for any ledger APDU commands
- // that could fail due to transport race conditions
- setTimeout(async () => {
- const oppositeKeys = await getOppositeKeys()
-
- const mainnet = isDeveloperMode
- ? oppositeKeys
- : {
- addressBTC: keys.bitcoinAddress ?? '',
- addressAVM: keys.avalancheKeys?.addresses.avm || '',
- addressPVM: keys.avalancheKeys?.addresses.pvm || '',
- addressCoreEth: keys.avalancheKeys?.addresses.coreEth || ''
- }
-
- const testnet = isDeveloperMode
- ? {
- addressBTC: keys.bitcoinAddress ?? '',
- addressAVM: keys.avalancheKeys?.addresses.avm || '',
- addressPVM: keys.avalancheKeys?.addresses.pvm || '',
- addressCoreEth: keys.avalancheKeys?.addresses.coreEth || ''
- }
- : oppositeKeys
-
- dispatch(
- setLedgerAddresses({
- [accountId]: {
- mainnet,
- testnet,
- walletId,
- index: 0,
- id: accountId
- }
- })
- )
- }, 500)
-
- Logger.info(
- 'Wallet created successfully, navigating to complete screen'
- )
- // Stop polling since we no longer need app detection
- LedgerService.stopAppPolling()
- // @ts-ignore TODO: make routes typesafe
- push('/accountSettings/ledger/complete')
- } catch (error) {
- Logger.error('Wallet creation failed', error)
- Alert.alert(
- 'Wallet Creation Failed',
- error instanceof Error
- ? error.message
- : 'Failed to create Ledger wallet. Please try again.',
- [{ text: 'OK' }]
- )
- setIsCreatingWallet(false)
- }
- } else {
- Logger.info(
- 'Wallet creation conditions not met, skipping wallet creation',
- {
- hasAvalancheKeys: !!keys.avalancheKeys,
- hasConnectedDeviceId: !!connectedDeviceId,
- hasSelectedDerivationPath: !!selectedDerivationPath,
- isCreatingWallet
- }
- )
- // @ts-ignore TODO: make routes typesafe
- push('/accountSettings/ledger/complete')
- }
- }, [
- keys.avalancheKeys,
- keys.solanaKeys,
- keys.bitcoinAddress,
- connectedDeviceId,
- selectedDerivationPath,
- isCreatingWallet,
- createLedgerWallet,
- connectedDeviceName,
- getOppositeKeys,
- isDeveloperMode,
- dispatch,
- push
- ])
-
const handleCancel = useCallback(async () => {
await disconnectDevice()
- resetSetup()
back()
- }, [disconnectDevice, resetSetup, back])
+ }, [disconnectDevice, back])
const progressDotsCurrentStep = useMemo(() => {
if (isSolanaSupportBlocked) {
@@ -232,12 +106,12 @@ export default function AppConnectionScreen(): JSX.Element {
return () => {
// Only stop polling if we're not in the middle of wallet creation
// If wallet creation succeeded, the connection should remain for the wallet to use
- if (!isCreatingWallet) {
+ if (!isUpdatingWallet) {
Logger.info('AppConnectionScreen unmounting, stopping app polling')
LedgerService.stopAppPolling()
}
}
- }, [isCreatingWallet])
+ }, [isUpdatingWallet])
const headerCenterOverlay = useMemo(() => {
const paddingTop = Platform.OS === 'ios' ? 15 : 50
@@ -268,37 +142,39 @@ export default function AppConnectionScreen(): JSX.Element {
)
}, [headerHeight, isSolanaSupportBlocked, progressDotsCurrentStep])
- // Handler for completing wallet creation
- const handleCompleteWallet = useCallback(() => {
- Logger.info('User clicked complete wallet button', {
- hasAvalancheKeys: !!keys.avalancheKeys,
- hasSolanaKeys: keys.solanaKeys && keys.solanaKeys.length > 0,
- solanaKeysCount: keys.solanaKeys?.length ?? 0
- })
- handleComplete()
- }, [keys, handleComplete])
-
const handleConnectAvalanche = useCallback(async () => {
try {
+ if (!deviceId) {
+ throw new Error('No device ID found')
+ }
+ await LedgerService.ensureConnection(deviceId)
setAppConnectionStep(AppConnectionStep.AVALANCHE_LOADING)
- // Open Avalanche app before getting keys
- await LedgerService.openApp(LedgerAppType.AVALANCHE)
// Get keys from service
const avalancheKeys = await LedgerService.getAvalancheKeys(
- 0,
+ accountIndex,
isDeveloperMode
)
- const { bitcoinAddress, xpAddress } =
- await LedgerService.getBitcoinAndXPAddresses(0, isDeveloperMode)
+ const oppositeAvalancheKeys = await LedgerService.getAvalancheKeys(
+ accountIndex,
+ !isDeveloperMode
+ )
// Update local state
- setKeys(prev => ({
- ...prev,
- avalancheKeys,
- bitcoinAddress,
- xpAddress
- }))
+ setKeys({
+ mainnet: {
+ avalancheKeys: isDeveloperMode
+ ? oppositeAvalancheKeys
+ : avalancheKeys,
+ solanaKeys: []
+ },
+ testnet: {
+ avalancheKeys: isDeveloperMode
+ ? avalancheKeys
+ : oppositeAvalancheKeys,
+ solanaKeys: []
+ }
+ })
// Show success toast notification
showSnackbar('Avalanche app connected')
@@ -317,21 +193,30 @@ export default function AppConnectionScreen(): JSX.Element {
[{ text: 'OK' }]
)
}
- }, [isDeveloperMode, isSolanaSupportBlocked])
+ }, [accountIndex, deviceId, isDeveloperMode, isSolanaSupportBlocked])
const handleConnectSolana = useCallback(async () => {
try {
+ if (!deviceId) {
+ throw new Error('No device ID found')
+ }
+ await LedgerService.ensureConnection(deviceId)
setAppConnectionStep(AppConnectionStep.SOLANA_LOADING)
- // Open Solana app before getting keys
- await LedgerService.openApp(LedgerAppType.SOLANA)
// Get keys from service
- const solanaKeys = await LedgerService.getSolanaKeys(0)
+ const solanaKeys = await LedgerService.getSolanaKeys(accountIndex)
// Update local state
setKeys(prev => ({
...prev,
- solanaKeys
+ mainnet: {
+ ...prev.mainnet,
+ solanaKeys
+ },
+ testnet: {
+ ...prev.testnet,
+ solanaKeys
+ }
}))
// Show success toast notification
@@ -348,7 +233,7 @@ export default function AppConnectionScreen(): JSX.Element {
[{ text: 'OK' }]
)
}
- }, [])
+ }, [accountIndex, deviceId])
const handleSkipSolana = useCallback(() => {
// Skip Solana and proceed to complete step
@@ -357,11 +242,20 @@ export default function AppConnectionScreen(): JSX.Element {
}, [setAppConnectionStep])
const renderFooter = useCallback(() => {
- let primary: { text: string; onPress?: () => void; disable?: boolean } = {
+ let primary: {
+ text: string | ReactNode
+ onPress?: () => void
+ disable?: boolean
+ } = {
text: 'Continue'
}
let secondary:
- | { text: string; onPress?: () => void; type?: ButtonType }
+ | {
+ text: string
+ onPress?: () => void
+ type?: ButtonType
+ disable?: boolean
+ }
| undefined
switch (currentAppConnectionStep) {
@@ -371,7 +265,8 @@ export default function AppConnectionScreen(): JSX.Element {
text: 'Continue',
onPress: handleConnectAvalanche,
disable:
- currentAppConnectionStep === AppConnectionStep.AVALANCHE_LOADING
+ currentAppConnectionStep === AppConnectionStep.AVALANCHE_LOADING ||
+ hasDeviceId === false
}
break
case AppConnectionStep.SOLANA_CONNECT:
@@ -379,7 +274,9 @@ export default function AppConnectionScreen(): JSX.Element {
primary = {
text: 'Continue',
onPress: handleConnectSolana,
- disable: currentAppConnectionStep === AppConnectionStep.SOLANA_LOADING
+ disable:
+ currentAppConnectionStep === AppConnectionStep.SOLANA_LOADING ||
+ hasDeviceId === false
}
secondary = {
text: 'Skip Solana',
@@ -389,12 +286,14 @@ export default function AppConnectionScreen(): JSX.Element {
break
case AppConnectionStep.COMPLETE:
primary = {
- text: 'Complete setup',
- onPress: handleCompleteWallet
+ text: isUpdatingWallet ? : 'Complete setup',
+ onPress: () => handleComplete(keys),
+ disable: isUpdatingWallet
}
secondary = {
- text: 'Cancel setup',
- onPress: handleCancel
+ text: 'Cancel',
+ onPress: handleCancel,
+ disable: isUpdatingWallet
}
break
}
@@ -411,6 +310,7 @@ export default function AppConnectionScreen(): JSX.Element {
{secondary && (