Skip to content

Commit

Permalink
chore: Fetch currencies from blockchain instead of using hardcoded me…
Browse files Browse the repository at this point in the history
…tadata

* fetch currencies from blockchain instead of using harcoded metadata

* handle undefined blockchain data for /gas

* implement useAssetRegistryMetadata hook
  • Loading branch information
Sharqiewicz authored Apr 30, 2024
1 parent 7413027 commit 063e0d7
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 77 deletions.
58 changes: 58 additions & 0 deletions src/hooks/useAssetRegistryMetadata.ts
Original file line number Diff line number Diff line change
@@ -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<OrmlTraitsAssetRegistryAssetMetadata | undefined>;
getNativeAssetMetadata: () => Promise<OrmlTraitsAssetRegistryAssetMetadata | undefined>;
}

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<OrmlTraitsAssetRegistryAssetMetadata[]>([]);

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<OrmlTraitsAssetRegistryAssetMetadata | undefined> =>
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 };
};
41 changes: 36 additions & 5 deletions src/hooks/useBuyout/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ 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: {
min: number;
max: number;
};
currencies: OrmlTraitsAssetRegistryAssetMetadata[];
nativeCurrency: OrmlTraitsAssetRegistryAssetMetadata;
nativeCurrency: OrmlTraitsAssetRegistryAssetMetadata | undefined;
sellFee: PerMill;
handleBuyout: (
currency: OrmlTraitsAssetRegistryAssetMetadata,
Expand Down Expand Up @@ -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<number>(0);
const [maximumBuyout, setMaximumBuyout] = useState<number>(0);
const [sellFee, setSellFee] = useState<PerMill>(new PerMill(0));
const { getNativeAssetMetadata, getAssetMetadata } = useAssetRegistryMetadata();

const [nativeCurrency, setNativeCurrency] = useState<OrmlTraitsAssetRegistryAssetMetadata | undefined>(undefined);
const [currencies, setCurrencies] = useState<OrmlTraitsAssetRegistryAssetMetadata[]>([]);

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() {
Expand Down Expand Up @@ -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,
};
};
16 changes: 10 additions & 6 deletions src/hooks/useBuyout/types.ts
Original file line number Diff line number Diff line change
@@ -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;
}
47 changes: 0 additions & 47 deletions src/hooks/useBuyout/utils.ts

This file was deleted.

11 changes: 11 additions & 0 deletions src/pages/gas/GasSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Skeleton } from '../../components/Skeleton';

export const GasSkeleton = () => (
<div className="w-full h-full flex justify-center items-center">
<div className="w-3/5">
{Array.from({ length: 12 }).map((_, index) => (
<Skeleton className="w-full h-8 mb-2" isLoading={true} key={index} />
))}
</div>
</div>
);
51 changes: 32 additions & 19 deletions src/pages/gas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,46 +9,59 @@ 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<BlockchainAsset | undefined>(currencies[0]);
const [selectedFromToken, setSelectedFromToken] = useState<BlockchainAsset | undefined>(undefined);
const [selectedFromTokenPriceUSD, setSelectedFromTokenPriceUSD] = useState<number>(0);
const [nativeTokenPrice, setNativeTokenPrice] = useState<number>(0);
const [submissionPending, setSubmissionPending] = useState<boolean>(false);
const [confirmationDialogVisible, setConfirmationDialogVisible] = useState<boolean>(false);

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 <GasSkeleton />;

return (
<div className="h-full flex items-center justify-center mt-4">
Expand All @@ -62,7 +75,7 @@ const Gas = () => {
<h1 className="text-[28px] mb-8">Get AMPE</h1>
<GasForm
submissionPending={submissionPending}
currencies={currencies}
currencies={currencies.length ? currencies : []}
selectedFromToken={selectedFromToken}
setSelectedFromToken={setSelectedFromToken}
nativeCurrency={nativeCurrency}
Expand Down

0 comments on commit 063e0d7

Please sign in to comment.