diff --git a/src/hooks/useAssetRegistryMetadata.ts b/src/hooks/useAssetRegistryMetadata.ts new file mode 100644 index 00000000..ded8ec45 --- /dev/null +++ b/src/hooks/useAssetRegistryMetadata.ts @@ -0,0 +1,58 @@ +import { useState, useEffect, useCallback } from 'preact/hooks'; +import { useNodeInfoState } from '../NodeInfoProvider'; +import { AssetId, OrmlTraitsAssetRegistryAssetMetadata } from './useBuyout/types'; +import { StorageKey } from '@polkadot/types'; +import { Codec } from '@polkadot/types-codec/types'; + +type CurrencyMetadataType = { + decimals: string; + name: string; + symbol: string; + // There are more coming, but are not used in this context +}; + +interface UseAssetRegistryMetadata { + getAssetMetadata: (assetId: AssetId) => Promise; + getNativeAssetMetadata: () => Promise; +} + +function convertToOrmlAssetRegistryAssetMetadata(metadata: [StorageKey, Codec]): OrmlTraitsAssetRegistryAssetMetadata { + const { decimals, name, symbol } = metadata[1].toHuman() as CurrencyMetadataType; + const assetId = (metadata[0].toHuman() as string[])[0] as string; + + return { + metadata: { decimals: Number(decimals), name, symbol }, + assetId, + }; +} + +export const useAssetRegistryMetadata = (): UseAssetRegistryMetadata => { + const { api } = useNodeInfoState().state; + const [metadataCache, setMetadataCache] = useState([]); + + const fetchMetadata = useCallback(async () => { + if (api) { + const fetchedMetadata = await api.query.assetRegistry.metadata.entries(); + setMetadataCache(fetchedMetadata.map((m) => convertToOrmlAssetRegistryAssetMetadata(m))); + } + }, [api]); + + useEffect(() => { + fetchMetadata().catch(console.error); + }, [fetchMetadata]); + + const getAssetMetadata = useCallback( + async (assetId: AssetId): Promise => + metadataCache.find( + // When JSON.stringify we check for the same object structure as OrmlTraitsAssetRegistryAssetMetadata.assetId's are really different + (m: OrmlTraitsAssetRegistryAssetMetadata) => JSON.stringify(m.assetId) === JSON.stringify(assetId), + ), + [metadataCache], + ); + + const getNativeAssetMetadata = useCallback(async () => { + return getAssetMetadata('Native'); + }, [getAssetMetadata]); + + return { getAssetMetadata, getNativeAssetMetadata }; +}; diff --git a/src/hooks/useBuyout/index.ts b/src/hooks/useBuyout/index.ts index 6d79126e..b44be8b6 100644 --- a/src/hooks/useBuyout/index.ts +++ b/src/hooks/useBuyout/index.ts @@ -9,10 +9,10 @@ import { doSubmitExtrinsic } from '../../pages/collators/dialogs/helpers'; import { useGlobalState } from '../../GlobalStateProvider'; import { OrmlTraitsAssetRegistryAssetMetadata } from './types'; -import { getMetadata } from './utils'; import { ToastMessage, showToast } from '../../shared/showToast'; import { PerMill } from '../../shared/parseNumbers/permill'; import { decimalToNative } from '../../shared/parseNumbers/metric'; +import { useAssetRegistryMetadata } from '../useAssetRegistryMetadata'; export interface BuyoutSettings { buyoutNativeToken: { @@ -20,7 +20,7 @@ export interface BuyoutSettings { max: number; }; currencies: OrmlTraitsAssetRegistryAssetMetadata[]; - nativeCurrency: OrmlTraitsAssetRegistryAssetMetadata; + nativeCurrency: OrmlTraitsAssetRegistryAssetMetadata | undefined; sellFee: PerMill; handleBuyout: ( currency: OrmlTraitsAssetRegistryAssetMetadata, @@ -55,11 +55,42 @@ function handleBuyoutError(error: string) { export const useBuyout = (): BuyoutSettings => { const { api } = useNodeInfoState().state; - const { tenantName } = useGlobalState(); const { walletAccount } = useGlobalState(); const [minimumBuyout, setMinimumBuyout] = useState(0); const [maximumBuyout, setMaximumBuyout] = useState(0); const [sellFee, setSellFee] = useState(new PerMill(0)); + const { getNativeAssetMetadata, getAssetMetadata } = useAssetRegistryMetadata(); + + const [nativeCurrency, setNativeCurrency] = useState(undefined); + const [currencies, setCurrencies] = useState([]); + + useEffect(() => { + async function fetchAllowedCurrencies() { + const allowedCurrencies = []; + if (api) { + const allowedCurrenciesEntries = await api.query.treasuryBuyoutExtension.allowedCurrencies.entries(); + + for (const currency of allowedCurrenciesEntries) { + if (currency.length && currency[0] && currency[0].toHuman()) { + const currencyXCMId: { XCM: number } = (currency[0].toHuman() as { XCM: number }[])[0]; + const currencyMetadata = await getAssetMetadata(currencyXCMId); + + if (currencyMetadata) { + allowedCurrencies.push(currencyMetadata); + } + } + } + } + return allowedCurrencies; + } + + async function fetchAndSetCurrencies() { + setCurrencies(await fetchAllowedCurrencies()); + setNativeCurrency(await getNativeAssetMetadata()); + } + + fetchAndSetCurrencies().catch(console.error); + }, [api, getNativeAssetMetadata, getAssetMetadata]); useEffect(() => { async function fetchBuyoutLimits() { @@ -129,9 +160,9 @@ export const useBuyout = (): BuyoutSettings => { min: minimumBuyout, max: maximumBuyout, }, - currencies: Object.values(getMetadata(tenantName).currencies), + currencies, sellFee, - nativeCurrency: getMetadata(tenantName).nativeCurrency.ampe, + nativeCurrency, handleBuyout, }; }; diff --git a/src/hooks/useBuyout/types.ts b/src/hooks/useBuyout/types.ts index 3eea0c37..c4fd71e8 100644 --- a/src/hooks/useBuyout/types.ts +++ b/src/hooks/useBuyout/types.ts @@ -1,13 +1,17 @@ +import { SpacewalkPrimitivesAsset } from '@pendulum-chain/types/interfaces'; + +export type AssetId = + | { + XCM: number; + } + | string + | SpacewalkPrimitivesAsset; + export interface OrmlTraitsAssetRegistryAssetMetadata { metadata: { decimals: number; name: string; symbol: string; - existentialDeposit: number; }; - assetId: - | { - XCM: number; - } - | string; + assetId: AssetId; } diff --git a/src/hooks/useBuyout/utils.ts b/src/hooks/useBuyout/utils.ts deleted file mode 100644 index 4b4c66b1..00000000 --- a/src/hooks/useBuyout/utils.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { OrmlTraitsAssetRegistryAssetMetadata } from './types'; -import { TenantName } from '../../models/Tenant'; - -const FOUCOCO_HARDCODED_METADATA: Record = { - ksm: { - metadata: { - decimals: 12, - name: 'Kusama', - symbol: 'KSM', - existentialDeposit: 1000, - }, - assetId: { - XCM: 0, - }, - }, -}; - -const AMPE_HARDCODED_METADATA: Record = { - ksm: { - metadata: { - decimals: 12, - name: 'Kusama', - symbol: 'KSM', - existentialDeposit: 1000, - }, - assetId: { - XCM: 0, - }, - }, -}; - -export const NATIVE_CURRENCY: Record = { - ampe: { - metadata: { - decimals: 12, - name: 'Amplitude', - symbol: 'AMPE', - existentialDeposit: 1000, - }, - assetId: 'Native', - }, -}; - -export function getMetadata(network: TenantName) { - const currencies = network === TenantName.Foucoco ? FOUCOCO_HARDCODED_METADATA : AMPE_HARDCODED_METADATA; - return { currencies, nativeCurrency: NATIVE_CURRENCY }; -} diff --git a/src/pages/gas/GasSkeleton.tsx b/src/pages/gas/GasSkeleton.tsx new file mode 100644 index 00000000..7e1fa62b --- /dev/null +++ b/src/pages/gas/GasSkeleton.tsx @@ -0,0 +1,11 @@ +import { Skeleton } from '../../components/Skeleton'; + +export const GasSkeleton = () => ( +
+
+ {Array.from({ length: 12 }).map((_, index) => ( + + ))} +
+
+); diff --git a/src/pages/gas/index.tsx b/src/pages/gas/index.tsx index 5feb2306..fe67d4cb 100644 --- a/src/pages/gas/index.tsx +++ b/src/pages/gas/index.tsx @@ -9,12 +9,13 @@ import { OrmlTraitsAssetRegistryAssetMetadata } from '../../hooks/useBuyout/type import { GasForm, IssueFormValues } from './GasForm'; import { calculateForCurrentFromToken, calculatePriceNativeForCurrentFromToken } from './helpers'; import { GasSuccessDialog } from './GasSuccessDialog'; +import { GasSkeleton } from './GasSkeleton'; const Gas = () => { const { currencies, buyoutNativeToken, sellFee, nativeCurrency, handleBuyout } = useBuyout(); const { pricesCache } = usePriceFetcher(); - const [selectedFromToken, setSelectedFromToken] = useState(currencies[0]); + const [selectedFromToken, setSelectedFromToken] = useState(undefined); const [selectedFromTokenPriceUSD, setSelectedFromTokenPriceUSD] = useState(0); const [nativeTokenPrice, setNativeTokenPrice] = useState(0); const [submissionPending, setSubmissionPending] = useState(false); @@ -22,33 +23,45 @@ const Gas = () => { useEffect(() => { const fetchPricesCache = async () => { - const tokensPrices = await pricesCache; + if (nativeCurrency && isOrmlAsset(selectedFromToken)) { + const tokensPrices = await pricesCache; - if (!isEmpty(tokensPrices) && isOrmlAsset(selectedFromToken)) { - setSelectedFromTokenPriceUSD(tokensPrices[selectedFromToken.metadata.symbol]); - const nativeTokenPrice = tokensPrices[nativeCurrency.metadata.symbol]; - // We add the sellFee to the native price to already accommodate for it in the calculations - const nativeTokenPriceWithFee = sellFee.addSelfToBase(nativeTokenPrice); - setNativeTokenPrice(nativeTokenPriceWithFee); + if (!isEmpty(tokensPrices)) { + setSelectedFromTokenPriceUSD(tokensPrices[selectedFromToken.metadata.symbol]); + const nativeTokenPrice = tokensPrices[nativeCurrency.metadata.symbol]; + // We add the sellFee to the native price to already accommodate for it in the calculations + const nativeTokenPriceWithFee = sellFee.addSelfToBase(nativeTokenPrice); + setNativeTokenPrice(nativeTokenPriceWithFee); + } } }; fetchPricesCache().catch(console.error); - }, [nativeCurrency.metadata.symbol, pricesCache, selectedFromToken, sellFee]); + }, [nativeCurrency, pricesCache, selectedFromToken, sellFee]); + + useEffect(() => { + if (!selectedFromToken) { + setSelectedFromToken(currencies[0] as OrmlTraitsAssetRegistryAssetMetadata); + } + }, [selectedFromToken, currencies]); const onSubmit = async (data: IssueFormValues) => { - // If the user has selected the min or max amount by clicking the badge button, we call the buyout extrinsic in the - // direction of the native token being the input token. This way we ensure that the amount is perfectly within the buyout limits and - // the transaction does not fail due to imprecise calculations. - const isExchangeAmount = data.isMin || data.isMax; - const token = isExchangeAmount ? nativeCurrency : (selectedFromToken as OrmlTraitsAssetRegistryAssetMetadata); - const amount = data.isMin ? buyoutNativeToken.min : data.isMax ? buyoutNativeToken.max : Number(data.fromAmount); + if (nativeCurrency && selectedFromToken) { + // If the user has selected the min or max amount by clicking the badge button, we call the buyout extrinsic in the + // direction of the native token being the input token. This way we ensure that the amount is perfectly within the buyout limits and + // the transaction does not fail due to imprecise calculations. + const isExchangeAmount = data.isMin || data.isMax; + const token = isExchangeAmount ? nativeCurrency : (selectedFromToken as OrmlTraitsAssetRegistryAssetMetadata); + const amount = data.isMin ? buyoutNativeToken.min : data.isMax ? buyoutNativeToken.max : Number(data.fromAmount); - handleBuyout(token, amount, setSubmissionPending, setConfirmationDialogVisible, isExchangeAmount); + handleBuyout(token, amount, setSubmissionPending, setConfirmationDialogVisible, isExchangeAmount); + } }; - const selectedTokenDecimals = (selectedFromToken as OrmlTraitsAssetRegistryAssetMetadata).metadata.decimals; - const nativeDecimals = (nativeCurrency as OrmlTraitsAssetRegistryAssetMetadata).metadata.decimals; + const selectedTokenDecimals = (selectedFromToken as OrmlTraitsAssetRegistryAssetMetadata)?.metadata.decimals ?? 0; + const nativeDecimals = (nativeCurrency as OrmlTraitsAssetRegistryAssetMetadata)?.metadata.decimals ?? 0; + + if (!selectedFromToken || !nativeCurrency) return ; return (
@@ -62,7 +75,7 @@ const Gas = () => {

Get AMPE