Skip to content

Commit

Permalink
refactor(wallet-mobile): wallet list, updates in the sync logic
Browse files Browse the repository at this point in the history
  • Loading branch information
stackchain committed Apr 20, 2024
1 parent 388481f commit 463372a
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const SelectWalletFromList = () => {
onSuccess: ([wallet, walletMeta]) => {
selectWalletMeta(walletMeta)
selectWallet(wallet)
walletManager.selectedWalletId = walletMeta.id
navigateToTxHistory()
},
onError: (error) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import * as React from 'react'
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'

import {Icon} from '../../../../components'
import {Loading} from '../../../../components/Loading/Loading'
import {Space} from '../../../../components/Space/Space'
import {WalletMeta} from '../../../../wallet-manager/types'
import {WalletInfo, WalletMeta} from '../../../../wallet-manager/types'
import {useWalletManager} from '../../../../wallet-manager/WalletManagerContext'
import {YoroiWallet} from '../../../../yoroi-wallets/cardano/types'
import {isByron, isHaskellShelley, isJormun} from '../../../../yoroi-wallets/cardano/utils'
import {features} from '../../..'
import {ChevronRightDarkIllustration, ChevronRightGrayIllustration} from '../../illustrations/ChevronRightIllustration'

type Props = {
Expand All @@ -16,6 +20,28 @@ type Props = {
export const WalletListItem = ({wallet, onPress}: Props) => {
const {styles, colors} = useStyles()
const {type} = getWalletItemMeta(wallet, colors)
const walletManager = useWalletManager()

const [selectedWalledId, setSelectedWalletId] = React.useState<YoroiWallet['id'] | null>(
walletManager.selectedWalledId,
)
React.useEffect(() => {
return walletManager.subscribe((event) => {
if (event.type === 'selected-wallet-id') {
setSelectedWalletId(event.id)
}
})
}, [walletManager])
const isSelected = selectedWalledId === wallet.id

const [walletInfo, setWalletInfo] = React.useState<WalletInfo>()
React.useEffect(() => {
const subscription = walletManager.walletInfos$.subscribe((walletInfos) => {
const newWalletInfo = walletInfos.get(wallet.id)
if (newWalletInfo) setWalletInfo(newWalletInfo)
})
return () => subscription.unsubscribe()
})

const [isButtonPressed, setIsButtonPressed] = React.useState(false)

Expand All @@ -42,6 +68,18 @@ export const WalletListItem = ({wallet, onPress}: Props) => {
</Text>
</View>

{features.walletListFeedback && (
<>
{walletInfo?.sync.status === 'syncing' && <Loading />}

<Space width="m" />

{isSelected && <Icon.Check size={20} />}

<Space width="m" />
</>
)}

{isButtonPressed ? <ChevronRightDarkIllustration /> : <ChevronRightGrayIllustration />}
</TouchableOpacity>
</View>
Expand Down
16 changes: 8 additions & 8 deletions apps/wallet-mobile/src/features/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
export const features = {
txHistory: {
export: __DEV__ || false,
search: __DEV__ || false,
nfts: __DEV__ || false,
export: __DEV__,
search: __DEV__,
nfts: __DEV__,
},
useTestnet: __DEV__ ? false : false,
startWithIndexScreen: __DEV__ ? false : false,
prefillWalletInfo: __DEV__ ? false : false,
showProdPoolsInDev: __DEV__ ? false : false,
moderatingNftsEnabled: __DEV__ ? false : false,
useTestnet: false,
prefillWalletInfo: false,
showProdPoolsInDev: __DEV__,
moderatingNftsEnabled: false,
walletListFeedback: __DEV__,
}

export const debugWalletInfo = {
Expand Down
12 changes: 11 additions & 1 deletion apps/wallet-mobile/src/wallet-manager/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {CardanoTypes} from '../yoroi-wallets/cardano/types'
import {CardanoTypes, YoroiWallet} from '../yoroi-wallets/cardano/types'
import {HWDeviceInfo} from '../yoroi-wallets/hw/hw'
import {NetworkId, WalletImplementationId} from '../yoroi-wallets/types/other'

Expand All @@ -20,5 +20,15 @@ export type AddressMode = 'single' | 'multiple'
export type WalletManagerEvent =
| {type: 'easy-confirmation'; enabled: boolean}
| {type: 'hw-device-info'; hwDeviceInfo: HWDeviceInfo}
| {type: 'selected-wallet-id'; id: YoroiWallet['id']}

export type WalletManagerSubscription = (event: WalletManagerEvent) => void

export type WalletInfo = {
sync: {
updatedAt: number
status: 'waiting' | 'syncing' | 'done' | 'error'
error?: Error
}
}
export type WalletInfos = Map<YoroiWallet['id'], WalletInfo>
96 changes: 61 additions & 35 deletions apps/wallet-mobile/src/wallet-manager/walletManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {parseSafe} from '@yoroi/common'
import {App} from '@yoroi/types'
import {catchError, concatMap, finalize, from, interval, of} from 'rxjs'
import {catchError, concatMap, finalize, from, interval, of, Subject} from 'rxjs'
import uuid from 'uuid'

import {getCardanoWalletFactory} from '../yoroi-wallets/cardano/getWallet'
Expand All @@ -10,42 +10,68 @@ import {makeWalletEncryptedStorage} from '../yoroi-wallets/storage'
import {Keychain} from '../yoroi-wallets/storage/Keychain'
import {rootStorage} from '../yoroi-wallets/storage/rootStorage'
import {NetworkId, WalletImplementationId} from '../yoroi-wallets/types'
import {AddressMode, WalletManagerEvent, WalletManagerSubscription, WalletMeta} from './types'
import {AddressMode, WalletInfos, WalletManagerEvent, WalletManagerSubscription, WalletMeta} from './types'
import {isWalletMeta, parseWalletMeta} from './validators'

const thirtyFiveSeconds = 35 * 1e3

export class WalletManager {
private readonly walletsRootStorage: App.Storage
private readonly rootStorage: App.Storage
private readonly openedWallets: Map<string, YoroiWallet> = new Map()
readonly #walletsRootStorage: App.Storage
readonly #rootStorage: App.Storage
readonly #openedWallets: Map<string, YoroiWallet> = new Map()
public readonly walletInfos$ = new Subject<WalletInfos>()
readonly #walletInfos: WalletInfos = new Map()
#selectedWalletId: YoroiWallet['id'] | null = null

private subscriptions: Array<WalletManagerSubscription> = []
private isSyncing = false

constructor() {
this.walletsRootStorage = rootStorage.join('wallet/')
this.rootStorage = rootStorage
this.#walletsRootStorage = rootStorage.join('wallet/')
this.#rootStorage = rootStorage
}

set selectedWalletId(id: YoroiWallet['id']) {
this.#selectedWalletId = id
this._notify({type: 'selected-wallet-id', id})
}

get selectedWalledId() {
return this.#selectedWalletId
}

startSyncingAllWallets() {
const syncWallets = () => {
if (this.isSyncing) {
return
}
if (this.isSyncing) return

this.isSyncing = true

from(this.openWallets())
.pipe(
concatMap((wallets) => {
this.#walletInfos.clear()
wallets.forEach((wallet) => {
this.#walletInfos.set(wallet.id, {sync: {status: 'waiting', updatedAt: Date.now()}})
this.walletInfos$.next(new Map(this.#walletInfos))
})
return from(wallets)
}),
concatMap((wallet) => {
return from(wallet.sync())
}),
catchError((error) => {
console.error('WalletManager::startSyncingAllWallets: error syncing wallet', error)
return of()
this.#walletInfos.set(wallet.id, {sync: {status: 'syncing', updatedAt: Date.now()}})
this.walletInfos$.next(new Map(this.#walletInfos))
return from(wallet.sync()).pipe(
finalize(() => {
if (this.#walletInfos.get(wallet.id)?.sync.status !== 'error') {
this.#walletInfos.set(wallet.id, {sync: {status: 'done', updatedAt: Date.now()}})
this.walletInfos$.next(new Map(this.#walletInfos))
}
}),
catchError((error) => {
this.#walletInfos.set(wallet.id, {sync: {status: 'error', error, updatedAt: Date.now()}})
this.walletInfos$.next(new Map(this.#walletInfos))
return of()
}),
)
}),
finalize(() => {
this.isSyncing = false
Expand All @@ -65,24 +91,24 @@ export class WalletManager {

getOpenedWalletsByNetwork = () => {
const openedWalletsByNetwork: Map<NetworkId, YoroiWallet['id']> = new Map()
this.openedWallets.forEach(({id, networkId}) => openedWalletsByNetwork.set(networkId, id))
this.#openedWallets.forEach(({id, networkId}) => openedWalletsByNetwork.set(networkId, id))
return openedWalletsByNetwork
}

async openWallets() {
const walletMetas = await this.listWallets()
const closedWallets = walletMetas.filter((meta) => !this.openedWallets.has(meta.id))
const closedWallets = walletMetas.filter((meta) => !this.#openedWallets.has(meta.id))
const wallets = await Promise.all(closedWallets.map((meta) => this.openWallet(meta)))
wallets.forEach((wallet) => this.openedWallets.set(wallet.id, wallet))
return [...this.openedWallets.values()]
wallets.forEach((wallet) => this.#openedWallets.set(wallet.id, wallet))
return [...this.#openedWallets.values()]
}

async listWallets() {
const deletedWalletIds = await this.deletedWalletIds()
const walletIds = await this.walletsRootStorage
const walletIds = await this.#walletsRootStorage
.getAllKeys()
.then((ids) => ids.filter((id) => !deletedWalletIds.includes(id)))
const walletMetas = await this.walletsRootStorage
const walletMetas = await this.#walletsRootStorage
.multiGet(walletIds, parseWalletMeta)
.then((tuples) => tuples.map(([_, walletMeta]) => walletMeta))
.then((walletMetas) => walletMetas.filter(isWalletMeta)) // filter corrupted wallet metas)
Expand All @@ -91,7 +117,7 @@ export class WalletManager {
}

async deletedWalletIds() {
const ids = await this.rootStorage.getItem('deletedWalletIds', parseDeletedWalletIds)
const ids = await this.#rootStorage.getItem('deletedWalletIds', parseDeletedWalletIds)

return ids ?? []
}
Expand All @@ -104,14 +130,14 @@ export class WalletManager {
deletedWalletsIds.map(async (id) => {
const encryptedStorage = makeWalletEncryptedStorage(id)

await this.walletsRootStorage.removeItem(id) // remove wallet meta
await this.walletsRootStorage.removeFolder(`${id}/`) // remove wallet folder
await this.#walletsRootStorage.removeItem(id) // remove wallet meta
await this.#walletsRootStorage.removeFolder(`${id}/`) // remove wallet folder
await encryptedStorage.rootKey.remove() // remove auth with password
await Keychain.removeWalletKey(id) // remove auth with os
}),
)

await this.rootStorage.setItem('deletedWalletIds', [])
await this.#rootStorage.setItem('deletedWalletIds', [])
}

// Note(ppershing): needs 'this' to be bound
Expand Down Expand Up @@ -175,10 +201,10 @@ export class WalletManager {
isEasyConfirmationEnabled: false,
}

await this.walletsRootStorage.setItem(id, walletMeta)
await this.#walletsRootStorage.setItem(id, walletMeta)

if (isYoroiWallet(wallet)) {
this.openedWallets.set(id, wallet)
this.#openedWallets.set(id, wallet)
return wallet
}

Expand All @@ -187,13 +213,13 @@ export class WalletManager {

async openWallet(walletMeta: WalletMeta): Promise<YoroiWallet> {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (this.openedWallets.has(walletMeta.id)) return this.openedWallets.get(walletMeta.id)!
if (this.#openedWallets.has(walletMeta.id)) return this.#openedWallets.get(walletMeta.id)!

const {id, walletImplementationId, networkId} = walletMeta
const walletFactory = getWalletFactory({networkId, implementationId: walletImplementationId})

const wallet = await walletFactory.restore({
storage: this.walletsRootStorage.join(`${id}/`),
storage: this.#walletsRootStorage.join(`${id}/`),
walletMeta,
})

Expand All @@ -205,15 +231,15 @@ export class WalletManager {

async removeWallet(id: string) {
const deletedWalletIds = await this.deletedWalletIds()
this.openedWallets.delete(id)
await this.rootStorage.setItem('deletedWalletIds', [...deletedWalletIds, id])
this.#openedWallets.delete(id)
await this.#rootStorage.setItem('deletedWalletIds', [...deletedWalletIds, id])
}

// TODO(ppershing): how should we deal with race conditions?
async _updateMetadata(id: string, newMeta: {isEasyConfirmationEnabled: boolean}) {
const walletMeta = await this.walletsRootStorage.getItem(id, parseWalletMeta)
const walletMeta = await this.#walletsRootStorage.getItem(id, parseWalletMeta)
const merged = {...walletMeta, ...newMeta}
return this.walletsRootStorage.setItem(id, merged)
return this.#walletsRootStorage.setItem(id, merged)
}

async updateHWDeviceInfo(wallet: YoroiWallet, hwDeviceInfo: HWDeviceInfo) {
Expand All @@ -232,7 +258,7 @@ export class WalletManager {
) {
const walletFactory = getWalletFactory({networkId, implementationId})
const id = uuid.v4()
const storage = this.walletsRootStorage.join(`${id}/`)
const storage = this.#walletsRootStorage.join(`${id}/`)

const wallet = await walletFactory.create({
storage,
Expand All @@ -255,7 +281,7 @@ export class WalletManager {
) {
const walletFactory = getWalletFactory({networkId, implementationId})
const id = uuid.v4()
const storage = this.walletsRootStorage.join(`${id}/`)
const storage = this.#walletsRootStorage.join(`${id}/`)

const wallet = await walletFactory.createBip44({
storage,
Expand Down

0 comments on commit 463372a

Please sign in to comment.