diff --git a/apps/wallet-mobile/src/features/Exchange/common/ShowBuyBanner/ShowBuyBanner.tsx b/apps/wallet-mobile/src/features/Exchange/common/ShowBuyBanner/ShowBuyBanner.tsx
index be5d3b5c2e..69e03bbcc8 100644
--- a/apps/wallet-mobile/src/features/Exchange/common/ShowBuyBanner/ShowBuyBanner.tsx
+++ b/apps/wallet-mobile/src/features/Exchange/common/ShowBuyBanner/ShowBuyBanner.tsx
@@ -1,7 +1,8 @@
import {Chain} from '@yoroi/types'
+import _ from 'lodash'
import * as React from 'react'
-import {useBalances} from '../../../../yoroi-wallets/hooks'
+import {useBalances, useTransactionInfos} from '../../../../yoroi-wallets/hooks'
import {Amounts, Quantities} from '../../../../yoroi-wallets/utils/utils'
import {useSelectedWallet} from '../../../WalletManager/common/hooks/useSelectedWallet'
import {useWalletManager} from '../../../WalletManager/context/WalletManagerProvider'
@@ -14,19 +15,21 @@ import {SanchonetFaucetBanner} from './SanchonetFaucetBanner'
export const ShowBuyBanner = () => {
const {wallet} = useSelectedWallet()
+ const transactionInfos = useTransactionInfos({wallet})
const {
selected: {network},
} = useWalletManager()
const balances = useBalances(wallet)
const primaryAmount = Amounts.getAmount(balances, wallet.portfolioPrimaryTokenInfo.id)
const hasZeroPt = Quantities.isZero(primaryAmount.quantity)
+ const hasZeroTx = _.isEmpty(transactionInfos)
const showSmallBanner = useShowBuyBannerSmall()
const {resetShowBuyBannerSmall} = useResetShowBuyBannerSmall()
- if (hasZeroPt && network === Chain.Network.Preprod) return
- if (hasZeroPt && network === Chain.Network.Sancho) return
- if (hasZeroPt) return
+ if (hasZeroPt && hasZeroTx && network === Chain.Network.Preprod) return
+ if (hasZeroPt && hasZeroTx && network === Chain.Network.Sancho) return
+ if (hasZeroPt && hasZeroTx) return
if (showSmallBanner) return
return null
diff --git a/apps/wallet-mobile/src/features/Staking/Governance/common/helpers.ts b/apps/wallet-mobile/src/features/Staking/Governance/common/helpers.ts
index d740e5319e..304f619fbb 100644
--- a/apps/wallet-mobile/src/features/Staking/Governance/common/helpers.ts
+++ b/apps/wallet-mobile/src/features/Staking/Governance/common/helpers.ts
@@ -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'
@@ -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]
}
diff --git a/apps/wallet-mobile/src/features/Transactions/useCases/TxDetails/TxDetails.tsx b/apps/wallet-mobile/src/features/Transactions/useCases/TxDetails/TxDetails.tsx
index e531f46800..fd582c9dbc 100644
--- a/apps/wallet-mobile/src/features/Transactions/useCases/TxDetails/TxDetails.tsx
+++ b/apps/wallet-mobile/src/features/Transactions/useCases/TxDetails/TxDetails.tsx
@@ -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'
@@ -173,7 +174,7 @@ export const TxDetails = () => {
-
+
@@ -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 (
- {strings.confirmations(transaction.blockNumber === 0 ? 0 : tipStatus.bestBlock.height - transaction.blockNumber)}
+ {strings.confirmations(transaction.blockNumber === 0 ? 0 : bestBlock.height - transaction.blockNumber)}
)
}
diff --git a/apps/wallet-mobile/src/features/WalletManager/common/hooks/useBestBlock.tsx b/apps/wallet-mobile/src/features/WalletManager/common/hooks/useBestBlock.tsx
new file mode 100644
index 0000000000..ca667fc087
--- /dev/null
+++ b/apps/wallet-mobile/src/features/WalletManager/common/hooks/useBestBlock.tsx
@@ -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}) => {
+ const {networkManager, network} = useSelectedNetwork()
+ const query = useQuery({
+ 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
+}
diff --git a/apps/wallet-mobile/src/features/WalletManager/network-manager/network-manager.ts b/apps/wallet-mobile/src/features/WalletManager/network-manager/network-manager.ts
index e8329e1862..d5098f604d 100644
--- a/apps/wallet-mobile/src/features/WalletManager/network-manager/network-manager.ts
+++ b/apps/wallet-mobile/src/features/WalletManager/network-manager/network-manager.ts
@@ -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'
@@ -128,8 +128,10 @@ export const networkConfigs: Readonly Api.Cardano.Api
}): Readonly> {
const managers = Object.entries(networkConfigs).reduce>(
(networkManagers, [network, config]) => {
@@ -137,13 +139,14 @@ export function buildNetworkManagers({
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)
diff --git a/apps/wallet-mobile/src/legacy/Dashboard/Dashboard.tsx b/apps/wallet-mobile/src/legacy/Dashboard/Dashboard.tsx
index cc27a4ec46..4fdb662192 100644
--- a/apps/wallet-mobile/src/legacy/Dashboard/Dashboard.tsx
+++ b/apps/wallet-mobile/src/legacy/Dashboard/Dashboard.tsx
@@ -5,7 +5,15 @@ import {useTheme} from '@yoroi/theme'
import BigNumber from 'bignumber.js'
import React from 'react'
import {defineMessages, useIntl} from 'react-intl'
-import {ActivityIndicator, RefreshControl, ScrollView, StyleSheet, View, ViewProps} from 'react-native'
+import {
+ ActivityIndicator,
+ RefreshControl,
+ ScrollView,
+ StyleSheet,
+ useWindowDimensions,
+ View,
+ ViewProps,
+} from 'react-native'
import {SafeAreaView} from 'react-native-safe-area-context'
import {Banner} from '../../components/Banner/Banner'
@@ -44,6 +52,7 @@ export const Dashboard = () => {
const {isLoading: isSyncing, sync} = useSync(wallet)
const isOnline = useIsOnline(wallet)
const {openModal, closeModal} = useModal()
+ const {height: windowHeight} = useWindowDimensions()
const balances = useBalances(wallet)
const primaryAmount = Amounts.getAmount(balances, wallet.portfolioPrimaryTokenInfo.id)
@@ -71,7 +80,7 @@ export const Dashboard = () => {
openModal(
'',
resetToTxHistory()} onCancel={() => closeModal()} />,
- 450,
+ windowHeight * 0.8,
)
}
diff --git a/apps/wallet-mobile/src/yoroi-wallets/cardano/cardano-wallet.ts b/apps/wallet-mobile/src/yoroi-wallets/cardano/cardano-wallet.ts
index 3cf22f7670..dae44280f1 100644
--- a/apps/wallet-mobile/src/yoroi-wallets/cardano/cardano-wallet.ts
+++ b/apps/wallet-mobile/src/yoroi-wallets/cardano/cardano-wallet.ts
@@ -30,7 +30,6 @@ import type {
FundInfoResponse,
PoolInfoRequest,
RawUtxo,
- TipStatusResponse,
Transaction,
TxStatusRequest,
TxStatusResponse,
@@ -1086,10 +1085,6 @@ export const makeCardanoWallet = (networkManager: Network.Manager, implementatio
return legacyApi.fetchTxStatus(request, networkManager.legacyApiBaseUrl)
}
- async fetchTipStatus(): Promise {
- return legacyApi.getTipStatus(networkManager.legacyApiBaseUrl)
- }
-
private isInitialized = false
private subscriptions: Array = []
diff --git a/apps/wallet-mobile/src/yoroi-wallets/cardano/types.ts b/apps/wallet-mobile/src/yoroi-wallets/cardano/types.ts
index cb3875cfcf..d80576eb94 100644
--- a/apps/wallet-mobile/src/yoroi-wallets/cardano/types.ts
+++ b/apps/wallet-mobile/src/yoroi-wallets/cardano/types.ts
@@ -17,7 +17,6 @@ import {WalletEncryptedStorage} from '../../kernel/storage/EncryptedStorage'
import type {
FundInfoResponse,
RawUtxo,
- TipStatusResponse,
TransactionInfo,
TxStatusRequest,
TxStatusResponse,
@@ -155,7 +154,6 @@ export interface YoroiWallet {
saveMemo(txId: string, memo: string): Promise
get transactions(): Record
get confirmationCounts(): Record
- fetchTipStatus(): Promise
fetchTxStatus(request: TxStatusRequest): Promise
// Utxos
@@ -232,7 +230,6 @@ const yoroiWalletKeys: Array = [
// Balances, TxDetails
'transactions',
'confirmationCounts',
- 'fetchTipStatus',
'fetchTxStatus',
// Other
diff --git a/apps/wallet-mobile/src/yoroi-wallets/hooks/index.ts b/apps/wallet-mobile/src/yoroi-wallets/hooks/index.ts
index 09a0b1f1cb..e53127cda5 100644
--- a/apps/wallet-mobile/src/yoroi-wallets/hooks/index.ts
+++ b/apps/wallet-mobile/src/yoroi-wallets/hooks/index.ts
@@ -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'
@@ -562,30 +562,6 @@ const fetchTxStatus = async (
}
}
-// TODO: tipStatus is a network responsability
-export const useTipStatus = ({
- wallet,
- options,
-}: {
- wallet: YoroiWallet
- options?: UseQueryOptions
-}) => {
- const {network} = useSelectedNetwork()
- const query = useQuery({
- 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)
diff --git a/apps/wallet-mobile/src/yoroi-wallets/mocks/wallet.ts b/apps/wallet-mobile/src/yoroi-wallets/mocks/wallet.ts
index e8e25096da..92cb84b8f6 100644
--- a/apps/wallet-mobile/src/yoroi-wallets/mocks/wallet.ts
+++ b/apps/wallet-mobile/src/yoroi-wallets/mocks/wallet.ts
@@ -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')
},
diff --git a/apps/wallet-mobile/translations/messages/src/legacy/Dashboard/Dashboard.json b/apps/wallet-mobile/translations/messages/src/legacy/Dashboard/Dashboard.json
index 41e949cef2..ffe11d7ece 100644
--- a/apps/wallet-mobile/translations/messages/src/legacy/Dashboard/Dashboard.json
+++ b/apps/wallet-mobile/translations/messages/src/legacy/Dashboard/Dashboard.json
@@ -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
}
}
]
\ No newline at end of file
diff --git a/packages/api/package.json b/packages/api/package.json
index 7d5f550cff..0ab222dd75 100644
--- a/packages/api/package.json
+++ b/packages/api/package.json
@@ -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"
diff --git a/packages/api/src/cardano/api/best-block.mocks.ts b/packages/api/src/cardano/api/best-block.mocks.ts
new file mode 100644
index 0000000000..eb830df8f0
--- /dev/null
+++ b/packages/api/src/cardano/api/best-block.mocks.ts
@@ -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,
+}
diff --git a/packages/api/src/cardano/api/best-block.test.ts b/packages/api/src/cardano/api/best-block.test.ts
new file mode 100644
index 0000000000..9ff006e25d
--- /dev/null
+++ b/packages/api/src/cardano/api/best-block.test.ts
@@ -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
+
+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)
+ })
+})
diff --git a/packages/api/src/cardano/api/best-block.ts b/packages/api/src/cardano/api/best-block.ts
new file mode 100644
index 0000000000..7ab19a0b52
--- /dev/null
+++ b/packages/api/src/cardano/api/best-block.ts
@@ -0,0 +1,39 @@
+import {z} from 'zod'
+import {createTypeGuardFromSchema, fetcher, Fetcher} from '@yoroi/common'
+import {Api} from '@yoroi/types'
+
+export const getBestBlock =
+ (baseUrl: string, request: Fetcher = fetcher) =>
+ async (): Promise => {
+ return request({
+ url: `${baseUrl}/bestblock`,
+ data: undefined,
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Response-Type': 'application/json',
+ },
+ }).then((response: Api.Cardano.BestBlock) => {
+ const parsedResponse = parseBestBlock(response)
+
+ if (!parsedResponse)
+ return Promise.reject(new Error('Invalid best block response'))
+ return Promise.resolve(parsedResponse)
+ })
+ }
+
+export const parseBestBlock = (
+ data: Api.Cardano.BestBlock,
+): Api.Cardano.BestBlock | undefined => {
+ return isBestBlock(data) ? data : undefined
+}
+
+const BestBlockSchema = z.object({
+ epoch: z.number(),
+ slot: z.number(),
+ globalSlot: z.number(),
+ hash: z.string(),
+ height: z.number(),
+})
+
+export const isBestBlock = createTypeGuardFromSchema(BestBlockSchema)
diff --git a/packages/api/src/cardano/api/cardano-api-maker.mocks.ts b/packages/api/src/cardano/api/cardano-api-maker.mocks.ts
index 972c7019a2..c01d45d752 100644
--- a/packages/api/src/cardano/api/cardano-api-maker.mocks.ts
+++ b/packages/api/src/cardano/api/cardano-api-maker.mocks.ts
@@ -1,6 +1,7 @@
/* istanbul ignore file */
import {Api} from '@yoroi/types'
import {paramsMockResponse} from './protocol-params.mocks'
+import {bestBlockMockResponse} from './best-block.mocks'
const loading = () => new Promise(() => {})
const unknownError = () => Promise.reject(new Error('Unknown error'))
@@ -26,6 +27,18 @@ const getProtocolParams = {
},
}
+const getBestBlock = {
+ success: () => Promise.resolve(bestBlockMockResponse),
+ delayed: (timeout?: number) =>
+ delayedResponse({data: bestBlockMockResponse, timeout}),
+ empty: () => Promise.resolve({}),
+ loading,
+ error: {
+ unknown: unknownError,
+ },
+}
+
export const mockCardanoApi: Api.Cardano.Api = {
getProtocolParams: getProtocolParams.success,
+ getBestBlock: getBestBlock.success,
} as const
diff --git a/packages/api/src/cardano/api/cardano-api-maker.ts b/packages/api/src/cardano/api/cardano-api-maker.ts
index 0ed82ef9af..7e2ce201a1 100644
--- a/packages/api/src/cardano/api/cardano-api-maker.ts
+++ b/packages/api/src/cardano/api/cardano-api-maker.ts
@@ -1,7 +1,9 @@
import {Fetcher, fetcher} from '@yoroi/common'
+import {freeze} from 'immer'
+import {Api, Chain} from '@yoroi/types'
import {getProtocolParams as getProtocolParamsWrapper} from './protocol-params'
-import {Api, Chain} from '@yoroi/types'
+import {getBestBlock as getBestBlockWrapper} from './best-block'
import {API_ENDPOINTS} from './config'
export const cardanoApiMaker = ({
@@ -13,8 +15,10 @@ export const cardanoApiMaker = ({
}): Readonly => {
const baseUrl = API_ENDPOINTS[network].root
const getProtocolParams = getProtocolParamsWrapper(baseUrl, request)
+ const getBestBlock = getBestBlockWrapper(baseUrl, request)
- return {
+ return freeze({
getProtocolParams,
- } as const
+ getBestBlock,
+ } as const)
}
diff --git a/packages/api/src/cardano/api/config.ts b/packages/api/src/cardano/api/config.ts
index 3e4859d441..8417fd4924 100644
--- a/packages/api/src/cardano/api/config.ts
+++ b/packages/api/src/cardano/api/config.ts
@@ -1,8 +1,9 @@
import {Chain} from '@yoroi/types'
+import {freeze} from 'immer'
export const API_ENDPOINTS: Readonly<
Record
-> = {
+> = freeze({
[Chain.Network.Mainnet]: {
root: 'https://zero.yoroiwallet.com',
},
@@ -15,4 +16,4 @@ export const API_ENDPOINTS: Readonly<
[Chain.Network.Preview]: {
root: 'https://yoroi-backend-zero-preview.emurgornd.com',
},
-} as const
+} as const)
diff --git a/packages/api/src/cardano/api/protocol-params.mocks.ts b/packages/api/src/cardano/api/protocol-params.mocks.ts
index e6797d1a36..d11036027f 100644
--- a/packages/api/src/cardano/api/protocol-params.mocks.ts
+++ b/packages/api/src/cardano/api/protocol-params.mocks.ts
@@ -1,6 +1,6 @@
-import {Api} from '@yoroi/types'
+import {Chain} from '@yoroi/types'
-export const paramsMockResponse: Api.Cardano.ProtocolParams = {
+export const paramsMockResponse: Chain.Cardano.ProtocolParams = {
coinsPerUtxoByte: '4310',
keyDeposit: '2000000',
linearFee: {coefficient: '44', constant: '155381'},
diff --git a/packages/claim/src/manager.test.ts b/packages/claim/src/manager.test.ts
index 19b57cd9c0..777196bed0 100644
--- a/packages/claim/src/manager.test.ts
+++ b/packages/claim/src/manager.test.ts
@@ -1,5 +1,9 @@
-import {fetchData} from '@yoroi/common'
-import {tokenInfoMocks, tokenMocks} from '@yoroi/portfolio'
+import {cacheRecordMaker, fetchData} from '@yoroi/common'
+import {
+ createTokenManagerMock,
+ tokenInfoMocks,
+ tokenMocks,
+} from '@yoroi/portfolio'
import {Api, Portfolio, Scan} from '@yoroi/types'
import {claimManagerMaker} from './manager'
@@ -27,31 +31,24 @@ describe('claimManagerMaker - postClaimTokens', () => {
jest.clearAllMocks()
})
- const tokenManagerMock = {
- sync: jest.fn(),
- api: {
- tokenActivity: jest.fn(),
- tokenDiscovery: jest.fn(),
- tokenImageInvalidate: jest.fn(),
- tokenInfo: jest.fn(),
- tokenInfos: jest.fn(),
- tokenTraits: jest.fn(),
- },
- clear: jest.fn(),
- destroy: jest.fn(),
- hydrate: jest.fn(),
- subscribe: jest.fn(),
- unsubscribe: jest.fn(),
- observable$: {} as any,
- }
+ const tokenManagerMock = createTokenManagerMock()
tokenManagerMock.sync.mockResolvedValue(
new Map([
[
tokenMocks.nftCryptoKitty.info.id,
- {record: tokenMocks.nftCryptoKitty.info},
+ cacheRecordMaker(
+ {expires: Date.now() + 3_600_000, hash: 'hash3'},
+ tokenMocks.nftCryptoKitty.info,
+ ),
+ ],
+ [
+ tokenMocks.rnftWhatever.info.id,
+ cacheRecordMaker(
+ {expires: Date.now() + 3_600_000, hash: 'hash3'},
+ tokenMocks.rnftWhatever.info,
+ ),
],
- [tokenMocks.rnftWhatever.info.id, {record: tokenMocks.rnftWhatever.info}],
]),
)
diff --git a/packages/claim/src/transformers.test.ts b/packages/claim/src/transformers.test.ts
index 2c81677982..386fa4941b 100644
--- a/packages/claim/src/transformers.test.ts
+++ b/packages/claim/src/transformers.test.ts
@@ -1,27 +1,12 @@
import {Api, Claim, Portfolio} from '@yoroi/types'
-import {tokenMocks} from '@yoroi/portfolio'
+import {tokenMocks, createTokenManagerMock} from '@yoroi/portfolio'
import {asClaimApiError, asClaimToken} from './transformers'
import {claimFaucetResponses} from './api-faucet.mocks'
import {claimApiMockResponses} from './manager.mocks'
+import {cacheRecordMaker} from '@yoroi/common'
-const tokenManagerMock = {
- sync: jest.fn(),
- api: {
- tokenActivity: jest.fn(),
- tokenDiscovery: jest.fn(),
- tokenImageInvalidate: jest.fn(),
- tokenInfo: jest.fn(),
- tokenInfos: jest.fn(),
- tokenTraits: jest.fn(),
- },
- clear: jest.fn(),
- destroy: jest.fn(),
- hydrate: jest.fn(),
- subscribe: jest.fn(),
- unsubscribe: jest.fn(),
- observable$: {} as any,
-}
+const tokenManagerMock = createTokenManagerMock()
describe('asClaimApiError', () => {
afterEach(() => {
@@ -65,11 +50,17 @@ describe('asClaimToken', () => {
new Map([
[
tokenMocks.nftCryptoKitty.info.id,
- {record: tokenMocks.nftCryptoKitty.info},
+ cacheRecordMaker(
+ {expires: Date.now() + 3_600_000, hash: 'hash3'},
+ tokenMocks.nftCryptoKitty.info,
+ ),
],
[
tokenMocks.rnftWhatever.info.id,
- {record: tokenMocks.rnftWhatever.info},
+ cacheRecordMaker(
+ {expires: Date.now() + 3_600_000, hash: 'hash3'},
+ tokenMocks.rnftWhatever.info,
+ ),
],
]),
)
@@ -98,11 +89,17 @@ describe('asClaimToken', () => {
new Map([
[
tokenMocks.nftCryptoKitty.info.id,
- {record: tokenMocks.nftCryptoKitty.info},
+ cacheRecordMaker(
+ {expires: Date.now() + 3_600_000, hash: 'hash3'},
+ tokenMocks.nftCryptoKitty.info,
+ ),
],
[
tokenMocks.rnftWhatever.info.id,
- {record: tokenMocks.rnftWhatever.info},
+ cacheRecordMaker(
+ {expires: Date.now() + 3_600_000, hash: 'hash3'},
+ tokenMocks.rnftWhatever.info,
+ ),
],
]),
)
@@ -123,11 +120,17 @@ describe('asClaimToken', () => {
new Map([
[
tokenMocks.nftCryptoKitty.info.id,
- {record: tokenMocks.nftCryptoKitty.info},
+ cacheRecordMaker(
+ {expires: Date.now() + 3_600_000, hash: 'hash3'},
+ tokenMocks.nftCryptoKitty.info,
+ ),
],
[
tokenMocks.rnftWhatever.info.id,
- {record: tokenMocks.rnftWhatever.info},
+ cacheRecordMaker(
+ {expires: Date.now() + 3_600_000, hash: 'hash3'},
+ tokenMocks.rnftWhatever.info,
+ ),
],
]),
)
@@ -156,13 +159,19 @@ describe('asClaimToken', () => {
new Map([
[
tokenMocks.nftCryptoKitty.info.id,
- {record: tokenMocks.nftCryptoKitty.info},
+ cacheRecordMaker(
+ {expires: Date.now() + 3_600_000, hash: 'hash3'},
+ tokenMocks.nftCryptoKitty.info,
+ ),
],
[
tokenMocks.rnftWhatever.info.id,
- {record: tokenMocks.rnftWhatever.info},
+ cacheRecordMaker(
+ {expires: Date.now() + 3_600_000, hash: 'hash3'},
+ tokenMocks.rnftWhatever.info,
+ ),
],
- ['invalid.', undefined],
+ ['invalid.', undefined] as any,
['dead.', {record: tokenMocks.rnftWhatever.info}],
]),
)
diff --git a/packages/portfolio/src/adapters/dullahan-api/api-maker.mocks.ts b/packages/portfolio/src/adapters/dullahan-api/api-maker.mocks.ts
index b6d5ba795f..07892f19da 100644
--- a/packages/portfolio/src/adapters/dullahan-api/api-maker.mocks.ts
+++ b/packages/portfolio/src/adapters/dullahan-api/api-maker.mocks.ts
@@ -5,6 +5,7 @@ import {Api, Portfolio} from '@yoroi/types'
import {tokenMocks} from '../token.mocks'
import {tokenTraitsMocks} from '../token-traits.mocks'
import {tokenActivityMocks} from '../token-activity.mocks'
+import {tokenHistoryMocks} from '../token-history.mocks'
export const responseTokenDiscoveryMocks = asyncBehavior.maker<
Api.Response
@@ -59,6 +60,19 @@ export const responseTokenActivity = asyncBehavior.maker<
emptyRepresentation: null,
})
+export const responseTokenHistory = asyncBehavior.maker<
+ Api.Response
+>({
+ data: {
+ tag: 'right',
+ value: {
+ status: 200,
+ data: tokenHistoryMocks.api.responseDataOnly,
+ },
+ },
+ emptyRepresentation: null,
+})
+
export const responseTokenImageInvalidate = asyncBehavior.maker({
data: undefined,
emptyRepresentation: null,
@@ -70,6 +84,7 @@ const success: Portfolio.Api.Api = {
tokenInfos: responseTokenInfosMocks.success,
tokenTraits: responseTokenTraits.success,
tokenActivity: responseTokenActivity.success,
+ tokenHistory: responseTokenHistory.success,
tokenImageInvalidate: responseTokenImageInvalidate.success,
}
@@ -79,6 +94,7 @@ const delayed: Portfolio.Api.Api = {
tokenInfos: responseTokenInfosMocks.delayed,
tokenTraits: responseTokenTraits.delayed,
tokenActivity: responseTokenActivity.delayed,
+ tokenHistory: responseTokenHistory.delayed,
tokenImageInvalidate: responseTokenImageInvalidate.delayed,
}
@@ -88,6 +104,7 @@ const loading: Portfolio.Api.Api = {
tokenInfos: responseTokenInfosMocks.loading,
tokenTraits: responseTokenTraits.loading,
tokenActivity: responseTokenActivity.loading,
+ tokenHistory: responseTokenHistory.loading,
tokenImageInvalidate: responseTokenImageInvalidate.loading,
}
@@ -137,6 +154,15 @@ const error: Portfolio.Api.Api = {
responseData: {message: 'Bad Request'},
},
}),
+ tokenHistory: () =>
+ Promise.resolve({
+ tag: 'left',
+ error: {
+ status: 400,
+ message: 'Bad Request',
+ responseData: {message: 'Bad Request'},
+ },
+ }),
tokenImageInvalidate: () => Promise.resolve(undefined),
}
@@ -146,6 +172,7 @@ const empty: Portfolio.Api.Api = {
tokenInfos: responseTokenInfosMocks.empty,
tokenTraits: responseTokenTraits.empty,
tokenActivity: responseTokenActivity.empty,
+ tokenHistory: responseTokenHistory.empty,
tokenImageInvalidate: responseTokenImageInvalidate.empty,
}
diff --git a/packages/portfolio/src/adapters/dullahan-api/api-maker.test.ts b/packages/portfolio/src/adapters/dullahan-api/api-maker.test.ts
index cc43a58d37..48a510cdab 100644
--- a/packages/portfolio/src/adapters/dullahan-api/api-maker.test.ts
+++ b/packages/portfolio/src/adapters/dullahan-api/api-maker.test.ts
@@ -6,6 +6,7 @@ import {tokenDiscoveryMocks} from '../token-discovery.mocks'
import {tokenMocks} from '../token.mocks'
import {tokenActivityMocks} from '../token-activity.mocks'
import {tokenImageInvalidateMocks} from '../token-image-invalidate.mocks'
+import {tokenHistoryMocks} from '../token-history.mocks'
describe('portfolioApiMaker', () => {
const mockNetwork: Chain.Network = Chain.Network.Mainnet
@@ -29,6 +30,7 @@ describe('portfolioApiMaker', () => {
expect(api).toHaveProperty('tokenInfos')
expect(api).toHaveProperty('tokenTraits')
expect(api).toHaveProperty('tokenActivity')
+ expect(api).toHaveProperty('tokenHistory')
expect(api).toHaveProperty('tokenImageInvalidate')
})
@@ -44,6 +46,7 @@ describe('portfolioApiMaker', () => {
expect(api).toHaveProperty('tokenInfos')
expect(api).toHaveProperty('tokenTraits')
expect(api).toHaveProperty('tokenActivity')
+ expect(api).toHaveProperty('tokenHistory')
expect(api).toHaveProperty('tokenImageInvalidate')
})
@@ -51,7 +54,7 @@ describe('portfolioApiMaker', () => {
mockRequest.mockResolvedValue({
tag: 'right',
value: {
- status: 200,
+ status: Api.HttpStatusCode.Ok,
data: {},
},
})
@@ -76,8 +79,12 @@ describe('portfolioApiMaker', () => {
tokenActivityMocks.api.request,
Portfolio.Token.ActivityWindow.OneDay,
)
+ await api.tokenHistory(
+ tokenHistoryMocks.api.request.tokenId,
+ tokenHistoryMocks.api.request.period,
+ )
- expect(mockRequest).toHaveBeenCalledTimes(5)
+ expect(mockRequest).toHaveBeenCalledTimes(6)
expect(mockRequest).toHaveBeenCalledWith({
method: 'get',
@@ -132,13 +139,22 @@ describe('portfolioApiMaker', () => {
'Content-Type': 'application/json',
},
})
+ expect(mockRequest).toHaveBeenCalledWith({
+ method: 'post',
+ url: apiConfig[Chain.Network.Mainnet].tokenHistory,
+ data: tokenHistoryMocks.api.request,
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ })
})
it('should return error when returning data is malformed', async () => {
mockRequest.mockResolvedValue({
tag: 'right',
value: {
- status: 200,
+ status: Api.HttpStatusCode.Ok,
data: {
['wrong']: [200, 'data'],
},
@@ -256,7 +272,7 @@ describe('portfolioApiMaker', () => {
},
})
- const resultTokenActivityUpdates = await api.tokenActivity(
+ const resultTokenActivity = await api.tokenActivity(
tokenActivityMocks.api.request,
Portfolio.Token.ActivityWindow.OneDay,
)
@@ -273,7 +289,7 @@ describe('portfolioApiMaker', () => {
},
})
- expect(resultTokenActivityUpdates).toEqual({
+ expect(resultTokenActivity).toEqual({
tag: 'left',
error: {
status: -3,
@@ -283,13 +299,39 @@ describe('portfolioApiMaker', () => {
},
},
})
+
+ const resultTokenHistory = await api.tokenHistory(
+ tokenHistoryMocks.api.request.tokenId,
+ tokenHistoryMocks.api.request.period,
+ )
+ expect(mockRequest).toHaveBeenCalledTimes(6)
+ expect(mockRequest).toHaveBeenCalledWith({
+ method: 'post',
+ url: apiConfig[Chain.Network.Mainnet].tokenHistory,
+ data: tokenHistoryMocks.api.request,
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ })
+
+ expect(resultTokenHistory).toEqual({
+ tag: 'left',
+ error: {
+ status: -3,
+ message: 'Failed to transform token history response',
+ responseData: {
+ ['wrong']: [200, 'data'],
+ },
+ },
+ })
})
it('should return the error and not throw', async () => {
mockRequest.mockResolvedValue({
tag: 'left',
value: {
- status: 500,
+ status: Api.HttpStatusCode.InternalServerError,
message: 'Internal Server Error',
responseData: {},
},
@@ -310,7 +352,7 @@ describe('portfolioApiMaker', () => {
await expect(api.tokenInfos(mockTokenIdsWithCache)).resolves.toEqual({
tag: 'left',
value: {
- status: 500,
+ status: Api.HttpStatusCode.InternalServerError,
message: 'Internal Server Error',
responseData: {},
},
@@ -331,7 +373,7 @@ describe('portfolioApiMaker', () => {
).resolves.toEqual({
tag: 'left',
value: {
- status: 500,
+ status: Api.HttpStatusCode.InternalServerError,
message: 'Internal Server Error',
responseData: {},
},
@@ -354,7 +396,7 @@ describe('portfolioApiMaker', () => {
).resolves.toEqual({
tag: 'left',
value: {
- status: 500,
+ status: Api.HttpStatusCode.InternalServerError,
message: 'Internal Server Error',
responseData: {},
},
@@ -377,7 +419,7 @@ describe('portfolioApiMaker', () => {
).resolves.toEqual({
tag: 'left',
value: {
- status: 500,
+ status: Api.HttpStatusCode.InternalServerError,
message: 'Internal Server Error',
responseData: {},
},
@@ -403,7 +445,7 @@ describe('portfolioApiMaker', () => {
).resolves.toEqual({
tag: 'left',
value: {
- status: 500,
+ status: Api.HttpStatusCode.InternalServerError,
message: 'Internal Server Error',
responseData: {},
},
@@ -420,13 +462,37 @@ describe('portfolioApiMaker', () => {
'Content-Type': 'application/json',
},
})
+
+ await expect(
+ api.tokenHistory(
+ tokenHistoryMocks.api.request.tokenId,
+ tokenHistoryMocks.api.request.period,
+ ),
+ ).resolves.toEqual({
+ tag: 'left',
+ value: {
+ status: Api.HttpStatusCode.InternalServerError,
+ message: 'Internal Server Error',
+ responseData: {},
+ },
+ })
+ expect(mockRequest).toHaveBeenCalledTimes(6)
+ expect(mockRequest).toHaveBeenCalledWith({
+ method: 'post',
+ url: apiConfig[Chain.Network.Mainnet].tokenHistory,
+ data: tokenHistoryMocks.api.request,
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ })
})
it('should return the data on success (traits)', async () => {
mockRequest.mockResolvedValue({
tag: 'right',
value: {
- status: 200,
+ status: Api.HttpStatusCode.Ok,
data: tokenMocks.nftCryptoKitty.traits,
},
})
@@ -453,7 +519,7 @@ describe('portfolioApiMaker', () => {
expect(result).toEqual({
tag: 'right',
value: {
- status: 200,
+ status: Api.HttpStatusCode.Ok,
data: tokenMocks.nftCryptoKitty.traits,
},
})
@@ -463,7 +529,7 @@ describe('portfolioApiMaker', () => {
mockRequest.mockResolvedValue({
tag: 'right',
value: {
- status: 200,
+ status: Api.HttpStatusCode.Ok,
data: tokenMocks.nftCryptoKitty.info,
},
})
@@ -490,7 +556,7 @@ describe('portfolioApiMaker', () => {
expect(result).toEqual({
tag: 'right',
value: {
- status: 200,
+ status: Api.HttpStatusCode.Ok,
data: tokenMocks.nftCryptoKitty.info,
},
})
@@ -527,17 +593,52 @@ describe('portfolioApiMaker', () => {
expect(result).toEqual({
tag: 'right',
value: {
- status: 200,
+ status: Api.HttpStatusCode.Ok,
data: tokenActivityMocks.api.responseDataOnly,
},
})
})
+ it('should return the data on success (tokenHistory)', async () => {
+ mockRequest.mockResolvedValue(tokenHistoryMocks.api.responses.success)
+
+ const api = portfolioApiMaker({
+ network: mockNetwork,
+ request: mockRequest,
+ maxIdsPerRequest: 10,
+ maxConcurrentRequests: 10,
+ })
+
+ const result = await api.tokenHistory(
+ tokenHistoryMocks.api.request.tokenId,
+ tokenHistoryMocks.api.request.period,
+ )
+
+ expect(mockRequest).toHaveBeenCalledTimes(1)
+ expect(mockRequest).toHaveBeenCalledWith({
+ method: 'post',
+ url: apiConfig[Chain.Network.Mainnet].tokenHistory,
+ data: tokenHistoryMocks.api.request,
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ })
+
+ expect(result).toEqual({
+ tag: 'right',
+ value: {
+ status: Api.HttpStatusCode.Ok,
+ data: tokenHistoryMocks.api.responseDataOnly,
+ },
+ })
+ })
+
it('should return error when returning data is malformed token-discovery', async () => {
mockRequest.mockResolvedValue({
tag: 'right',
value: {
- status: 200,
+ status: Api.HttpStatusCode.Ok,
data: 0,
},
})
@@ -577,7 +678,7 @@ describe('portfolioApiMaker', () => {
mockRequest.mockResolvedValue({
tag: 'right',
value: {
- status: 200,
+ status: Api.HttpStatusCode.Ok,
data: {
...tokenDiscoveryMocks.nftCryptoKitty,
supply: undefined,
@@ -605,7 +706,7 @@ describe('portfolioApiMaker', () => {
mockRequest.mockResolvedValue({
tag: 'right',
value: {
- status: 200,
+ status: Api.HttpStatusCode.Ok,
data: tokenDiscoveryMocks.nftCryptoKitty,
},
})
@@ -616,7 +717,7 @@ describe('portfolioApiMaker', () => {
expect(right).toEqual({
tag: 'right',
value: {
- status: 200,
+ status: Api.HttpStatusCode.Ok,
data: tokenDiscoveryMocks.nftCryptoKitty,
},
})
@@ -626,7 +727,7 @@ describe('portfolioApiMaker', () => {
mockRequest.mockResolvedValue({
tag: 'right',
value: {
- status: 200,
+ status: Api.HttpStatusCode.Ok,
data: {},
},
})
diff --git a/packages/portfolio/src/adapters/dullahan-api/api-maker.ts b/packages/portfolio/src/adapters/dullahan-api/api-maker.ts
index f49cfac336..a101999e22 100644
--- a/packages/portfolio/src/adapters/dullahan-api/api-maker.ts
+++ b/packages/portfolio/src/adapters/dullahan-api/api-maker.ts
@@ -13,12 +13,14 @@ import {
toDullahanRequest,
toProcessedMediaRequest,
toSecondaryTokenInfos,
- toTokenActivityUpdates,
+ toTokenActivity,
+ toTokenHistory,
} from './transformers'
import {
DullahanApiCachedIdsRequest,
DullahanApiTokenActivityResponse,
DullahanApiTokenDiscoveryResponse,
+ DullahanApiTokenHistoryResponse,
DullahanApiTokenInfoResponse,
DullahanApiTokenInfosResponse,
DullahanApiTokenTraitsResponse,
@@ -267,7 +269,7 @@ export const portfolioApiMaker = ({
return firstError
try {
- const transformedResponseData = toTokenActivityUpdates(activities)
+ const transformedResponseData = toTokenActivity(activities)
const transformedResponse: Api.Response =
freeze(
@@ -297,6 +299,51 @@ export const portfolioApiMaker = ({
}
},
+ async tokenHistory(tokenId, period) {
+ const response = await request({
+ method: 'post',
+ url: config.tokenHistory,
+ data: {tokenId, period},
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ },
+ })
+
+ if (isRight(response)) {
+ const history: Portfolio.Token.History | undefined = toTokenHistory(
+ response.value.data,
+ )
+
+ if (!history) {
+ return freeze(
+ {
+ tag: 'left',
+ error: {
+ status: -3,
+ message: 'Failed to transform token history response',
+ responseData: response.value.data,
+ },
+ },
+ true,
+ )
+ }
+
+ return freeze(
+ {
+ tag: 'right',
+ value: {
+ status: response.value.status,
+ data: history,
+ },
+ },
+ true,
+ )
+ }
+
+ return response
+ },
+
async tokenImageInvalidate(ids) {
const tasks = ids.map(
(id) => () =>
@@ -325,8 +372,7 @@ export const apiConfig: ApiConfig = freeze(
tokenInfos: 'https://zero.yoroiwallet.com/tokens/info/multi',
tokenTraits: 'https://zero.yoroiwallet.com/tokens/nft/traits',
tokenActivity: 'https://zero.yoroiwallet.com/tokens/activity/multi',
- tokenPriceHistory:
- 'https://add50d9d-76d7-47b7-b17f-e34021f63a02.mock.pstmn.io/v1/token-price-history',
+ tokenHistory: 'https://zero.yoroiwallet.com/tokens/history/price',
tokenImageInvalidate:
'https://mainnet.processed-media.yoroiwallet.com/invalidate',
},
@@ -340,8 +386,8 @@ export const apiConfig: ApiConfig = freeze(
'https://yoroi-backend-zero-preprod.emurgornd.com/tokens/nft/traits',
tokenActivity:
'https://yoroi-backend-zero-preprod.emurgornd.com/tokens/activity/multi',
- tokenPriceHistory:
- 'https://add50d9d-76d7-47b7-b17f-e34021f63a02.mock.pstmn.io/v1/token-price-history',
+ tokenHistory:
+ 'https://yoroi-backend-zero-preprod.emurgornd.com/tokens/history/price',
tokenImageInvalidate:
'https://preprod.processed-media.yoroiwallet.com/invalidate',
},
@@ -356,8 +402,8 @@ export const apiConfig: ApiConfig = freeze(
'https://yoroi-backend-zero-sanchonet.emurgornd.com/tokens/nft/traits',
tokenActivity:
'https://yoroi-backend-zero-sanchonet.emurgornd.com/tokens/activity/multi',
- tokenPriceHistory:
- 'https://add50d9d-76d7-47b7-b17f-e34021f63a02.mock.pstmn.io/v1/token-price-history',
+ tokenHistory:
+ 'https://yoroi-backend-zero-sanchonet.emurgornd.com/tokens/history/price',
tokenImageInvalidate:
'https://preprod.processed-media.yoroiwallet.com/invalidate',
},
@@ -371,8 +417,8 @@ export const apiConfig: ApiConfig = freeze(
'https://yoroi-backend-zero-preview.emurgornd.com/tokens/nft/traits',
tokenActivity:
'https://yoroi-backend-zero-preview.emurgornd.com/tokens/activity/multi',
- tokenPriceHistory:
- 'https://add50d9d-76d7-47b7-b17f-e34021f63a02.mock.pstmn.io/v1/token-price-history',
+ tokenHistory:
+ 'https://yoroi-backend-zero-preview.emurgornd.com/tokens/history/price',
tokenImageInvalidate:
'https://preview.processed-media.yoroiwallet.com/invalidate',
},
diff --git a/packages/portfolio/src/adapters/dullahan-api/token-activity.mocks.ts b/packages/portfolio/src/adapters/dullahan-api/token-activity.mocks.ts
index 930c6851a1..7e24eaf861 100644
--- a/packages/portfolio/src/adapters/dullahan-api/token-activity.mocks.ts
+++ b/packages/portfolio/src/adapters/dullahan-api/token-activity.mocks.ts
@@ -53,7 +53,7 @@ const apiResponseSuccessDataOnly: Readonly = {
[tokenInfoMocks.ftNameless.id]: [Api.HttpStatusCode.Ok, ftNameless],
}
-export const duallahanTokenActivityUpdatesMocks = {
+export const duallahanTokenActivityMocks = {
primaryETH,
rnftWhatever,
ftNoTicker,
diff --git a/packages/portfolio/src/adapters/dullahan-api/transformers.test.ts b/packages/portfolio/src/adapters/dullahan-api/transformers.test.ts
index 7c9813798c..52498eeb42 100644
--- a/packages/portfolio/src/adapters/dullahan-api/transformers.test.ts
+++ b/packages/portfolio/src/adapters/dullahan-api/transformers.test.ts
@@ -2,14 +2,19 @@ import {
toDullahanRequest,
toProcessedMediaRequest,
toSecondaryTokenInfos,
- toTokenActivityUpdates,
+ toTokenActivity,
+ toTokenHistory,
} from './transformers'
import {Portfolio, Api} from '@yoroi/types'
import {tokenMocks} from '../token.mocks'
-import {DullahanApiTokenActivityResponse} from './types'
+import {
+ DullahanApiTokenActivityResponse,
+ DullahanApiTokenHistoryResponse,
+} from './types'
import {tokenActivityMocks} from '../token-activity.mocks'
-import {duallahanTokenActivityUpdatesMocks} from './token-activity.mocks'
+import {duallahanTokenActivityMocks} from './token-activity.mocks'
+import {tokenHistoryMocks} from '../token-history.mocks'
describe('transformers', () => {
describe('toSecondaryTokenInfos', () => {
@@ -107,24 +112,42 @@ describe('transformers', () => {
})
})
- describe('toTokenActivityUpdates', () => {
- it('should return an empty object if apiTokenActivityUpdates response is empty', () => {
+ describe('toTokenActivity', () => {
+ it('should return an empty object if apiTokenActivity response is empty', () => {
const apiTokenInfosResponse: DullahanApiTokenActivityResponse = {}
- expect(toTokenActivityUpdates(apiTokenInfosResponse)).toEqual({})
+ expect(toTokenActivity(apiTokenInfosResponse)).toEqual({})
})
it('should return the data and deal with empty records', () => {
const responseWithEmptyRecords = {
- ...duallahanTokenActivityUpdatesMocks.api.responseSuccessDataOnly,
+ ...duallahanTokenActivityMocks.api.responseSuccessDataOnly,
'token.4': undefined,
'token.5': [Api.HttpStatusCode.InternalServerError, 'Not found'],
} as any
- const result = toTokenActivityUpdates(responseWithEmptyRecords)
+ const result = toTokenActivity(responseWithEmptyRecords)
expect(result).toEqual(tokenActivityMocks.api.responseDataOnly)
})
})
+
+ describe('toTokenHistory', () => {
+ it('should return undefined if apiTokenHistory response is malformed', () => {
+ expect(
+ toTokenHistory({
+ whatever: false,
+ } as unknown as DullahanApiTokenHistoryResponse),
+ ).toEqual(undefined)
+ })
+
+ it('should return the data', () => {
+ const result = toTokenHistory(
+ tokenHistoryMocks.ftNamelessRaw as unknown as DullahanApiTokenHistoryResponse,
+ )
+
+ expect(result).toEqual(tokenHistoryMocks.api.responseDataOnly)
+ })
+ })
})
describe('toDullahanRequest', () => {
diff --git a/packages/portfolio/src/adapters/dullahan-api/transformers.ts b/packages/portfolio/src/adapters/dullahan-api/transformers.ts
index 339385ac39..4bc6c06a48 100644
--- a/packages/portfolio/src/adapters/dullahan-api/transformers.ts
+++ b/packages/portfolio/src/adapters/dullahan-api/transformers.ts
@@ -6,6 +6,7 @@ import {parseSecondaryTokenInfoWithCacheRecord} from '../../validators/token-inf
import {
DullahanApiCachedIdsRequest,
DullahanApiTokenActivityResponse,
+ DullahanApiTokenHistoryResponse,
DullahanApiTokenInfosResponse,
} from './types'
import {z} from 'zod'
@@ -49,46 +50,58 @@ export const toSecondaryTokenInfos = (
)
}
-export const toTokenActivityUpdates = (
+export const toTokenActivity = (
apiTokenActivityResponse: Readonly,
) => {
- const tokenActivityUpdates: Record<
- Portfolio.Token.Id,
- Portfolio.Token.Activity
- > = {}
+ const tokenActivity: Record = {}
return freeze(
- Object.entries(apiTokenActivityResponse).reduce(
- (acc, [id, tokenActivity]) => {
- if (!Array.isArray(tokenActivity)) return acc
- const castedId = id as Portfolio.Token.Id
+ Object.entries(apiTokenActivityResponse).reduce((acc, [id, response]) => {
+ if (!Array.isArray(response)) return acc
+ const castedId = id as Portfolio.Token.Id
- const [statusCode, tokenActivityData] = tokenActivity
- if (statusCode !== Api.HttpStatusCode.Ok) return acc
+ const [statusCode, tokenActivityData] = response
+ if (statusCode !== Api.HttpStatusCode.Ok) return acc
- TokenAcvitivyResponseSchema.parse(tokenActivityData)
+ TokenActivityResponseSchema.parse(tokenActivityData)
- const parsedTokenActivity: Portfolio.Token.Activity = {
- price: {
- ts: tokenActivityData.price.ts,
- open: new BigNumber(tokenActivityData.price.open),
- close: new BigNumber(tokenActivityData.price.close),
- low: new BigNumber(tokenActivityData.price.low),
- high: new BigNumber(tokenActivityData.price.high),
- change: tokenActivityData.price.change,
- },
- }
+ const parsedTokenActivity: Portfolio.Token.Activity = {
+ price: {
+ ts: tokenActivityData.price.ts,
+ open: new BigNumber(tokenActivityData.price.open),
+ close: new BigNumber(tokenActivityData.price.close),
+ low: new BigNumber(tokenActivityData.price.low),
+ high: new BigNumber(tokenActivityData.price.high),
+ change: tokenActivityData.price.change,
+ },
+ }
- acc[castedId] = parsedTokenActivity
+ acc[castedId] = parsedTokenActivity
- return acc
- },
- tokenActivityUpdates,
- ),
+ return acc
+ }, tokenActivity),
true,
)
}
+export const toTokenHistory = (
+ apiTokenHistoryResponse: Readonly,
+) => {
+ if (!TokenHistoryResponseSchema.safeParse(apiTokenHistoryResponse).success)
+ return undefined
+
+ return freeze({
+ prices: apiTokenHistoryResponse.prices.map((tokenHistoryRecord) => ({
+ ts: tokenHistoryRecord.ts,
+ open: new BigNumber(tokenHistoryRecord.open),
+ close: new BigNumber(tokenHistoryRecord.close),
+ low: new BigNumber(tokenHistoryRecord.low),
+ high: new BigNumber(tokenHistoryRecord.high),
+ change: tokenHistoryRecord.change,
+ })),
+ })
+}
+
export const toDullahanRequest = (
request: ReadonlyArray>,
) =>
@@ -96,7 +109,7 @@ export const toDullahanRequest = (
([tokenId, hash]) => `${tokenId}:${hash}`,
) as DullahanApiCachedIdsRequest
-const TokenAcvitivyResponseSchema = z.object({
+const TokenActivityResponseSchema = z.object({
price: z.object({
ts: z.number(),
open: z.string(),
@@ -107,6 +120,19 @@ const TokenAcvitivyResponseSchema = z.object({
}),
})
+const TokenHistoryResponseSchema = z.object({
+ prices: z.array(
+ z.object({
+ ts: z.number(),
+ open: z.string(),
+ close: z.string(),
+ low: z.string(),
+ high: z.string(),
+ change: z.number(),
+ }),
+ ),
+})
+
export const toProcessedMediaRequest = (request: Portfolio.Token.Id) => {
const [policy, name] = request.split('.') as [string, string]
return {policy, name}
diff --git a/packages/portfolio/src/adapters/dullahan-api/types.ts b/packages/portfolio/src/adapters/dullahan-api/types.ts
index d5f5c9d20b..25160eabbd 100644
--- a/packages/portfolio/src/adapters/dullahan-api/types.ts
+++ b/packages/portfolio/src/adapters/dullahan-api/types.ts
@@ -42,6 +42,8 @@ export type DullahanApiTokenActivityResponse = Readonly<{
[key: Portfolio.Token.Id]: DullahanApiTokenActivity
}>
+export type DullahanApiTokenHistoryResponse = Readonly
+
export type ProcessedMediaApiTokenImageInvalidateRequest = {
name: string
policy: string
diff --git a/packages/portfolio/src/adapters/token-activity.mocks.ts b/packages/portfolio/src/adapters/token-activity.mocks.ts
index 10c8d5e2ce..ac5e72d044 100644
--- a/packages/portfolio/src/adapters/token-activity.mocks.ts
+++ b/packages/portfolio/src/adapters/token-activity.mocks.ts
@@ -2,7 +2,7 @@ import {Api, Portfolio} from '@yoroi/types'
import {freeze} from 'immer'
import {BigNumber} from 'bignumber.js'
import {tokenInfoMocks} from './token-info.mocks'
-import {duallahanTokenActivityUpdatesMocks} from './dullahan-api/token-activity.mocks'
+import {duallahanTokenActivityMocks} from './dullahan-api/token-activity.mocks'
const primaryETH: Portfolio.Token.Activity = {
price: {
@@ -58,16 +58,14 @@ const apiResponseSuccessDataOnly = {
const apiResponseTokenActivity: Readonly<
Record<
'success' | 'error',
- Api.Response<
- typeof duallahanTokenActivityUpdatesMocks.api.responseSuccessDataOnly
- >
+ Api.Response
>
> = {
success: {
tag: 'right',
value: {
status: 200,
- data: duallahanTokenActivityUpdatesMocks.api.responseSuccessDataOnly,
+ data: duallahanTokenActivityMocks.api.responseSuccessDataOnly,
},
},
error: {
diff --git a/packages/portfolio/src/adapters/token-history.mocks.ts b/packages/portfolio/src/adapters/token-history.mocks.ts
new file mode 100644
index 0000000000..583628f4f6
--- /dev/null
+++ b/packages/portfolio/src/adapters/token-history.mocks.ts
@@ -0,0 +1,83 @@
+import {Api, Portfolio} from '@yoroi/types'
+import {freeze} from 'immer'
+import {BigNumber} from 'bignumber.js'
+
+const ftNamelessRaw = {
+ prices: [
+ {
+ ts: 1722849529169,
+ open: '500000',
+ close: '1000000',
+ low: '500000',
+ high: '1000000',
+ change: 100,
+ },
+ {
+ ts: 1722949529169,
+ open: '500000',
+ close: '1000000',
+ low: '500000',
+ high: '1000000',
+ change: 100,
+ },
+ ],
+}
+
+const ftNameless: Portfolio.Token.History = {
+ prices: [
+ {
+ ts: 1722849529169,
+ open: new BigNumber(500_000),
+ low: new BigNumber(500_000),
+ close: new BigNumber(1_000_000),
+ high: new BigNumber(1_000_000),
+ change: 100,
+ },
+ {
+ ts: 1722949529169,
+ open: new BigNumber(500_000),
+ low: new BigNumber(500_000),
+ close: new BigNumber(1_000_000),
+ high: new BigNumber(1_000_000),
+ change: 100,
+ },
+ ],
+}
+
+const apiResponseTokenHistory: Readonly<
+ Record<'success' | 'error', Api.Response>
+> = {
+ success: {
+ tag: 'right',
+ value: {
+ status: 200,
+ data: ftNamelessRaw,
+ },
+ },
+ error: {
+ tag: 'left',
+ error: {
+ status: 500,
+ responseData: null,
+ message: 'Internal Server Error',
+ },
+ },
+}
+
+const apiRequestTokenHistoryArgs: Readonly<{
+ tokenId: Portfolio.Token.Id
+ period: Portfolio.Token.HistoryPeriod
+}> = {
+ tokenId: 'ft.nameless',
+ period: Portfolio.Token.HistoryPeriod.OneDay,
+}
+
+export const tokenHistoryMocks = freeze({
+ ftNameless,
+ ftNamelessRaw,
+ api: {
+ responses: apiResponseTokenHistory,
+ request: apiRequestTokenHistoryArgs,
+ responseDataOnly: ftNameless,
+ },
+})
diff --git a/packages/portfolio/src/balance-manager.test.ts b/packages/portfolio/src/balance-manager.test.ts
index 32a3964ca7..bffab5c420 100644
--- a/packages/portfolio/src/balance-manager.test.ts
+++ b/packages/portfolio/src/balance-manager.test.ts
@@ -10,6 +10,7 @@ import {portfolioBalanceStorageMaker} from './adapters/mmkv-storage/balance-stor
import {tokenInfoMocks} from './adapters/token-info.mocks'
import {isFt} from './helpers/is-ft'
import {isNft} from './helpers/is-nft'
+import {createTokenManagerMock} from './token-manager.mock'
const tokenInfoStorage = observableStorageMaker(
mountMMKVStorage({
@@ -37,23 +38,7 @@ describe('portfolioBalanceManagerMaker', () => {
primaryBreakdownStorage,
primaryTokenId,
})
- const tokenManager: Portfolio.Manager.Token = {
- destroy: jest.fn(),
- hydrate: jest.fn(),
- subscribe: jest.fn(),
- unsubscribe: jest.fn(),
- observable$: new BehaviorSubject({} as any).asObservable(),
- sync: jest.fn().mockResolvedValue(new Map()),
- clear: jest.fn(),
- api: {
- tokenInfo: jest.fn(),
- tokenInfos: jest.fn(),
- tokenDiscovery: jest.fn(),
- tokenTraits: jest.fn(),
- tokenActivity: jest.fn(),
- tokenImageInvalidate: jest.fn(),
- },
- }
+ const tokenManager = createTokenManagerMock()
it('should be instantiated', () => {
const manager = portfolioBalanceManagerMaker({
@@ -85,23 +70,7 @@ describe('hydrate', () => {
primaryBreakdownStorage,
primaryTokenId,
})
- const tokenManager: Portfolio.Manager.Token = {
- destroy: jest.fn(),
- hydrate: jest.fn(),
- subscribe: jest.fn(),
- unsubscribe: jest.fn(),
- observable$: new BehaviorSubject({} as any).asObservable(),
- sync: jest.fn().mockResolvedValue(new Map()),
- clear: jest.fn(),
- api: {
- tokenInfo: jest.fn(),
- tokenInfos: jest.fn(),
- tokenDiscovery: jest.fn(),
- tokenTraits: jest.fn(),
- tokenActivity: jest.fn(),
- tokenImageInvalidate: jest.fn(),
- },
- }
+ const tokenManager = createTokenManagerMock()
afterEach(() => {
storage.clear()
@@ -174,24 +143,7 @@ describe('destroy', () => {
primaryBreakdownStorage,
primaryTokenId,
})
- const tokenManagerObservable = new BehaviorSubject({} as any).asObservable()
- const tokenManager: jest.Mocked = {
- destroy: jest.fn(),
- hydrate: jest.fn(),
- subscribe: jest.fn(),
- unsubscribe: jest.fn(),
- observable$: tokenManagerObservable,
- sync: jest.fn().mockResolvedValue(new Map()),
- clear: jest.fn(),
- api: {
- tokenInfo: jest.fn(),
- tokenInfos: jest.fn(),
- tokenDiscovery: jest.fn(),
- tokenTraits: jest.fn(),
- tokenActivity: jest.fn(),
- tokenImageInvalidate: jest.fn(),
- },
- }
+ const tokenManager = createTokenManagerMock()
const queueDestroy = jest.fn()
const observerDestroy = jest.fn()
@@ -258,24 +210,7 @@ describe('primary updates', () => {
primaryBreakdownStorage,
primaryTokenId,
})
- const tokenManagerObservable = new BehaviorSubject({} as any)
- const tokenManager: jest.Mocked = {
- destroy: jest.fn(),
- hydrate: jest.fn(),
- subscribe: jest.fn(),
- unsubscribe: jest.fn(),
- observable$: tokenManagerObservable.asObservable(),
- sync: jest.fn().mockResolvedValue(new Map()),
- clear: jest.fn(),
- api: {
- tokenInfo: jest.fn(),
- tokenInfos: jest.fn(),
- tokenDiscovery: jest.fn(),
- tokenTraits: jest.fn(),
- tokenActivity: jest.fn(),
- tokenImageInvalidate: jest.fn(),
- },
- }
+ const tokenManager = createTokenManagerMock()
afterEach(() => {
storage.clear()
@@ -418,23 +353,9 @@ describe('sync & refresh', () => {
primaryTokenId,
})
const tokenManagerObservable = new BehaviorSubject({} as any)
- const tokenManager: jest.Mocked = {
- destroy: jest.fn(),
- hydrate: jest.fn(),
- subscribe: jest.fn(),
- unsubscribe: jest.fn(),
- observable$: tokenManagerObservable.asObservable(),
- sync: jest.fn().mockResolvedValue(new Map()),
- clear: jest.fn(),
- api: {
- tokenInfo: jest.fn(),
- tokenInfos: jest.fn(),
- tokenDiscovery: jest.fn(),
- tokenTraits: jest.fn(),
- tokenActivity: jest.fn(),
- tokenImageInvalidate: jest.fn(),
- },
- }
+ const tokenManager = createTokenManagerMock(
+ tokenManagerObservable.asObservable(),
+ )
afterEach(() => {
storage.clear()
@@ -710,23 +631,7 @@ describe('clear', () => {
primaryBreakdownStorage,
primaryTokenId,
})
- const tokenManager: Portfolio.Manager.Token = {
- destroy: jest.fn(),
- hydrate: jest.fn(),
- subscribe: jest.fn(),
- unsubscribe: jest.fn(),
- observable$: new BehaviorSubject({} as any).asObservable(),
- sync: jest.fn().mockResolvedValue(new Map()),
- clear: jest.fn(),
- api: {
- tokenInfo: jest.fn(),
- tokenInfos: jest.fn(),
- tokenDiscovery: jest.fn(),
- tokenTraits: jest.fn(),
- tokenActivity: jest.fn(),
- tokenImageInvalidate: jest.fn(),
- },
- }
+ const tokenManager = createTokenManagerMock()
afterEach(() => {
storage.clear()
diff --git a/packages/portfolio/src/index.ts b/packages/portfolio/src/index.ts
index 14fe8e508e..c6b07dcc37 100644
--- a/packages/portfolio/src/index.ts
+++ b/packages/portfolio/src/index.ts
@@ -42,6 +42,7 @@ export * from './translators/reactjs/usePortfolioTokenInfo'
export * from './balance-manager'
export * from './token-manager'
+export * from './token-manager.mock'
export * from './types'
export * from './constants'
diff --git a/packages/portfolio/src/token-manager.mock.ts b/packages/portfolio/src/token-manager.mock.ts
new file mode 100644
index 0000000000..cbdad48d6f
--- /dev/null
+++ b/packages/portfolio/src/token-manager.mock.ts
@@ -0,0 +1,24 @@
+import {Portfolio} from '@yoroi/types'
+import {BehaviorSubject, Observable} from 'rxjs'
+
+export const createTokenManagerMock = (
+ tokenManagerObservable?: Observable,
+): jest.Mocked => ({
+ destroy: jest.fn(),
+ hydrate: jest.fn(),
+ subscribe: jest.fn(),
+ unsubscribe: jest.fn(),
+ observable$:
+ tokenManagerObservable ?? new BehaviorSubject({} as any).asObservable(),
+ sync: jest.fn().mockResolvedValue(new Map()),
+ clear: jest.fn(),
+ api: {
+ tokenInfo: jest.fn(),
+ tokenInfos: jest.fn(),
+ tokenDiscovery: jest.fn(),
+ tokenTraits: jest.fn(),
+ tokenActivity: jest.fn(),
+ tokenHistory: jest.fn(),
+ tokenImageInvalidate: jest.fn(),
+ },
+})
diff --git a/packages/types/src/chain/cardano.ts b/packages/types/src/chain/cardano.ts
index 99886fbfa6..85df82723c 100644
--- a/packages/types/src/chain/cardano.ts
+++ b/packages/types/src/chain/cardano.ts
@@ -17,6 +17,14 @@ export type ChainCardanoProtocolParams = Readonly<{
epoch: number
}>
+export type ChainCardanoBestBlock = Readonly<{
+ epoch: number
+ slot: number
+ globalSlot: number
+ hash: string
+ height: number
+}>
+
// START legacy
export type CardanoUnsignedTx = CardanoTxInfo & {
unsignedTx: UnsignedTxType
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index 0a6b69dfbc..ecc912b53d 100644
--- a/packages/types/src/index.ts
+++ b/packages/types/src/index.ts
@@ -121,6 +121,7 @@ import {
CardanoUnsignedTx,
CardanoVoting,
ChainCardanoProtocolParams,
+ ChainCardanoBestBlock,
} from './chain/cardano'
import {ExchangeBlockchainCode} from './exchange/blockchain'
import {ExchangeManagerOptions} from './exchange/build'
@@ -176,6 +177,7 @@ import {
PortfolioApi,
PortfolioApiTokenActivityResponse,
PortfolioApiTokenDiscoveryResponse,
+ PortfolioApiTokenHistoryResponse,
PortfolioApiTokenInfosResponse,
PortfolioApiTokenTraitsResponse,
} from './portfolio/api'
@@ -252,6 +254,10 @@ import {
ClaimApiClaimTokensRequestPayload,
ClaimApiClaimTokensResponse,
} from './claim/api'
+import {
+ PortfolioTokenHistory,
+ PortfolioTokenHistoryPeriod,
+} from './portfolio/history'
export namespace App {
export namespace Errors {
@@ -446,9 +452,11 @@ export namespace Api {
export type TokenId = ApiTokenId
export type ProtocolParams = ChainCardanoProtocolParams
+ export type BestBlock = ChainCardanoBestBlock
export interface Api {
- getProtocolParams: () => Promise
+ getProtocolParams: () => Promise
+ getBestBlock: () => Promise
}
}
}
@@ -524,6 +532,7 @@ export namespace Portfolio {
export type TokenDiscoveryResponse = PortfolioApiTokenDiscoveryResponse
export type TokenTraitsResponse = PortfolioApiTokenTraitsResponse
export type TokenActivityResponse = PortfolioApiTokenActivityResponse
+ export type TokenHistoryResponse = PortfolioApiTokenHistoryResponse
export type Api = PortfolioApi
}
@@ -571,6 +580,10 @@ export namespace Portfolio {
export type ActivityWindow = PortfolioTokenActivityWindow
export const ActivityWindow = PortfolioTokenActivityWindow
export type ActivityRecord = PortfolioTokenActivityRecord
+
+ export type History = PortfolioTokenHistory
+ export type HistoryPeriod = PortfolioTokenHistoryPeriod
+ export const HistoryPeriod = PortfolioTokenHistoryPeriod
}
}
@@ -589,6 +602,7 @@ export namespace Chain {
export type Address = CardanoAddress
export type TokenId = CardanoTokenId
export type ProtocolParams = ChainCardanoProtocolParams
+ export type BestBlock = ChainCardanoBestBlock
}
}
diff --git a/packages/types/src/network/manager.ts b/packages/types/src/network/manager.ts
index 37607d4b33..fa1807fa04 100644
--- a/packages/types/src/network/manager.ts
+++ b/packages/types/src/network/manager.ts
@@ -1,5 +1,8 @@
import {AppObservableStorage} from '../app/observable-storage'
-import {ChainCardanoProtocolParams} from '../chain/cardano'
+import {
+ ChainCardanoProtocolParams,
+ ChainCardanoBestBlock,
+} from '../chain/cardano'
import {ChainSupportedNetworks} from '../chain/network'
import {ExplorersExplorer} from '../explorers/explorer'
import {ExplorersManager} from '../explorers/manager'
@@ -21,6 +24,7 @@ export type NetworkConfig = {
// NOTE: NetworkConfig will be a generic type in the future
export type NetworkApi = {
protocolParams: () => Promise>
+ bestBlock: () => Promise
}
export type NetworkManager = Readonly<
{
diff --git a/packages/types/src/portfolio/api.ts b/packages/types/src/portfolio/api.ts
index 650841f7aa..66deba286a 100644
--- a/packages/types/src/portfolio/api.ts
+++ b/packages/types/src/portfolio/api.ts
@@ -5,6 +5,7 @@ import {
import {ApiResponse} from '../api/response'
import {PortfolioTokenActivity, PortfolioTokenActivityWindow} from './activity'
import {PortfolioTokenDiscovery} from './discovery'
+import {PortfolioTokenHistory, PortfolioTokenHistoryPeriod} from './history'
import {PortfolioTokenInfo} from './info'
import {PortfolioTokenId} from './token'
import {PortfolioTokenTraits} from './traits'
@@ -21,6 +22,8 @@ export type PortfolioApiTokenActivityResponse = {
export type PortfolioApiTokenTraitsResponse = PortfolioTokenTraits
+export type PortfolioApiTokenHistoryResponse = PortfolioTokenHistory
+
export type PortfolioApi = Readonly<{
tokenInfo(
id: PortfolioTokenId,
@@ -38,5 +41,9 @@ export type PortfolioApi = Readonly<{
ids: ReadonlyArray,
window: PortfolioTokenActivityWindow,
): Promise>>
+ tokenHistory(
+ id: PortfolioTokenId,
+ period: PortfolioTokenHistoryPeriod,
+ ): Promise>>
tokenImageInvalidate(ids: ReadonlyArray): Promise
}>
diff --git a/packages/types/src/portfolio/history.ts b/packages/types/src/portfolio/history.ts
new file mode 100644
index 0000000000..54b59f317a
--- /dev/null
+++ b/packages/types/src/portfolio/history.ts
@@ -0,0 +1,14 @@
+import {PortfolioTokenActivityRecord} from './activity'
+
+export type PortfolioTokenHistory = Readonly<{
+ prices: ReadonlyArray
+}>
+
+export enum PortfolioTokenHistoryPeriod {
+ OneDay = '1d',
+ OneWeek = '1w',
+ OneMonth = '1m',
+ SixMonth = '6m',
+ OneYear = '1y',
+ All = 'all',
+}