Skip to content

Commit

Permalink
feat: initialize empty balances
Browse files Browse the repository at this point in the history
  • Loading branch information
0xKheops committed Dec 6, 2023
1 parent 448fbaa commit a28e045
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 16 deletions.
56 changes: 41 additions & 15 deletions apps/extension/src/core/domains/balances/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<string, AddressesByToken<Token>> = {}
tokens.forEach((token) => {
// filter out tokens on chains/evmNetworks which have no rpcs
Expand All @@ -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] ?? {},
Expand Down
16 changes: 15 additions & 1 deletion packages/balances/src/BalanceModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> | undefined
Expand Down Expand Up @@ -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.")
},
Expand Down Expand Up @@ -153,6 +164,9 @@ interface BalanceModuleCommon<
callback: SubscriptionCallback<Balances>
): Promise<UnsubscribeFn>

/** 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<TTokenType>): Promise<Balances>

Expand Down
13 changes: 13 additions & 0 deletions packages/balances/src/modules/EvmErc20Module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions packages/balances/src/modules/EvmNativeModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions packages/balances/src/modules/SubstrateAssetsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
16 changes: 16 additions & 0 deletions packages/balances/src/modules/SubstrateEquilibriumModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
21 changes: 21 additions & 0 deletions packages/balances/src/modules/SubstrateNativeModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
16 changes: 16 additions & 0 deletions packages/balances/src/modules/SubstratePsp22Module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions packages/balances/src/modules/SubstrateTokensModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions packages/balances/src/types/balancetypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down

0 comments on commit a28e045

Please sign in to comment.