Skip to content
This repository has been archived by the owner on Oct 10, 2023. It is now read-only.

Commit

Permalink
Store LedgerAddresses by KeystoreId (#2360)
Browse files Browse the repository at this point in the history
  • Loading branch information
veado authored Aug 18, 2022
1 parent e3d8c0f commit 69a356b
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 47 deletions.
12 changes: 8 additions & 4 deletions src/renderer/hooks/useLedger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import * as FP from 'fp-ts/function'
import { useObservableState } from 'observable-hooks'
import * as RxOp from 'rxjs/operators'

import { KeystoreId } from '../../shared/api/types'
import { useWalletContext } from '../contexts/WalletContext'
import { LedgerAddressRD } from '../services/wallet/types'
import { useNetwork } from './useNetwork'

export const useLedger = (chain: Chain) => {
export const useLedger = (chain: Chain, id: KeystoreId) => {
const { network } = useNetwork()

const { askLedgerAddress$, getLedgerAddress$, verifyLedgerAddress, removeLedgerAddress } = useWalletContext()
Expand All @@ -19,7 +20,10 @@ export const useLedger = (chain: Chain) => {
async (walletIndex: number) => await verifyLedgerAddress({ chain, network, walletIndex }),
[chain, verifyLedgerAddress, network]
)
const removeAddress = useCallback(() => removeLedgerAddress(chain, network), [chain, removeLedgerAddress, network])
const removeAddress = useCallback(
() => removeLedgerAddress({ id, chain, network }),
[removeLedgerAddress, chain, network, id]
)

const address = useObservableState<LedgerAddressRD>(
FP.pipe(getLedgerAddress$(chain, network), RxOp.shareReplay(1)),
Expand All @@ -31,10 +35,10 @@ export const useLedger = (chain: Chain) => {
// Note: Subscription is needed to get all values
// and to let `askLedgerAddressByChain` update state of `LedgerAddressRD`
// Check implementation of `askLedgerAddressByChain` in `src/renderer/services/wallet/ledger.ts`
const sub = askLedgerAddress$(chain, network, walletIndex).subscribe()
const sub = askLedgerAddress$({ id, chain, network, walletIndex }).subscribe()
return () => sub.unsubscribe()
},
[askLedgerAddress$, chain, network]
[askLedgerAddress$, chain, id, network]
)

return {
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/hooks/useSymDepositAddresses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { useWalletContext } from '../contexts/WalletContext'
import { INITIAL_SYM_DEPOSIT_ADDRESSES } from '../services/chain/const'
import { SymDepositAddresses } from '../services/chain/types'
import { LedgerAddressRD } from '../services/wallet/types'
import { useLedger } from './useLedger'
import { useNetwork } from './useNetwork'

/**
Expand Down Expand Up @@ -79,7 +78,8 @@ export const useSymDepositAddresses = (oAsset: O.Option<Asset>) => {

const assetLedgerAddress = RD.toOption(assetLedgerAddressRD)

const { address: runeLedgerAddressRD } = useLedger(THORChain)
const runeLedgerAddressRD = useObservableState<LedgerAddressRD>(getLedgerAddress$(THORChain, network), RD.initial)

const runeLedgerAddress = RD.toOption(runeLedgerAddressRD)

const setAssetWalletType = useCallback(
Expand Down
3 changes: 3 additions & 0 deletions src/renderer/services/wallet/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
BalancesState,
BalancesStateFilter,
KeystoreState,
KeystoreLedgerAddressesMap,
LedgerAddressesMap,
LedgerAddressMap,
LoadTxsHandler
Expand Down Expand Up @@ -64,4 +65,6 @@ export const INITIAL_LEDGER_ADDRESSES_MAP: LedgerAddressesMap = {
[Chain.Terra]: INITIAL_LEDGER_ADDRESS_MAP
}

export const INITIAL_KEYSTORE_LEDGER_ADDRESSES_MAP: KeystoreLedgerAddressesMap = new Map() // empty by default

export const MAX_WALLET_NAME_CHARS = 50
98 changes: 68 additions & 30 deletions src/renderer/services/wallet/ledger.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,88 @@
import * as RD from '@devexperts/remote-data-ts'
import { Chain } from '@xchainjs/xchain-util'
import * as FP from 'fp-ts/lib/function'
import * as O from 'fp-ts/lib/Option'
import * as Rx from 'rxjs'
import * as RxOp from 'rxjs/operators'

import { LedgerErrorId, Network } from '../../../shared/api/types'
import { KeystoreId, LedgerErrorId, Network } from '../../../shared/api/types'
import { isError } from '../../../shared/utils/guard'
import { eqLedgerAddressMap } from '../../helpers/fp/eq'
import { observableState } from '../../helpers/stateHelper'
import { INITIAL_LEDGER_ADDRESSES_MAP } from './const'
import { INITIAL_KEYSTORE_LEDGER_ADDRESSES_MAP, INITIAL_LEDGER_ADDRESSES_MAP } from './const'
import {
GetLedgerAddressHandler,
KeystoreState,
KeystoreState$,
KeystoreLedgerAddressesMap,
LedgerAddressesMap,
LedgerAddressLD,
LedgerAddressRD,
LedgerService,
VerifyLedgerAddressHandler
VerifyLedgerAddressHandler,
AskLedgerAddressesHandler,
RemoveLedgerAddressHandler,
isKeystoreUnlocked
} from './types'
import { hasImportedKeystore } from './util'

export const createLedgerService = ({ keystore$ }: { keystore$: KeystoreState$ }): LedgerService => {
// State of all added Ledger addresses
// State of all Ledger addresses added to a keystore wallet
const {
get$: ledgerAddresses$,
get: ledgerAddresses,
set: setLedgerAddresses
} = observableState<LedgerAddressesMap>(INITIAL_LEDGER_ADDRESSES_MAP)
get$: keystoreLedgerAddresses$,
get: keystoreLedgerAddresses,
set: setKeystoreLedgerAddresses
} = observableState<KeystoreLedgerAddressesMap>(INITIAL_KEYSTORE_LEDGER_ADDRESSES_MAP)

const selectedKeystoreId$: Rx.Observable<O.Option<KeystoreId>> = FP.pipe(
keystore$,
// Check unlocked keystore only
RxOp.map(FP.flow(O.chain(O.fromPredicate(isKeystoreUnlocked)))),
RxOp.map(FP.flow(O.map(({ id }) => id)))
)

const ledgerAddresses = (id: KeystoreId): LedgerAddressesMap =>
FP.pipe(
keystoreLedgerAddresses().get(id),
O.fromNullable,
O.getOrElse(() => INITIAL_LEDGER_ADDRESSES_MAP)
)

const ledgerAddresses$ = FP.pipe(
Rx.combineLatest([selectedKeystoreId$, keystoreLedgerAddresses$]),
RxOp.map(([oKeystoreId, addressesMap]) =>
FP.pipe(
oKeystoreId,
O.fold(
() => INITIAL_LEDGER_ADDRESSES_MAP,
(id) =>
FP.pipe(
addressesMap.get(id),
O.fromNullable,
O.getOrElse(() => INITIAL_LEDGER_ADDRESSES_MAP)
)
)
)
)
)

const setLedgerAddresses = (id: KeystoreId, addressesMap: LedgerAddressesMap) => {
const updated = keystoreLedgerAddresses().set(id, addressesMap)
setKeystoreLedgerAddresses(updated)
}

const setLedgerAddressRD = ({
addressRD,
chain,
network
network,
id
}: {
addressRD: LedgerAddressRD
chain: Chain
network: Network
id: KeystoreId
}) => {
const addresses = ledgerAddresses()
// TODO(@asgdx-team) Let's think about to use `immer` or similar library for deep, immutable state changes
return setLedgerAddresses({ ...addresses, [chain]: { ...addresses[chain], [network]: addressRD } })
const addresses = ledgerAddresses(id)
return setLedgerAddresses(id, { ...addresses, [chain]: { ...addresses[chain], [network]: addressRD } })
}

/**
Expand All @@ -60,18 +102,20 @@ export const createLedgerService = ({ keystore$ }: { keystore$: KeystoreState$ }
/**
* Removes ledger address from memory
*/
const removeLedgerAddress = (chain: Chain, network: Network): void =>
const removeLedgerAddress: RemoveLedgerAddressHandler = ({ id, chain, network }) =>
setLedgerAddressRD({
addressRD: RD.initial,
chain,
network
network,
id
})

/**
* Sets ledger address in `pending` state
*/
const setPendingLedgerAddress = (chain: Chain, network: Network): void =>
const setPendingLedgerAddress = ({ id, chain, network }: { id: KeystoreId; chain: Chain; network: Network }): void =>
setLedgerAddressRD({
id,
addressRD: RD.pending,
chain,
network
Expand All @@ -80,17 +124,17 @@ export const createLedgerService = ({ keystore$ }: { keystore$: KeystoreState$ }
/**
* Ask Ledger to get address from it
*/
const askLedgerAddress$ = (chain: Chain, network: Network, walletIndex: number): LedgerAddressLD =>
const askLedgerAddress$: AskLedgerAddressesHandler = ({ id, chain, network, walletIndex }) =>
FP.pipe(
// remove address from memory
removeLedgerAddress(chain, network),
removeLedgerAddress({ id, chain, network }),
// set pending
() => setPendingLedgerAddress(chain, network),
() => setPendingLedgerAddress({ id, chain, network }),
// ask for ledger address
() => Rx.from(window.apiHDWallet.getLedgerAddress({ chain, network, walletIndex })),
RxOp.map(RD.fromEither),
// store address in memory
RxOp.tap((addressRD: LedgerAddressRD) => setLedgerAddressRD({ chain, addressRD, network })),
RxOp.tap((addressRD: LedgerAddressRD) => setLedgerAddressRD({ chain, addressRD, network, id })),
RxOp.catchError((error) =>
Rx.of(
RD.failure({
Expand All @@ -102,24 +146,18 @@ export const createLedgerService = ({ keystore$ }: { keystore$: KeystoreState$ }
RxOp.startWith(RD.pending)
)

// Whenever keystore has been removed, reset all stored ledger addresses
const keystoreSub = keystore$.subscribe((keystoreState: KeystoreState) => {
// Whenever all keystores have been removed, reset all stored ledger addresses
keystore$.subscribe((keystoreState: KeystoreState) => {
if (!hasImportedKeystore(keystoreState)) {
setLedgerAddresses(INITIAL_LEDGER_ADDRESSES_MAP)
setKeystoreLedgerAddresses(INITIAL_KEYSTORE_LEDGER_ADDRESSES_MAP)
}
})

const dispose = () => {
keystoreSub.unsubscribe()
setLedgerAddresses(INITIAL_LEDGER_ADDRESSES_MAP)
}

return {
ledgerAddresses$,
askLedgerAddress$,
getLedgerAddress$,
verifyLedgerAddress,
removeLedgerAddress,
dispose
removeLedgerAddress
}
}
29 changes: 26 additions & 3 deletions src/renderer/services/wallet/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,13 +186,34 @@ export type VerifyLedgerAddressHandler = (params: {
walletIndex: number
}) => Promise<boolean>

export type AskLedgerAddressesHandler = ({
id,
chain,
network,
walletIndex
}: {
id: KeystoreId
chain: Chain
network: Network
walletIndex: number
}) => LedgerAddressLD

export type RemoveLedgerAddressHandler = ({
id,
chain,
network
}: {
id: KeystoreId
chain: Chain
network: Network
}) => void

export type LedgerService = {
ledgerAddresses$: Rx.Observable<LedgerAddressesMap>
askLedgerAddress$: (chain: Chain, network: Network, walletIndex: number) => LedgerAddressLD
askLedgerAddress$: AskLedgerAddressesHandler
getLedgerAddress$: GetLedgerAddressHandler
verifyLedgerAddress: VerifyLedgerAddressHandler
removeLedgerAddress: (chain: Chain, network: Network) => void
dispose: FP.Lazy<void>
removeLedgerAddress: RemoveLedgerAddressHandler
}

// TODO(@Veado) Move type to clients/type
Expand Down Expand Up @@ -222,6 +243,8 @@ export type LedgerAddressMap$ = Rx.Observable<LedgerAddressMap>
export type LedgerAddressesMap = Record<Chain, LedgerAddressMap>
export type LedgerAddressesMap$ = Rx.Observable<LedgerAddressesMap>

export type KeystoreLedgerAddressesMap = Map<KeystoreId, LedgerAddressesMap>

export type KeystoreWalletsRD = RD.RemoteData<Error, KeystoreWallets>
export type KeystoreWalletsLD = LiveData<Error, KeystoreWallets>

Expand Down
18 changes: 10 additions & 8 deletions src/renderer/views/wallet/WalletSettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ type Props = {
export const WalletSettingsView: React.FC<Props> = ({ keystoreUnlocked }): JSX.Element => {
const intl = useIntl()

const { id: keystoreId } = keystoreUnlocked

const { walletsUI } = useKeystoreWallets()

const {
Expand Down Expand Up @@ -89,56 +91,56 @@ export const WalletSettingsView: React.FC<Props> = ({ keystoreUnlocked }): JSX.E
verifyAddress: verifyLedgerThorAddress,
address: thorLedgerAddressRD,
removeAddress: removeLedgerThorAddress
} = useLedger(THORChain)
} = useLedger(THORChain, keystoreId)

const {
askAddress: askLedgerBnbAddress,
verifyAddress: verifyLedgerBnbAddress,
address: bnbLedgerAddressRD,
removeAddress: removeLedgerBnbAddress
} = useLedger(BNBChain)
} = useLedger(BNBChain, keystoreId)

const {
askAddress: askLedgerBtcAddress,
verifyAddress: verifyLedgerBtcAddress,
address: btcLedgerAddressRD,
removeAddress: removeLedgerBtcAddress
} = useLedger(BTCChain)
} = useLedger(BTCChain, keystoreId)

const {
askAddress: askLedgerLtcAddress,
verifyAddress: verifyLedgerLtcAddress,
address: ltcLedgerAddressRD,
removeAddress: removeLedgerLtcAddress
} = useLedger(LTCChain)
} = useLedger(LTCChain, keystoreId)

const {
askAddress: askLedgerBchAddress,
verifyAddress: verifyLedgerBchAddress,
address: bchLedgerAddressRD,
removeAddress: removeLedgerBchAddress
} = useLedger(BCHChain)
} = useLedger(BCHChain, keystoreId)

const {
askAddress: askLedgerDOGEAddress,
verifyAddress: verifyLedgerDOGEAddress,
address: dogeLedgerAddressRD,
removeAddress: removeLedgerDOGEAddress
} = useLedger(DOGEChain)
} = useLedger(DOGEChain, keystoreId)

const {
askAddress: askLedgerEthAddress,
verifyAddress: verifyLedgerEthAddress,
address: ethLedgerAddressRD,
removeAddress: removeLedgerEthAddress
} = useLedger(ETHChain)
} = useLedger(ETHChain, keystoreId)

const {
askAddress: askLedgerCosmosAddress,
verifyAddress: verifyLedgerCosmosAddress,
address: cosmosLedgerAddressRD,
removeAddress: removeLedgerCosmosAddress
} = useLedger(CosmosChain)
} = useLedger(CosmosChain, keystoreId)

const addLedgerAddressHandler = (chain: Chain, walletIndex: number) => {
if (isThorChain(chain)) return askLedgerThorAddress(walletIndex)
Expand Down

0 comments on commit 69a356b

Please sign in to comment.