diff --git a/apps/tangle-dapp/app/liquid-staking/page.tsx b/apps/tangle-dapp/app/liquid-staking/page.tsx index 8d0dfca38..d6812958a 100644 --- a/apps/tangle-dapp/app/liquid-staking/page.tsx +++ b/apps/tangle-dapp/app/liquid-staking/page.tsx @@ -7,7 +7,7 @@ import LsStakeCard from '../../components/LiquidStaking/stakeAndUnstake/LsStakeC import LsUnstakeCard from '../../components/LiquidStaking/stakeAndUnstake/LsUnstakeCard'; import UnstakeRequestsTable from '../../components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable'; import { LsSearchParamKey } from '../../constants/liquidStaking/types'; -import ParachainPoolsTable from '../../containers/ParachainPoolsTable'; +import LsPoolsTable from '../../containers/LsPoolsTable'; import { useLsStore } from '../../data/liquidStaking/useLsStore'; import useSearchParamState from '../../hooks/useSearchParamState'; import isLsParachainChainId from '../../utils/liquidStaking/isLsParachainChainId'; @@ -29,7 +29,7 @@ const LiquidStakingTokenPage: FC = () => { }); const { selectedProtocolId } = useLsStore(); - + const isParachainChain = isLsParachainChainId(selectedProtocolId); return ( @@ -54,7 +54,7 @@ const LiquidStakingTokenPage: FC = () => {
{isStaking ? ( isParachainChain ? ( - + ) : ( ) diff --git a/apps/tangle-dapp/components/LiquidStaking/AddressLink.tsx b/apps/tangle-dapp/components/LiquidStaking/AddressLink.tsx index 389a2918b..b899694aa 100644 --- a/apps/tangle-dapp/components/LiquidStaking/AddressLink.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/AddressLink.tsx @@ -3,10 +3,10 @@ import { ExternalLinkLine } from '@webb-tools/icons'; import { shortenString, Typography } from '@webb-tools/webb-ui-components'; import { FC, useCallback } from 'react'; -import { AnySubstrateAddress } from '../../types/utils'; +import { SubstrateAddress } from '../../types/utils'; export type AddressLinkProps = { - address: AnySubstrateAddress | HexString; + address: SubstrateAddress | HexString; }; const AddressLink: FC = ({ address }) => { diff --git a/apps/tangle-dapp/components/LiquidStaking/VaultsAndAssetsTable.tsx b/apps/tangle-dapp/components/LiquidStaking/VaultsAndAssetsTable.tsx index 1b7caf8c1..acdfa4e78 100644 --- a/apps/tangle-dapp/components/LiquidStaking/VaultsAndAssetsTable.tsx +++ b/apps/tangle-dapp/components/LiquidStaking/VaultsAndAssetsTable.tsx @@ -14,6 +14,7 @@ import { twMerge } from 'tailwind-merge'; import useVaults from '../../app/liquid-staking/useVaults'; import StatItem from '../../components/StatItem'; import TableCellWrapper from '../../components/tables/TableCellWrapper'; +import { PagePath } from '../../types'; import { Asset, Vault } from '../../types/liquidStaking'; import LsTokenIcon from '../LsTokenIcon'; @@ -80,8 +81,7 @@ const vaultColumns = [ cell: ({ row }) => (
- {/* TODO: add proper href */} - + @@ -278,7 +263,7 @@ const ParachainPoolsTable: FC = () => { fw="bold" className="text-mono-200 dark:text-mono-0" > - Select Token Vault + Select Pool { ); }; -export default ParachainPoolsTable; +export default LsPoolsTable; diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/polkadot.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/polkadot.tsx index 13cac11d4..9e286c428 100644 --- a/apps/tangle-dapp/data/liquidStaking/adapters/polkadot.tsx +++ b/apps/tangle-dapp/data/liquidStaking/adapters/polkadot.tsx @@ -35,11 +35,10 @@ import { fetchTokenSymbol, } from '../fetchHelpers'; -const SS58_PREFIX = 0; const DECIMALS = 18; export type PolkadotValidator = { - address: SubstrateAddress; + address: SubstrateAddress; identity: string; commission: BN; apy?: number; @@ -70,7 +69,7 @@ const fetchValidators = async ( return { id: address.toString(), - address: assertSubstrateAddress(address.toString(), SS58_PREFIX), + address: assertSubstrateAddress(address.toString()), identity: identityName ?? address.toString(), totalValueStaked: totalValueStaked ?? BN_ZERO, apy: 0, diff --git a/apps/tangle-dapp/data/liquidStaking/useLsPools.ts b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts new file mode 100644 index 000000000..4f23b33e4 --- /dev/null +++ b/apps/tangle-dapp/data/liquidStaking/useLsPools.ts @@ -0,0 +1,107 @@ +import { BN, u8aToString } from '@polkadot/util'; +import { useCallback, useMemo } from 'react'; + +import { LsPool, LsProtocolId } from '../../constants/liquidStaking/types'; +import useApiRx from '../../hooks/useApiRx'; +import useNetworkFeatures from '../../hooks/useNetworkFeatures'; +import { NetworkFeature } from '../../types'; +import assertSubstrateAddress from '../../utils/assertSubstrateAddress'; +import permillToPercentage from '../../utils/permillToPercentage'; + +const useLsPools = (): Map | null | Error => { + const networkFeatures = useNetworkFeatures(); + + if (!networkFeatures.includes(NetworkFeature.LsPools)) { + // TODO: Handle case where the active network doesn't support liquid staking pools. + } + + const { result: rawMetadataEntries } = useApiRx( + useCallback((api) => { + return api.query.lst.metadata.entries(); + }, []), + ); + + const { result: rawBondedPools } = useApiRx( + useCallback((api) => { + return api.query.lst.bondedPools.entries(); + }, []), + ); + + const tanglePools = useMemo(() => { + if (rawBondedPools === null) { + return null; + } + + return rawBondedPools.flatMap(([key, valueOpt]) => { + // Skip empty values. + if (valueOpt.isNone) { + return []; + } + + const tanglePool = valueOpt.unwrap(); + + // Ignore all non-open pools. + if (!tanglePool.state.isOpen) { + return []; + } + + return [[key.args[0].toNumber(), tanglePool] as const]; + }); + }, [rawBondedPools]); + + const poolsMap = useMemo(() => { + if (tanglePools === null) { + return null; + } + + const keyValuePairs = tanglePools.map(([id, tanglePool]) => { + const metadataEntryBytes = + rawMetadataEntries === null + ? undefined + : rawMetadataEntries.find( + ([idKey]) => idKey.args[0].toNumber() === id, + )?.[1]; + + const metadata = + metadataEntryBytes === undefined + ? undefined + : u8aToString(metadataEntryBytes); + + // TODO: Under what circumstances would this be `None`? During pool creation, the various addresses seem required, not optional. + const owner = assertSubstrateAddress( + tanglePool.roles.root.unwrap().toString(), + ); + + const commissionPercentage = tanglePool.commission.current.isNone + ? undefined + : permillToPercentage(tanglePool.commission.current.unwrap()[0]); + + const pool: LsPool = { + id, + metadata, + owner, + commissionPercentage, + // TODO: Dummy values. + apyPercentage: 0.1, + chainId: LsProtocolId.POLKADOT, + totalStaked: new BN(1234560000000000), + ownerStaked: new BN(12300003567), + validators: [ + '5FfP4SU5jXY9ZVfR1kY1pUXuJ3G1bfjJoQDRz4p7wSH3Mmdn' as any, + '5FnL9Pj3NX7E6yC1a2tN4kVdR7y2sAqG8vRsF4PN6yLeu2mL' as any, + '5CF8H7P3qHfZzBtPXH6G6e3Wc3V2wVn6tQHgYJ5HGKK1eC5z' as any, + '5GV8vP8Bh3fGZm2P7YNxMzUd9Wy4k3RSRvkq7RXVjxGGM1cy' as any, + '5DPy4XU6nNV2t2NQkz3QvPB2X5GJ5ZJ1wqMzC4Rxn2WLbXVD' as any, + ], + }; + + return [id, pool] as const; + }); + + return new Map(keyValuePairs); + }, [rawMetadataEntries, tanglePools]); + + return poolsMap; +}; + +export default useLsPools; diff --git a/apps/tangle-dapp/data/liquidStaking/useParachainLsFees.ts b/apps/tangle-dapp/data/liquidStaking/useParachainLsFees.ts index a724c715d..b82a6577b 100644 --- a/apps/tangle-dapp/data/liquidStaking/useParachainLsFees.ts +++ b/apps/tangle-dapp/data/liquidStaking/useParachainLsFees.ts @@ -10,12 +10,12 @@ const useParachainLsFees = () => { useCallback((api) => { return api.query.lstMinting.fees().pipe( map((fees) => { - const mintFee = permillToPercentage(fees[0]); - const redeemFee = permillToPercentage(fees[1]); + const mintFeePercentage = permillToPercentage(fees[0]); + const redeemFeePercentage = permillToPercentage(fees[1]); return { - mintFee, - redeemFee, + mintFeePercentage, + redeemFeePercentage, }; }), ); diff --git a/apps/tangle-dapp/hooks/useApi.ts b/apps/tangle-dapp/hooks/useApi.ts index 5c3011a84..4b5cc5b7f 100644 --- a/apps/tangle-dapp/hooks/useApi.ts +++ b/apps/tangle-dapp/hooks/useApi.ts @@ -2,6 +2,7 @@ import { ApiPromise } from '@polkadot/api'; import { useCallback, useEffect, useState } from 'react'; import useNetworkStore from '../context/useNetworkStore'; +import ensureError from '../utils/ensureError'; import { getApiPromise } from '../utils/polkadot'; import usePromise from './usePromise'; @@ -22,6 +23,7 @@ export type ApiFetcher = (api: ApiPromise) => Promise | T; */ function useApi(fetcher: ApiFetcher, overrideRpcEndpoint?: string) { const [result, setResult] = useState(null); + const [error, setError] = useState(null); const { rpcEndpoint } = useNetworkStore(); const { result: api } = usePromise( @@ -38,11 +40,32 @@ function useApi(fetcher: ApiFetcher, overrideRpcEndpoint?: string) { return; } - const newResult = fetcher(api); + let newResult; + + // Fetch the data, and catch any errors that are thrown. + // In certain cases, the fetcher may fail with an error. For example, + // if a pallet isn't available on the active chain. Another example would + // be if the active chain is mainnet, but the fetcher is trying to fetch + // data from a testnet pallet that hasn't been deployed to mainnet yet. + try { + newResult = fetcher(api); + } catch (possibleError) { + const error = ensureError(possibleError); + + console.error( + 'Error while fetching data, this can happen when TypeScript type definitions are outdated or accessing pallets on the wrong chain:', + error, + ); + + setError(error); + + return; + } if (newResult instanceof Promise) { newResult.then((data) => setResult(data)); } else { + setError(null); setResult(newResult); } }, [api, fetcher]); @@ -52,7 +75,7 @@ function useApi(fetcher: ApiFetcher, overrideRpcEndpoint?: string) { refetch(); }, [refetch]); - return { result, refetch }; + return { result, error, refetch }; } export default useApi; diff --git a/apps/tangle-dapp/hooks/useApiRx.ts b/apps/tangle-dapp/hooks/useApiRx.ts index 8bb08e736..d1f1863b1 100644 --- a/apps/tangle-dapp/hooks/useApiRx.ts +++ b/apps/tangle-dapp/hooks/useApiRx.ts @@ -62,7 +62,27 @@ function useApiRx( return; } - const observable = factory(apiRx); + let observable; + + // In certain cases, the factory may fail with an error. For example, + // if a pallet isn't available on the active chain. Another example would + // be if the active chain is mainnet, but the factory is trying to fetch + // data from a testnet pallet that hasn't been deployed to mainnet yet. + try { + observable = factory(apiRx); + } catch (possibleError) { + const error = ensureError(possibleError); + + console.error( + 'Error creating subscription, this can happen when TypeScript type definitions are outdated or accessing pallets on the wrong chain:', + error, + ); + + setError(error); + setLoading(false); + + return; + } // The factory is not yet ready to produce an observable. // Discard any previous data diff --git a/apps/tangle-dapp/hooks/useNetworkFeatures.ts b/apps/tangle-dapp/hooks/useNetworkFeatures.ts index 007f2e79a..0bc89f862 100644 --- a/apps/tangle-dapp/hooks/useNetworkFeatures.ts +++ b/apps/tangle-dapp/hooks/useNetworkFeatures.ts @@ -1,5 +1,3 @@ -import { useMemo } from 'react'; - import { NETWORK_FEATURE_MAP } from '../constants/networks'; import useNetworkStore from '../context/useNetworkStore'; import { NetworkFeature } from '../types'; @@ -7,7 +5,7 @@ import { NetworkFeature } from '../types'; const useNetworkFeatures = (): Readonly => { const { network } = useNetworkStore(); - return useMemo(() => NETWORK_FEATURE_MAP[network.id], [network.id]); + return NETWORK_FEATURE_MAP[network.id]; }; export default useNetworkFeatures; diff --git a/apps/tangle-dapp/types/index.ts b/apps/tangle-dapp/types/index.ts index 6f185859b..f1b6bae07 100755 --- a/apps/tangle-dapp/types/index.ts +++ b/apps/tangle-dapp/types/index.ts @@ -221,6 +221,7 @@ export type ServiceParticipant = { export enum NetworkFeature { Faucet, EraStakersOverview, + LsPools, } export const ExplorerType = { diff --git a/apps/tangle-dapp/types/utils.ts b/apps/tangle-dapp/types/utils.ts index 5ad4b1b95..bf2b77911 100644 --- a/apps/tangle-dapp/types/utils.ts +++ b/apps/tangle-dapp/types/utils.ts @@ -122,9 +122,4 @@ export type Brand = Type & { __brand: Name }; export type RemoveBrand = { __brand: never }; -export type AnySubstrateAddress = Brand; - -export type SubstrateAddress = Brand< - string, - 'SubstrateAddress' & { ss58Format: SS58 } ->; +export type SubstrateAddress = Brand; diff --git a/apps/tangle-dapp/utils/assertAnySubstrateAddress.ts b/apps/tangle-dapp/utils/assertAnySubstrateAddress.ts deleted file mode 100644 index b9217cb21..000000000 --- a/apps/tangle-dapp/utils/assertAnySubstrateAddress.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { isAddress } from '@polkadot/util-crypto'; -import assert from 'assert'; - -import { AnySubstrateAddress } from '../types/utils'; - -type AnySubstrateAddressAssertionFn = ( - address: string, -) => asserts address is AnySubstrateAddress; - -const assertAnySubstrateAddress: AnySubstrateAddressAssertionFn = (address) => { - assert(isAddress(address), 'Address should be a valid Substrate address'); -}; - -export default assertAnySubstrateAddress; diff --git a/apps/tangle-dapp/utils/assertSubstrateAddress.ts b/apps/tangle-dapp/utils/assertSubstrateAddress.ts index 615daedf5..195bb1da2 100644 --- a/apps/tangle-dapp/utils/assertSubstrateAddress.ts +++ b/apps/tangle-dapp/utils/assertSubstrateAddress.ts @@ -3,16 +3,10 @@ import assert from 'assert'; import { SubstrateAddress } from '../types/utils'; -const assertSubstrateAddress = ( - address: string, - ss58Prefix: SS58, -): SubstrateAddress => { - assert( - isAddress(address, undefined, ss58Prefix), - 'Address should be a valid Substrate address', - ); +const assertSubstrateAddress = (address: string): SubstrateAddress => { + assert(isAddress(address), 'Address should be a valid Substrate address'); - return address as SubstrateAddress; + return address as SubstrateAddress; }; export default assertSubstrateAddress; diff --git a/apps/tangle-dapp/utils/isAnySubstrateAddress.ts b/apps/tangle-dapp/utils/isAnySubstrateAddress.ts index 05c5fa94f..b42f0d40c 100644 --- a/apps/tangle-dapp/utils/isAnySubstrateAddress.ts +++ b/apps/tangle-dapp/utils/isAnySubstrateAddress.ts @@ -1,10 +1,10 @@ import { isAddress } from '@polkadot/util-crypto'; -import { AnySubstrateAddress, RemoveBrand } from '../types/utils'; +import { SubstrateAddress, RemoveBrand } from '../types/utils'; const isAnySubstrateAddress = ( address: string, -): address is AnySubstrateAddress & RemoveBrand => { +): address is SubstrateAddress & RemoveBrand => { return isAddress(address); }; diff --git a/apps/tangle-dapp/utils/permillToPercentage.ts b/apps/tangle-dapp/utils/permillToPercentage.ts index 6f448b6fd..5700ace97 100644 --- a/apps/tangle-dapp/utils/permillToPercentage.ts +++ b/apps/tangle-dapp/utils/permillToPercentage.ts @@ -1,6 +1,6 @@ import { Permill } from '@polkadot/types/interfaces'; -const permillToPercentage = (permill: Permill) => { +const permillToPercentage = (permill: Permill): number => { return permill.toNumber() / 1_000_000; }; diff --git a/apps/tangle-dapp/utils/polkadot/identity.ts b/apps/tangle-dapp/utils/polkadot/identity.ts index 8e6ac85b7..735ba2ada 100644 --- a/apps/tangle-dapp/utils/polkadot/identity.ts +++ b/apps/tangle-dapp/utils/polkadot/identity.ts @@ -21,7 +21,10 @@ export const extractDataFromIdentityInfo = ( type: IdentityDataType, ): string | null => { const displayData = info[type]; - if (displayData.isNone) return null; + + if (displayData.isNone) { + return null; + } const displayDataObject: { raw?: string } = JSON.parse( displayData.toString(), diff --git a/apps/tangle-dapp/utils/scaleAmountByPercentage.ts b/apps/tangle-dapp/utils/scaleAmountByPercentage.ts new file mode 100644 index 000000000..0a18cc1f0 --- /dev/null +++ b/apps/tangle-dapp/utils/scaleAmountByPercentage.ts @@ -0,0 +1,14 @@ +import { BN } from '@polkadot/util'; + +const scaleAmountByPercentage = (amount: BN, percentage: number): BN => { + // Scale factor for 4 decimal places (0.xxxx). + const scale = new BN(10_000); + + // Scale the percentage to an integer. + const scaledPercentage = new BN(Math.round(percentage * scale.toNumber())); + + // Multiply the amount by the scaled percentage and then divide by the scale. + return amount.mul(scaledPercentage).div(scale); +}; + +export default scaleAmountByPercentage; diff --git a/apps/tangle-dapp/utils/scaleAmountByPermill.ts b/apps/tangle-dapp/utils/scaleAmountByPermill.ts deleted file mode 100644 index 8c215f041..000000000 --- a/apps/tangle-dapp/utils/scaleAmountByPermill.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { BN } from '@polkadot/util'; - -const scaleAmountByPermill = (amount: BN, permill: number): BN => { - // Scale factor for 4 decimal places (0.xxxx). - const scale = new BN(10_000); - - // Scale the permill to an integer. - const scaledPermill = new BN(Math.round(permill * scale.toNumber())); - - // Multiply the amount by the scaled permill and then divide by the scale. - return amount.mul(scaledPermill).div(scale); -}; - -export default scaleAmountByPermill; diff --git a/libs/dapp-config/src/constants/tangle.ts b/libs/dapp-config/src/constants/tangle.ts index 9652c2193..c8ffd76c8 100644 --- a/libs/dapp-config/src/constants/tangle.ts +++ b/libs/dapp-config/src/constants/tangle.ts @@ -30,9 +30,7 @@ export const TANGLE_LOCAL_POLKADOT_JS_DASHBOARD_URL = getPolkadotJsDashboardUrl( TANGLE_LOCAL_WS_RPC_ENDPOINT, ); -export const TANGLE_MAINNET_SS58_PREFIX = 5845; -export const TANGLE_TESTNET_SS58_PREFIX = 5845; -export const TANGLE_LOCAL_SS58_PREFIX = 42; +export const TANGLE_SS58_PREFIX = 5845; // Note that the chain decimal count is usually constant, and set when // the blockchain is deployed. It could be technically changed due to diff --git a/libs/webb-ui-components/src/constants/networks.ts b/libs/webb-ui-components/src/constants/networks.ts index f779489c7..b6f7db91c 100644 --- a/libs/webb-ui-components/src/constants/networks.ts +++ b/libs/webb-ui-components/src/constants/networks.ts @@ -15,9 +15,7 @@ import { TANGLE_LOCAL_WS_RPC_ENDPOINT, TANGLE_LOCAL_HTTP_RPC_ENDPOINT, TANGLE_LOCAL_POLKADOT_JS_DASHBOARD_URL, - TANGLE_MAINNET_SS58_PREFIX, - TANGLE_TESTNET_SS58_PREFIX, - TANGLE_LOCAL_SS58_PREFIX, + TANGLE_SS58_PREFIX, } from '@webb-tools/dapp-config/constants/tangle'; import { SUBQUERY_ENDPOINT } from './index'; @@ -75,7 +73,7 @@ export const TANGLE_MAINNET_NETWORK = { polkadotJsDashboardUrl: TANGLE_MAINNET_POLKADOT_JS_DASHBOARD_URL, nativeExplorerUrl: TANGLE_MAINNET_NATIVE_EXPLORER_URL, evmExplorerUrl: TANGLE_MAINNET_EVM_EXPLORER_URL, - ss58Prefix: TANGLE_MAINNET_SS58_PREFIX, + ss58Prefix: TANGLE_SS58_PREFIX, } as const satisfies Network; export const TANGLE_TESTNET_NATIVE_NETWORK = { @@ -91,7 +89,7 @@ export const TANGLE_TESTNET_NATIVE_NETWORK = { polkadotJsDashboardUrl: TANGLE_TESTNET_POLKADOT_JS_DASHBOARD_URL, nativeExplorerUrl: TANGLE_TESTNET_NATIVE_EXPLORER_URL, evmExplorerUrl: TANGLE_TESTNET_EVM_EXPLORER_URL, - ss58Prefix: TANGLE_TESTNET_SS58_PREFIX, + ss58Prefix: TANGLE_SS58_PREFIX, } as const satisfies Network; /** @@ -108,7 +106,7 @@ export const TANGLE_LOCAL_DEV_NETWORK = { wsRpcEndpoint: TANGLE_LOCAL_WS_RPC_ENDPOINT, httpRpcEndpoint: TANGLE_LOCAL_HTTP_RPC_ENDPOINT, polkadotJsDashboardUrl: TANGLE_LOCAL_POLKADOT_JS_DASHBOARD_URL, - ss58Prefix: TANGLE_LOCAL_SS58_PREFIX, + ss58Prefix: 42, } as const satisfies Network; export const TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK = {