Skip to content

Commit

Permalink
refactor(wallet-mobile): network api to include get best block
Browse files Browse the repository at this point in the history
  • Loading branch information
stackchain committed Sep 18, 2024
1 parent ddc4bd1 commit 8a86947
Show file tree
Hide file tree
Showing 21 changed files with 215 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import * as React from 'react'

import {governaceAfterBlock} from '../../../../kernel/config'
import {YoroiWallet} from '../../../../yoroi-wallets/cardano/types'
import {useStakingKey, useTipStatus} from '../../../../yoroi-wallets/hooks'
import {useStakingKey} from '../../../../yoroi-wallets/hooks'
import {CardanoMobile} from '../../../../yoroi-wallets/wallets'
import {useBestBlock} from '../../../WalletManager/common/hooks/useBestBlock'
import {useSelectedWallet} from '../../../WalletManager/common/hooks/useSelectedWallet'
import {GovernanceVote} from '../types'

Expand All @@ -30,7 +31,7 @@ export const mapStakingKeyStateToGovernanceAction = (state: StakingKeyState): Go
}

export const useIsGovernanceFeatureEnabled = (wallet: YoroiWallet) => {
const {bestBlock} = useTipStatus({wallet, options: {suspense: true}})
const bestBlock = useBestBlock({options: {suspense: true}})
return bestBlock.height >= governaceAfterBlock[wallet.networkManager.network]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ import {useModal} from '../../../../components/Modal/ModalContext'
import {Text} from '../../../../components/Text'
import {isEmptyString} from '../../../../kernel/utils'
import {MultiToken} from '../../../../yoroi-wallets/cardano/MultiToken'
import {CardanoTypes, YoroiWallet} from '../../../../yoroi-wallets/cardano/types'
import {useTipStatus, useTransactionInfos} from '../../../../yoroi-wallets/hooks'
import {CardanoTypes} from '../../../../yoroi-wallets/cardano/types'
import {useTransactionInfos} from '../../../../yoroi-wallets/hooks'
import {TransactionInfo} from '../../../../yoroi-wallets/types/other'
import {formatDateAndTime, formatTokenWithSymbol} from '../../../../yoroi-wallets/utils/format'
import {asQuantity} from '../../../../yoroi-wallets/utils/utils'
import {usePrivacyMode} from '../../../Settings/PrivacyMode/PrivacyMode'
import {useBestBlock} from '../../../WalletManager/common/hooks/useBestBlock'
import {useSelectedWallet} from '../../../WalletManager/common/hooks/useSelectedWallet'
import {useWalletManager} from '../../../WalletManager/context/WalletManagerProvider'
import {messages, useStrings} from '../../common/strings'
Expand Down Expand Up @@ -173,7 +174,7 @@ export const TxDetails = () => {
</View>

<Boundary loading={{size: 'small'}}>
<Confirmations transaction={transaction} wallet={wallet} />
<Confirmations transaction={transaction} />
</Boundary>

<Label>{strings.transactionId}</Label>
Expand Down Expand Up @@ -201,18 +202,17 @@ export const TxDetails = () => {
)
}

const Confirmations = ({transaction, wallet}: {transaction: TransactionInfo; wallet: YoroiWallet}) => {
const Confirmations = ({transaction}: {transaction: TransactionInfo}) => {
const strings = useStrings()
const tipStatus = useTipStatus({
wallet,
const bestBlock = useBestBlock({
options: {
refetchInterval: 5000,
refetchInterval: 5_000,
},
})

return (
<Text secondary>
{strings.confirmations(transaction.blockNumber === 0 ? 0 : tipStatus.bestBlock.height - transaction.blockNumber)}
{strings.confirmations(transaction.blockNumber === 0 ? 0 : bestBlock.height - transaction.blockNumber)}
</Text>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {Chain} from '@yoroi/types'
import {useQuery, UseQueryOptions} from 'react-query'

import {useSelectedNetwork} from './useSelectedNetwork'

export const useBestBlock = ({options}: {options?: UseQueryOptions<Chain.Cardano.BestBlock, Error>}) => {
const {networkManager, network} = useSelectedNetwork()
const query = useQuery<Chain.Cardano.BestBlock, Error>({
suspense: true,
staleTime: 10_000,
retry: 3,
retryDelay: 1_000,
queryKey: [network, 'tipStatus'],
queryFn: () => networkManager.api.bestBlock(),
...options,
})

if (!query.data) throw new Error('Failed to retrive tipStatus')

return query.data
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {CardanoApi} from '@yoroi/api'
import {mountAsyncStorage, mountMMKVStorage, observableStorageMaker} from '@yoroi/common'
import {explorerManager} from '@yoroi/explorers'
import {createPrimaryTokenInfo} from '@yoroi/portfolio'
import {Chain, Network} from '@yoroi/types'
import {Api, Chain, Network} from '@yoroi/types'
import {freeze} from 'immer'

import {logger} from '../../../kernel/logger/logger'
Expand Down Expand Up @@ -128,22 +128,25 @@ export const networkConfigs: Readonly<Record<Chain.SupportedNetworks, Readonly<N

export function buildNetworkManagers({
tokenManagers,
apiMaker = CardanoApi.cardanoApiMaker,
}: {
tokenManagers: NetworkTokenManagers
apiMaker?: ({network}: {network: Chain.SupportedNetworks}) => Api.Cardano.Api
}): Readonly<Record<Chain.SupportedNetworks, Network.Manager>> {
const managers = Object.entries(networkConfigs).reduce<Record<Chain.SupportedNetworks, Network.Manager>>(
(networkManagers, [network, config]) => {
const tokenManager = tokenManagers[network as Chain.SupportedNetworks]
const networkRootStorage = mountMMKVStorage({path: `/`, id: `${network}.manager.v1`})
const rootStorage = observableStorageMaker(networkRootStorage)
const legacyRootStorage = observableStorageMaker(mountAsyncStorage({path: `/legacy/${network}/v1/`}))
const {getProtocolParams} = CardanoApi.cardanoApiMaker({network: config.network})
const {getProtocolParams, getBestBlock} = apiMaker({network: config.network})
const api = {
protocolParams: () =>
getProtocolParams().catch((error) => {
logger.error(`networkManager: ${network} protocolParams has failed, using hardcoded`, {error})
return Promise.resolve(protocolParamsPlaceholder)
}),
bestBlock: getBestBlock,
}

const info = dateToEpochInfo(config.eras)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import type {
FundInfoResponse,
PoolInfoRequest,
RawUtxo,
TipStatusResponse,
Transaction,
TxStatusRequest,
TxStatusResponse,
Expand Down Expand Up @@ -1086,10 +1085,6 @@ export const makeCardanoWallet = (networkManager: Network.Manager, implementatio
return legacyApi.fetchTxStatus(request, networkManager.legacyApiBaseUrl)
}

async fetchTipStatus(): Promise<TipStatusResponse> {
return legacyApi.getTipStatus(networkManager.legacyApiBaseUrl)
}

private isInitialized = false

private subscriptions: Array<WalletSubscription> = []
Expand Down
3 changes: 0 additions & 3 deletions apps/wallet-mobile/src/yoroi-wallets/cardano/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {WalletEncryptedStorage} from '../../kernel/storage/EncryptedStorage'
import type {
FundInfoResponse,
RawUtxo,
TipStatusResponse,
TransactionInfo,
TxStatusRequest,
TxStatusResponse,
Expand Down Expand Up @@ -155,7 +154,6 @@ export interface YoroiWallet {
saveMemo(txId: string, memo: string): Promise<void>
get transactions(): Record<string, TransactionInfo>
get confirmationCounts(): Record<string, null | number>
fetchTipStatus(): Promise<TipStatusResponse>
fetchTxStatus(request: TxStatusRequest): Promise<TxStatusResponse>

// Utxos
Expand Down Expand Up @@ -232,7 +230,6 @@ const yoroiWalletKeys: Array<keyof YoroiWallet> = [
// Balances, TxDetails
'transactions',
'confirmationCounts',
'fetchTipStatus',
'fetchTxStatus',

// Other
Expand Down
26 changes: 1 addition & 25 deletions apps/wallet-mobile/src/yoroi-wallets/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {logger} from '../../kernel/logger/logger'
import {deriveAddressFromXPub} from '../cardano/account-manager/derive-address-from-xpub'
import {getSpendingKey, getStakingKey} from '../cardano/addressInfo/addressInfo'
import {WalletEvent, YoroiWallet} from '../cardano/types'
import {TipStatusResponse, TRANSACTION_DIRECTION, TRANSACTION_STATUS, TxSubmissionStatus} from '../types/other'
import {TRANSACTION_DIRECTION, TRANSACTION_STATUS, TxSubmissionStatus} from '../types/other'
import {YoroiSignedTx, YoroiUnsignedTx} from '../types/yoroi'
import {delay} from '../utils/timeUtils'
import {Utxos} from '../utils/utils'
Expand Down Expand Up @@ -547,30 +547,6 @@ const fetchTxStatus = async (
}
}

// TODO: tipStatus is a network responsability
export const useTipStatus = ({
wallet,
options,
}: {
wallet: YoroiWallet
options?: UseQueryOptions<TipStatusResponse, Error>
}) => {
const {network} = useSelectedNetwork()
const query = useQuery<TipStatusResponse, Error>({
suspense: true,
staleTime: 10000,
retry: 3,
retryDelay: 1000,
queryKey: [network, 'tipStatus'],
queryFn: () => wallet.fetchTipStatus(),
...options,
})

if (!query.data) throw new Error('Failed to retrive tipStatus')

return query.data
}

export const useBalances = (wallet: YoroiWallet): Balance.Amounts => {
const utxos = useUtxos(wallet)

Expand Down
19 changes: 0 additions & 19 deletions apps/wallet-mobile/src/yoroi-wallets/mocks/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,25 +188,6 @@ const wallet: YoroiWallet = {
action('fetchTxStatus')(...args)
return {}
},
fetchTipStatus: async (...args: unknown[]) => {
action('fetchTipStatus')(...args)
return Promise.resolve({
bestBlock: {
epoch: 210,
slot: 76027,
globalSlot: 60426427,
hash: '2cf5a471a0c58cbc22534a0d437fbd91576ef10b98eea7ead5887e28f7a4fed8',
height: 3617708,
},
safeBlock: {
epoch: 210,
slot: 75415,
globalSlot: 60425815,
hash: 'ca18a2b607411dd18fbb2c1c0e653ec8a6a3f794f46ce050b4a07cf8ba4ab916',
height: 3617698,
},
})
},
submitTransaction: () => {
throw new Error('Not implemented: submitTransaction')
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
"start": {
"line": 45,
"column": 9,
"index": 1191
"index": 1205
},
"end": {
"line": 48,
"column": 3,
"index": 1281
"index": 1295
}
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
"defaultMessage": "!!!Go to Staking Center",
"file": "src/legacy/Dashboard/Dashboard.tsx",
"start": {
"line": 229,
"line": 238,
"column": 23,
"index": 7486
"index": 7594
},
"end": {
"line": 232,
"line": 241,
"column": 3,
"index": 7619
"index": 7727
}
}
]
1 change: 1 addition & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@
"peerDependencies": {
"@yoroi/common": "^1.5.4",
"axios": "^1.5.0",
"immer": "^10.0.3",
"react": ">= 16.8.0 <= 19.0.0",
"react-query": "^3.39.3",
"zod": "^3.22.1"
Expand Down
9 changes: 9 additions & 0 deletions packages/api/src/cardano/api/best-block.mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {Chain} from '@yoroi/types'

export const bestBlockMockResponse: Chain.Cardano.BestBlock = {
epoch: 510,
slot: 130081,
globalSlot: 135086881,
hash: 'ab0093eb78bcb0146355741388632eb50c69407df8fa32de85e5f198d725e8f4',
height: 10850697,
}
79 changes: 79 additions & 0 deletions packages/api/src/cardano/api/best-block.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {getBestBlock, isBestBlock} from './best-block'
import {bestBlockMockResponse} from './best-block.mocks'
import {fetcher, Fetcher} from '@yoroi/common'
import axios from 'axios'

jest.mock('axios')
const mockedAxios = axios as jest.MockedFunction<typeof axios>

describe('getBestBlock', () => {
const baseUrl = 'https://localhost'
const mockFetch = jest.fn()
const customFetcher: Fetcher = jest
.fn()
.mockResolvedValue(bestBlockMockResponse)

it('returns parsed data when response is valid', async () => {
mockFetch.mockResolvedValue(bestBlockMockResponse)
const tipStatus = getBestBlock(baseUrl, mockFetch)
const result = await tipStatus()
expect(result).toEqual(bestBlockMockResponse)
})

it('throws an error if response is invalid', async () => {
mockFetch.mockResolvedValue(null)
const tipStatus = getBestBlock(baseUrl, mockFetch)
await expect(tipStatus()).rejects.toThrow('Invalid best block response')
})

it('rejects when response data fails validation', async () => {
const invalidResponse = {unexpectedField: 'invalid data'}
mockFetch.mockResolvedValue(invalidResponse)
const tipStatus = getBestBlock(baseUrl, mockFetch)

await expect(tipStatus()).rejects.toThrow('Invalid best block response')
})

it('uses a custom fetcher function', async () => {
const tipStatus = getBestBlock(baseUrl, customFetcher)
const result = await tipStatus()
expect(customFetcher).toHaveBeenCalled()
expect(result).toEqual(bestBlockMockResponse)

// coverage
const tipStatus2 = getBestBlock(baseUrl)
expect(tipStatus2).toBeDefined()
})

it('uses fetcher and returns data on successful fetch', async () => {
mockedAxios.mockResolvedValue({data: bestBlockMockResponse})
const tipStatus = getBestBlock(baseUrl, fetcher)
const result = await tipStatus()

expect(mockedAxios).toHaveBeenCalled()
expect(result).toEqual(bestBlockMockResponse)
})

it('throws an error on network issues', async () => {
const networkError = new Error('Network Error')
mockFetch.mockRejectedValue(networkError)
const tipStatus = getBestBlock(baseUrl, mockFetch)
await expect(tipStatus()).rejects.toThrow(networkError.message)
})
})

describe('isBestBlock', () => {
it('returns true for a valid best block response', () => {
expect(isBestBlock(bestBlockMockResponse)).toBe(true)
})

it('returns false for an invalid best block response', () => {
const invalidResponse = {...bestBlockMockResponse, epoch: 'invalid'}
expect(isBestBlock(invalidResponse)).toBe(false)
})

it('returns false for an incomplete best block response', () => {
const incompleteResponse = {bestBlock: {epoch: 1}} // Missing fields
expect(isBestBlock(incompleteResponse)).toBe(false)
})
})
Loading

0 comments on commit 8a86947

Please sign in to comment.