diff --git a/apps/extension/src/ui/domains/Portfolio/AssetDetails/useChainTokenBalances.ts b/apps/extension/src/ui/domains/Portfolio/AssetDetails/useChainTokenBalances.ts index 988d6a5d2e..a6adcb787c 100644 --- a/apps/extension/src/ui/domains/Portfolio/AssetDetails/useChainTokenBalances.ts +++ b/apps/extension/src/ui/domains/Portfolio/AssetDetails/useChainTokenBalances.ts @@ -124,8 +124,8 @@ export const useChainTokenBalances = ({ chainId, balances }: ChainTokenBalancesP })), ) - // BITENSOR - const subtensor1 = tokenBalances.each.flatMap((b) => + // BITTENSOR + const subtensor = tokenBalances.each.flatMap((b) => b.subtensor.map((subtensor, index) => ({ key: `${b.id}-subtensor-${index}`, title: getLockTitle({ label: "subtensor-staking" }), @@ -140,10 +140,10 @@ export const useChainTokenBalances = ({ chainId, balances }: ChainTokenBalancesP })), ) - return [...available, ...locked, ...reserved, ...staked, ...crowdloans, ...subtensor1] + return [...available, ...locked, ...reserved, ...staked, ...crowdloans, ...subtensor] .filter((row) => row && row.tokens.gt(0)) .sort(sortBigBy("tokens", true)) - }, [summary, account, t, tokenBalances.each, currency]) + }, [summary, account, t, tokenBalances, currency]) const detailRows = useEnhanceDetailRows(rawDetailRows) @@ -170,8 +170,8 @@ const useEnhanceDetailRows = (detailRows: DetailRow[]) => { // fetch the validator name for each subtensor staking lock, so we can display it in the description const hotkeys = useMemo(() => { return detailRows - .filter((row) => row.meta?.type === "subtensor-staking") - .flatMap((row) => (row.meta?.hotkeys as string[]) ?? []) + .filter((row) => row.meta?.type === "subtensor-staking" && !!row.meta?.hotkey) + .map((row) => row.meta?.hotkey as string) }, [detailRows]) const { data: validators, isLoading: isLoadingValidators } = useGetBittensorValidators({ @@ -184,7 +184,7 @@ const useEnhanceDetailRows = (detailRows: DetailRow[]) => { if (row.meta?.type === "subtensor-staking") return { ...row, - description: validators?.find((v) => v?.hotkey.ss58 === row.meta.hotkeys[0])?.name, + description: validators?.find((v) => v?.hotkey.ss58 === row.meta.hotkey)?.name, isLoading: isLoadingValidators, } as DetailRow diff --git a/packages/balances/src/modules/SubstrateNativeModule/index.ts b/packages/balances/src/modules/SubstrateNativeModule/index.ts index 295c8435d8..d3792bee2c 100644 --- a/packages/balances/src/modules/SubstrateNativeModule/index.ts +++ b/packages/balances/src/modules/SubstrateNativeModule/index.ts @@ -148,7 +148,9 @@ export const SubNativeModule: NewBalanceModule< { pallet: "Staking", items: ["Ledger"] }, { pallet: "Crowdloan", items: ["Funds"] }, { pallet: "Paras", items: ["Parachains"] }, - { pallet: "SubtensorModule", items: ["TotalColdkeyStake", "StakingHotkeys"] }, + // TotalColdkeyStake is used until v.2.2.1, then it is replaced by StakingHotkeys+Stake + // Need to keep TotalColdkeyStake for a while so chaindata keeps including it in miniMetadatas, so it doesnt break old versions of the wallet + { pallet: "SubtensorModule", items: ["TotalColdkeyStake", "StakingHotkeys", "Stake"] }, ]) const miniMetadata = encodeMetadata(tag === "v15" ? { tag, metadata } : { tag, metadata }) @@ -280,7 +282,7 @@ export const SubNativeModule: NewBalanceModule< .filter((b) => b.values.length > 0) .reduce>((acc, b) => { const bId = getBalanceId(b) - acc[bId] = mergeBalances(acc[bId], b, source) + acc[bId] = mergeBalances(acc[bId], b, source, false) return acc }, {}) @@ -288,7 +290,7 @@ export const SubNativeModule: NewBalanceModule< const mergedBalances: Record = {} Object.entries(accumulatedUpdates).forEach(([bId, b]) => { // merge the values from the new balance into the existing balance, if there is one - mergedBalances[bId] = mergeBalances(currentBalances[bId], b, source) + mergedBalances[bId] = mergeBalances(currentBalances[bId], b, source, true) // update initialisingBalances to remove balances which have been updated const intialisingForToken = initialisingBalances.get(b.tokenId) diff --git a/packages/balances/src/modules/SubstrateNativeModule/subscribeSubtensorStaking.ts b/packages/balances/src/modules/SubstrateNativeModule/subscribeSubtensorStaking.ts index a6fc17f8e4..2b4ad8d5b2 100644 --- a/packages/balances/src/modules/SubstrateNativeModule/subscribeSubtensorStaking.ts +++ b/packages/balances/src/modules/SubstrateNativeModule/subscribeSubtensorStaking.ts @@ -2,7 +2,8 @@ import { ChainConnector } from "@talismn/chain-connector" import { ChaindataProvider } from "@talismn/chaindata-provider" import { decodeScale, encodeStateKey } from "@talismn/scale" import { isEthereumAddress } from "@talismn/util" -import { combineLatest, scan, share } from "rxjs" +import { toPairs } from "lodash" +import { scan, share, switchMap } from "rxjs" import type { SubNativeModule } from "./index" import log from "../../log" @@ -68,8 +69,8 @@ export async function subscribeSubtensorStaking( miniMetadatas, moduleType: "substrate-native", coders: { - totalColdkeyStake: ["SubtensorModule", "TotalColdkeyStake"], stakingHotkeys: ["SubtensorModule", "StakingHotkeys"], + stake: ["SubtensorModule", "Stake"], }, }) @@ -95,36 +96,36 @@ export async function subscribeSubtensorStaking( continue } - type TotalColdkeyStake = { + type StakingHotkeys = { address: string - stake?: bigint + hotkeys?: string[] } - const subscribeTotalColdkeyStake = ( + const subscribeStakingHotkeys = ( addresses: string[], - callback: SubscriptionCallback, + callback: SubscriptionCallback, ) => { - const scaleCoder = chainStorageCoders.get(chainId)?.totalColdkeyStake - const queries = addresses.flatMap((address): RpcStateQuery | [] => { + const scaleCoder = chainStorageCoders.get(chainId)?.stakingHotkeys + const queries = addresses.flatMap((address): RpcStateQuery | [] => { const stateKey = encodeStateKey( scaleCoder, - `Invalid address in ${chainId} totalColdkeyStake query ${address}`, + `Invalid address in ${chainId} stakingHotkeys query ${address}`, address, ) if (!stateKey) return [] const decodeResult = (change: string | null) => { /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */ - type DecodedType = bigint + type DecodedType = string[] const decoded = decodeScale( scaleCoder, change, - `Failed to decode totalColdkeyStake on chain ${chainId}`, + `Failed to decode stakingHotkeys on chain ${chainId}`, ) - const stake: DecodedType | undefined = decoded ?? undefined + const hotkeys: DecodedType | undefined = decoded ?? undefined - return { address, stake } + return { address, hotkeys } } return { chainId, stateKey, decodeResult } @@ -134,48 +135,47 @@ export async function subscribeSubtensorStaking( return () => subscription.then((unsubscribe) => unsubscribe()) } - const totalColdkeyStakeByAddress$ = asObservable(subscribeTotalColdkeyStake)(addresses).pipe( + const stakingHotkeysByAddress$ = asObservable(subscribeStakingHotkeys)(addresses).pipe( scan((state, next) => { - for (const totalColdkeyStake of next) { - const { address, stake } = totalColdkeyStake - if (typeof stake === "bigint") state.set(address, stake) - else state.delete(totalColdkeyStake.address) + for (const { address, hotkeys } of next) { + if (hotkeys?.length) state.set(address, hotkeys) + else state.delete(address) } return state - }, new Map()), + }, new Map()), share(), ) - type StakingHotkeys = { - address: string - hotkeys?: string[] - } - const subscribeStakingHotkeys = ( - addresses: string[], - callback: SubscriptionCallback, + type HotkeyStakeDef = { address: string; hotkey: string } + type HotkeyStake = { address: string; hotkey: string; stake?: bigint } + + const subscribeStakes = ( + defs: HotkeyStakeDef[], + callback: SubscriptionCallback, ) => { - const scaleCoder = chainStorageCoders.get(chainId)?.stakingHotkeys - const queries = addresses.flatMap((address): RpcStateQuery | [] => { + const scaleCoder = chainStorageCoders.get(chainId)?.stake + const queries = defs.flatMap(({ address, hotkey }): RpcStateQuery | [] => { const stateKey = encodeStateKey( scaleCoder, - `Invalid address in ${chainId} stakingHotkeys query ${address}`, + `Invalid input in ${chainId} stake query ${address}/${hotkey}`, + hotkey, address, ) if (!stateKey) return [] const decodeResult = (change: string | null) => { /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */ - type DecodedType = string[] + type DecodedType = bigint const decoded = decodeScale( scaleCoder, change, - `Failed to decode stakingHotkeys on chain ${chainId}`, + `Failed to decode stake on chain ${chainId}`, ) - const hotkeys: DecodedType | undefined = decoded ?? undefined + const stake: DecodedType | undefined = decoded ?? undefined - return { address, hotkeys } + return { address, hotkey, stake } } return { chainId, stateKey, decodeResult } @@ -185,53 +185,51 @@ export async function subscribeSubtensorStaking( return () => subscription.then((unsubscribe) => unsubscribe()) } - const stakingHotkeysByAddress$ = asObservable(subscribeStakingHotkeys)(addresses).pipe( - scan((state, next) => { - for (const { address, hotkeys } of next) { - if (hotkeys?.length) state.set(address, hotkeys) - else state.delete(address) - } - return state - }, new Map()), - share(), - ) + // subscribe to hotkeys for each address + // then for each address/hotkey pair, subscribe to the staked amount + const subscription = stakingHotkeysByAddress$ + .pipe( + switchMap((hotkeysByAddress) => { + const stakeDefs: HotkeyStakeDef[] = toPairs(hotkeysByAddress).flatMap( + ([address, hotkeys]) => + hotkeys.map((hotkey: string): HotkeyStakeDef => ({ address, hotkey })), + ) - const subscription = combineLatest([ - totalColdkeyStakeByAddress$, - stakingHotkeysByAddress$, - ]).subscribe({ - next: ([totalColdkeyStakeByAddress, hotkeysByAddress]) => { - const balances = Array.from(totalColdkeyStakeByAddress) - .map(([address, stake]) => { - return { - source: "substrate-native", - status: "live", - address, - multiChainId: { subChainId: chainId }, - chainId, - tokenId, - values: [ - { - source: "subtensor-staking", - type: "subtensor", - label: "subtensor-staking", - amount: stake.toString(), - meta: hotkeysByAddress.has(address) - ? { - type: "subtensor-staking", - hotkeys: hotkeysByAddress.get(address), - } - : undefined, - }, - ], - } as SubNativeBalance - }) - .filter(Boolean) as SubNativeBalance[] - - if (balances.length > 0) callback(null, balances) - }, - error: (error) => callback(error), - }) + return asObservable(subscribeStakes)(stakeDefs) + }), + ) + .subscribe({ + next: (stakes) => { + const balances = stakes + .filter(({ stake }) => typeof stake === "bigint") + .map(({ address, hotkey, stake }) => { + return { + source: "substrate-native", + status: "live", + address, + multiChainId: { subChainId: chainId }, + chainId, + tokenId, + values: [ + { + source: "subtensor-staking", + type: "subtensor", + label: "subtensor-staking", + amount: stake!.toString(), + meta: { + type: "subtensor-staking", + hotkey, + }, + }, + ], + } as SubNativeBalance + }) + .filter(Boolean) as SubNativeBalance[] + + if (balances.length > 0) callback(null, balances) + }, + error: (error) => callback(error), + }) resultUnsubscribes.push(() => subscription.unsubscribe()) } diff --git a/packages/balances/src/modules/SubstrateNativeModule/util/mergeBalances.ts b/packages/balances/src/modules/SubstrateNativeModule/util/mergeBalances.ts index 462418a54e..9e80606a5b 100644 --- a/packages/balances/src/modules/SubstrateNativeModule/util/mergeBalances.ts +++ b/packages/balances/src/modules/SubstrateNativeModule/util/mergeBalances.ts @@ -12,12 +12,14 @@ export type { BalanceLockType } from "./balanceLockTypes" * @param balance1 SubNativeBalance * @param balance2 SubNativeBalance * @param source source that this merge is for (will discard previous values from that source) + * @param clear whether to clear the previous values from the source * @returns SubNativeBalance */ export const mergeBalances = ( balance1: SubNativeBalance | undefined, balance2: SubNativeBalance, source: string, + clear: boolean, ) => { if (balance1 === undefined) return balance2 assert( @@ -27,7 +29,7 @@ export const mergeBalances = ( // locks and freezes should completely replace the previous rather than merging together const existingValues = Object.fromEntries( balance1.values - .filter((v) => !v.source || v.source !== source) + .filter((v) => !clear || !v.source || v.source !== source) .map((value) => [getValueId(value), value]), ) const newValues = Object.fromEntries(balance2.values.map((value) => [getValueId(value), value])) @@ -38,5 +40,6 @@ export const mergeBalances = ( status: balance2.status, // only the status field should actually be different apart from the values values: Object.values(mergedValues), } + return merged } diff --git a/packages/balances/src/types/balancetypes.ts b/packages/balances/src/types/balancetypes.ts index b2fd53236a..d116e6c567 100644 --- a/packages/balances/src/types/balancetypes.ts +++ b/packages/balances/src/types/balancetypes.ts @@ -125,10 +125,11 @@ type BaseAmountWithLabel = { export const getValueId = (amount: AmountWithLabel) => { const getMetaId = () => { - const meta = amount.meta as { poolId?: number; paraId?: number } | undefined + const meta = amount.meta as { poolId?: number; paraId?: number; hotkey?: string } | undefined if (!meta) return "" if (amount.type === "crowdloan") return meta.paraId?.toString() ?? "" if (amount.type === "nompool") return meta.poolId?.toString() ?? "" + if (amount.type === "subtensor") return meta.hotkey?.toString() ?? "" return "" }