From a28e04534bdc21c8b4e22f6447afcf8e8ca90af3 Mon Sep 17 00:00:00 2001 From: Kheops <26880866+0xKheops@users.noreply.github.com> Date: Wed, 6 Dec 2023 18:35:28 +0900 Subject: [PATCH] feat: initialize empty balances --- .../src/core/domains/balances/store.ts | 56 ++++++++++++++----- packages/balances/src/BalanceModule.ts | 16 +++++- .../balances/src/modules/EvmErc20Module.ts | 13 +++++ .../balances/src/modules/EvmNativeModule.ts | 13 +++++ .../src/modules/SubstrateAssetsModule.ts | 17 ++++++ .../src/modules/SubstrateEquilibriumModule.ts | 16 ++++++ .../src/modules/SubstrateNativeModule.ts | 21 +++++++ .../src/modules/SubstratePsp22Module.ts | 16 ++++++ .../src/modules/SubstrateTokensModule.ts | 18 ++++++ packages/balances/src/types/balancetypes.ts | 2 + 10 files changed, 172 insertions(+), 16 deletions(-) diff --git a/apps/extension/src/core/domains/balances/store.ts b/apps/extension/src/core/domains/balances/store.ts index ec480aba53..7f2a09356a 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, @@ -413,19 +414,12 @@ export class BalanceStore { const addresses = await firstValueFrom(this.#addresses) 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,47 @@ 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() + + for (const balanceModule of balanceModules) { + const addressesByToken = addressesByTokenByModule[balanceModule.type] ?? {} + for (const [tokenId, addresses] of Object.entries(addressesByToken)) + for (const address of addresses) { + if (!existingBalances.find((b) => b.address === address && b.tokenId === tokenId)) + missingBalances.push(balanceModule.getPlaceholderBalance(tokenId, address)) + } + } + + if (missingBalances.length) { + const updates = Object.entries(new Balances(missingBalances).toJSON()).map( + ([id, balance]) => ({ + id, + ...balance, + status: BalanceStatusLive(subscriptionId), + }) + ) + await balancesDb.balances.bulkPut(updates) + } + const closeSubscriptionCallbacks = balanceModules.map((balanceModule) => balanceModule.subscribeBalances( addressesByTokenByModule[balanceModule.type] ?? {}, 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 69825e5332..d965e25fc8 100644 --- a/packages/balances/src/modules/EvmErc20Module.ts +++ b/packages/balances/src/modules/EvmErc20Module.ts @@ -155,6 +155,19 @@ 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 diff --git a/packages/balances/src/modules/EvmNativeModule.ts b/packages/balances/src/modules/EvmNativeModule.ts index 95eb615c7a..9cc17e8e84 100644 --- a/packages/balances/src/modules/EvmNativeModule.ts +++ b/packages/balances/src/modules/EvmNativeModule.ts @@ -122,6 +122,19 @@ 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) diff --git a/packages/balances/src/modules/SubstrateAssetsModule.ts b/packages/balances/src/modules/SubstrateAssetsModule.ts index c0dc960409..e620f1459a 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?.[0] + 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..219be05810 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?.[0] + 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..009b504e16 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?.[0] + 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 17c3af34ea..712c433dd8 100644 --- a/packages/balances/src/modules/SubstratePsp22Module.ts +++ b/packages/balances/src/modules/SubstratePsp22Module.ts @@ -198,6 +198,22 @@ export const SubPsp22Module: NewBalanceModule< return tokens }, + getPlaceholderBalance(tokenId, address): SubPsp22Balance { + const match = /([\d\w]+)-substrate-psp22/.exec(tokenId) + const chainId = match?.[0] + 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 diff --git a/packages/balances/src/modules/SubstrateTokensModule.ts b/packages/balances/src/modules/SubstrateTokensModule.ts index 974b946ebf..b8c3141c32 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?.[0] + 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/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 = {