Skip to content

Commit

Permalink
feat: distinct balance values for each hotkey
Browse files Browse the repository at this point in the history
  • Loading branch information
0xKheops committed Dec 12, 2024
1 parent 0b4c113 commit 194cc66
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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" }),
Expand All @@ -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)

Expand All @@ -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({
Expand All @@ -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

Expand Down
8 changes: 5 additions & 3 deletions packages/balances/src/modules/SubstrateNativeModule/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down Expand Up @@ -280,15 +282,15 @@ export const SubNativeModule: NewBalanceModule<
.filter((b) => b.values.length > 0)
.reduce<Record<string, SubNativeBalance>>((acc, b) => {
const bId = getBalanceId(b)
acc[bId] = mergeBalances(acc[bId], b, source)
acc[bId] = mergeBalances(acc[bId], b, source, false)
return acc
}, {})

// then merge these with the current balances
const mergedBalances: Record<string, SubNativeBalance> = {}
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -68,8 +69,8 @@ export async function subscribeSubtensorStaking(
miniMetadatas,
moduleType: "substrate-native",
coders: {
totalColdkeyStake: ["SubtensorModule", "TotalColdkeyStake"],
stakingHotkeys: ["SubtensorModule", "StakingHotkeys"],
stake: ["SubtensorModule", "Stake"],
},
})

Expand All @@ -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<TotalColdkeyStake[]>,
callback: SubscriptionCallback<StakingHotkeys[]>,
) => {
const scaleCoder = chainStorageCoders.get(chainId)?.totalColdkeyStake
const queries = addresses.flatMap((address): RpcStateQuery<TotalColdkeyStake> | [] => {
const scaleCoder = chainStorageCoders.get(chainId)?.stakingHotkeys
const queries = addresses.flatMap((address): RpcStateQuery<StakingHotkeys> | [] => {
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<DecodedType>(
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 }
Expand All @@ -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<string, bigint>()),
}, new Map<string, string[]>()),
share(),
)

type StakingHotkeys = {
address: string
hotkeys?: string[]
}
const subscribeStakingHotkeys = (
addresses: string[],
callback: SubscriptionCallback<StakingHotkeys[]>,
type HotkeyStakeDef = { address: string; hotkey: string }
type HotkeyStake = { address: string; hotkey: string; stake?: bigint }

const subscribeStakes = (
defs: HotkeyStakeDef[],
callback: SubscriptionCallback<HotkeyStake[]>,
) => {
const scaleCoder = chainStorageCoders.get(chainId)?.stakingHotkeys
const queries = addresses.flatMap((address): RpcStateQuery<StakingHotkeys> | [] => {
const scaleCoder = chainStorageCoders.get(chainId)?.stake
const queries = defs.flatMap(({ address, hotkey }): RpcStateQuery<HotkeyStake> | [] => {
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<DecodedType>(
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 }
Expand All @@ -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<string, string[]>()),
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())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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]))
Expand All @@ -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
}
3 changes: 2 additions & 1 deletion packages/balances/src/types/balancetypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,11 @@ type BaseAmountWithLabel<TLabel extends string> = {

export const getValueId = (amount: AmountWithLabel<string>) => {
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 ""
}

Expand Down

0 comments on commit 194cc66

Please sign in to comment.