diff --git a/packages/next-common/components/summary/polkadotTreasurySummary/index.jsx b/packages/next-common/components/summary/polkadotTreasurySummary/index.jsx index eaaa524093..14418c62e8 100644 --- a/packages/next-common/components/summary/polkadotTreasurySummary/index.jsx +++ b/packages/next-common/components/summary/polkadotTreasurySummary/index.jsx @@ -6,6 +6,7 @@ import { MythTokenAssetsProvider } from "./context/mythTokenAssets"; import FellowshipTreasuryOnAssetHub from "./fellowshipTreasuryOnAssetHub"; import Loans from "./loans"; import Bounties from "./bounties"; +import MultiAssetBounties from "./multiAssetBounties"; import CollapsePanel from "next-common/components/summary/polkadotTreasurySummary/common/collapsePanel"; import TreasuryStatus from "./treasuryStatus"; @@ -15,6 +16,7 @@ function PolkadotTreasurySummaryInContext() { + diff --git a/packages/next-common/components/summary/polkadotTreasurySummary/multiAssetBounties/index.jsx b/packages/next-common/components/summary/polkadotTreasurySummary/multiAssetBounties/index.jsx new file mode 100644 index 0000000000..f371d0fb9d --- /dev/null +++ b/packages/next-common/components/summary/polkadotTreasurySummary/multiAssetBounties/index.jsx @@ -0,0 +1,70 @@ +import SummaryItem from "next-common/components/summary/layout/item"; +import Link from "next-common/components/link"; +import LoadableContent from "next-common/components/common/loadableContent"; +import NativeTokenSymbolAsset from "../common/nativeTokenSymbolAsset"; +import TokenSymbolAsset from "../common/tokenSymbolAsset"; +import FiatPriceLabel from "../common/fiatPriceLabel"; +import { usePolkadotTreasury } from "next-common/context/treasury/polkadotTreasury"; +import Tooltip from "next-common/components/tooltip"; +import { toPrecision } from "next-common/utils"; +import { useChainSettings } from "next-common/context/chain"; + +export default function MultiAssetBounties() { + const { + multiAssetBountiesCount, + multiAssetBountiesTotalByAsset: totalByAsset, + isMultiAssetBountiesLoading: isLoading, + } = usePolkadotTreasury(); + + const { symbol: chainSymbol } = useChainSettings(); + + const entries = Object.values(totalByAsset || {}); + + const Title = ( + <> + + Multi-Asset Bounties + + {isLoading ? null : ( + <> + {" ยท "} + + {multiAssetBountiesCount} + + + )} + + ); + + return ( + + +
+ +
+ {entries.map(({ symbol, decimals, total }) => + symbol === chainSymbol ? ( + + ) : ( + + ), + )} +
+
+
+
+ ); +} diff --git a/packages/next-common/context/treasury/polkadotTreasury/hooks/useQueryMultiAssetBountiesData.js b/packages/next-common/context/treasury/polkadotTreasury/hooks/useQueryMultiAssetBountiesData.js new file mode 100644 index 0000000000..d580bda6d1 --- /dev/null +++ b/packages/next-common/context/treasury/polkadotTreasury/hooks/useQueryMultiAssetBountiesData.js @@ -0,0 +1,80 @@ +import { useEffect, useMemo, useState } from "react"; +import BigNumber from "bignumber.js"; +import { getAssetInfoFromPapiAssetKind } from "next-common/utils/treasury/multiAssetBounty/papiAssetKind"; + +const MULTI_ASSET_BOUNTY_ACTIVE_STATUS_TYPES = [ + "FundingAttempted", + "Funded", + "Active", +]; + +function filterActiveMultiAssetBounties(items) { + return items.filter((item) => + MULTI_ASSET_BOUNTY_ACTIVE_STATUS_TYPES.includes(item?.status?.type), + ); +} + +export function useQueryMultiAssetsBounties(papi, checkPallet) { + const [bounties, setBounties] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + if (!papi) { + return; + } + + if (!checkPallet("MultiAssetBounties", "Bounties")) { + setIsLoading(false); + setBounties([]); + return; + } + + setIsLoading(true); + papi.query.MultiAssetBounties.Bounties.getEntries() + .then((entries) => { + const all = entries.map(({ keyArgs, value }) => ({ + index: keyArgs?.[0], + ...value, + })); + setBounties(filterActiveMultiAssetBounties(all)); + }) + .finally(() => { + setIsLoading(false); + }); + }, [papi, checkPallet]); + + return { + bounties, + bountiesCount: bounties?.length ?? 0, + isLoading, + }; +} + +export function useMultiAssetsBountiesTotalBalance( + bounties, + chainDecimals, + chainSymbol, +) { + const totalByAsset = useMemo(() => { + if (!bounties?.length) { + return {}; + } + + const result = {}; + bounties.forEach((bounty) => { + const { symbol, decimals } = getAssetInfoFromPapiAssetKind( + bounty?.asset_kind, + chainDecimals, + chainSymbol, + ); + const amount = bounty?.value != null ? String(bounty.value) : "0"; + if (!result[symbol]) { + result[symbol] = { symbol, decimals, total: new BigNumber(0) }; + } + result[symbol].total = result[symbol].total.plus(amount); + }); + return result; + }, [bounties, chainDecimals, chainSymbol]); + + return { totalByAsset }; +} diff --git a/packages/next-common/context/treasury/polkadotTreasury/index.jsx b/packages/next-common/context/treasury/polkadotTreasury/index.jsx index f89eab649e..4c6e3922bb 100644 --- a/packages/next-common/context/treasury/polkadotTreasury/index.jsx +++ b/packages/next-common/context/treasury/polkadotTreasury/index.jsx @@ -8,9 +8,14 @@ import { useBountiesTotalBalance, useQueryBounties, } from "./hooks/useQueryBountiesData"; +import { + useQueryMultiAssetsBounties, + useMultiAssetsBountiesTotalBalance, +} from "./hooks/useQueryMultiAssetBountiesData"; import useQueryFellowshipSalaryBalance from "./hooks/useQueryFellowshipSalaryBalance"; import usePolkadotTreasuryTotal from "next-common/utils/hooks/usePolkadotTreasuryTotal"; -import { PapiProvider, useContextPapiApi } from "next-common/context/papi"; +import { PapiProvider, useContextPapi } from "next-common/context/papi"; +import { useChainSettings } from "next-common/context/chain"; const PolkadotTreasuryContext = createContext(); @@ -23,7 +28,8 @@ export default function PolkadotTreasuryProvider({ children }) { } function PolkadotTreasuryProviderInner({ children }) { - const papi = useContextPapiApi(); + const { api: papi, checkPallet } = useContextPapi(); + const { decimals: chainDecimals, symbol: chainSymbol } = useChainSettings(); const { treasuryAccount, relayChainTreasuryBalance: nativeTreasuryBalanceOnRelayChain, @@ -56,6 +62,7 @@ function PolkadotTreasuryProviderInner({ children }) { bountiesCount, isLoading: isQueryBountiesLoading, } = useQueryBounties(papi); + const { balance: dotTreasuryBalanceOnBounties, isLoading: isBountiesTotalBalanceLoading, @@ -64,6 +71,19 @@ function PolkadotTreasuryProviderInner({ children }) { const isDotTreasuryBalanceOnBountiesLoading = isQueryBountiesLoading || isBountiesTotalBalanceLoading; + const { + bounties: multiAssetBounties, + bountiesCount: multiAssetBountiesCount, + isLoading: isMultiAssetBountiesLoading, + } = useQueryMultiAssetsBounties(papi, checkPallet); + + const { totalByAsset: multiAssetBountiesTotalByAsset } = + useMultiAssetsBountiesTotalBalance( + multiAssetBounties, + chainDecimals, + chainSymbol, + ); + return ( {children} diff --git a/packages/next-common/utils/treasury/multiAssetBounty/papiAssetKind.js b/packages/next-common/utils/treasury/multiAssetBounty/papiAssetKind.js new file mode 100644 index 0000000000..819906a805 --- /dev/null +++ b/packages/next-common/utils/treasury/multiAssetBounty/papiAssetKind.js @@ -0,0 +1,77 @@ +import { ASSET_HUB_GENERAL_INDEX_SYMBOL } from "next-common/asset"; +import { SYMBOL_DECIMALS } from "next-common/utils/consts/asset"; +import { isNil } from "lodash-es"; + +const ASSET_HUB_PARACHAIN_ID = 1000; +const ASSET_HUB_PALLET_INSTANCE = 50; + +function papiGetJunctions(interior) { + if (!interior) { + return null; + } + if (interior === "Here" || interior?.type === "Here") { + return []; + } + const { type, value } = interior; + if (!type || value == null) { + return null; + } + return Array.isArray(value) ? value : [value]; +} + +function papiIsAssetHubLocation(location) { + if (!location) { + return false; + } + const junctions = papiGetJunctions(location.interior); + if (junctions === null) { + return false; + } + if (junctions.length === 0) { + return true; + } + return junctions.some( + (j) => + j?.type === "Parachain" && Number(j?.value) === ASSET_HUB_PARACHAIN_ID, + ); +} + +function papiGetSymbolFromAssetId(assetId) { + const junctions = papiGetJunctions(assetId?.interior); + if (!junctions?.length) { + return null; + } + const palletInstance = junctions.find( + (j) => j?.type === "PalletInstance", + )?.value; + const generalIndex = junctions.find((j) => j?.type === "GeneralIndex")?.value; + if ( + Number(palletInstance) === ASSET_HUB_PALLET_INSTANCE && + !isNil(generalIndex) + ) { + return ASSET_HUB_GENERAL_INDEX_SYMBOL[String(generalIndex)] ?? null; + } + return null; +} + +export function getAssetInfoFromPapiAssetKind( + papiAssetKind, + chainDecimals, + chainSymbol, +) { + if (!papiAssetKind) { + return { symbol: chainSymbol, decimals: chainDecimals }; + } + // papi format: { type: "V5", value: { location, asset_id } } + const inner = papiAssetKind?.value ?? papiAssetKind; + const { location, asset_id: assetId } = inner ?? {}; + if (!papiIsAssetHubLocation(location)) { + return { symbol: chainSymbol, decimals: chainDecimals }; + } + const symbol = papiGetSymbolFromAssetId(assetId); + if (!symbol) { + return { symbol: chainSymbol, decimals: chainDecimals }; + } + const decimals = SYMBOL_DECIMALS[symbol] ?? chainDecimals; + return { symbol, decimals }; +}