diff --git a/apps/extension/src/core/domains/balances/handler.ts b/apps/extension/src/core/domains/balances/handler.ts index 26bda3ee18..647e7cc64e 100644 --- a/apps/extension/src/core/domains/balances/handler.ts +++ b/apps/extension/src/core/domains/balances/handler.ts @@ -1,19 +1,39 @@ import { DEBUG } from "@core/constants" import { getNomPoolStake } from "@core/domains/balances/helpers" import { + AddressesAndEvmNetwork, + AddressesAndTokens, Balances, RequestBalance, RequestBalancesByParamsSubscribe, RequestNomPoolStake, } from "@core/domains/balances/types" +import { + EnabledChains, + enabledChainsStore, + isChainEnabled, +} from "@core/domains/chains/store.enabledChains" +import { + EnabledEvmNetworks, + enabledEvmNetworksStore, + isEvmNetworkEnabled, +} from "@core/domains/ethereum/store.enabledEvmNetworks" +import { + EnabledTokens, + enabledTokensStore, + isTokenEnabled, +} from "@core/domains/tokens/store.enabledTokens" import { createSubscription, unsubscribe } from "@core/handlers/subscriptions" import { ExtensionHandler } from "@core/libs/Handler" import { balanceModules } from "@core/rpcs/balance-modules" import { chaindataProvider } from "@core/rpcs/chaindata" -import { Port } from "@core/types/base" -import { AddressesByToken } from "@talismn/balances" -import { Token } from "@talismn/chaindata-provider" +import { AddressesByChain, Port } from "@core/types/base" +import { AddressesByToken, MiniMetadata, UnsubscribeFn, db as balancesDb } from "@talismn/balances" +import { ChainId, ChainList, EvmNetworkList, Token, TokenList } from "@talismn/chaindata-provider" import { MessageTypes, RequestTypes, ResponseType } from "core/types" +import { liveQuery } from "dexie" +import { isEqual } from "lodash" +import { BehaviorSubject, combineLatest } from "rxjs" export class BalancesHandler extends ExtensionHandler { public async handle( @@ -38,108 +58,198 @@ export class BalancesHandler extends ExtensionHandler { // TODO: Replace this call with something internal to the balances store // i.e. refactor the balances store to allow us to subscribe to arbitrary balances here, // instead of being limited to the accounts which are in the wallet's keystore - case "pri(balances.byparams.subscribe)": { - // create subscription callback - const callback = createSubscription<"pri(balances.byparams.subscribe)">(id, port) - - const { addressesByChain, addressesAndEvmNetworks, addressesAndTokens } = - request as RequestBalancesByParamsSubscribe - - // - // Collect the required data from chaindata. - // - - const [chains, evmNetworks, tokens] = await Promise.all([ - chaindataProvider.chains(), - chaindataProvider.evmNetworks(), - chaindataProvider.tokens(), - ]) - - // - // Convert the inputs of `addressesByChain` and `addressesAndEvmNetworks` into what we need - // for each balance module: `addressesByToken`. - // - - const addressesByToken: AddressesByToken = [ - ...Object.entries(addressesByChain) - // convert chainIds into chains - .map(([chainId, addresses]) => [chains[chainId], addresses] as const), - - ...addressesAndEvmNetworks.evmNetworks - // convert evmNetworkIds into evmNetworks - .map(({ id }) => [evmNetworks[id], addressesAndEvmNetworks.addresses] as const), - ] - // filter out requested chains/evmNetworks which don't exist - .filter(([chainOrNetwork]) => chainOrNetwork !== undefined) - // filter out requested chains/evmNetworks which have no rpcs - .filter(([chainOrNetwork]) => (chainOrNetwork.rpcs?.length ?? 0) > 0) - - // convert chains and evmNetworks into a list of tokenIds - .flatMap(([chainOrNetwork, addresses]) => - (chainOrNetwork.tokens || []).map(({ id: tokenId }) => [tokenId, addresses] as const) - ) - - // collect all of the addresses for each tokenId into a map of { [tokenId]: addresses } - .reduce((addressesByToken, [tokenId, addresses]) => { - if (!addressesByToken[tokenId]) addressesByToken[tokenId] = [] - addressesByToken[tokenId].push(...addresses) - return addressesByToken - }, {} as AddressesByToken) - - for (const tokenId of addressesAndTokens.tokenIds) { - if (!addressesByToken[tokenId]) addressesByToken[tokenId] = [] - addressesByToken[tokenId].push( - ...addressesAndTokens.addresses.filter((a) => !addressesByToken[tokenId].includes(a)) - ) - } - - // - // Separate out the tokens in `addressesByToken` into groups based on `token.type` - // Input: { [token.id]: addresses, [token2.id]: addresses } - // Output: { [token.type]: { [token.id]: addresses }, [token2.type]: { [token2.id]: addresses } } - // - // This lets us only send each token to the balance module responsible for querying its balance. - // - - const addressesByTokenByModule: Record> = [ - ...Object.entries(addressesByToken) - // convert tokenIds into tokens - .map(([tokenId, addresses]) => [tokens[tokenId], addresses] as const), - ] - // filter out tokens which don't exist - .filter(([token]) => token !== undefined && token.isDefault !== false) // TODO filter based on if token is enabled - - // group each `{ [token.id]: addresses }` by token.type - .reduce((byModule, [token, addresses]) => { - if (!byModule[token.type]) byModule[token.type] = {} - byModule[token.type][token.id] = addresses - return byModule - }, {} as Record>) - - // subscribe to balances by params - const closeSubscriptionCallbacks = balanceModules.map((balanceModule) => - balanceModule.subscribeBalances( - addressesByTokenByModule[balanceModule.type] ?? {}, - (error, result) => { - // eslint-disable-next-line no-console - if (error) DEBUG && console.error(error) - else callback({ type: "upsert", balances: (result ?? new Balances([])).toJSON() }) - } - ) - ) - - // unsub on port disconnect - port.onDisconnect.addListener((): void => { - unsubscribe(id) - closeSubscriptionCallbacks.forEach((cb) => cb.then((close) => close())) - }) - - // subscription created - return true - } + case "pri(balances.byparams.subscribe)": + return subscribeBalancesByParams(id, port, request as RequestBalancesByParamsSubscribe) default: throw new Error(`Unable to handle message of type ${type}`) } } } + +type BalanceSubscriptionParams = { + addressesByTokenByModule: Record> + miniMetadataIds: string[] +} + +const subscribeBalancesByParams = async ( + id: string, + port: Port, + request: RequestBalancesByParamsSubscribe +): Promise => { + const { addressesByChain, addressesAndEvmNetworks, addressesAndTokens } = + request as RequestBalancesByParamsSubscribe + + // create subscription callback + const callback = createSubscription<"pri(balances.byparams.subscribe)">(id, port) + + const obsSubscriptionParams = new BehaviorSubject({ + addressesByTokenByModule: {}, + miniMetadataIds: [], + }) + + let closeSubscriptionCallbacks: Promise[] = [] + + // watch for changes to all stores, mainly important for onboarding as they start empty + combineLatest([ + // chains + liveQuery(async () => await chaindataProvider.chains()), + // evmNetworks + liveQuery(async () => await chaindataProvider.evmNetworks()), + // tokens + liveQuery(async () => await chaindataProvider.tokens()), + // miniMetadatas - not used here but we must retrigger the subscription when this changes + liveQuery(async () => await balancesDb.miniMetadatas.toArray()), + // enabled state of evm networks + enabledEvmNetworksStore.observable, + // enabled state of substrate chains + enabledChainsStore.observable, + // enable state of tokens + enabledTokensStore.observable, + ]).subscribe({ + next: async ([ + chains, + evmNetworks, + tokens, + miniMetadatas, + enabledEvmNetworks, + enabledChains, + enabledTokens, + ]) => { + const newSubscriptionParams = getSubscriptionParams( + addressesByChain, + addressesAndEvmNetworks, + addressesAndTokens, + chains, + evmNetworks, + tokens, + enabledChains, + enabledEvmNetworks, + enabledTokens, + miniMetadatas + ) + + // restart subscription only if params change + if (!isEqual(obsSubscriptionParams.value, newSubscriptionParams)) + obsSubscriptionParams.next(newSubscriptionParams) + }, + }) + + // restart subscriptions each type params update + obsSubscriptionParams.subscribe(async ({ addressesByTokenByModule }) => { + // close previous subscriptions + await Promise.all(closeSubscriptionCallbacks) + + // subscribe to balances by params + closeSubscriptionCallbacks = balanceModules.map((balanceModule) => + balanceModule.subscribeBalances( + addressesByTokenByModule[balanceModule.type] ?? {}, + (error, result) => { + // eslint-disable-next-line no-console + if (error) DEBUG && console.error(error) + else callback({ type: "upsert", balances: (result ?? new Balances([])).toJSON() }) + } + ) + ) + }) + + // unsub on port disconnect + port.onDisconnect.addListener((): void => { + unsubscribe(id) + closeSubscriptionCallbacks.forEach((cb) => cb.then((close) => close())) + }) + + // subscription created + return true +} + +const getSubscriptionParams = ( + addressesByChain: AddressesByChain, + addressesAndEvmNetworks: AddressesAndEvmNetwork, + addressesAndTokens: AddressesAndTokens, + chains: ChainList, + evmNetworks: EvmNetworkList, + tokens: TokenList, + enabledChains: EnabledChains, + enabledEvmNetworks: EnabledEvmNetworks, + enabledTokens: EnabledTokens, + miniMetadatas: MiniMetadata[] +): BalanceSubscriptionParams => { + // + // Convert the inputs of `addressesByChain` and `addressesAndEvmNetworks` into what we need + // for each balance module: `addressesByToken`. + // + const addressesByToken: AddressesByToken = [ + ...Object.entries(addressesByChain) + // convert chainIds into chains + .map(([chainId, addresses]) => [chains[chainId], addresses] as const) + .filter(([chain]) => isChainEnabled(chain, enabledChains)), + + ...addressesAndEvmNetworks.evmNetworks + // convert evmNetworkIds into evmNetworks + .map(({ id }) => [evmNetworks[id], addressesAndEvmNetworks.addresses] as const) + .filter(([evmNetwork]) => isEvmNetworkEnabled(evmNetwork, enabledEvmNetworks)), + ] + // filter out requested chains/evmNetworks which don't exist + .filter(([chainOrNetwork]) => chainOrNetwork !== undefined) + // filter out requested chains/evmNetworks which have no rpcs + .filter(([chainOrNetwork]) => (chainOrNetwork.rpcs?.length ?? 0) > 0) + + // convert chains and evmNetworks into a list of tokenIds + .flatMap(([chainOrNetwork, addresses]) => + Object.values(tokens) + .filter((t) => t.chain?.id === chainOrNetwork.id || t.evmNetwork?.id === chainOrNetwork.id) + .filter((t) => isTokenEnabled(t, enabledTokens)) + .map((t) => [t.id, addresses] as const) + ) + + // collect all of the addresses for each tokenId into a map of { [tokenId]: addresses } + .reduce((addressesByToken, [tokenId, addresses]) => { + if (!addressesByToken[tokenId]) addressesByToken[tokenId] = [] + addressesByToken[tokenId].push(...addresses) + return addressesByToken + }, {} as AddressesByToken) + + for (const tokenId of addressesAndTokens.tokenIds) { + if (!addressesByToken[tokenId]) addressesByToken[tokenId] = [] + addressesByToken[tokenId].push( + ...addressesAndTokens.addresses.filter((a) => !addressesByToken[tokenId].includes(a)) + ) + } + + // + // Separate out the tokens in `addressesByToken` into groups based on `token.type` + // Input: { [token.id]: addresses, [token2.id]: addresses } + // Output: { [token.type]: { [token.id]: addresses }, [token2.type]: { [token2.id]: addresses } } + // + // This lets us only send each token to the balance module responsible for querying its balance. + // + const addressesByTokenByModule: Record> = [ + ...Object.entries(addressesByToken) + // convert tokenIds into tokens + .map(([tokenId, addresses]) => [tokens[tokenId], addresses] as const), + ] + // filter out tokens which don't exist + .filter(([token]) => !!token) + + // group each `{ [token.id]: addresses }` by token.type + .reduce((byModule, [token, addresses]) => { + if (!byModule[token.type]) byModule[token.type] = {} + byModule[token.type][token.id] = addresses + return byModule + }, {} as Record>) + + const chainIds = Object.keys(addressesByChain).concat( + ...addressesAndTokens.tokenIds.map((tid) => tokens[tid].chain?.id as ChainId).filter(Boolean) + ) + + // this restarts subscription if metadata changes for any of the chains we subscribe to + const miniMetadataIds = miniMetadatas + .filter((mm) => chainIds.includes(mm.chainId)) + .map((m) => m.id) + + return { + addressesByTokenByModule, + miniMetadataIds, + } +} diff --git a/apps/extension/src/core/domains/balances/store.ts b/apps/extension/src/core/domains/balances/store.ts index 0b2e75176f..ea46b46269 100644 --- a/apps/extension/src/core/domains/balances/store.ts +++ b/apps/extension/src/core/domains/balances/store.ts @@ -12,6 +12,7 @@ import { validateHexString } from "@core/util/validateHexString" import keyring from "@polkadot/ui-keyring" import { SingleAddress } from "@polkadot/ui-keyring/observable/types" import { assert } from "@polkadot/util" +import { isEthereumAddress } from "@polkadot/util-crypto" import * as Sentry from "@sentry/browser" import { AddressesByToken, @@ -55,7 +56,7 @@ export class BalanceStore { #chains: ChainIdAndRpcs[] = [] #evmNetworks: EvmNetworkIdAndRpcs[] = [] - #tokens: ReplaySubject = new ReplaySubject(1) + #tokens: TokenIdAndType[] = [] #miniMetadataIds = new Set() /** @@ -154,7 +155,6 @@ export class BalanceStore { (evmNetwork.substrateChain && chains[evmNetwork.substrateChain.id]?.account) || null, })) - // TODO: Only connect to chains on which the user has a non-zero balance. this.setChains(chainsToFetch, evmNetworksToFetch, enabledTokensList, miniMetadatas) }, error: (error) => { @@ -247,27 +247,28 @@ export class BalanceStore { const noMiniMetadataChanges = existingMiniMetadataIds.size === miniMetadatas.length && miniMetadatas.every((m) => existingMiniMetadataIds.has(m.id)) - if (noChainChanges && noEvmNetworkChanges && noMiniMetadataChanges) return + + const newTokens = Object.values(tokens).map(({ id, type, chain, evmNetwork }) => ({ + id, + type, + chain, + evmNetwork, + })) + const existingTokens = this.#tokens + const noTokenChanges = isEqual(newTokens, existingTokens) + + if (noChainChanges && noEvmNetworkChanges && noMiniMetadataChanges && noTokenChanges) return // Update chains and networks this.#chains = newChains this.#evmNetworks = newEvmNetworks + this.#tokens = newTokens const chainsMap = Object.fromEntries(this.#chains.map((chain) => [chain.id, chain])) const evmNetworksMap = Object.fromEntries( this.#evmNetworks.map((evmNetwork) => [evmNetwork.id, evmNetwork]) ) - // update tokens - this.#tokens.next( - Object.values(tokens).map(({ id, type, chain, evmNetwork }) => ({ - id, - type, - chain, - evmNetwork, - })) - ) - this.#miniMetadataIds = new Set(miniMetadatas.map((m) => m.id)) // Delete stored balances for chains and networks which no longer exist @@ -411,21 +412,14 @@ export class BalanceStore { const generation = this.#subscriptionsGeneration const addresses = await firstValueFrom(this.#addresses) - const tokens = await firstValueFrom(this.#tokens) + const tokens = this.#tokens const chainDetails = Object.fromEntries( - this.#chains.map(({ id, genesisHash, rpcs }) => [id, { genesisHash, rpcs }]) + this.#chains.map(({ id, genesisHash, rpcs, account }) => [id, { genesisHash, rpcs, account }]) ) const evmNetworkDetails = Object.fromEntries( this.#evmNetworks.map(({ id, rpcs }) => [id, { rpcs }]) ) - // For the following TODOs, try and put them inside the relevant balance module when it makes sense. - // Otherwise fall back to writing the workaround in here (but also then add it to the web app portfolio!) - // - // TODO: Don't fetch evm balances for substrate addresses - // TODO: Don't fetch evm balances for ethereum accounts on chains whose native account format is secp256k1 (i.e. moonbeam/river/base) - // On these chains we can fetch the balance purely via substrate (and fetching via both evm+substrate will double up the balance) - // const addressesByTokenByModule: Record> = {} tokens.forEach((token) => { // filter out tokens on chains/evmNetworks which have no rpcs @@ -435,15 +429,44 @@ export class BalanceStore { if (!hasRpcs) return if (!addressesByTokenByModule[token.type]) addressesByTokenByModule[token.type] = {} - // filter out substrate addresses which have a genesis hash that doesn't match the genesisHash of the token's chain - addressesByTokenByModule[token.type][token.id] = Object.keys(addresses).filter( - (address) => - !token.chain || - !addresses[address] || - addresses[address]?.includes(chainDetails[token.chain.id]?.genesisHash ?? "") - ) + + addressesByTokenByModule[token.type][token.id] = Object.keys(addresses) + .filter( + // filter out substrate addresses which have a genesis hash that doesn't match the genesisHash of the token's chain + (address) => + !token.chain || + !addresses[address] || + addresses[address]?.includes(chainDetails[token.chain.id]?.genesisHash ?? "") + ) + .filter((address) => { + // for each account, fetch balances only from compatible chains + return isEthereumAddress(address) + ? !!token.evmNetwork?.id || chainDetails[token.chain?.id ?? ""]?.account === "secp256k1" + : !!token.chain?.id + }) }) + // create placeholder rows for all missing balances, so FE knows they are initializing + const missingBalances: BalanceJson[] = [] + const existingBalances = await balancesDb.balances.toArray() + const existingBalancesKeys = new Set(existingBalances.map((b) => `${b.tokenId}:${b.address}`)) + + for (const balanceModule of balanceModules) { + const addressesByToken = addressesByTokenByModule[balanceModule.type] ?? {} + for (const [tokenId, addresses] of Object.entries(addressesByToken)) + for (const address of addresses) { + if (!existingBalancesKeys.has(`${tokenId}:${address}`)) + missingBalances.push(balanceModule.getPlaceholderBalance(tokenId, address)) + } + } + + if (missingBalances.length) { + const updates = Object.entries(new Balances(missingBalances).toJSON()).map( + ([id, balance]) => ({ id, ...balance }) + ) + await balancesDb.balances.bulkPut(updates) + } + const closeSubscriptionCallbacks = balanceModules.map((balanceModule) => balanceModule.subscribeBalances( addressesByTokenByModule[balanceModule.type] ?? {}, diff --git a/apps/extension/src/core/domains/tokenRates/store.ts b/apps/extension/src/core/domains/tokenRates/store.ts index 3ac9e18840..1605f35080 100644 --- a/apps/extension/src/core/domains/tokenRates/store.ts +++ b/apps/extension/src/core/domains/tokenRates/store.ts @@ -1,4 +1,5 @@ import { db } from "@core/db" +import { enabledTokensStore, filterEnabledTokens } from "@core/domains/tokens/store.enabledTokens" import { unsubscribe } from "@core/handlers/subscriptions" import { log } from "@core/log" import { chaindataProvider } from "@core/rpcs/chaindata" @@ -7,7 +8,7 @@ import { TokenList } from "@talismn/chaindata-provider" import { fetchTokenRates } from "@talismn/token-rates" import { Subscription, liveQuery } from "dexie" import debounce from "lodash/debounce" -import { BehaviorSubject } from "rxjs" +import { BehaviorSubject, combineLatest } from "rxjs" const MIN_REFRESH_INTERVAL = 60_000 // 60_000ms = 60s = 1 minute const REFRESH_INTERVAL = 300_000 // 5 minutes @@ -42,10 +43,15 @@ export class TokenRatesStore { // refresh when token list changes : crucial for first popup load after install or db migration const obsTokens = liveQuery(() => chaindataProvider.tokens()) - subTokenList = obsTokens.subscribe( - debounce(async (tokens) => { - if (this.#subscriptions.observed) await this.updateTokenRates(tokens) - }, 500) // debounce to delay in case on first load first token list is empty + const obsEnabledTokens = enabledTokensStore.observable + + subTokenList = combineLatest([obsTokens, obsEnabledTokens]).subscribe( + debounce(async ([tokens, enabledTokens]) => { + if (this.#subscriptions.observed) { + const tokensList = filterEnabledTokens(tokens, enabledTokens) + await this.updateTokenRates(tokensList) + } + }, 500) ) } else { // watching state check @@ -67,12 +73,14 @@ export class TokenRatesStore { async hydrateStore(): Promise { try { - const tokens = await chaindataProvider.tokens() - // TODO change filter to enabled tokens - const enabledTokens = Object.fromEntries( - Object.entries(tokens).filter(([, token]) => token.isDefault !== false) - ) - await this.updateTokenRates(enabledTokens) + const [tokens, enabledTokens] = await Promise.all([ + chaindataProvider.tokens(), + enabledTokensStore.get(), + ]) + + const tokensList = filterEnabledTokens(tokens, enabledTokens) + + await this.updateTokenRates(tokensList) return true } catch (error) { @@ -81,6 +89,9 @@ export class TokenRatesStore { } } + /** + * WARNING: Make sure the tokens list `tokens` only includes enabled tokens. + */ private async updateTokenRates(tokens: TokenList): Promise { const now = Date.now() const strTokenIds = Object.keys(tokens ?? {}).join(",") diff --git a/apps/extension/src/core/domains/tokens/store.enabledTokens.ts b/apps/extension/src/core/domains/tokens/store.enabledTokens.ts index fc91ef52aa..63e5e4c83e 100644 --- a/apps/extension/src/core/domains/tokens/store.enabledTokens.ts +++ b/apps/extension/src/core/domains/tokens/store.enabledTokens.ts @@ -1,5 +1,5 @@ import { StorageProvider } from "@core/libs/Store" -import { Token, TokenId } from "@talismn/chaindata-provider" +import { Token, TokenId, TokenList } from "@talismn/chaindata-provider" import { CustomErc20Token, CustomEvmNativeToken, CustomNativeToken } from "./types" @@ -39,3 +39,9 @@ export const isTokenEnabled = ( ) => { return enabledTokens[token.id] ?? (isCustomToken(token) || token.isDefault) } + +export const filterEnabledTokens = (tokens: TokenList, enabledTokens: EnabledTokens) => { + return Object.fromEntries( + Object.entries(tokens).filter(([, token]) => isTokenEnabled(token as Token, enabledTokens)) + ) as TokenList +} diff --git a/apps/extension/src/ui/apps/dashboard/routes/Networks/ChainsList.tsx b/apps/extension/src/ui/apps/dashboard/routes/Networks/ChainsList.tsx index 167a067c9a..0b4836bd82 100644 --- a/apps/extension/src/ui/apps/dashboard/routes/Networks/ChainsList.tsx +++ b/apps/extension/src/ui/apps/dashboard/routes/Networks/ChainsList.tsx @@ -1,6 +1,7 @@ import { enabledChainsStore, isChainEnabled } from "@core/domains/chains/store.enabledChains" import { Chain, isCustomChain } from "@talismn/chaindata-provider" import { ChevronRightIcon } from "@talismn/icons" +import { classNames } from "@talismn/util" import { sendAnalyticsEvent } from "@ui/api/analytics" import { ChainLogo } from "@ui/domains/Asset/ChainLogo" import useChains from "@ui/hooks/useChains" @@ -8,6 +9,7 @@ import { useEnabledChainsState } from "@ui/hooks/useEnabledChainsState" import { useSetting } from "@ui/hooks/useSettings" import sortBy from "lodash/sortBy" import { ChangeEventHandler, useCallback, useMemo, useRef } from "react" +import { useTranslation } from "react-i18next" import { useNavigate } from "react-router-dom" import { useIntersection } from "react-use" import { ListButton, Toggle } from "talisman-ui" @@ -16,6 +18,7 @@ import { ANALYTICS_PAGE } from "./analytics" import { CustomPill, TestnetPill } from "./Pills" export const ChainsList = ({ search }: { search?: string }) => { + const { t } = useTranslation("admin") const [useTestnets] = useSetting("useTestnets") const { chains: allChains } = useChains("all") const chains = useMemo( @@ -64,10 +67,40 @@ export const ChainsList = ({ search }: { search?: string }) => { [] ) + const enableAll = useCallback( + (enable = false) => + () => { + enabledChainsStore.set(Object.fromEntries(filteredChains.map((n) => [n.id, enable]))) + }, + [filteredChains] + ) + if (!sortedChains) return null return (
+
+ +
+ +
{sortedChains.map((chain) => ( { + const { t } = useTranslation("admin") + const [useTestnets] = useSetting("useTestnets") const { evmNetworks: allEvmNetworks } = useEvmNetworks("all") const evmNetworks = useMemo( @@ -77,10 +81,42 @@ export const EvmNetworksList = ({ search }: { search?: string }) => { [] ) + const enableAll = useCallback( + (enable = false) => + () => { + enabledEvmNetworksStore.set( + Object.fromEntries(filteredEvmNetworks.map((n) => [n.id, enable])) + ) + }, + [filteredEvmNetworks] + ) + if (!sortedNetworks) return null return (
+
+ +
+ +
{sortedNetworks.map((network) => ( { const { t } = useTranslation() - const balancesToDisplay = useDisplayBalances(balances) const currency = useSelectedCurrency() const { portfolio, available, locked } = useMemo(() => { - const { total, frozen, reserved, transferable } = balancesToDisplay.sum.fiat(currency) + const { total, frozen, reserved, transferable } = balances.sum.fiat(currency) return { portfolio: total, available: transferable, locked: frozen + reserved, } - }, [balancesToDisplay.sum, currency]) + }, [balances.sum, currency]) return ( <> @@ -43,35 +44,69 @@ const FullscreenPortfolioAssets = ({ balances }: { balances: Balances }) => {
- +
) } -const PageContent = ({ balances }: { balances: Balances }) => { - const balancesToDisplay = useDisplayBalances(balances) - const hasAccounts = useHasAccounts() +const EnableNetworkMessage: FC<{ type?: "substrate" | "evm" }> = ({ type }) => { + const { t } = useTranslation() + const navigate = useNavigate() + const handleClick = useCallback(() => { + if (type === "substrate") navigate("/networks/polkadot") + else if (type === "evm") navigate("/networks/ethereum") + else navigate("/networks") + }, [navigate, type]) return ( -
- {hasAccounts === false && ( -
- -
- )} - {hasAccounts && } +
+
{t("Enable some networks to display your assets")}
+
+ +
) } +const PageContent = () => { + const { networkBalances, evmNetworks, chains, accountType } = usePortfolio() + const balances = useDisplayBalances(networkBalances) + const hasAccounts = useHasAccounts() + + if (hasAccounts === undefined) return null + + if (!hasAccounts) + return ( +
+ +
+ ) + + if (!accountType && !evmNetworks.length && !chains.length) return + if (accountType === "sr25519" && !chains.length) return + if ( + accountType === "ethereum" && + !evmNetworks.length && + !chains.filter((c) => c.account === "secp256k1").length + ) + return + + return +} + export const PortfolioAssets = () => { - const { networkBalances } = usePortfolio() const { pageOpenEvent } = useAnalytics() useEffect(() => { pageOpenEvent("portfolio assets") }, [pageOpenEvent]) - return + return ( +
+ +
+ ) } diff --git a/apps/extension/src/ui/apps/popup/pages/Portfolio/PortfolioAssets.tsx b/apps/extension/src/ui/apps/popup/pages/Portfolio/PortfolioAssets.tsx index 0de2a80d72..983a33e6c3 100644 --- a/apps/extension/src/ui/apps/popup/pages/Portfolio/PortfolioAssets.tsx +++ b/apps/extension/src/ui/apps/popup/pages/Portfolio/PortfolioAssets.tsx @@ -1,6 +1,7 @@ import { Balance, Balances } from "@core/domains/balances/types" import { ChevronLeftIcon, CopyIcon, MoreHorizontalIcon, SendIcon } from "@talismn/icons" import { classNames } from "@talismn/util" +import { api } from "@ui/api" import { AccountContextMenu } from "@ui/apps/dashboard/routes/Portfolio/AccountContextMenu" import { AccountTypeIcon } from "@ui/domains/Account/AccountTypeIcon" import { Address } from "@ui/domains/Account/Address" @@ -17,10 +18,11 @@ import { useFormattedAddress } from "@ui/hooks/useFormattedAddress" import { useSearchParamsSelectedAccount } from "@ui/hooks/useSearchParamsSelectedAccount" import { useSearchParamsSelectedFolder } from "@ui/hooks/useSearchParamsSelectedFolder" import { useSendFundsPopup } from "@ui/hooks/useSendFundsPopup" -import { useCallback, useEffect, useMemo } from "react" +import { FC, useCallback, useEffect, useMemo } from "react" import { useTranslation } from "react-i18next" import { useNavigate } from "react-router-dom" import { + Button, ContextMenuTrigger, IconButton, Tooltip, @@ -28,13 +30,47 @@ import { TooltipTrigger, } from "talisman-ui" -const PageContent = ({ - allBalances, - networkBalances, -}: { - allBalances: Balances - networkBalances: Balances -}) => { +const EnableNetworkMessage: FC<{ type?: "substrate" | "evm" }> = ({ type }) => { + const { t } = useTranslation() + const handleClick = useCallback(() => { + if (type === "substrate") api.dashboardOpen("/networks/polkadot") + else if (type === "evm") api.dashboardOpen("/networks/ethereum") + else api.dashboardOpen("/networks") + window.close() + }, [type]) + + return ( +
+
{t("Enable some networks to display your assets")}
+
+ +
+
+ ) +} + +const MainContent: FC<{ balances: Balances }> = ({ balances }) => { + const { evmNetworks, chains } = usePortfolio() + const { account } = useSearchParamsSelectedAccount() + + if (!account?.type && !evmNetworks.length && !chains.length) return + if (account?.type === "sr25519" && !chains.length) + return + if ( + account?.type === "ethereum" && + !evmNetworks.length && + !chains.filter((c) => c.account === "secp256k1").length + ) + return + + return +} + +const PageContent = () => { + const allBalances = useBalances() + const { networkBalances } = usePortfolio() const { account } = useSearchParamsSelectedAccount() const { folder } = useSearchParamsSelectedFolder() @@ -166,20 +202,18 @@ const PageContent = ({
- +
) } export const PortfolioAssets = () => { - const allBalances = useBalances() - const { networkBalances } = usePortfolio() const { popupOpenEvent } = useAnalytics() useEffect(() => { popupOpenEvent("portfolio assets") }, [popupOpenEvent]) - return + return } diff --git a/apps/extension/src/ui/atoms/balances.ts b/apps/extension/src/ui/atoms/balances.ts index d5b17d0b51..ed15da3637 100644 --- a/apps/extension/src/ui/atoms/balances.ts +++ b/apps/extension/src/ui/atoms/balances.ts @@ -24,7 +24,6 @@ const NO_OP = () => {} const rawBalancesState = atom({ key: "rawBalancesState", - default: [], effects: [ // sync from db ({ setSelf }) => { @@ -37,7 +36,7 @@ const rawBalancesState = atom({ return () => sub.unsubscribe() }, - // instruct backend to keep db syncrhonized while this atom is in use + // instruct backend to keep db synchronized while this atom is in use () => api.balances(NO_OP), ], /** diff --git a/apps/extension/src/ui/atoms/chaindata.ts b/apps/extension/src/ui/atoms/chaindata.ts index 2b351a714e..1b0bb7bc00 100644 --- a/apps/extension/src/ui/atoms/chaindata.ts +++ b/apps/extension/src/ui/atoms/chaindata.ts @@ -30,7 +30,6 @@ const filterNoTestnet = ({ isTestnet }: { isTestnet?: boolean }) => isTestnet == export const evmNetworksEnabledState = atom({ key: "evmNetworksEnabledState", - default: {}, effects: [ ({ setSelf }) => { const sub = enabledEvmNetworksStore.observable.subscribe(setSelf) @@ -43,7 +42,6 @@ export const evmNetworksEnabledState = atom({ export const allEvmNetworksState = atom<(EvmNetwork | CustomEvmNetwork)[]>({ key: "allEvmNetworksState", - default: [], effects: [ // sync from db ({ setSelf }) => { @@ -83,16 +81,6 @@ export const evmNetworksWithTestnetsMapState = selector({ }, }) -// export const evmNetworkQuery = selectorFamily({ -// key: "evmNetworkQuery", -// get: -// (evmNetworkId: EvmNetworkId) => -// ({ get }) => { -// const networks = get(evmNetworksWithTestnetsMapState) -// return networks[evmNetworkId] -// }, -// }) - export const evmNetworksWithoutTestnetsState = selector({ key: "evmNetworksWithoutTestnetsState", get: ({ get }) => { @@ -111,7 +99,6 @@ export const evmNetworksWithoutTestnetsMapState = selector({ export const chainsEnabledState = atom({ key: "chainsEnabledState", - default: {}, effects: [ ({ setSelf }) => { const sub = enabledChainsStore.observable.subscribe(setSelf) @@ -124,7 +111,6 @@ export const chainsEnabledState = atom({ export const allChainsState = atom<(Chain | CustomChain)[]>({ key: "allChainsState", - default: [], effects: [ // sync from db ({ setSelf }) => { @@ -132,7 +118,7 @@ export const allChainsState = atom<(Chain | CustomChain)[]>({ const sub = obs.subscribe(setSelf) return () => sub.unsubscribe() }, - // instruct backend to keep db syncrhonized while this atom is in use + // instruct backend to keep db synchronized while this atom is in use () => api.chains(NO_OP), ], }) @@ -163,16 +149,6 @@ export const chainsWithTestnetsMapState = selector({ }, }) -// export const chainQuery = selectorFamily({ -// key: "chainQuery", -// get: -// (chainId: ChainId) => -// ({ get }) => { -// const networks = get(chainsWithTestnetsMapState) -// return networks[chainId] -// }, -// }) - export const chainsWithoutTestnetsState = selector({ key: "chainsWithoutTestnetsState", get: ({ get }) => { @@ -191,7 +167,6 @@ export const chainsWithoutTestnetsMapState = selector({ export const tokensEnabledState = atom({ key: "tokensEnabledState", - default: {}, effects: [ ({ setSelf }) => { const sub = enabledTokensStore.observable.subscribe(setSelf) @@ -204,7 +179,6 @@ export const tokensEnabledState = atom({ export const allTokensMapState = atom({ key: "allTokensMapState", - default: {}, effects: [ // sync from db ({ setSelf }) => { @@ -213,7 +187,7 @@ export const allTokensMapState = atom({ return () => sub.unsubscribe() }, - // instruct backend to keep db syncrhonized while this atom is in use + // instruct backend to keep db synchronized while this atom is in use () => api.tokens(NO_OP), ], }) @@ -235,10 +209,10 @@ export const allTokensState = selector({ export const tokensWithTestnetsState = selector({ key: "tokensWithTestnetsState", get: ({ get }) => { - const tokensMap = get(allTokensState) + const tokens = get(allTokensState) const chainsMap = get(chainsWithTestnetsMapState) const evmNetworksMap = get(evmNetworksWithTestnetsMapState) - return Object.values(tokensMap).filter( + return tokens.filter( (token) => (token.chain && chainsMap[token.chain.id]) || (token.evmNetwork && evmNetworksMap[token.evmNetwork.id]) diff --git a/apps/extension/src/ui/atoms/tokenRates.ts b/apps/extension/src/ui/atoms/tokenRates.ts index 02a2cdd25b..28840bb842 100644 --- a/apps/extension/src/ui/atoms/tokenRates.ts +++ b/apps/extension/src/ui/atoms/tokenRates.ts @@ -17,7 +17,7 @@ export const tokenRatesState = atom({ const sub = obs.subscribe(setSelf) return () => sub.unsubscribe() }, - // instruct backend to keep db syncrhonized while this atom is in use + // instruct backend to keep db synchronized while this atom is in use () => api.tokenRates(NO_OP), ], }) diff --git a/apps/extension/src/ui/domains/Account/DerivedAccountPickerBase.tsx b/apps/extension/src/ui/domains/Account/DerivedAccountPickerBase.tsx index 3d27230da7..6faa5d66d7 100644 --- a/apps/extension/src/ui/domains/Account/DerivedAccountPickerBase.tsx +++ b/apps/extension/src/ui/domains/Account/DerivedAccountPickerBase.tsx @@ -25,14 +25,19 @@ const PagerButton: FC<{ disabled?: boolean; children: ReactNode; onClick?: () => ) -const AccountButtonShimmer = () => ( +const AccountButtonShimmer: FC<{ withBalances: boolean }> = ({ withBalances }) => (
-
+
) @@ -46,6 +51,7 @@ const AccountButton: FC = ({ selected, onClick, isBalanceLoading, + withBalances, }) => { const { balanceDetails, totalUsd } = useBalanceDetails(balances) @@ -66,18 +72,20 @@ const AccountButton: FC = ({
- - - - - - - {balanceDetails && ( - -
{balanceDetails}
-
- )} -
+ {withBalances && ( + + + + + + + {balanceDetails && ( + +
{balanceDetails}
+
+ )} +
+ )}
{connected ? ( @@ -96,16 +104,19 @@ export type DerivedAccountBase = AccountJson & { address: string balances: Balances isBalanceLoading: boolean + connected?: boolean selected?: boolean } type AccountButtonProps = DerivedAccountBase & { + withBalances: boolean onClick: () => void } type DerivedAccountPickerBaseProps = { accounts: (DerivedAccountBase | null)[] + withBalances: boolean canPageBack?: boolean disablePaging?: boolean onPagerFirstClick?: () => void @@ -122,6 +133,7 @@ export const DerivedAccountPickerBase: FC = ({ onPagerPrevClick, onPagerNextClick, onAccountClick, + withBalances = true, }) => { const handleToggleAccount = useCallback( (acc: DerivedAccountBase) => () => { @@ -137,11 +149,12 @@ export const DerivedAccountPickerBase: FC = ({ account ? ( ) : ( - + ) )}
diff --git a/apps/extension/src/ui/domains/Account/DerivedFromMnemonicAccountPicker.tsx b/apps/extension/src/ui/domains/Account/DerivedFromMnemonicAccountPicker.tsx index 2e0e0755de..a60ee88fb2 100644 --- a/apps/extension/src/ui/domains/Account/DerivedFromMnemonicAccountPicker.tsx +++ b/apps/extension/src/ui/domains/Account/DerivedFromMnemonicAccountPicker.tsx @@ -1,13 +1,17 @@ import { formatSuri } from "@core/domains/accounts/helpers" import { AccountAddressType, RequestAccountCreateFromSuri } from "@core/domains/accounts/types" import { AddressesAndEvmNetwork } from "@core/domains/balances/types" +import { isChainEnabled } from "@core/domains/chains/store.enabledChains" import { getEthDerivationPath } from "@core/domains/ethereum/helpers" +import { isEvmNetworkEnabled } from "@core/domains/ethereum/store.enabledEvmNetworks" import { AddressesByChain } from "@core/types/base" import { convertAddress } from "@talisman/util/convertAddress" import { api } from "@ui/api" import useAccounts from "@ui/hooks/useAccounts" import useBalancesByParams from "@ui/hooks/useBalancesByParams" import useChains from "@ui/hooks/useChains" +import { useEnabledChainsState } from "@ui/hooks/useEnabledChainsState" +import { useEnabledEvmNetworksState } from "@ui/hooks/useEnabledEvmNetworksState" import { useEvmNetworks } from "@ui/hooks/useEvmNetworks" import { FC, useCallback, useEffect, useMemo, useState } from "react" @@ -70,9 +74,12 @@ const useDerivedAccounts = ( } }, [itemsPerPage, mnemonic, name, pageIndex, type]) - const { chains } = useChains("all") + const { chains } = useChains("enabledWithoutTestnets") const { evmNetworks } = useEvmNetworks("enabledWithoutTestnets") + const enabledChains = useEnabledChainsState() + const enabledEvmNetworks = useEnabledEvmNetworksState() + const { expectedBalancesCount, addressesByChain, addressesAndEvmNetworks } = useMemo(() => { const expectedBalancesCount = type === "ethereum" @@ -87,21 +94,23 @@ const useDerivedAccounts = ( const evmNetworkIds = type === "ethereum" ? BALANCE_CHECK_EVM_NETWORK_IDS : [] const chainIds = type === "ethereum" ? [] : BALANCE_CHECK_SUBSTRATE_CHAIN_IDS - const testChains = (chains || []).filter((chain) => chainIds.includes(chain.id)) const addressesByChain: AddressesByChain = type === "ethereum" ? {} - : testChains.reduce( - (prev, curr) => ({ - ...prev, - [curr.id]: derivedAccounts - .filter((acc) => !!acc) - .map((acc) => acc as DerivedFromMnemonicAccount) - .map((account) => convertAddress(account.address, curr.prefix)), - }), - {} - ) + : (chains || []) + .filter((chain) => chainIds.includes(chain.id)) + .filter((chain) => isChainEnabled(chain, enabledChains)) + .reduce( + (prev, curr) => ({ + ...prev, + [curr.id]: derivedAccounts + .filter((acc) => !!acc) + .map((acc) => acc as DerivedFromMnemonicAccount) + .map((account) => convertAddress(account.address, curr.prefix)), + }), + {} + ) const addressesAndEvmNetworks: AddressesAndEvmNetwork = type === "ethereum" @@ -112,6 +121,7 @@ const useDerivedAccounts = ( .filter(Boolean) as string[], evmNetworks: (evmNetworks || []) .filter((chain) => evmNetworkIds.includes(chain.id)) + .filter((chain) => isEvmNetworkEnabled(chain, enabledEvmNetworks)) .map(({ id, nativeToken }) => ({ id, nativeToken: { id: nativeToken?.id as string }, @@ -124,7 +134,14 @@ const useDerivedAccounts = ( addressesByChain, addressesAndEvmNetworks, } - }, [chains, derivedAccounts, evmNetworks, type]) + }, [chains, derivedAccounts, enabledChains, enabledEvmNetworks, evmNetworks, type]) + + const withBalances = useMemo( + () => + (addressesByChain && Object.values(addressesByChain).some((addresses) => addresses.length)) || + !!addressesAndEvmNetworks?.evmNetworks.length, + [addressesAndEvmNetworks?.evmNetworks.length, addressesByChain] + ) const balances = useBalancesByParams({ addressesByChain, @@ -176,6 +193,7 @@ const useDerivedAccounts = ( return { accounts, + withBalances, error, } } @@ -198,7 +216,7 @@ export const DerivedFromMnemonicAccountPicker: FC = ( const itemsPerPage = 5 const [pageIndex, setPageIndex] = useState(0) const [selectedAccounts, setSelectedAccounts] = useState([]) - const { accounts, error } = useDerivedAccounts( + const { accounts, withBalances, error } = useDerivedAccounts( name, mnemonic, type, @@ -232,6 +250,7 @@ export const DerivedFromMnemonicAccountPicker: FC = ( <> 0} onAccountClick={handleToggleAccount} onPagerFirstClick={handlePageFirst} diff --git a/apps/extension/src/ui/domains/Account/LedgerEthereumAccountPicker.tsx b/apps/extension/src/ui/domains/Account/LedgerEthereumAccountPicker.tsx index 54b9e29166..4fa8a7f8fd 100644 --- a/apps/extension/src/ui/domains/Account/LedgerEthereumAccountPicker.tsx +++ b/apps/extension/src/ui/domains/Account/LedgerEthereumAccountPicker.tsx @@ -1,12 +1,14 @@ import { DEBUG } from "@core/constants" import { AddressesAndEvmNetwork } from "@core/domains/balances/types" import { getEthLedgerDerivationPath } from "@core/domains/ethereum/helpers" +import { isEvmNetworkEnabled } from "@core/domains/ethereum/store.enabledEvmNetworks" import { LedgerEthDerivationPathType } from "@core/domains/ethereum/types" import { convertAddress } from "@talisman/util/convertAddress" import { LedgerAccountDefEthereum } from "@ui/domains/Account/AccountAdd/AccountAddLedger/context" import { useLedgerEthereum } from "@ui/hooks/ledger/useLedgerEthereum" import useAccounts from "@ui/hooks/useAccounts" import useBalancesByParams from "@ui/hooks/useBalancesByParams" +import { useEnabledEvmNetworksState } from "@ui/hooks/useEnabledEvmNetworksState" import { useEvmNetworks } from "@ui/hooks/useEvmNetworks" import { FC, useCallback, useEffect, useMemo, useState } from "react" import { useTranslation } from "react-i18next" @@ -73,6 +75,7 @@ const useLedgerEthereumAccounts = ( const { evmNetworks } = useEvmNetworks("enabledWithoutTestnets") // which balances to fetch + const enabledEvmNetworks = useEnabledEvmNetworksState() const addressesAndEvmNetworks = useMemo(() => { // start fetching balances only when all accounts are known to prevent recreating subscription 5 times if (derivedAccounts.filter(Boolean).length < derivedAccounts.length) return undefined @@ -84,11 +87,17 @@ const useLedgerEthereumAccounts = ( .filter(Boolean) as string[], evmNetworks: (evmNetworks || []) .filter((chain) => BALANCE_CHECK_EVM_NETWORK_IDS.includes(chain.id)) + .filter((chain) => isEvmNetworkEnabled(chain, enabledEvmNetworks)) .map(({ id, nativeToken }) => ({ id, nativeToken: { id: nativeToken?.id as string } })), } return result - }, [derivedAccounts, evmNetworks]) + }, [derivedAccounts, enabledEvmNetworks, evmNetworks]) + + const withBalances = useMemo( + () => !!addressesAndEvmNetworks?.evmNetworks.length, + [addressesAndEvmNetworks?.evmNetworks.length] + ) const balances = useBalancesByParams({ addressesAndEvmNetworks }) @@ -127,6 +136,7 @@ const useLedgerEthereumAccounts = ( return { accounts, + withBalances, isBusy, error, connectionStatus, @@ -149,7 +159,7 @@ export const LedgerEthereumAccountPicker: FC = const itemsPerPage = 5 const [pageIndex, setPageIndex] = useState(0) const [selectedAccounts, setSelectedAccounts] = useState([]) - const { accounts, error, isBusy } = useLedgerEthereumAccounts( + const { accounts, error, isBusy, withBalances } = useLedgerEthereumAccounts( name, derivationPathType, selectedAccounts, @@ -178,6 +188,7 @@ export const LedgerEthereumAccountPicker: FC = <> 0} disablePaging={isBusy} onAccountClick={handleToggleAccount} diff --git a/apps/extension/src/ui/domains/Account/LedgerSubstrateAccountPicker.tsx b/apps/extension/src/ui/domains/Account/LedgerSubstrateAccountPicker.tsx index 1bc24f67d0..2366d9456b 100644 --- a/apps/extension/src/ui/domains/Account/LedgerSubstrateAccountPicker.tsx +++ b/apps/extension/src/ui/domains/Account/LedgerSubstrateAccountPicker.tsx @@ -1,3 +1,4 @@ +import { isChainEnabled } from "@core/domains/chains/store.enabledChains" import { log } from "@core/log" import { AddressesByChain } from "@core/types/base" import { convertAddress } from "@talisman/util/convertAddress" @@ -7,6 +8,7 @@ import { useLedgerSubstrateApp } from "@ui/hooks/ledger/useLedgerSubstrateApp" import useAccounts from "@ui/hooks/useAccounts" import useBalancesByParams from "@ui/hooks/useBalancesByParams" import useChain from "@ui/hooks/useChain" +import { useEnabledChainsState } from "@ui/hooks/useEnabledChainsState" import { FC, useCallback, useEffect, useMemo, useState } from "react" import { useTranslation } from "react-i18next" @@ -22,6 +24,11 @@ const useLedgerChainAccounts = ( const { t } = useTranslation() const chain = useChain(chainId) const app = useLedgerSubstrateApp(chain?.genesisHash) + const enabledChains = useEnabledChainsState() + const withBalances = useMemo( + () => !!chain && isChainEnabled(chain, enabledChains), + [chain, enabledChains] + ) const [ledgerAccounts, setLedgerAccounts] = useState<(LedgerSubstrateAccount | undefined)[]>([ ...Array(itemsPerPage), @@ -71,17 +78,17 @@ const useLedgerChainAccounts = ( // start fetching balances only when all accounts are known to prevent recreating subscription 5 times if (ledgerAccounts.filter(Boolean).length < ledgerAccounts.length) return undefined - const result: AddressesByChain = chain - ? { - [chain.id]: ledgerAccounts - .filter((acc) => !!acc) - .map((acc) => acc as LedgerSubstrateAccount) - .map((account) => convertAddress(account.address, chain.prefix)), - } - : {} + if (!chain || !isChainEnabled(chain, enabledChains)) return {} + + const result: AddressesByChain = { + [chain.id]: ledgerAccounts + .filter((acc) => !!acc) + .map((acc) => acc as LedgerSubstrateAccount) + .map((account) => convertAddress(account.address, chain.prefix)), + } return result - }, [chain, ledgerAccounts]) + }, [chain, enabledChains, ledgerAccounts]) const balances = useBalancesByParams({ addressesByChain }) @@ -129,6 +136,7 @@ const useLedgerChainAccounts = ( isBusy, error, connectionStatus, + withBalances, } } @@ -147,7 +155,7 @@ export const LedgerSubstrateAccountPicker: FC const itemsPerPage = 5 const [pageIndex, setPageIndex] = useState(0) const [selectedAccounts, setSelectedAccounts] = useState([]) - const { accounts, error, isBusy } = useLedgerChainAccounts( + const { accounts, withBalances, error, isBusy } = useLedgerChainAccounts( chainId, selectedAccounts, pageIndex, @@ -176,6 +184,7 @@ export const LedgerSubstrateAccountPicker: FC <> 0} onAccountClick={handleToggleAccount} diff --git a/apps/extension/src/ui/domains/Asset/Buy/BuyTokensForm.tsx b/apps/extension/src/ui/domains/Asset/Buy/BuyTokensForm.tsx index b5c92adc0f..1aec74a894 100644 --- a/apps/extension/src/ui/domains/Asset/Buy/BuyTokensForm.tsx +++ b/apps/extension/src/ui/domains/Asset/Buy/BuyTokensForm.tsx @@ -58,7 +58,7 @@ const useSupportedTokenIds = (chains?: Chain[], tokens?: Token[], address?: stri useEffect(() => { // pull up to date list from github // note that there is a 5min cache on github files - fetch(`${githubChaindataBaseUrl}/tokens-buyable.json`) + fetch(`${githubChaindataBaseUrl}/data/tokens-buyable.json`) .then(async (response) => { const tokenIds: string[] = await response.json() setSupportedTokenIds(tokenIds) diff --git a/apps/extension/src/ui/domains/Portfolio/AssetsTable/DashboardAssetsTable.tsx b/apps/extension/src/ui/domains/Portfolio/AssetsTable/DashboardAssetsTable.tsx index 2965bb8124..7b62c0243a 100644 --- a/apps/extension/src/ui/domains/Portfolio/AssetsTable/DashboardAssetsTable.tsx +++ b/apps/extension/src/ui/domains/Portfolio/AssetsTable/DashboardAssetsTable.tsx @@ -5,47 +5,19 @@ import { classNames } from "@talismn/util" import Fiat from "@ui/domains/Asset/Fiat" import { useAnalytics } from "@ui/hooks/useAnalytics" import { useBalancesStatus } from "@ui/hooks/useBalancesStatus" -import { FC, useCallback } from "react" +import { useCallback } from "react" import { Trans, useTranslation } from "react-i18next" import { useNavigate } from "react-router-dom" import { TokenLogo } from "../../Asset/TokenLogo" import { AssetBalanceCellValue } from "../AssetBalanceCellValue" import { useNomPoolStakingBanner } from "../NomPoolStakingContext" +import { useSelectedAccount } from "../SelectedAccountContext" import { useTokenBalancesSummary } from "../useTokenBalancesSummary" import { NetworksLogoStack } from "./NetworksLogoStack" import { usePortfolioNetworkIds } from "./usePortfolioNetworkIds" import { usePortfolioSymbolBalances } from "./usePortfolioSymbolBalances" -const AssetRowSkeleton: FC<{ className?: string }> = ({ className }) => { - return ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ) -} - type AssetRowProps = { balances: Balances } @@ -165,16 +137,23 @@ const AssetRow = ({ balances }: AssetRowProps) => { />
- + {status.status === "initializing" ? ( +
+
+
+
+ ) : ( + + )}
@@ -185,38 +164,26 @@ type AssetsTableProps = { balances: Balances } -const getSkeletonOpacity = (index: number) => { - // tailwind parses files to find classes that it should include in it's bundle - // so we can't dynamically compute the className - switch (index) { - case 0: - return "opacity-100" - case 1: - return "opacity-80" - case 2: - return "opacity-60" - case 3: - return "opacity-40" - case 4: - return "opacity-30" - case 5: - return "opacity-20" - case 6: - return "opacity-10" - default: - return "opacity-0" - } -} - export const DashboardAssetsTable = ({ balances }: AssetsTableProps) => { const { t } = useTranslation() // group by token (symbol) - const { symbolBalances, skeletons } = usePortfolioSymbolBalances(balances) + const { account } = useSelectedAccount() + const { symbolBalances } = usePortfolioSymbolBalances(balances) + + // assume balance subscription is initializing if there are no balances + if (!balances.count) return null + + if (!symbolBalances.length) + return ( +
+ {account ? t("No assets were found on this account.") : t("No assets were found.")} +
+ ) return (
-
Asset
+
{t("Asset")}
{t("Locked")}
{t("Available")}
@@ -224,9 +191,6 @@ export const DashboardAssetsTable = ({ balances }: AssetsTableProps) => { {symbolBalances.map(([symbol, b]) => ( ))} - {[...Array(skeletons).keys()].map((i) => ( - - ))}
) } diff --git a/apps/extension/src/ui/domains/Portfolio/AssetsTable/PopupAssetsTable.tsx b/apps/extension/src/ui/domains/Portfolio/AssetsTable/PopupAssetsTable.tsx index 77285363ad..4091236f4c 100644 --- a/apps/extension/src/ui/domains/Portfolio/AssetsTable/PopupAssetsTable.tsx +++ b/apps/extension/src/ui/domains/Portfolio/AssetsTable/PopupAssetsTable.tsx @@ -17,70 +17,17 @@ import { useNavigate } from "react-router-dom" import { TokenLogo } from "../../Asset/TokenLogo" import { useNomPoolStakingBanner } from "../NomPoolStakingContext" -import { useSelectedAccount } from "../SelectedAccountContext" import { StaleBalancesIcon } from "../StaleBalancesIcon" import { useTokenBalancesSummary } from "../useTokenBalancesSummary" import { NetworksLogoStack } from "./NetworksLogoStack" import { usePortfolioNetworkIds } from "./usePortfolioNetworkIds" import { usePortfolioSymbolBalances } from "./usePortfolioSymbolBalances" -const getSkeletonOpacity = (index: number) => { - // tailwind parses files to find classes that it should include - // so we can't dynamically compute the className - switch (index) { - case 0: - return "opacity-100" - case 1: - return "opacity-90" - case 2: - return "opacity-80" - case 3: - return "opacity-70" - case 4: - return "opacity-60" - case 5: - return "opacity-50" - case 6: - return "opacity-40" - case 7: - return "opacity-30" - case 8: - return "opacity-20" - case 9: - return "opacity-10" - default: - return "opacity-0" - } -} - type AssetRowProps = { balances: Balances locked?: boolean } -const AssetRowSkeleton = ({ className }: { className?: string }) => { - return ( -
-
-
-
-
-
-
-
-
-
-
-
-
- ) -} - const AssetRow = ({ balances, locked }: AssetRowProps) => { const networkIds = usePortfolioNetworkIds(balances) const { genericEvent } = useAnalytics() @@ -172,26 +119,35 @@ const AssetRow = ({ balances, locked }: AssetRowProps) => {
-
- - {locked ? : null} - -
-
- {fiat === null ? "-" : } -
+ {status.status === "initializing" ? ( + <> +
+
+ + ) : ( + <> +
+ + {locked ? : null} + +
+
+ {fiat === null ? "-" : } +
+ + )}
@@ -268,16 +224,13 @@ const BalancesGroup = ({ label, fiatAmount, className, children }: GroupProps) = } export const PopupAssetsTable = ({ balances }: GroupedAssetsTableProps) => { - const { account } = useSelectedAccount() + const { t } = useTranslation() + const { account } = useSearchParamsSelectedAccount() + const currency = useSelectedCurrency() // group by status by token (symbol) - const { - availableSymbolBalances: available, - lockedSymbolBalances: locked, - skeletons, - } = usePortfolioSymbolBalances(balances) - - const currency = useSelectedCurrency() + const { availableSymbolBalances: available, lockedSymbolBalances: locked } = + usePortfolioSymbolBalances(balances) // calculate totals const { total, totalAvailable, totalLocked } = useMemo(() => { @@ -285,9 +238,17 @@ export const PopupAssetsTable = ({ balances }: GroupedAssetsTableProps) => { return { total, totalAvailable: transferable, totalLocked: locked + reserved } }, [balances.sum, currency]) - const { t } = useTranslation() + // assume balance subscription is initializing if there are no balances + if (!balances.count) return null - if (!available.length && !locked.length) return null + if (!available.length && !locked.length) + return ( + +
+ {account ? t("No assets to display for this account.") : t("No assets to display.")} +
+
+ ) return ( @@ -307,10 +268,7 @@ export const PopupAssetsTable = ({ balances }: GroupedAssetsTableProps) => { {available.map(([symbol, b]) => ( ))} - {[...Array(skeletons).keys()].map((i) => ( - - ))} - {!skeletons && !available.length && ( + {!available.length && (
{account ? t("There are no available balances for this account.") diff --git a/apps/extension/src/ui/domains/Portfolio/AssetsTable/usePortfolioSymbolBalances.ts b/apps/extension/src/ui/domains/Portfolio/AssetsTable/usePortfolioSymbolBalances.ts index 75a1adf189..d195fb18ab 100644 --- a/apps/extension/src/ui/domains/Portfolio/AssetsTable/usePortfolioSymbolBalances.ts +++ b/apps/extension/src/ui/domains/Portfolio/AssetsTable/usePortfolioSymbolBalances.ts @@ -1,16 +1,9 @@ -import { - DEFAULT_PORTFOLIO_TOKENS_ETHEREUM, - DEFAULT_PORTFOLIO_TOKENS_SUBSTRATE, -} from "@core/constants" import { Balance, Balances } from "@core/domains/balances/types" import { FiatSumBalancesFormatter } from "@talismn/balances" import { TokenRateCurrency } from "@talismn/token-rates" import { useSelectedCurrency } from "@ui/hooks/useCurrency" import { useMemo } from "react" -import { usePortfolio } from "../context" -import { useSelectedAccount } from "../SelectedAccountContext" - type SymbolBalances = [string, Balances] const sortSymbolBalancesBy = (type: "total" | "available" | "locked", currency: TokenRateCurrency) => @@ -147,38 +140,5 @@ export const usePortfolioSymbolBalances = (balances: Balances) => { [currency, symbolBalances] ) - const { account, accounts } = useSelectedAccount() - const { networkFilter } = usePortfolio() - - const hasEthereumAccount = useMemo(() => accounts.some((a) => a.type === "ethereum"), [accounts]) - - // if specific account we have 2 rows minimum, if all accounts we have 4 - const skeletons = useMemo(() => { - // in this case we don't know the number of min rows, balances should be already loaded anyway - if (networkFilter) return symbolBalances.length ? 0 : 1 - - // If no accounts then it means "all accounts", expect all default tokens (substrate + eth) - // if account has a genesis hash then we expect only 1 chain - // otherwise we expect default tokens for account type - const expectedRows = (() => { - if (!account) - return ( - DEFAULT_PORTFOLIO_TOKENS_SUBSTRATE.length + - (hasEthereumAccount ? DEFAULT_PORTFOLIO_TOKENS_ETHEREUM.length : 0) - ) - if (account.genesisHash) return 1 - - // DEFAULT_TOKENS are only shown for accounts with no balance - const accountHasSomeBalance = - balances.find({ address: account?.address }).sum.planck.total > 0n - if (accountHasSomeBalance) return 0 - - if (account.type === "ethereum") return DEFAULT_PORTFOLIO_TOKENS_ETHEREUM.length - return DEFAULT_PORTFOLIO_TOKENS_SUBSTRATE.length - })() - - return symbolBalances.length < expectedRows ? expectedRows - symbolBalances.length : 0 - }, [account, hasEthereumAccount, networkFilter, balances, symbolBalances.length]) - - return { symbolBalances, availableSymbolBalances, lockedSymbolBalances, skeletons } + return { symbolBalances, availableSymbolBalances, lockedSymbolBalances } } diff --git a/apps/extension/src/ui/domains/Portfolio/context.tsx b/apps/extension/src/ui/domains/Portfolio/context.tsx index ba3f948c46..f538586198 100644 --- a/apps/extension/src/ui/domains/Portfolio/context.tsx +++ b/apps/extension/src/ui/domains/Portfolio/context.tsx @@ -1,6 +1,7 @@ import { AccountAddressType } from "@core/domains/accounts/types" import { Balances } from "@core/domains/balances/types" import { Token } from "@core/domains/tokens/types" +import { KeypairType } from "@polkadot/util-crypto/types" import { provideContext } from "@talisman/util/provideContext" import { ChainId, EvmNetworkId } from "@talismn/chaindata-provider" import { useSelectedAccount } from "@ui/domains/Portfolio/SelectedAccountContext" @@ -136,7 +137,7 @@ const usePortfolioProvider = () => { [account, balances, myBalances] ) - const accountType = useMemo(() => { + const accountType = useMemo(() => { if (account?.type === "ethereum") return "ethereum" if (account?.type) return "sr25519" // all substrate return undefined @@ -169,6 +170,7 @@ const usePortfolioProvider = () => { hydrate, allBalances, isLoading, + accountType, } } diff --git a/apps/extension/src/ui/domains/Portfolio/useDisplayBalances.ts b/apps/extension/src/ui/domains/Portfolio/useDisplayBalances.ts index d65d129316..73738f8fef 100644 --- a/apps/extension/src/ui/domains/Portfolio/useDisplayBalances.ts +++ b/apps/extension/src/ui/domains/Portfolio/useDisplayBalances.ts @@ -5,6 +5,7 @@ import { import { AccountJsonAny } from "@core/domains/accounts/types" import { Balance, Balances } from "@core/domains/balances/types" import { useSelectedAccount } from "@ui/domains/Portfolio/SelectedAccountContext" +import { useSearchParamsSelectedAccount } from "@ui/hooks/useSearchParamsSelectedAccount" import { useMemo } from "react" // TODO: default tokens should be controlled from chaindata @@ -37,7 +38,11 @@ const shouldDisplayBalance = (account: AccountJsonAny | undefined, balances: Bal } export const useDisplayBalances = (balances: Balances) => { - const { account } = useSelectedAccount() + const { account: dashboardAccount } = useSelectedAccount() + const { account: popupAccount } = useSearchParamsSelectedAccount() - return useMemo(() => balances.find(shouldDisplayBalance(account, balances)), [account, balances]) + return useMemo( + () => balances.find(shouldDisplayBalance(dashboardAccount || popupAccount, balances)), + [balances, dashboardAccount, popupAccount] + ) } diff --git a/apps/extension/src/ui/hooks/__tests__/useBalances.spec.ts b/apps/extension/src/ui/hooks/__tests__/useBalances.spec.ts index ee1e36e36b..ff436ddce2 100644 --- a/apps/extension/src/ui/hooks/__tests__/useBalances.spec.ts +++ b/apps/extension/src/ui/hooks/__tests__/useBalances.spec.ts @@ -1,14 +1,14 @@ import { Balances } from "@talismn/balances" -import { renderHook } from "@testing-library/react" -import { RecoilRoot } from "recoil" +import { renderHook, waitFor } from "@testing-library/react" +import { TestWrapper } from "../../../../tests/TestWrapper" import { useBalances } from "../useBalances" describe("useBalances tests", () => { test("Can get useBalances data", async () => { const { result } = renderHook(() => useBalances(), { - wrapper: RecoilRoot, + wrapper: TestWrapper, }) - expect(result.current).toBeInstanceOf(Balances) + await waitFor(() => expect(result.current).toBeInstanceOf(Balances)) }) }) diff --git a/apps/extension/src/ui/hooks/useBalancesByParams.tsx b/apps/extension/src/ui/hooks/useBalancesByParams.tsx index 3db87cffed..e5c2b76c46 100644 --- a/apps/extension/src/ui/hooks/useBalancesByParams.tsx +++ b/apps/extension/src/ui/hooks/useBalancesByParams.tsx @@ -45,12 +45,12 @@ export const useBalancesByParams = ({ async (update) => { switch (update.type) { case "reset": { - const newBalances = new Balances(update.balances, hydrate) + const newBalances = new Balances(update.balances) return subject.next(newBalances) } case "upsert": { - const newBalances = new Balances(update.balances, hydrate) + const newBalances = new Balances(update.balances) return subject.next(subject.value.add(newBalances)) } @@ -65,7 +65,7 @@ export const useBalancesByParams = ({ } } ), - [addressesByChain, addressesAndEvmNetworks, addressesAndTokens, hydrate] + [addressesByChain, addressesAndEvmNetworks, addressesAndTokens] ) // subscrition must be reinitialized (using the key) if parameters change @@ -83,6 +83,6 @@ export const useBalancesByParams = ({ const [debouncedBalances, setDebouncedBalances] = useState(() => balances) useDebounce(() => setDebouncedBalances(balances), 100, [balances]) - return debouncedBalances + return useMemo(() => new Balances(debouncedBalances, hydrate), [debouncedBalances, hydrate]) } export default useBalancesByParams diff --git a/apps/extension/src/ui/hooks/useBalancesStatus.ts b/apps/extension/src/ui/hooks/useBalancesStatus.ts index a014f094fb..edd49e312e 100644 --- a/apps/extension/src/ui/hooks/useBalancesStatus.ts +++ b/apps/extension/src/ui/hooks/useBalancesStatus.ts @@ -4,6 +4,7 @@ import { useMemo } from "react" export type BalancesStatus = | { status: "live" } | { status: "fetching" } + | { status: "initializing" } | { status: "stale"; staleChains: string[] } /** @@ -23,6 +24,8 @@ export const useBalancesStatus = (balances: Balances) => const hasCachedBalances = balances.each.some((b) => b.status === "cache") if (hasCachedBalances) return { status: "fetching" } + if (balances.each.every((b) => b.status === "initializing")) return { status: "initializing" } + // live return { status: "live" } }, [balances]) diff --git a/apps/extension/src/ui/hooks/useSearchParamsSelectedAccount.tsx b/apps/extension/src/ui/hooks/useSearchParamsSelectedAccount.tsx index 8973cd846a..28c924e3bc 100644 --- a/apps/extension/src/ui/hooks/useSearchParamsSelectedAccount.tsx +++ b/apps/extension/src/ui/hooks/useSearchParamsSelectedAccount.tsx @@ -6,7 +6,7 @@ export const useSearchParamsSelectedAccount = () => { const [searchParams] = useSearchParams() const address = searchParams.get("account") - const account = useAccountByAddress(address) ?? undefined + const account = useAccountByAddress(address !== "all" ? address : undefined) ?? undefined return { account } } diff --git a/apps/extension/tests/TestWrapper.tsx b/apps/extension/tests/TestWrapper.tsx new file mode 100644 index 0000000000..3d0e932c54 --- /dev/null +++ b/apps/extension/tests/TestWrapper.tsx @@ -0,0 +1,10 @@ +import React from "react" +import { RecoilRoot } from "recoil" + +export const TestWrapper: React.FC = ({ children }) => { + return ( + + loading children
}>{children} + + ) +} diff --git a/packages/balances/src/BalanceModule.ts b/packages/balances/src/BalanceModule.ts index fff000df9a..15024139d7 100644 --- a/packages/balances/src/BalanceModule.ts +++ b/packages/balances/src/BalanceModule.ts @@ -3,7 +3,14 @@ import { ChainConnector } from "@talismn/chain-connector" import { ChainConnectorEvm } from "@talismn/chain-connector-evm" import { ChainId, ChaindataProvider, IToken } from "@talismn/chaindata-provider" -import { AddressesByToken, Balances, SubscriptionCallback, UnsubscribeFn } from "./types" +import { + Address, + AddressesByToken, + BalanceJson, + Balances, + SubscriptionCallback, + UnsubscribeFn, +} from "./types" export type ExtendableTokenType = IToken export type ExtendableChainMeta = Record | undefined @@ -83,6 +90,10 @@ export const DefaultBalanceModule = < return () => {} }, + getPlaceholderBalance() { + throw new Error("Balance placeholder is not implemented in this module.") + }, + async fetchBalances() { throw new Error("Balance fetching is not implemented in this module.") }, @@ -153,6 +164,9 @@ interface BalanceModuleCommon< callback: SubscriptionCallback ): Promise + /** Used to provision balances in db while they are fetching for the first time */ + getPlaceholderBalance(tokenId: TTokenType["id"], address: Address): BalanceJson + /** Fetch balances for this module with optional filtering */ fetchBalances(addressesByToken: AddressesByToken): Promise diff --git a/packages/balances/src/modules/EvmErc20Module.ts b/packages/balances/src/modules/EvmErc20Module.ts index 65a8ce9861..d965e25fc8 100644 --- a/packages/balances/src/modules/EvmErc20Module.ts +++ b/packages/balances/src/modules/EvmErc20Module.ts @@ -1,8 +1,10 @@ import { assert } from "@polkadot/util" +import { ChainConnectorEvm } from "@talismn/chain-connector-evm" import { BalancesConfigTokenParams, EvmChainId, EvmNetworkId, + EvmNetworkList, NewTokenType, TokenList, githubTokenLogoUrl, @@ -153,9 +155,23 @@ export const EvmErc20Module: NewBalanceModule< return tokens }, + getPlaceholderBalance(tokenId, address): EvmErc20Balance { + const evmNetworkId = tokenId.split("-")[0] as EvmNetworkId + return { + source: "evm-erc20", + status: "initializing", + address: address, + multiChainId: { evmChainId: evmNetworkId }, + evmNetworkId, + tokenId, + free: "0", + } + }, + async subscribeBalances(addressesByToken, callback) { let subscriptionActive = true const subscriptionInterval = 6_000 // 6_000ms == 6 seconds + const initDelay = 1_500 // 1_500ms == 1.5 seconds const cache = new Map() // TODO remove this log @@ -165,14 +181,15 @@ export const EvmErc20Module: NewBalanceModule< // if subscriptionInterval is 6 seconds, this means we only poll chains with a zero balance every 30 seconds let zeroBalanceSubscriptionIntervalCounter = 0 + const evmNetworks = await chaindataProvider.evmNetworks() + const tokens = await chaindataProvider.tokens() + const poll = async () => { if (!subscriptionActive) return zeroBalanceSubscriptionIntervalCounter = (zeroBalanceSubscriptionIntervalCounter + 1) % 5 try { - const tokens = await chaindataProvider.tokens() - // regroup tokens by network const addressesByTokenByEvmNetwork = groupAddressesByTokenByEvmNetwork( addressesByToken, @@ -194,7 +211,14 @@ export const EvmErc20Module: NewBalanceModule< } try { - const balances = await this.fetchBalances(addressesByToken) + if (!chainConnectors.evm) + throw new Error(`This module requires an evm chain connector`) + const balances = await fetchBalances( + chainConnectors.evm, + evmNetworks, + tokens, + addressesByToken + ) // Don't call callback with balances which have not changed since the last poll. const json = balances.toJSON() @@ -212,7 +236,8 @@ export const EvmErc20Module: NewBalanceModule< setTimeout(poll, subscriptionInterval) } } - setTimeout(poll, subscriptionInterval) + + setTimeout(poll, initDelay) return () => { subscriptionActive = false @@ -220,109 +245,119 @@ export const EvmErc20Module: NewBalanceModule< }, async fetchBalances(addressesByToken) { + if (!chainConnectors.evm) throw new Error(`This module requires an evm chain connector`) + // TODO remove this log log.debug("fetchBalances", "evm-erc20", addressesByToken) const evmNetworks = await chaindataProvider.evmNetworks() const tokens = await chaindataProvider.tokens() - const addressesByTokenGroupedByEvmNetwork = groupAddressesByTokenByEvmNetwork( - addressesByToken, - tokens - ) + return fetchBalances(chainConnectors.evm, evmNetworks, tokens, addressesByToken) + }, + } +} - const balances = ( - await Promise.allSettled( - Object.entries(addressesByTokenGroupedByEvmNetwork).map( - async ([evmNetworkId, addressesByToken]) => { - if (!chainConnectors.evm) - throw new Error(`This module requires an evm chain connector`) +const fetchBalances = async ( + evmChainConnector: ChainConnectorEvm, + evmNetworks: EvmNetworkList, + tokens: TokenList, + addressesByToken: AddressesByToken +) => { + const addressesByTokenGroupedByEvmNetwork = groupAddressesByTokenByEvmNetwork( + addressesByToken, + tokens + ) + + const balances = ( + await Promise.allSettled( + Object.entries(addressesByTokenGroupedByEvmNetwork).map( + async ([evmNetworkId, addressesByToken]) => { + if (!evmChainConnector) throw new Error(`This module requires an evm chain connector`) + + const evmNetwork = evmNetworks[evmNetworkId] + if (!evmNetwork) throw new Error(`Evm network ${evmNetworkId} not found`) + + const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(evmNetworkId) + if (!publicClient) + throw new Error(`Could not get rpc provider for evm network ${evmNetworkId}`) + + const tokensAndAddresses = Object.entries(addressesByToken).reduce( + (tokensAndAddresses, [tokenId, addresses]) => { + const token = tokens[tokenId] + if (!token) { + log.debug(`Token ${tokenId} not found`) + return tokensAndAddresses + } - const evmNetwork = evmNetworks[evmNetworkId] - if (!evmNetwork) throw new Error(`Evm network ${evmNetworkId} not found`) - - const publicClient = await chainConnector.getPublicClientForEvmNetwork(evmNetworkId) - if (!publicClient) - throw new Error(`Could not get rpc provider for evm network ${evmNetworkId}`) - - const tokensAndAddresses = Object.entries(addressesByToken).reduce( - (tokensAndAddresses, [tokenId, addresses]) => { - const token = tokens[tokenId] - if (!token) { - log.debug(`Token ${tokenId} not found`) - return tokensAndAddresses - } - - // TODO: Fix @talismn/balances-react: it shouldn't pass every token to every module - if (token.type !== "evm-erc20") { - log.debug(`This module doesn't handle tokens of type ${token.type}`) - return tokensAndAddresses - } - - const tokenAndAddresses: [EvmErc20Token | CustomEvmErc20Token, string[]] = [ - token, - addresses, - ] - - return [...tokensAndAddresses, tokenAndAddresses] - }, - [] as Array<[EvmErc20Token | CustomEvmErc20Token, string[]]> - ) + // TODO: Fix @talismn/balances-react: it shouldn't pass every token to every module + if (token.type !== "evm-erc20") { + log.debug(`This module doesn't handle tokens of type ${token.type}`) + return tokensAndAddresses + } - // fetch all balances - const balanceRequests = tokensAndAddresses.flatMap(([token, addresses]) => { - return addresses.map( - async (address) => - new Balance({ - source: "evm-erc20", - - status: "live", - - address: address, - multiChainId: { evmChainId: evmNetwork.id }, - evmNetworkId, - tokenId: token.id, - - free: await getFreeBalance( - publicClient, - token.contractAddress as `0x${string}`, - address as `0x${string}` - ), - }) - ) - }) - - // wait for balance fetches to complete - const balanceResults = await Promise.allSettled(balanceRequests) - - // filter out errors - const balances = balanceResults - .map((result) => { - if (result.status === "rejected") { - log.debug(result.reason) - return false - } - return result.value - }) - .filter((balance): balance is Balance => balance !== false) + const tokenAndAddresses: [EvmErc20Token | CustomEvmErc20Token, string[]] = [ + token, + addresses, + ] - // return to caller - return new Balances(balances) - } + return [...tokensAndAddresses, tokenAndAddresses] + }, + [] as Array<[EvmErc20Token | CustomEvmErc20Token, string[]]> ) - ) + + // fetch all balances + const balanceRequests = tokensAndAddresses.flatMap(([token, addresses]) => { + return addresses.map( + async (address) => + new Balance({ + source: "evm-erc20", + + status: "live", + + address: address, + multiChainId: { evmChainId: evmNetwork.id }, + evmNetworkId, + tokenId: token.id, + + free: await getFreeBalance( + publicClient, + token.contractAddress as `0x${string}`, + address as `0x${string}` + ), + }) + ) + }) + + // wait for balance fetches to complete + const balanceResults = await Promise.allSettled(balanceRequests) + + // filter out errors + const balances = balanceResults + .map((result) => { + if (result.status === "rejected") { + log.debug(result.reason) + return false + } + return result.value + }) + .filter((balance): balance is Balance => balance !== false) + + // return to caller + return new Balances(balances) + } ) - .map((result) => { - if (result.status === "rejected") { - log.debug(result.reason) - return false - } - return result.value - }) - .filter((balances): balances is Balances => balances !== false) + ) + ) + .map((result) => { + if (result.status === "rejected") { + log.debug(result.reason) + return false + } + return result.value + }) + .filter((balances): balances is Balances => balances !== false) - return balances.reduce((allBalances, balances) => allBalances.add(balances), new Balances([])) - }, - } + return balances.reduce((allBalances, balances) => allBalances.add(balances), new Balances([])) } function groupAddressesByTokenByEvmNetwork( diff --git a/packages/balances/src/modules/EvmNativeModule.ts b/packages/balances/src/modules/EvmNativeModule.ts index 042b29d0f2..9cc17e8e84 100644 --- a/packages/balances/src/modules/EvmNativeModule.ts +++ b/packages/balances/src/modules/EvmNativeModule.ts @@ -1,8 +1,11 @@ +import { ChainConnectorEvm } from "@talismn/chain-connector-evm" import { BalancesConfigTokenParams, EvmChainId, EvmNetworkId, + EvmNetworkList, NewTokenType, + TokenList, githubTokenLogoUrl, } from "@talismn/chaindata-provider" import { hasOwnProperty, isEthereumAddress } from "@talismn/util" @@ -11,7 +14,15 @@ import { PublicClient } from "viem" import { DefaultBalanceModule, NewBalanceModule } from "../BalanceModule" import log from "../log" -import { Address, Amount, Balance, BalanceJsonList, Balances, NewBalanceType } from "../types" +import { + Address, + AddressesByToken, + Amount, + Balance, + BalanceJsonList, + Balances, + NewBalanceType, +} from "../types" import { abiMulticall } from "./abis/multicall" type ModuleType = "evm-native" @@ -111,17 +122,34 @@ export const EvmNativeModule: NewBalanceModule< return { [nativeToken.id]: nativeToken } }, + getPlaceholderBalance(tokenId, address): EvmNativeBalance { + const evmNetworkId = tokenId.split("-")[0] as EvmNetworkId + return { + source: "evm-native", + status: "initializing", + address: address, + multiChainId: { evmChainId: evmNetworkId }, + evmNetworkId, + tokenId, + free: "0", + } + }, + async subscribeBalances(addressesByToken, callback) { // TODO remove log.debug("subscribeBalances", "evm-native", addressesByToken) let subscriptionActive = true const subscriptionInterval = 6_000 // 6_000ms == 6 seconds + const initDelay = 500 // 500ms == 0.5 seconds const cache = new Map() // for chains with a zero balance we only call fetchBalances once every 5 subscriptionIntervals // if subscriptionInterval is 6 seconds, this means we only poll chains with a zero balance every 30 seconds let zeroBalanceSubscriptionIntervalCounter = 0 + const evmNetworks = await chaindataProvider.evmNetworks() + const tokens = await chaindataProvider.tokens() + const poll = async () => { if (!subscriptionActive) return @@ -144,7 +172,14 @@ export const EvmNativeModule: NewBalanceModule< try { const tokenAddresses = { [tokenId]: addressesByToken[tokenId] } - const balances = await this.fetchBalances(tokenAddresses) + if (!chainConnectors.evm) + throw new Error(`This module requires an evm chain connector`) + const balances = await fetchBalances( + chainConnectors.evm, + evmNetworks, + tokens, + tokenAddresses + ) // Don't call callback with balances which have not changed since the last poll. const json = balances.toJSON() @@ -162,7 +197,8 @@ export const EvmNativeModule: NewBalanceModule< setTimeout(poll, subscriptionInterval) } } - setTimeout(poll, subscriptionInterval) + + setTimeout(poll, initDelay) return () => { subscriptionActive = false @@ -170,75 +206,82 @@ export const EvmNativeModule: NewBalanceModule< }, async fetchBalances(addressesByToken) { - // TODO remove - log.debug("subscribeBalances", "evm-native", addressesByToken) + if (!chainConnectors.evm) throw new Error(`This module requires an evm chain connector`) + const evmNetworks = await chaindataProvider.evmNetworks() const tokens = await chaindataProvider.tokens() - const balances = ( - await Promise.allSettled( - Object.entries(addressesByToken).map(async ([tokenId, addresses]) => { - if (!chainConnectors.evm) throw new Error(`This module requires an evm chain connector`) + return fetchBalances(chainConnectors.evm, evmNetworks, tokens, addressesByToken) + }, + } +} - const token = tokens[tokenId] - if (!token) throw new Error(`Token ${tokenId} not found`) +const fetchBalances = async ( + evmChainConnector: ChainConnectorEvm, + evmNetworks: EvmNetworkList, + tokens: TokenList, + addressesByToken: AddressesByToken +) => { + const balances = ( + await Promise.allSettled( + Object.entries(addressesByToken).map(async ([tokenId, addresses]) => { + if (!evmChainConnector) throw new Error(`This module requires an evm chain connector`) - // TODO: Fix @talismn/balances-react: it shouldn't pass every token to every module - if (token.type !== "evm-native") - throw new Error(`This module doesn't handle tokens of type ${token.type}`) + const token = tokens[tokenId] + if (!token) throw new Error(`Token ${tokenId} not found`) - const evmNetworkId = token.evmNetwork?.id - if (!evmNetworkId) throw new Error(`Token ${tokenId} has no evm network`) + // TODO: Fix @talismn/balances-react: it shouldn't pass every token to every module + if (token.type !== "evm-native") + throw new Error(`This module doesn't handle tokens of type ${token.type}`) - const evmNetwork = evmNetworks[evmNetworkId] - if (!evmNetwork) throw new Error(`Evm network ${evmNetworkId} not found`) + const evmNetworkId = token.evmNetwork?.id + if (!evmNetworkId) throw new Error(`Token ${tokenId} has no evm network`) - const publicClient = await chainConnectors.evm.getPublicClientForEvmNetwork( - evmNetworkId - ) + const evmNetwork = evmNetworks[evmNetworkId] + if (!evmNetwork) throw new Error(`Evm network ${evmNetworkId} not found`) - if (!publicClient) - throw new Error(`Could not get rpc provider for evm network ${evmNetworkId}`) + const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(evmNetworkId) - // fetch all balances - const freeBalances = await getFreeBalances(publicClient, addresses) + if (!publicClient) + throw new Error(`Could not get rpc provider for evm network ${evmNetworkId}`) - const balanceResults = addresses - .map((address, i) => { - if (freeBalances[i] === "error") return false + // fetch all balances + const freeBalances = await getFreeBalances(publicClient, addresses) - return new Balance({ - source: "evm-native", + const balanceResults = addresses + .map((address, i) => { + if (freeBalances[i] === "error") return false - status: "live", + return new Balance({ + source: "evm-native", - address: address, - multiChainId: { evmChainId: evmNetwork.id }, - evmNetworkId, - tokenId, + status: "live", - free: freeBalances[i].toString(), - }) - }) - .filter((balance): balance is Balance => balance !== false) + address: address, + multiChainId: { evmChainId: evmNetwork.id }, + evmNetworkId, + tokenId, - // return to caller - return new Balances(balanceResults) + free: freeBalances[i].toString(), + }) }) - ) - ) - .map((result) => { - if (result.status === "rejected") { - log.debug(result.reason) - return false - } - return result.value - }) - .filter((balances): balances is Balances => balances !== false) + .filter((balance): balance is Balance => balance !== false) - return balances.reduce((allBalances, balances) => allBalances.add(balances), new Balances([])) - }, - } + // return to caller + return new Balances(balanceResults) + }) + ) + ) + .map((result) => { + if (result.status === "rejected") { + log.debug(result.reason) + return false + } + return result.value + }) + .filter((balances): balances is Balances => balances !== false) + + return balances.reduce((allBalances, balances) => allBalances.add(balances), new Balances([])) } async function getFreeBalance( diff --git a/packages/balances/src/modules/SubstrateAssetsModule.ts b/packages/balances/src/modules/SubstrateAssetsModule.ts index c0dc960409..20fd2e5859 100644 --- a/packages/balances/src/modules/SubstrateAssetsModule.ts +++ b/packages/balances/src/modules/SubstrateAssetsModule.ts @@ -242,6 +242,23 @@ export const SubAssetsModule: NewBalanceModule< return tokens }, + getPlaceholderBalance(tokenId, address): SubAssetsBalance { + const match = /([\d\w-]+)-substrate-assets/.exec(tokenId) + const chainId = match?.[1] + if (!chainId) throw new Error(`Can't detect chainId for token ${tokenId}`) + + return { + source: "substrate-assets", + status: "initializing", + address, + multiChainId: { subChainId: chainId }, + chainId, + tokenId, + free: "0", + locks: "0", + } + }, + // TODO: Don't create empty subscriptions async subscribeBalances(addressesByToken, callback) { const queries = await buildQueries( diff --git a/packages/balances/src/modules/SubstrateEquilibriumModule.ts b/packages/balances/src/modules/SubstrateEquilibriumModule.ts index 89e7211461..7d7b0dfb17 100644 --- a/packages/balances/src/modules/SubstrateEquilibriumModule.ts +++ b/packages/balances/src/modules/SubstrateEquilibriumModule.ts @@ -211,6 +211,22 @@ export const SubEquilibriumModule: NewBalanceModule< return tokens }, + getPlaceholderBalance(tokenId, address): SubEquilibriumBalance { + const match = /([\d\w-]+)-substrate-equilibrium/.exec(tokenId) + const chainId = match?.[1] + if (!chainId) throw new Error(`Can't detect chainId for token ${tokenId}`) + + return { + source: "substrate-equilibrium", + status: "initializing", + address, + multiChainId: { subChainId: chainId }, + chainId, + tokenId, + free: "0", + } + }, + // TODO: Don't create empty subscriptions async subscribeBalances(addressesByToken, callback) { const queries = await buildQueries( diff --git a/packages/balances/src/modules/SubstrateNativeModule.ts b/packages/balances/src/modules/SubstrateNativeModule.ts index 2ec2ad88ff..e44555e637 100644 --- a/packages/balances/src/modules/SubstrateNativeModule.ts +++ b/packages/balances/src/modules/SubstrateNativeModule.ts @@ -298,6 +298,27 @@ export const SubNativeModule: NewBalanceModule< return { [nativeToken.id]: nativeToken } }, + getPlaceholderBalance(tokenId, address): SubNativeBalance { + const match = /([\d\w-]+)-substrate-native/.exec(tokenId) + const chainId = match?.[1] + if (!chainId) throw new Error(`Can't detect chainId for token ${tokenId}`) + + return { + source: "substrate-native", + status: "initializing", + address, + multiChainId: { subChainId: chainId }, + chainId, + tokenId, + free: "0", + reserves: [{ label: "reserved", amount: "0" }], + locks: [ + { label: "fees", amount: "0", includeInTransferable: true, excludeFromFeePayable: true }, + { label: "misc", amount: "0" }, + ], + } + }, + async subscribeBalances(addressesByToken, callback) { assert(chainConnectors.substrate, "This module requires a substrate chain connector") diff --git a/packages/balances/src/modules/SubstratePsp22Module.ts b/packages/balances/src/modules/SubstratePsp22Module.ts index 7d3ac9614f..814e33344e 100644 --- a/packages/balances/src/modules/SubstratePsp22Module.ts +++ b/packages/balances/src/modules/SubstratePsp22Module.ts @@ -15,13 +15,14 @@ import { ChainId, NewTokenType, SubChainId, + TokenList, githubTokenLogoUrl, } from "@talismn/chaindata-provider" import isEqual from "lodash/isEqual" import { DefaultBalanceModule, NewBalanceModule, NewTransferParamsType } from "../BalanceModule" import log from "../log" -import { Amount, Balance, BalanceJson, Balances, NewBalanceType } from "../types" +import { AddressesByToken, Amount, Balance, BalanceJson, Balances, NewBalanceType } from "../types" import psp22Abi from "./abis/psp22.json" type ModuleType = "substrate-psp22" @@ -197,17 +198,38 @@ export const SubPsp22Module: NewBalanceModule< return tokens }, + getPlaceholderBalance(tokenId, address): SubPsp22Balance { + const match = /([\d\w-]+)-substrate-psp22/.exec(tokenId) + const chainId = match?.[1] + if (!chainId) throw new Error(`Can't detect chainId for token ${tokenId}`) + + return { + source: "substrate-psp22", + status: "initializing", + address, + multiChainId: { subChainId: chainId }, + chainId, + tokenId, + free: "0", + } + }, + // TODO: Don't create empty subscriptions async subscribeBalances(addressesByToken, callback) { let subscriptionActive = true const subscriptionInterval = 12_000 // 12_000ms == 12 seconds + const initDelay = 3_000 // 3000ms == 3 seconds const cache = new Map() + const tokens = await chaindataProvider.tokens() + const poll = async () => { if (!subscriptionActive) return try { - const balances = await this.fetchBalances(addressesByToken) + assert(chainConnectors.substrate, "This module requires a substrate chain connector") + + const balances = await fetchBalances(chainConnectors.substrate, tokens, addressesByToken) // Don't call callback with balances which have not changed since the last poll. const updatedBalances = new Balances( @@ -226,7 +248,8 @@ export const SubPsp22Module: NewBalanceModule< setTimeout(poll, subscriptionInterval) } } - setTimeout(poll, subscriptionInterval) + + setTimeout(poll, initDelay) return () => { subscriptionActive = false @@ -238,81 +261,7 @@ export const SubPsp22Module: NewBalanceModule< const tokens = await chaindataProvider.tokens() - const registry = new TypeRegistry() - const Psp22Abi = new Abi(psp22Abi) - - const balanceRequests = Object.entries(addressesByToken) - .flatMap(([tokenId, addresses]) => addresses.map((address) => [tokenId, address])) - .flatMap(async ([tokenId, address]) => { - const token = tokens[tokenId] - if (!token) { - log.debug(`Token ${tokenId} not found`) - return [] - } - - if (token.type !== "substrate-psp22") { - log.debug(`This module doesn't handle tokens of type ${token.type}`) - return [] - } - - const contractCall = makeContractCaller({ - chainConnector, - chainId: token.chain.id, - registry, - }) - - if (token.contractAddress === undefined) { - log.debug(`Token ${tokenId} of type substrate-psp22 doesn't have a contractAddress`) - return [] - } - - const result = await contractCall( - address, - token.contractAddress, - registry.createType( - "Vec", - Psp22Abi.findMessage("PSP22::balance_of").toU8a([ - // ACCOUNT - address, - ]) - ) - ) - - const balance = registry - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .createType("Balance", hexToU8a((result.toJSON()?.result as any)?.ok?.data).slice(1)) - .toString() - - return new Balance({ - source: "substrate-psp22", - - status: "live", - - address, - multiChainId: { subChainId: token.chain.id }, - chainId: token.chain.id, - tokenId, - - free: balance, - }) - }) - - // wait for balance fetches to complete - const balanceResults = await Promise.allSettled(balanceRequests) - - // filter out errors - const balances = balanceResults - .map((result) => { - if (result.status === "rejected") { - log.debug(result.reason) - return false - } - return result.value - }) - .filter((balance): balance is Balance => balance !== false) - - // return to caller - return new Balances(balances) + return fetchBalances(chainConnectors.substrate, tokens, addressesByToken) }, async transferToken({ @@ -435,3 +384,85 @@ const makeContractCaller = ), ]) ) + +const fetchBalances = async ( + chainConnector: ChainConnector, + tokens: TokenList, + addressesByToken: AddressesByToken +) => { + const registry = new TypeRegistry() + const Psp22Abi = new Abi(psp22Abi) + + const balanceRequests = Object.entries(addressesByToken) + .flatMap(([tokenId, addresses]) => addresses.map((address) => [tokenId, address])) + .flatMap(async ([tokenId, address]) => { + const token = tokens[tokenId] + if (!token) { + log.debug(`Token ${tokenId} not found`) + return [] + } + + if (token.type !== "substrate-psp22") { + log.debug(`This module doesn't handle tokens of type ${token.type}`) + return [] + } + + const contractCall = makeContractCaller({ + chainConnector, + chainId: token.chain.id, + registry, + }) + + if (token.contractAddress === undefined) { + log.debug(`Token ${tokenId} of type substrate-psp22 doesn't have a contractAddress`) + return [] + } + + const result = await contractCall( + address, + token.contractAddress, + registry.createType( + "Vec", + Psp22Abi.findMessage("PSP22::balance_of").toU8a([ + // ACCOUNT + address, + ]) + ) + ) + + const balance = registry + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .createType("Balance", hexToU8a((result.toJSON()?.result as any)?.ok?.data).slice(1)) + .toString() + + return new Balance({ + source: "substrate-psp22", + + status: "live", + + address, + multiChainId: { subChainId: token.chain.id }, + chainId: token.chain.id, + tokenId, + + free: balance, + }) + }) + + // wait for balance fetches to complete + const balanceResults = await Promise.allSettled(balanceRequests) + + // filter out errors + const balances = balanceResults + .map((result) => { + if (result.status === "rejected") { + log.debug(result.reason) + return false + } + return result.value + }) + .filter((balance): balance is Balance => balance !== false) + + // return to caller + return new Balances(balances) +} diff --git a/packages/balances/src/modules/SubstrateTokensModule.ts b/packages/balances/src/modules/SubstrateTokensModule.ts index 974b946ebf..b1b962011c 100644 --- a/packages/balances/src/modules/SubstrateTokensModule.ts +++ b/packages/balances/src/modules/SubstrateTokensModule.ts @@ -189,6 +189,24 @@ export const SubTokensModule: NewBalanceModule< return tokens }, + getPlaceholderBalance(tokenId, address): SubTokensBalance { + const match = /([\d\w-]+)-substrate-tokens/.exec(tokenId) + const chainId = match?.[1] + if (!chainId) throw new Error(`Can't detect chainId for token ${tokenId}`) + + return { + source: "substrate-tokens", + status: "initializing", + address, + multiChainId: { subChainId: chainId }, + chainId, + tokenId, + free: "0", + locks: "0", + reserves: "0", + } + }, + // TODO: Don't create empty subscriptions async subscribeBalances(addressesByToken, callback) { const queries = await buildQueries( diff --git a/packages/balances/src/modules/util/index.ts b/packages/balances/src/modules/util/index.ts index f0d79c1317..6abb085a32 100644 --- a/packages/balances/src/modules/util/index.ts +++ b/packages/balances/src/modules/util/index.ts @@ -235,8 +235,7 @@ export const deriveStatuses = ( balances: BalanceJson[] ): BalanceJson[] => { balances.forEach((balance) => { - if (balance.status === "live" || balance.status === "cache" || balance.status === "stale") - return balance + if (["live", "cache", "stale", "initializing"].includes(balance.status)) return balance if (validSubscriptionIds.size < 1) { balance.status = "cache" diff --git a/packages/balances/src/types/balancetypes.ts b/packages/balances/src/types/balancetypes.ts index f835f8ff6d..cf323004ab 100644 --- a/packages/balances/src/types/balancetypes.ts +++ b/packages/balances/src/types/balancetypes.ts @@ -51,6 +51,8 @@ export type BalanceStatus = | "cache" // balance was retrieved from the chain but we're unable to create a new subscription | "stale" + // balance has never been retrieved yet + | "initializing" /** `IBalance` is a common interface which all balance types must implement. */ export type IBalance = { diff --git a/packages/chaindata-provider-extension/src/ChaindataProviderExtension.ts b/packages/chaindata-provider-extension/src/ChaindataProviderExtension.ts index 4914ddfb89..eb2564ae72 100644 --- a/packages/chaindata-provider-extension/src/ChaindataProviderExtension.ts +++ b/packages/chaindata-provider-extension/src/ChaindataProviderExtension.ts @@ -568,25 +568,6 @@ export class ChaindataProviderExtension implements ChaindataProvider { .filter((token) => "isCustom" in token && token.isCustom) .map((token) => token.id) - // // workaround for chains that don't have a native token provisionned from chaindata - // // TODO : remove when fixed in chaindata - // const chain = await this.#db.chains.get(chainId) - // let shouldUpdateChain = false - // if ( - // chain && - // (!chain.nativeToken?.id || !newTokens.find((token) => token.id === chain.nativeToken?.id)) - // ) { - // const token = newTokens.find( - // (token) => token.type === "substrate-native" && token?.chain?.id === chainId - // ) - // if (token) { - // chain.nativeToken = { id: token.id } - // shouldUpdateChain = true - // } - // } - - // console.log("updateChainTokens %s %s", chainId, shouldUpdateChain, chain) - await this.#db.transaction("rw", this.#db.tokens, this.#db.chains, async () => { await this.#db.tokens.bulkDelete(notCustomTokenIds) await this.#db.tokens.bulkPut(newTokens.filter((token) => !customTokenIds.includes(token.id))) @@ -650,7 +631,8 @@ export class ChaindataProviderExtension implements ChaindataProvider { } await this.#db.transaction("rw", this.#db.tokens, async () => { const deleteChains = chainIdFilter ? new Set(chainIdFilter) : undefined - await this.#db.tokens + + const tokensToDelete = (await this.#db.tokens.toArray()) .filter((token) => { // don't delete custom tokens if ("isCustom" in token) return false @@ -659,12 +641,13 @@ export class ChaindataProviderExtension implements ChaindataProvider { if (deleteChains === undefined) return true // delete tokens on chainIdFilter chains is it is specified - if (!token.chain?.id) return true - if (deleteChains.has(token.chain.id)) return true + if (token.chain?.id && deleteChains.has(token.chain.id)) return true return false }) - .delete() + .map((token) => token.id) + + if (tokensToDelete.length) await this.#db.tokens.bulkDelete(tokensToDelete) // add all except ones matching custom existing ones (user may customize built-in tokens) const customTokenIds = new Set((await this.#db.tokens.toArray()).map((token) => token.id)) @@ -679,6 +662,7 @@ export class ChaindataProviderExtension implements ChaindataProvider { return false }) + await this.#db.tokens.bulkPut(newTokens) }) this.#lastHydratedTokensAt = now @@ -695,8 +679,12 @@ export class ChaindataProviderExtension implements ChaindataProvider { } async getIsBuiltInEvmNetwork(evmNetworkId: EvmNetworkId) { - const evmNetwork = await fetchEvmNetwork(evmNetworkId) - return !!evmNetwork + try { + const evmNetwork = await fetchEvmNetwork(evmNetworkId) + return !!evmNetwork + } catch (e) { + return false + } } transaction( diff --git a/packages/chaindata-provider/src/constants.ts b/packages/chaindata-provider/src/constants.ts index b634fb5249..d2706db974 100644 --- a/packages/chaindata-provider/src/constants.ts +++ b/packages/chaindata-provider/src/constants.ts @@ -1,10 +1,13 @@ import { ChainId, EvmNetworkId, TokenId } from "./types" +// @dev : temporarily change branch here when testing changes in chaindata +const CHAINDATA_BRANCH = "main" + // -// Chaindata (Published to GitHub Pages) Constants +// Chaindata published files (dist folder) // -export const chaindataUrl = "https://talismansociety.github.io/chaindata" +export const chaindataUrl = `https://raw.githubusercontent.com/TalismanSociety/chaindata/${CHAINDATA_BRANCH}/dist` export const chaindataChainsAllUrl = `${chaindataUrl}/chains/all.json` export const chaindataChainsSummaryUrl = `${chaindataUrl}/chains/summary.json` @@ -32,13 +35,13 @@ export const githubApi = "https://api.github.com" export const githubChaindataOrg = "TalismanSociety" export const githubChaindataRepo = "chaindata" -export const githubChaindataBranch = "main" +export const githubChaindataBranch = CHAINDATA_BRANCH export const githubChaindataBaseUrl = `https://raw.githubusercontent.com/${githubChaindataOrg}/${githubChaindataRepo}/${githubChaindataBranch}` -export const githubChainsUrl = `${githubChaindataBaseUrl}/chaindata.json` -export const githubTestnetChainsUrl = `${githubChaindataBaseUrl}/testnets-chaindata.json` -export const githubEvmNetworksUrl = `${githubChaindataBaseUrl}/evm-networks.json` +export const githubChainsUrl = `${githubChaindataBaseUrl}/data/chaindata.json` +export const githubTestnetChainsUrl = `${githubChaindataBaseUrl}/data/testnets-chaindata.json` +export const githubEvmNetworksUrl = `${githubChaindataBaseUrl}/data/evm-networks.json` export const githubChaindataChainsAssetsDir = "assets/chains" export const githubChaindataTokensAssetsDir = "assets/tokens" diff --git a/packages/chaindata-provider/src/types/EvmNetwork.ts b/packages/chaindata-provider/src/types/EvmNetwork.ts index 73af620101..d470f3d5e4 100644 --- a/packages/chaindata-provider/src/types/EvmNetwork.ts +++ b/packages/chaindata-provider/src/types/EvmNetwork.ts @@ -15,7 +15,7 @@ export type EvmNetwork = { // TODO: Create ethereum tokens store (and reference here by id). // Or extend substrate tokens store to support both substrate and ethereum tokens. nativeToken: { id: TokenId } | null - // TODO remove tokens property, as tokens already reference their network + /** @deprecated tokens already reference their network */ tokens: Array<{ id: TokenId }> | null explorerUrl: string | null rpcs: Array | null diff --git a/packages/token-rates/src/TokenRates.ts b/packages/token-rates/src/TokenRates.ts index aa7d44ae6a..a97af40d60 100644 --- a/packages/token-rates/src/TokenRates.ts +++ b/packages/token-rates/src/TokenRates.ts @@ -32,8 +32,6 @@ export async function fetchTokenRates(tokens: Record) { // ignore testnet tokens .filter(({ isTestnet }) => !isTestnet) - // TODO enabled tokens only - .filter(({ isDefault }) => isDefault !== false) // ignore tokens which don't have a coingeckoId .filter(hasCoingeckoId)