diff --git a/apps/tangle-dapp/src/abi/erc20.ts b/apps/tangle-dapp/src/abi/erc20.ts new file mode 100644 index 0000000000..7ef0410500 --- /dev/null +++ b/apps/tangle-dapp/src/abi/erc20.ts @@ -0,0 +1,68 @@ +import { AbiFunction } from 'viem'; + +const ERC20_ABI = [ + { + inputs: [{ name: 'account', type: 'address' }], + name: 'balanceOf', + outputs: [{ name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { name: 'spender', type: 'address' }, + { name: 'amount', type: 'uint256' }, + ], + name: 'approve', + outputs: [{ name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + ], + name: 'allowance', + outputs: [{ name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'decimals', + outputs: [{ name: '', type: 'uint8' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'symbol', + outputs: [{ name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { name: 'recipient', type: 'address' }, + { name: 'amount', type: 'uint256' }, + ], + name: 'transfer', + outputs: [{ name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { name: 'sender', type: 'address' }, + { name: 'recipient', type: 'address' }, + { name: 'amount', type: 'uint256' }, + ], + name: 'transferFrom', + outputs: [{ name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, +] as const satisfies AbiFunction[]; + +export default ERC20_ABI; diff --git a/apps/tangle-dapp/src/app/providers.tsx b/apps/tangle-dapp/src/app/providers.tsx index fbb6c86ed4..df76dbec87 100644 --- a/apps/tangle-dapp/src/app/providers.tsx +++ b/apps/tangle-dapp/src/app/providers.tsx @@ -8,8 +8,8 @@ import { type PropsWithChildren, type ReactNode } from 'react'; import type { State } from 'wagmi'; import { z } from 'zod'; -import HyperlaneWarpContext from '../pages/bridge/context/HyperlaneWarpContext'; -import BridgeTxQueueProvider from '../pages/bridge/context/BridgeTxQueueContext/BridgeTxQueueProvider'; +import HyperlaneWarpContext from '../context/HyperlaneWarpContext'; +import BridgeTxQueueProvider from '../context/BridgeTxQueueContext/BridgeTxQueueProvider'; import PolkadotApiProvider from '@webb-tools/tangle-shared-ui/context/PolkadotApiProvider'; import { RestakeContextProvider } from '@webb-tools/tangle-shared-ui/context/RestakeContext'; diff --git a/apps/tangle-dapp/src/components/Lists/AssetList.tsx b/apps/tangle-dapp/src/components/Lists/AssetList.tsx index a22effdaaf..a19988e95e 100644 --- a/apps/tangle-dapp/src/components/Lists/AssetList.tsx +++ b/apps/tangle-dapp/src/components/Lists/AssetList.tsx @@ -1,4 +1,3 @@ -import { EVMTokenBridgeEnum } from '@webb-tools/evm-contract-metadata'; import { ArrowRightUp, Search, TokenIcon } from '@webb-tools/icons'; import { Input, @@ -20,7 +19,6 @@ export type AssetConfig = { balance?: string; explorerUrl?: string; address?: Address; - assetBridgeType?: EVMTokenBridgeEnum; }; type AssetListProps = { diff --git a/apps/tangle-dapp/src/components/Lists/LogoListItem.tsx b/apps/tangle-dapp/src/components/Lists/LogoListItem.tsx index b9de54f85d..3bce1124bf 100644 --- a/apps/tangle-dapp/src/components/Lists/LogoListItem.tsx +++ b/apps/tangle-dapp/src/components/Lists/LogoListItem.tsx @@ -52,7 +52,7 @@ const LogoListItem: FC = ({ - {rightUpperText !== undefined && rightBottomText !== undefined && ( + {(rightUpperText !== undefined || rightBottomText !== undefined) && (
{rightUpperText ?? EMPTY_VALUE_PLACEHOLDER} diff --git a/apps/tangle-dapp/src/components/account/PointsReminder.tsx b/apps/tangle-dapp/src/components/account/PointsReminder.tsx index fab836fd90..209aeaf347 100644 --- a/apps/tangle-dapp/src/components/account/PointsReminder.tsx +++ b/apps/tangle-dapp/src/components/account/PointsReminder.tsx @@ -20,7 +20,7 @@ const PointsReminder: FC<{ className?: string }> = ({ className }) => {
    -
  • Deposit restaking assets
  • +
  • Deposit & delegate restaking assets
  • Build dApps
  • Develop blueprints
  • Deploy instances
  • diff --git a/apps/tangle-dapp/src/pages/bridge/components/BridgeConfirmationModal.tsx b/apps/tangle-dapp/src/components/bridge/BridgeConfirmationModal.tsx similarity index 94% rename from apps/tangle-dapp/src/pages/bridge/components/BridgeConfirmationModal.tsx rename to apps/tangle-dapp/src/components/bridge/BridgeConfirmationModal.tsx index 30a0b360c0..ce34f3d0c6 100644 --- a/apps/tangle-dapp/src/pages/bridge/components/BridgeConfirmationModal.tsx +++ b/apps/tangle-dapp/src/components/bridge/BridgeConfirmationModal.tsx @@ -29,12 +29,12 @@ import cx from 'classnames'; import { FC, useCallback } from 'react'; import { makeExplorerUrl } from '@webb-tools/api-provider-environment/transaction/utils'; -import { FeeDetail, FeeDetailProps } from '../components/FeeDetail'; -import { ROUTER_TX_EXPLORER_URL } from '../constants'; -import useBridgeStore from '../context/useBridgeStore'; -import { useBridgeTxQueue } from '../context/BridgeTxQueueContext'; -import { useHyperlaneTransfer } from '../hooks/useHyperlaneTransfer'; -import { useRouterTransfer } from '../hooks/useRouterTransfer'; +import { FeeDetail, FeeDetailProps } from './FeeDetail'; +import { ROUTER_TX_EXPLORER_URL } from '../../constants/bridge'; +import { useBridgeTxQueue } from '../../context/BridgeTxQueueContext'; +import { useHyperlaneTransfer } from '../../data/bridge/useHyperlaneTransfer'; +import { useRouterTransfer } from '../../data/bridge/useRouterTransfer'; +import { Decimal } from 'decimal.js'; interface BridgeConfirmationModalProps { isOpen: boolean; @@ -53,6 +53,8 @@ interface BridgeConfirmationModalProps { receiverAddress: string; refundAddress: string; } | null; + sendingAmount: Decimal | null; + receivingAmount: Decimal | null; } export const BridgeConfirmationModal = ({ @@ -65,9 +67,9 @@ export const BridgeConfirmationModal = ({ activeAccountAddress, destinationAddress, routerTransferData, + sendingAmount, + receivingAmount, }: BridgeConfirmationModalProps) => { - const { sendingAmount, receivingAmount } = useBridgeStore(); - const { addTxToQueue, setIsOpenQueueDropdown, @@ -337,7 +339,7 @@ const ConfirmationItem: FC<{ Amount
    - + {amount ?? EMPTY_VALUE_PLACEHOLDER} diff --git a/apps/tangle-dapp/src/pages/bridge/components/BridgeTxQueueDropdown.tsx b/apps/tangle-dapp/src/components/bridge/BridgeTxQueueDropdown.tsx similarity index 97% rename from apps/tangle-dapp/src/pages/bridge/components/BridgeTxQueueDropdown.tsx rename to apps/tangle-dapp/src/components/bridge/BridgeTxQueueDropdown.tsx index fa6d1da913..da349454ec 100644 --- a/apps/tangle-dapp/src/pages/bridge/components/BridgeTxQueueDropdown.tsx +++ b/apps/tangle-dapp/src/components/bridge/BridgeTxQueueDropdown.tsx @@ -9,7 +9,7 @@ import { Typography } from '@webb-tools/webb-ui-components/typography/Typography import { FC, useMemo } from 'react'; import { twMerge } from 'tailwind-merge'; -import { useBridgeTxQueue } from '../context/BridgeTxQueueContext'; +import { useBridgeTxQueue } from '../../context/BridgeTxQueueContext'; import BridgeTxQueueItem from './BridgeTxQueueItem'; const BridgeTxQueueDropdown: FC<{ diff --git a/apps/tangle-dapp/src/pages/bridge/components/BridgeTxQueueItem.tsx b/apps/tangle-dapp/src/components/bridge/BridgeTxQueueItem.tsx similarity index 98% rename from apps/tangle-dapp/src/pages/bridge/components/BridgeTxQueueItem.tsx rename to apps/tangle-dapp/src/components/bridge/BridgeTxQueueItem.tsx index 7601b23aa3..c637afe070 100644 --- a/apps/tangle-dapp/src/pages/bridge/components/BridgeTxQueueItem.tsx +++ b/apps/tangle-dapp/src/components/bridge/BridgeTxQueueItem.tsx @@ -10,7 +10,7 @@ import { Decimal } from 'decimal.js'; import { FC } from 'react'; import { twMerge } from 'tailwind-merge'; -import { useBridgeTxQueue } from '../context/BridgeTxQueueContext'; +import { useBridgeTxQueue } from '../../context/BridgeTxQueueContext'; interface BridgeTxQueueItemProps { tx: BridgeQueueTxItem; diff --git a/apps/tangle-dapp/src/pages/bridge/components/FeeDetail.tsx b/apps/tangle-dapp/src/components/bridge/FeeDetail.tsx similarity index 55% rename from apps/tangle-dapp/src/pages/bridge/components/FeeDetail.tsx rename to apps/tangle-dapp/src/components/bridge/FeeDetail.tsx index 7dae12d8a5..d841841cd3 100644 --- a/apps/tangle-dapp/src/pages/bridge/components/FeeDetail.tsx +++ b/apps/tangle-dapp/src/components/bridge/FeeDetail.tsx @@ -1,18 +1,20 @@ import { ChevronDownIcon } from '@heroicons/react/24/solid'; import { EVMTokenBridgeEnum } from '@webb-tools/evm-contract-metadata'; -import { TokenIcon } from '@webb-tools/icons'; +import { ArrowRightUp, TokenIcon } from '@webb-tools/icons'; import { getFlexBasic } from '@webb-tools/icons/utils'; import { Accordion, AccordionButtonBase, AccordionContent, AccordionItem, + shortenHex, Typography, } from '@webb-tools/webb-ui-components'; import cx from 'classnames'; import { twMerge } from 'tailwind-merge'; import { BridgeToken } from '@webb-tools/tangle-shared-ui/types'; +import { Decimal } from 'decimal.js'; export interface FeeDetailProps { token: BridgeToken; @@ -27,16 +29,18 @@ export interface FeeDetailProps { isCollapsible?: boolean; bridgeFeeTokenType: string; gasFeeTokenType?: string; + sendingAmount: Decimal | null; + receivingAmount: Decimal | null; + recipientExplorerUrl?: string; } export const FeeDetail = ({ token, amounts, - estimatedTime, className, isCollapsible = true, bridgeFeeTokenType, - gasFeeTokenType, + recipientExplorerUrl, }: FeeDetailProps) => { return ( - Fee + + Total Receiving +
    - {token.bridgeType === EVMTokenBridgeEnum.Router ? ( - - ) : ( - - )} + - {amounts.bridgeFee.split('.')[0]}. - {amounts.bridgeFee.split('.')[1].slice(0, 4)}{' '} - {bridgeFeeTokenType} + {amounts.receiving.split('.')[0]} + {amounts.receiving.split('.')[1] + ? `.${amounts.receiving.split('.')[1].slice(0, 4)}` + : ''}{' '}
    @@ -98,99 +100,100 @@ export const FeeDetail = ({ 'radix-state-closed:animate-accordion-slide-up', )} > -
    +
    +
    +
    - Bridge + + Sending + -
    + + {amounts.sending} + +
    + +
    + + Bridge Fee + + + + {amounts.bridgeFee.split('.')[0]} + {amounts.bridgeFee.split('.')[1] + ? `.${amounts.bridgeFee.split('.')[1].slice(0, 4)}` + : ''}{' '} + {bridgeFeeTokenType} + +
    + +
    + + Bridge Route + + +
    {token.bridgeType === EVMTokenBridgeEnum.Router ? ( ) : ( )} - + {token.bridgeType.charAt(0).toUpperCase() + token.bridgeType.slice(1)}
    -
    - Amount - -
    - - - - {amounts.sending} - -
    -
    - - {estimatedTime !== undefined && ( + {amounts.gasFee && (
    - Estimated Time + + Gas Fee + - ~ {estimatedTime} + {amounts.gasFee}
    )} - {amounts.gasFee && ( + {recipientExplorerUrl && ( )} - -
    - -
    - Total Receiving - -
    - - - - {amounts.receiving.split('.')[0]}. - {amounts.receiving.split('.')[1].slice(0, 4)}{' '} - {bridgeFeeTokenType} - -
    -
    diff --git a/apps/tangle-dapp/src/components/claims/ClaimingAccountInput.tsx b/apps/tangle-dapp/src/components/claims/ClaimingAccountInput.tsx index 11a3830075..678e292b4e 100644 --- a/apps/tangle-dapp/src/components/claims/ClaimingAccountInput.tsx +++ b/apps/tangle-dapp/src/components/claims/ClaimingAccountInput.tsx @@ -1,7 +1,6 @@ import { useWebContext } from '@webb-tools/api-provider-environment/webb-context'; import { WebbError, WebbErrorCodes } from '@webb-tools/dapp-types/WebbError'; import { ChevronDown } from '@webb-tools/icons'; -import isViemError from '@webb-tools/web3-api-provider/utils/isViemError'; import { WebbWeb3Provider } from '@webb-tools/web3-api-provider/webb-provider'; import { useWebbUI } from '@webb-tools/webb-ui-components'; import { Avatar } from '@webb-tools/webb-ui-components/components/Avatar'; @@ -15,6 +14,7 @@ import type { FC } from 'react'; import { twMerge } from 'tailwind-merge'; import useWalletAccounts from '../../hooks/useWalletAccounts'; +import { BaseError } from 'viem'; type Props = { activeAccountAddress: string; @@ -35,9 +35,10 @@ const ClaimingAccountInput: FC = ({ try { await walletClient.requestPermissions({ eth_accounts: {} }); } catch (error) { - const message = isViemError(error) - ? error.shortMessage - : WebbError.from(WebbErrorCodes.SwitchAccountFailed).message; + const message = + error instanceof BaseError + ? error.shortMessage + : WebbError.from(WebbErrorCodes.SwitchAccountFailed).message; notificationApi({ variant: 'error', message }); } diff --git a/apps/tangle-dapp/src/constants/bridge.ts b/apps/tangle-dapp/src/constants/bridge.ts new file mode 100644 index 0000000000..605d2d8f18 --- /dev/null +++ b/apps/tangle-dapp/src/constants/bridge.ts @@ -0,0 +1,1177 @@ +import { + ChainMap, + ChainMetadata, + ChainTechnicalStack, + ExplorerFamily, + TokenStandard, + WarpCoreConfig, +} from '@hyperlane-xyz/sdk'; +import { ProtocolType } from '@hyperlane-xyz/utils'; +import { PresetTypedChainId } from '@webb-tools/dapp-types'; +import { + EVMTokenBridgeEnum, + EVMTokenEnum, + EVMTokens, +} from '@webb-tools/evm-contract-metadata'; + +import { + BridgeChainsConfigType, + BridgeToken, +} from '@webb-tools/tangle-shared-ui/types'; +import { + assertAddressBy, + assertEvmAddress, + isSolanaAddress, +} from '@webb-tools/webb-ui-components'; +import { Abi } from 'viem'; +import ERC20_ABI from '../abi/erc20'; + +// TODO: Include assertion logic, as the Abi type can't be directly imported from viem since the 'type' field clashes (string vs. 'function'). +const assertAbi = (abi: unknown): Abi => abi as Abi; + +export const BRIDGE_TOKENS: Record = { + [PresetTypedChainId.Polygon]: [ + { + tokenSymbol: 'routerTNT', + tokenType: EVMTokenEnum.TNT, + bridgeType: EVMTokenBridgeEnum.Router, + address: assertEvmAddress(EVMTokens.polygon.router.TNT.address), + abi: assertAbi(EVMTokens.polygon.router.TNT.abi), + decimals: EVMTokens.polygon.router.TNT.decimals, + chainId: PresetTypedChainId.Polygon, + }, + { + tokenSymbol: 'USDT', + tokenType: EVMTokenEnum.USDT, + bridgeType: EVMTokenBridgeEnum.Hyperlane, + address: assertEvmAddress('0xF6D2c670ebF3BC82c4Dbd1275f8c35eF80d9cd02'), + abi: assertAbi(ERC20_ABI), + decimals: 6, + chainId: PresetTypedChainId.Polygon, + hyperlaneSyntheticAddress: assertEvmAddress( + '0x3240f00998fe098165b1f8fCaDBaE292e62560aD', + ), + }, + { + tokenSymbol: 'USDC', + tokenType: EVMTokenEnum.USDC, + bridgeType: EVMTokenBridgeEnum.Hyperlane, + address: assertEvmAddress('0x7C547f860b71399846E5CC2487f60A2b34396CC2'), + abi: assertAbi(ERC20_ABI), + decimals: 6, + chainId: PresetTypedChainId.Polygon, + hyperlaneSyntheticAddress: assertEvmAddress( + '0x002A0d7Adf2F4A007E0c600EA4Cde65094120122', + ), + }, + { + tokenSymbol: 'wstETH', + tokenType: 'wstETH' as EVMTokenEnum, + bridgeType: EVMTokenBridgeEnum.Hyperlane, + address: assertEvmAddress('0x7dC9C90Ac46B936F55621C8C8741bd4AC72BC0c2'), + abi: assertAbi(ERC20_ABI), + decimals: 18, + chainId: PresetTypedChainId.Polygon, + hyperlaneSyntheticAddress: assertEvmAddress( + '0x53Df23FD53504208aA53f75b834954b9E3766efa', + ), + }, + { + tokenSymbol: 'DAI', + tokenType: 'DAI' as EVMTokenEnum, + bridgeType: EVMTokenBridgeEnum.Hyperlane, + address: assertEvmAddress('0x05e44F7B2EECB0b2efF11153ef34DCcCbC3D25D7'), + abi: assertAbi(ERC20_ABI), + decimals: 18, + chainId: PresetTypedChainId.Polygon, + hyperlaneSyntheticAddress: assertEvmAddress( + '0xF852cE3E163ae2A2B43f05C6696B50D386ca44d5', + ), + }, + ], + [PresetTypedChainId.Arbitrum]: [ + { + tokenSymbol: 'routerTNT', + tokenType: EVMTokenEnum.TNT, + bridgeType: EVMTokenBridgeEnum.Router, + address: assertEvmAddress(EVMTokens.arbitrum.router.TNT.address), + abi: assertAbi(EVMTokens.arbitrum.router.TNT.abi), + decimals: EVMTokens.arbitrum.router.TNT.decimals, + chainId: PresetTypedChainId.Arbitrum, + }, + { + tokenSymbol: 'USDT', + tokenType: EVMTokenEnum.USDT, + bridgeType: EVMTokenBridgeEnum.Hyperlane, + address: assertEvmAddress('0x0778878E53c632da21cd3951A434a54f418d7CB8'), + abi: assertAbi(ERC20_ABI), + decimals: 6, + chainId: PresetTypedChainId.Arbitrum, + hyperlaneSyntheticAddress: assertEvmAddress( + '0x1237F64027D3766D4976478b757dac9Ca63e6425', + ), + }, + { + tokenSymbol: 'USDC', + tokenType: EVMTokenEnum.USDC, + bridgeType: EVMTokenBridgeEnum.Hyperlane, + address: assertEvmAddress('0x510b0140a4A5b12c7127fB0A6A946200d66a64C2'), + abi: assertAbi(ERC20_ABI), + decimals: 6, + chainId: PresetTypedChainId.Arbitrum, + hyperlaneSyntheticAddress: assertEvmAddress( + '0x19b07a373E77bb54ee60A1456EB4412111f0ec0a', + ), + }, + { + tokenSymbol: 'wstETH', + tokenType: 'wstETH' as EVMTokenEnum, + bridgeType: EVMTokenBridgeEnum.Hyperlane, + address: assertEvmAddress('0x108F919b5A76B64e80dBf74130Ff6441A62F6405'), + abi: assertAbi(ERC20_ABI), + decimals: 18, + chainId: PresetTypedChainId.Arbitrum, + hyperlaneSyntheticAddress: assertEvmAddress( + '0xF140e9C0fEa2fbb64B55199F6A7957B3d19FBAB0', + ), + }, + { + tokenSymbol: 'DAI', + tokenType: 'DAI' as EVMTokenEnum, + bridgeType: EVMTokenBridgeEnum.Hyperlane, + address: assertEvmAddress('0x7A153C00352DCb87E11684ce504bfE4dC170acCb'), + abi: assertAbi(ERC20_ABI), + decimals: 18, + chainId: PresetTypedChainId.Arbitrum, + hyperlaneSyntheticAddress: assertEvmAddress( + '0xd91CE8398761a85b0989850736619bf0F8b3C76e', + ), + }, + { + tokenSymbol: 'ARB', + tokenType: 'ARB' as EVMTokenEnum, + bridgeType: EVMTokenBridgeEnum.Hyperlane, + address: assertEvmAddress('0x99Ce18058C6fE35216D8626C3D183526240CcCbb'), + abi: assertAbi(ERC20_ABI), + decimals: 18, + chainId: PresetTypedChainId.Arbitrum, + hyperlaneSyntheticAddress: assertEvmAddress( + '0xf44511BAFE78CF8DAaa2804d075B40DEFFFe63b2', + ), + }, + ], + [PresetTypedChainId.Optimism]: [ + { + tokenSymbol: 'routerTNT', + tokenType: EVMTokenEnum.TNT, + bridgeType: EVMTokenBridgeEnum.Router, + address: assertEvmAddress(EVMTokens.optimism.router.TNT.address), + abi: assertAbi(EVMTokens.optimism.router.TNT.abi), + decimals: EVMTokens.optimism.router.TNT.decimals, + chainId: PresetTypedChainId.Optimism, + }, + { + tokenSymbol: 'USDT', + tokenType: EVMTokenEnum.USDT, + bridgeType: EVMTokenBridgeEnum.Hyperlane, + address: assertEvmAddress('0xFF2b0Dab4956e69bc2c78542C396EEcD9eAB3460'), + abi: assertAbi(ERC20_ABI), + decimals: 6, + chainId: PresetTypedChainId.Optimism, + hyperlaneSyntheticAddress: assertEvmAddress( + '0xC5B342D3CfAd241D9300Cb76116CA4a5e30Cf2Ac', + ), + }, + { + tokenSymbol: 'USDC', + tokenType: EVMTokenEnum.USDC, + bridgeType: EVMTokenBridgeEnum.Hyperlane, + address: assertEvmAddress('0x7C547f860b71399846E5CC2487f60A2b34396CC2'), + abi: assertAbi(ERC20_ABI), + decimals: 6, + chainId: PresetTypedChainId.Optimism, + hyperlaneSyntheticAddress: assertEvmAddress( + '0x872E68E6d97bA08840d816229408A4aaAF3e6D4B', + ), + }, + { + tokenSymbol: 'wstETH', + tokenType: 'wstETH' as EVMTokenEnum, + bridgeType: EVMTokenBridgeEnum.Hyperlane, + address: assertEvmAddress('0x44Df1A79760f09153Cf31e7bD7F42bE432e30f9C'), + abi: assertAbi(ERC20_ABI), + decimals: 18, + chainId: PresetTypedChainId.Optimism, + hyperlaneSyntheticAddress: assertEvmAddress( + '0xC8cCc1cd6B6880353A0aA9a492Ced1972fDEF9c9', + ), + }, + ], + [PresetTypedChainId.Linea]: [ + { + tokenSymbol: 'routerTNT', + tokenType: EVMTokenEnum.TNT, + bridgeType: EVMTokenBridgeEnum.Router, + address: assertEvmAddress(EVMTokens.linea.router.TNT.address), + abi: assertAbi(EVMTokens.linea.router.TNT.abi), + decimals: EVMTokens.linea.router.TNT.decimals, + chainId: PresetTypedChainId.Linea, + }, + ], + [PresetTypedChainId.Base]: [ + { + tokenSymbol: 'routerTNT', + tokenType: EVMTokenEnum.TNT, + bridgeType: EVMTokenBridgeEnum.Router, + address: assertEvmAddress(EVMTokens.base.router.TNT.address), + abi: assertAbi(EVMTokens.base.router.TNT.abi), + decimals: EVMTokens.base.router.TNT.decimals, + chainId: PresetTypedChainId.Base, + }, + { + tokenSymbol: 'USDC', + tokenType: EVMTokenEnum.USDC, + bridgeType: EVMTokenBridgeEnum.Hyperlane, + address: assertEvmAddress('0x8eC690c7b6253a0F834A90E820891BCeC9FA4B3E'), + abi: assertAbi(ERC20_ABI), + decimals: 6, + chainId: PresetTypedChainId.Base, + hyperlaneSyntheticAddress: assertEvmAddress( + '0x6d55528963D147BEA3e925538F2e32C24Fa0119a', + ), + }, + { + tokenSymbol: 'cbBTC', + tokenType: 'cbBTC' as EVMTokenEnum, + bridgeType: EVMTokenBridgeEnum.Hyperlane, + address: assertEvmAddress('0xab4E420B21DFa57b103aA09636C1CA88657CDEC7'), + abi: assertAbi(ERC20_ABI), + decimals: 8, + chainId: PresetTypedChainId.Base, + hyperlaneSyntheticAddress: assertEvmAddress( + '0x25Bd880dfd52b42242b0ef0d97b5eC66BABa0d01', + ), + }, + { + tokenSymbol: 'AVAIL', + tokenType: 'AVAIL' as EVMTokenEnum, + bridgeType: EVMTokenBridgeEnum.Hyperlane, + address: assertEvmAddress('0xe2C35423680D0F55F2C226dD75600826f66debA3'), + abi: assertAbi(ERC20_ABI), + decimals: 18, + chainId: PresetTypedChainId.Base, + hyperlaneSyntheticAddress: assertEvmAddress( + '0x4b7c2a96d1E9f3D37F979A8c74e17d53473fbf40', + ), + }, + { + tokenSymbol: 'wstETH', + tokenType: 'wstETH' as EVMTokenEnum, + bridgeType: EVMTokenBridgeEnum.Hyperlane, + address: assertEvmAddress('0xC3C2bC8664a260d3C60aa91BB3Ea54BfDc705cD6'), + abi: assertAbi(ERC20_ABI), + decimals: 18, + chainId: PresetTypedChainId.Base, + hyperlaneSyntheticAddress: assertEvmAddress( + '0x052EDA809Cd496E14DFdE30F039E674A06f5A850', + ), + }, + ], + [PresetTypedChainId.BSC]: [ + { + tokenSymbol: 'routerTNT', + tokenType: EVMTokenEnum.TNT, + bridgeType: EVMTokenBridgeEnum.Router, + address: assertEvmAddress(EVMTokens.bsc.router.TNT.address), + abi: assertAbi(EVMTokens.bsc.router.TNT.abi), + decimals: EVMTokens.bsc.router.TNT.decimals, + chainId: PresetTypedChainId.BSC, + }, + { + tokenSymbol: 'USDT', + tokenType: EVMTokenEnum.USDT, + bridgeType: EVMTokenBridgeEnum.Hyperlane, + address: assertEvmAddress('0xab4E420B21DFa57b103aA09636C1CA88657CDEC7'), + abi: assertAbi(ERC20_ABI), + decimals: 18, + chainId: PresetTypedChainId.BSC, + hyperlaneSyntheticAddress: assertEvmAddress( + '0xb4c2a9A412d4ae746706CAc8aacf6340EB3D6134', + ), + }, + { + tokenSymbol: 'USDC', + tokenType: EVMTokenEnum.USDC, + bridgeType: EVMTokenBridgeEnum.Hyperlane, + address: assertEvmAddress('0x44E293671560272323423d051191de8AD3b2126b'), + abi: assertAbi(ERC20_ABI), + decimals: 18, + chainId: PresetTypedChainId.BSC, + hyperlaneSyntheticAddress: assertEvmAddress( + '0x88dd0d1DA4155f453a5933310df48Ce7d7fEAbfF', + ), + }, + ], + [PresetTypedChainId.SolanaMainnet]: [ + { + tokenSymbol: 'routerTNT', + tokenType: EVMTokenEnum.TNT, + bridgeType: EVMTokenBridgeEnum.Router, + address: assertAddressBy( + 'FcermohxLgTo8xnJXpPyW6D2swUMepVjQVNiiNLw38pC', + isSolanaAddress, + ), + abi: [], + decimals: 18, + chainId: PresetTypedChainId.SolanaMainnet, + }, + ], +}; + +export const BRIDGE_CHAINS: BridgeChainsConfigType = { + [PresetTypedChainId.TangleMainnetEVM]: { + [PresetTypedChainId.Polygon]: { + supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.Polygon], + }, + [PresetTypedChainId.Arbitrum]: { + supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.Arbitrum], + }, + [PresetTypedChainId.Optimism]: { + supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.Optimism], + }, + [PresetTypedChainId.Linea]: { + supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.Linea], + }, + [PresetTypedChainId.Base]: { + supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.Base], + }, + [PresetTypedChainId.BSC]: { + supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.BSC], + }, + [PresetTypedChainId.SolanaMainnet]: { + supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.SolanaMainnet], + }, + }, + [PresetTypedChainId.Arbitrum]: { + [PresetTypedChainId.TangleMainnetEVM]: { + supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.Arbitrum], + }, + }, + [PresetTypedChainId.Polygon]: { + [PresetTypedChainId.TangleMainnetEVM]: { + supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.Polygon], + }, + }, + [PresetTypedChainId.Optimism]: { + [PresetTypedChainId.TangleMainnetEVM]: { + supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.Optimism], + }, + }, + [PresetTypedChainId.Linea]: { + [PresetTypedChainId.TangleMainnetEVM]: { + supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.Linea], + }, + }, + [PresetTypedChainId.Base]: { + [PresetTypedChainId.TangleMainnetEVM]: { + supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.Base], + }, + }, + [PresetTypedChainId.BSC]: { + [PresetTypedChainId.TangleMainnetEVM]: { + supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.BSC], + }, + }, + // [PresetTypedChainId.SolanaMainnet]: { + // [PresetTypedChainId.TangleMainnetEVM]: { + // supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.SolanaMainnet], + // }, + // }, +}; + +export const ROUTER_QUOTE_URL = `https://api-beta.pathfinder.routerprotocol.com/api/v2/quote`; + +export const ROUTER_TRANSACTION_URL = `https://api-beta.pathfinder.routerprotocol.com/api/v2/transaction`; + +export const ROUTER_NATIVE_TOKEN_ADDRESS = + '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; + +export const ROUTER_PARTNER_ID = 252; + +export enum ROUTER_ERROR_CODE { + LOW_AMOUNT_INPUT = 'AMOUNT-LOW-W-VALUE', +} + +export const ROUTER_TX_EXPLORER_URL = 'https://explorer.routernitro.com/tx/'; + +export const HYPERLANE_REGISTRY_URL = + process.env.NEXT_PUBLIC_HYPERLANE_REGISTRY_URL || + 'https://github.com/hyperlane-xyz/hyperlane-registry'; + +export const HYPERLANE_CHAINS: ChainMap = { + arbitrum: { + blockExplorers: [ + { + apiUrl: 'https://api.arbiscan.io/api', + family: ExplorerFamily.Etherscan, + name: 'Arbiscan', + url: 'https://arbiscan.io', + }, + ], + blocks: { + confirmations: 1, + estimateBlockTime: 3, + reorgPeriod: 5, + }, + chainId: 42161, + deployer: { + name: 'Abacus Works', + url: 'https://www.hyperlane.xyz', + }, + displayName: 'Arbitrum', + domainId: 42161, + gasCurrencyCoinGeckoId: 'ethereum', + gnosisSafeTransactionServiceUrl: + 'https://safe-transaction-arbitrum.safe.global/', + index: { + from: 143649797, + }, + name: 'arbitrum', + nativeToken: { + decimals: 18, + name: 'Ether', + symbol: 'ETH', + }, + protocol: ProtocolType.Ethereum, + rpcUrls: [ + { + http: 'https://arbitrum.llamarpc.com', + }, + { + http: 'https://rpc.ankr.com/arbitrum', + }, + { + http: 'https://arb1.arbitrum.io/rpc', + }, + ], + technicalStack: ChainTechnicalStack.ArbitrumNitro, + }, + optimism: { + blockExplorers: [ + { + apiUrl: 'https://api-optimistic.etherscan.io/api', + family: ExplorerFamily.Etherscan, + name: 'Etherscan', + url: 'https://optimistic.etherscan.io', + }, + ], + blocks: { + confirmations: 1, + estimateBlockTime: 3, + reorgPeriod: 10, + }, + chainId: 10, + deployer: { + name: 'Abacus Works', + url: 'https://www.hyperlane.xyz', + }, + displayName: 'Optimism', + domainId: 10, + gasCurrencyCoinGeckoId: 'ethereum', + gnosisSafeTransactionServiceUrl: + 'https://safe-transaction-optimism.safe.global/', + name: 'optimism', + nativeToken: { + decimals: 18, + name: 'Ether', + symbol: 'ETH', + }, + protocol: ProtocolType.Ethereum, + rpcUrls: [ + { + http: 'https://mainnet.optimism.io', + }, + ], + technicalStack: ChainTechnicalStack.OpStack, + }, + polygon: { + blockExplorers: [ + { + apiUrl: 'https://api.polygonscan.com/api', + family: ExplorerFamily.Etherscan, + name: 'PolygonScan', + url: 'https://polygonscan.com', + }, + ], + blocks: { + confirmations: 3, + estimateBlockTime: 2, + reorgPeriod: 'finalized', + }, + chainId: 137, + deployer: { + name: 'Abacus Works', + url: 'https://www.hyperlane.xyz', + }, + displayName: 'Polygon', + domainId: 137, + gasCurrencyCoinGeckoId: 'polygon-ecosystem-token', + gnosisSafeTransactionServiceUrl: + 'https://safe-transaction-polygon.safe.global/', + name: 'polygon', + nativeToken: { + decimals: 18, + name: 'Polygon Ecosystem Token', + symbol: 'POL', + }, + protocol: ProtocolType.Ethereum, + rpcUrls: [ + { + http: 'https://polygon-bor.publicnode.com', + }, + { + http: 'https://polygon-rpc.com', + }, + { + http: 'https://rpc.ankr.com/polygon', + }, + ], + technicalStack: ChainTechnicalStack.Other, + }, + bsc: { + blockExplorers: [ + { + apiUrl: 'https://api.bscscan.com/api', + family: ExplorerFamily.Etherscan, + name: 'BscScan', + url: 'https://bscscan.com', + }, + ], + blocks: { + confirmations: 1, + estimateBlockTime: 3, + reorgPeriod: 'finalized', + }, + chainId: 56, + deployer: { + name: 'Abacus Works', + url: 'https://www.hyperlane.xyz', + }, + displayName: 'Binance Smart Chain', + displayNameShort: 'Binance', + domainId: 56, + gasCurrencyCoinGeckoId: 'binancecoin', + gnosisSafeTransactionServiceUrl: + 'https://safe-transaction-bsc.safe.global/', + name: 'bsc', + nativeToken: { + decimals: 18, + name: 'BNB', + symbol: 'BNB', + }, + protocol: ProtocolType.Ethereum, + rpcUrls: [ + { + http: 'https://rpc.ankr.com/bsc', + }, + { + http: 'https://bsc.drpc.org', + }, + { + http: 'https://bscrpc.com', + }, + ], + technicalStack: ChainTechnicalStack.Other, + }, + base: { + blockExplorers: [ + { + apiUrl: 'https://api.basescan.org/api', + family: ExplorerFamily.Etherscan, + name: 'BaseScan', + url: 'https://basescan.org', + }, + ], + blocks: { + confirmations: 3, + estimateBlockTime: 2, + reorgPeriod: 10, + }, + chainId: 8453, + deployer: { + name: 'Abacus Works', + url: 'https://www.hyperlane.xyz', + }, + displayName: 'Base', + domainId: 8453, + gasCurrencyCoinGeckoId: 'ethereum', + gnosisSafeTransactionServiceUrl: + 'https://safe-transaction-base.safe.global/', + name: 'base', + nativeToken: { + decimals: 18, + name: 'Ether', + symbol: 'ETH', + }, + protocol: ProtocolType.Ethereum, + rpcUrls: [ + { + http: 'https://base.publicnode.com/', + }, + { + http: 'https://mainnet.base.org', + }, + { + http: 'https://base.blockpi.network/v1/rpc/public', + }, + ], + technicalStack: ChainTechnicalStack.Other, + }, + tangle: { + blockExplorers: [ + { + apiUrl: 'https://explorer.tangle.tools/api', + family: ExplorerFamily.Blockscout, + name: 'Tangle EVM Explorer', + url: 'https://explorer.tangle.tools', + }, + ], + blocks: { + confirmations: 1, + estimateBlockTime: 6, + reorgPeriod: 'finalized', + }, + chainId: 5845, + deployer: { + name: 'Abacus Works', + url: 'https://www.hyperlane.xyz', + }, + displayName: 'Tangle', + domainId: 5845, + gasCurrencyCoinGeckoId: 'tangle-network', + isTestnet: false, + name: 'tangle', + nativeToken: { + decimals: 18, + name: 'Tangle Network Token', + symbol: 'TNT', + }, + protocol: ProtocolType.Ethereum, + rpcUrls: [ + { + http: 'https://rpc.tangle.tools', + }, + ], + technicalStack: ChainTechnicalStack.PolkadotSubstrate, + }, +}; + +export const HYPERLANE_WARP_ROUTE_CONFIGS: WarpCoreConfig = { + tokens: [ + { + addressOrDenom: '0x0778878E53c632da21cd3951A434a54f418d7CB8', + chainName: 'arbitrum', + collateralAddressOrDenom: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', + connections: [ + { + token: 'ethereum|tangle|0x1237F64027D3766D4976478b757dac9Ca63e6425', + }, + ], + decimals: 6, + name: 'Tether USD', + standard: TokenStandard.EvmHypCollateral, + symbol: 'USDT', + }, + { + addressOrDenom: '0x1237F64027D3766D4976478b757dac9Ca63e6425', + chainName: 'tangle', + connections: [ + { + token: 'ethereum|arbitrum|0x0778878E53c632da21cd3951A434a54f418d7CB8', + }, + ], + decimals: 6, + name: 'Tether USD', + standard: TokenStandard.EvmHypSynthetic, + symbol: 'USDT', + }, + { + addressOrDenom: '0xFF2b0Dab4956e69bc2c78542C396EEcD9eAB3460', + chainName: 'optimism', + collateralAddressOrDenom: '0x94b008aA00579c1307B0EF2c499aD98a8ce58e58', + connections: [ + { + token: 'ethereum|tangle|0xC5B342D3CfAd241D9300Cb76116CA4a5e30Cf2Ac', + }, + ], + decimals: 6, + name: 'Tether USD', + standard: TokenStandard.EvmHypCollateral, + symbol: 'USDT', + }, + { + addressOrDenom: '0xC5B342D3CfAd241D9300Cb76116CA4a5e30Cf2Ac', + chainName: 'tangle', + connections: [ + { + token: 'ethereum|optimism|0xFF2b0Dab4956e69bc2c78542C396EEcD9eAB3460', + }, + ], + decimals: 6, + name: 'Tether USD', + standard: TokenStandard.EvmHypSynthetic, + symbol: 'USDT', + }, + { + addressOrDenom: '0xF6D2c670ebF3BC82c4Dbd1275f8c35eF80d9cd02', + chainName: 'polygon', + collateralAddressOrDenom: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F', + connections: [ + { + token: 'ethereum|tangle|0x3240f00998fe098165b1f8fCaDBaE292e62560aD', + }, + ], + decimals: 6, + name: '(PoS) Tether USD', + standard: TokenStandard.EvmHypCollateral, + symbol: 'USDT', + }, + { + addressOrDenom: '0x3240f00998fe098165b1f8fCaDBaE292e62560aD', + chainName: 'tangle', + connections: [ + { + token: 'ethereum|polygon|0xF6D2c670ebF3BC82c4Dbd1275f8c35eF80d9cd02', + }, + ], + decimals: 6, + name: '(PoS) Tether USD', + standard: TokenStandard.EvmHypSynthetic, + symbol: 'USDT', + }, + { + addressOrDenom: '0xab4E420B21DFa57b103aA09636C1CA88657CDEC7', + chainName: 'bsc', + collateralAddressOrDenom: '0x55d398326f99059fF775485246999027B3197955', + connections: [ + { + token: 'ethereum|tangle|0xb4c2a9A412d4ae746706CAc8aacf6340EB3D6134', + }, + ], + decimals: 18, + name: 'Tether USD', + standard: TokenStandard.EvmHypCollateral, + symbol: 'USDT', + }, + { + addressOrDenom: '0xb4c2a9A412d4ae746706CAc8aacf6340EB3D6134', + chainName: 'tangle', + connections: [ + { + token: 'ethereum|bsc|0xab4E420B21DFa57b103aA09636C1CA88657CDEC7', + }, + ], + decimals: 18, + name: 'Tether USD', + standard: TokenStandard.EvmHypSynthetic, + symbol: 'USDT', + }, + { + addressOrDenom: '0x510b0140a4A5b12c7127fB0A6A946200d66a64C2', + chainName: 'arbitrum', + collateralAddressOrDenom: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + connections: [ + { + token: 'ethereum|tangle|0x19b07a373E77bb54ee60A1456EB4412111f0ec0a', + }, + ], + decimals: 6, + name: 'USD Coin', + standard: TokenStandard.EvmHypCollateral, + symbol: 'USDC', + }, + { + addressOrDenom: '0x19b07a373E77bb54ee60A1456EB4412111f0ec0a', + chainName: 'tangle', + connections: [ + { + token: 'ethereum|arbitrum|0x510b0140a4A5b12c7127fB0A6A946200d66a64C2', + }, + ], + decimals: 6, + name: 'USD Coin', + standard: TokenStandard.EvmHypSynthetic, + symbol: 'USDC', + }, + { + addressOrDenom: '0x7C547f860b71399846E5CC2487f60A2b34396CC2', + chainName: 'polygon', + collateralAddressOrDenom: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', + connections: [ + { + token: 'ethereum|tangle|0x002A0d7Adf2F4A007E0c600EA4Cde65094120122', + }, + ], + decimals: 6, + name: 'USD Coin', + standard: TokenStandard.EvmHypCollateral, + symbol: 'USDC', + }, + { + addressOrDenom: '0x002A0d7Adf2F4A007E0c600EA4Cde65094120122', + chainName: 'tangle', + connections: [ + { + token: 'ethereum|polygon|0x7C547f860b71399846E5CC2487f60A2b34396CC2', + }, + ], + decimals: 6, + name: 'USD Coin', + standard: TokenStandard.EvmHypSynthetic, + symbol: 'USDC', + }, + { + addressOrDenom: '0x7C547f860b71399846E5CC2487f60A2b34396CC2', + chainName: 'optimism', + collateralAddressOrDenom: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', + connections: [ + { + token: 'ethereum|tangle|0x872E68E6d97bA08840d816229408A4aaAF3e6D4B', + }, + ], + decimals: 6, + name: 'USD Coin', + standard: TokenStandard.EvmHypCollateral, + symbol: 'USDC', + }, + { + addressOrDenom: '0x872E68E6d97bA08840d816229408A4aaAF3e6D4B', + chainName: 'tangle', + connections: [ + { + token: 'ethereum|optimism|0x7C547f860b71399846E5CC2487f60A2b34396CC2', + }, + ], + decimals: 6, + name: 'USD Coin', + standard: TokenStandard.EvmHypSynthetic, + symbol: 'USDC', + }, + { + addressOrDenom: '0x8eC690c7b6253a0F834A90E820891BCeC9FA4B3E', + chainName: 'base', + collateralAddressOrDenom: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', + connections: [ + { + token: 'ethereum|tangle|0x6d55528963D147BEA3e925538F2e32C24Fa0119a', + }, + ], + decimals: 6, + name: 'USD Coin', + standard: TokenStandard.EvmHypCollateral, + symbol: 'USDC', + }, + { + addressOrDenom: '0x6d55528963D147BEA3e925538F2e32C24Fa0119a', + chainName: 'tangle', + connections: [ + { + token: 'ethereum|base|0x8eC690c7b6253a0F834A90E820891BCeC9FA4B3E', + }, + ], + decimals: 6, + name: 'USD Coin', + standard: TokenStandard.EvmHypSynthetic, + symbol: 'USDC', + }, + { + addressOrDenom: '0x44E293671560272323423d051191de8AD3b2126b', + chainName: 'bsc', + collateralAddressOrDenom: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', + connections: [ + { + token: 'ethereum|tangle|0x88dd0d1DA4155f453a5933310df48Ce7d7fEAbfF', + }, + ], + decimals: 18, + name: 'USD Coin', + standard: TokenStandard.EvmHypCollateral, + symbol: 'USDC', + }, + { + addressOrDenom: '0x88dd0d1DA4155f453a5933310df48Ce7d7fEAbfF', + chainName: 'tangle', + connections: [ + { + token: 'ethereum|bsc|0x44E293671560272323423d051191de8AD3b2126b', + }, + ], + decimals: 18, + name: 'USD Coin', + standard: TokenStandard.EvmHypSynthetic, + symbol: 'USDC', + }, + { + addressOrDenom: '0xab4E420B21DFa57b103aA09636C1CA88657CDEC7', + chainName: 'base', + collateralAddressOrDenom: '0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf', + connections: [ + { + token: 'ethereum|tangle|0x25Bd880dfd52b42242b0ef0d97b5eC66BABa0d01', + }, + ], + decimals: 8, + name: 'Coinbase Wrapped BTC', + standard: TokenStandard.EvmHypCollateral, + symbol: 'cbBTC', + }, + { + addressOrDenom: '0x25Bd880dfd52b42242b0ef0d97b5eC66BABa0d01', + chainName: 'tangle', + connections: [ + { + token: 'ethereum|base|0xab4E420B21DFa57b103aA09636C1CA88657CDEC7', + }, + ], + decimals: 8, + name: 'Coinbase Wrapped BTC', + standard: TokenStandard.EvmHypSynthetic, + symbol: 'cbBTC', + }, + { + addressOrDenom: '0xe2C35423680D0F55F2C226dD75600826f66debA3', + chainName: 'base', + collateralAddressOrDenom: '0xd89d90d26B48940FA8F58385Fe84625d468E057a', + connections: [ + { + token: 'ethereum|tangle|0x4b7c2a96d1E9f3D37F979A8c74e17d53473fbf40', + }, + ], + decimals: 18, + name: 'Avail (Wormhole)', + standard: TokenStandard.EvmHypCollateral, + symbol: 'AVAIL', + }, + { + addressOrDenom: '0x4b7c2a96d1E9f3D37F979A8c74e17d53473fbf40', + chainName: 'tangle', + connections: [ + { + token: 'ethereum|base|0xe2C35423680D0F55F2C226dD75600826f66debA3', + }, + ], + decimals: 18, + name: 'Avail (Wormhole)', + standard: TokenStandard.EvmHypSynthetic, + symbol: 'AVAIL', + }, + { + addressOrDenom: '0x108F919b5A76B64e80dBf74130Ff6441A62F6405', + chainName: 'arbitrum', + collateralAddressOrDenom: '0x5979D7b546E38E414F7E9822514be443A4800529', + connections: [ + { + token: 'ethereum|tangle|0xF140e9C0fEa2fbb64B55199F6A7957B3d19FBAB0', + }, + ], + decimals: 18, + name: 'Wrapped liquid staked Ether 2.0', + standard: TokenStandard.EvmHypCollateral, + symbol: 'wstETH', + }, + { + addressOrDenom: '0xF140e9C0fEa2fbb64B55199F6A7957B3d19FBAB0', + chainName: 'tangle', + connections: [ + { + token: 'ethereum|arbitrum|0x108F919b5A76B64e80dBf74130Ff6441A62F6405', + }, + ], + decimals: 18, + name: 'Wrapped liquid staked Ether 2.0', + standard: TokenStandard.EvmHypSynthetic, + symbol: 'wstETH', + }, + { + addressOrDenom: '0x7dC9C90Ac46B936F55621C8C8741bd4AC72BC0c2', + chainName: 'polygon', + collateralAddressOrDenom: '0x03b54A6e9a984069379fae1a4fC4dBAE93B3bCCD', + connections: [ + { + token: 'ethereum|tangle|0x53Df23FD53504208aA53f75b834954b9E3766efa', + }, + ], + decimals: 18, + name: 'Wrapped liquid staked Ether 2.0 (PoS)', + standard: TokenStandard.EvmHypCollateral, + symbol: 'wstETH', + }, + { + addressOrDenom: '0x53Df23FD53504208aA53f75b834954b9E3766efa', + chainName: 'tangle', + connections: [ + { + token: 'ethereum|polygon|0x7dC9C90Ac46B936F55621C8C8741bd4AC72BC0c2', + }, + ], + decimals: 18, + name: 'Wrapped liquid staked Ether 2.0 (PoS)', + standard: TokenStandard.EvmHypSynthetic, + symbol: 'wstETH', + }, + { + addressOrDenom: '0xC3C2bC8664a260d3C60aa91BB3Ea54BfDc705cD6', + chainName: 'base', + collateralAddressOrDenom: '0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452', + connections: [ + { + token: 'ethereum|tangle|0x052EDA809Cd496E14DFdE30F039E674A06f5A850', + }, + ], + decimals: 18, + name: 'Wrapped liquid staked Ether 2.0', + standard: TokenStandard.EvmHypCollateral, + symbol: 'wstETH', + }, + { + addressOrDenom: '0x052EDA809Cd496E14DFdE30F039E674A06f5A850', + chainName: 'tangle', + connections: [ + { + token: 'ethereum|base|0xC3C2bC8664a260d3C60aa91BB3Ea54BfDc705cD6', + }, + ], + decimals: 18, + name: 'Wrapped liquid staked Ether 2.0', + standard: TokenStandard.EvmHypSynthetic, + symbol: 'wstETH', + }, + { + addressOrDenom: '0x44Df1A79760f09153Cf31e7bD7F42bE432e30f9C', + chainName: 'optimism', + collateralAddressOrDenom: '0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb', + connections: [ + { + token: 'ethereum|tangle|0xC8cCc1cd6B6880353A0aA9a492Ced1972fDEF9c9', + }, + ], + decimals: 18, + name: 'Wrapped liquid staked Ether 2.0', + standard: TokenStandard.EvmHypCollateral, + symbol: 'wstETH', + }, + { + addressOrDenom: '0xC8cCc1cd6B6880353A0aA9a492Ced1972fDEF9c9', + chainName: 'tangle', + connections: [ + { + token: 'ethereum|optimism|0x44Df1A79760f09153Cf31e7bD7F42bE432e30f9C', + }, + ], + decimals: 18, + name: 'Wrapped liquid staked Ether 2.0', + standard: TokenStandard.EvmHypSynthetic, + symbol: 'wstETH', + }, + { + addressOrDenom: '0x7A153C00352DCb87E11684ce504bfE4dC170acCb', + chainName: 'arbitrum', + collateralAddressOrDenom: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1', + connections: [ + { + token: 'ethereum|tangle|0xd91CE8398761a85b0989850736619bf0F8b3C76e', + }, + ], + decimals: 18, + name: 'Dai Stablecoin', + standard: TokenStandard.EvmHypCollateral, + symbol: 'DAI', + }, + { + addressOrDenom: '0xd91CE8398761a85b0989850736619bf0F8b3C76e', + chainName: 'tangle', + connections: [ + { + token: 'ethereum|arbitrum|0x7A153C00352DCb87E11684ce504bfE4dC170acCb', + }, + ], + decimals: 18, + name: 'Dai Stablecoin', + standard: TokenStandard.EvmHypSynthetic, + symbol: 'DAI', + }, + { + addressOrDenom: '0x05e44F7B2EECB0b2efF11153ef34DCcCbC3D25D7', + chainName: 'polygon', + collateralAddressOrDenom: '0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063', + connections: [ + { + token: 'ethereum|tangle|0xF852cE3E163ae2A2B43f05C6696B50D386ca44d5', + }, + ], + decimals: 18, + name: '(PoS) Dai Stablecoin', + standard: TokenStandard.EvmHypCollateral, + symbol: 'DAI', + }, + { + addressOrDenom: '0xF852cE3E163ae2A2B43f05C6696B50D386ca44d5', + chainName: 'tangle', + connections: [ + { + token: 'ethereum|polygon|0x05e44F7B2EECB0b2efF11153ef34DCcCbC3D25D7', + }, + ], + decimals: 18, + name: '(PoS) Dai Stablecoin', + standard: TokenStandard.EvmHypSynthetic, + symbol: 'DAI', + }, + { + addressOrDenom: '0x99Ce18058C6fE35216D8626C3D183526240CcCbb', + chainName: 'arbitrum', + collateralAddressOrDenom: '0x912CE59144191C1204E64559FE8253a0e49E6548', + connections: [ + { + token: 'ethereum|tangle|0xf44511BAFE78CF8DAaa2804d075B40DEFFFe63b2', + }, + ], + decimals: 18, + name: 'Arbitrum', + standard: TokenStandard.EvmHypCollateral, + symbol: 'ARB', + }, + { + addressOrDenom: '0xf44511BAFE78CF8DAaa2804d075B40DEFFFe63b2', + chainName: 'tangle', + connections: [ + { + token: 'ethereum|arbitrum|0x99Ce18058C6fE35216D8626C3D183526240CcCbb', + }, + ], + decimals: 18, + name: 'Arbitrum', + standard: TokenStandard.EvmHypSynthetic, + symbol: 'ARB', + }, + ], +}; + +export const HYPERLANE_WARP_ROUTE_WHITELIST: Array | null = [ + 'USDT/arbitrum-tangle', + 'USDT/optimism-tangle', + 'USDT/polygon-tangle', + 'USDT/bsc-tangle', + 'USDC/arbitrum-tangle', + 'USDC/polygon-tangle', + 'USDC/optimism-tangle', + 'USDC/base-tangle', + 'USDC/bsc-tangle', + 'cbBTC/base-tangle', + 'AVAIL/base-tangle', + 'wstETH/arbitrum-tangle', + 'wstETH/polygon-tangle', + 'wstETH/optimism-tangle', + 'DAI/arbitrum-tangle', + 'DAI/polygon-tangle', +]; diff --git a/apps/tangle-dapp/src/constants/restake.ts b/apps/tangle-dapp/src/constants/restake.ts index caae0d735c..172b29d789 100644 --- a/apps/tangle-dapp/src/constants/restake.ts +++ b/apps/tangle-dapp/src/constants/restake.ts @@ -1,8 +1,5 @@ import { PresetTypedChainId } from '@webb-tools/dapp-types/ChainId'; -/** - * The supported restake deposit typed chain ids. - */ export const SUPPORTED_RESTAKE_DEPOSIT_TYPED_CHAIN_IDS = [ PresetTypedChainId.TangleTestnetNative, PresetTypedChainId.TangleTestnetEVM, diff --git a/apps/tangle-dapp/src/containers/Layout.tsx b/apps/tangle-dapp/src/containers/Layout.tsx index ab6f337aee..52db81a5be 100644 --- a/apps/tangle-dapp/src/containers/Layout.tsx +++ b/apps/tangle-dapp/src/containers/Layout.tsx @@ -13,7 +13,7 @@ import { MobileSidebar, Sidebar } from '../components'; import DebugMetrics from './DebugMetrics'; import WalletAndChainContainer from './WalletAndChainContainer'; -import BridgeTxQueueDropdown from '../pages/bridge/components/BridgeTxQueueDropdown'; +import BridgeTxQueueDropdown from '../components/bridge/BridgeTxQueueDropdown'; // Some specific overrides for the social links for use in the // footer in Tangle dApp, since it defaults to the Webb socials. diff --git a/apps/tangle-dapp/src/containers/AssetsAndBalancesTable.tsx b/apps/tangle-dapp/src/containers/VaultsAndBalancesTable.tsx similarity index 87% rename from apps/tangle-dapp/src/containers/AssetsAndBalancesTable.tsx rename to apps/tangle-dapp/src/containers/VaultsAndBalancesTable.tsx index 8ab6bdfe5f..0cc591b28a 100644 --- a/apps/tangle-dapp/src/containers/AssetsAndBalancesTable.tsx +++ b/apps/tangle-dapp/src/containers/VaultsAndBalancesTable.tsx @@ -28,7 +28,7 @@ import { ArrowRight } from '@webb-tools/icons'; import { PagePath, QueryParamKey } from '../types'; import { Link } from 'react-router'; import sortByLocaleCompare from '../utils/sortByLocaleCompare'; -import useRestakeVaultAssets from '@webb-tools/tangle-shared-ui/data/restake/useRestakeVaultAssets'; +import useRestakeVaults from '@webb-tools/tangle-shared-ui/data/restake/useRestakeVaults'; import useIsAccountConnected from '../hooks/useIsAccountConnected'; import TableCellWrapper from '@webb-tools/tangle-shared-ui/components/tables/TableCellWrapper'; import LsTokenIcon from '@webb-tools/tangle-shared-ui/components/LsTokenIcon'; @@ -44,6 +44,7 @@ import pluralize from '@webb-tools/webb-ui-components/utils/pluralize'; import useRestakeAssetsTvl from '@webb-tools/tangle-shared-ui/data/restake/useRestakeAssetsTvl'; import { RestakeAssetId } from '@webb-tools/tangle-shared-ui/utils/createRestakeAssetId'; import sortByBn from '../utils/sortByBn'; +import assertRestakeAssetId from '@webb-tools/tangle-shared-ui/utils/assertRestakeAssetId'; type Row = { vaultId: number; @@ -210,28 +211,6 @@ const COLUMNS = [ ); }, }), - // TODO: Hiding for now. See #2708. - // COLUMN_HELPER.accessor('points', { - // header: () => ( - // - // ), - // cell: (props) => { - // const points = props.getValue(); - - // if (points === undefined) { - // return EMPTY_VALUE_PLACEHOLDER; - // } - - // return ( - // - // {addCommasToNumber(points)} - // - // ); - // }, - // }), COLUMN_HELPER.display({ id: 'restake-action', header: () => null, @@ -258,7 +237,7 @@ const COLUMNS = [ }), ]; -const AssetsAndBalancesTable: FC = () => { +const VaultsAndBalancesTable: FC = () => { const [sorting, setSorting] = useState([ // Default sorting by TVL in descending order. { id: 'tvl' satisfies keyof Row, desc: true }, @@ -272,7 +251,7 @@ const AssetsAndBalancesTable: FC = () => { const rewardConfig = useRestakeRewardConfig(); const { delegatorInfo } = useRestakeDelegatorInfo(); const isAccountConnected = useIsAccountConnected(); - const { vaultAssets } = useRestakeVaultAssets(); + const { vaults } = useRestakeVaults(); const assetsTvl = useRestakeAssetsTvl(); const getTotalLockedInAsset = useCallback( @@ -304,8 +283,8 @@ const AssetsAndBalancesTable: FC = () => { [delegatorInfo?.delegations, delegatorInfo?.deposits], ); - const assetRows = useMemo(() => { - return Object.entries(vaultAssets).flatMap(([assetId, metadata]) => { + const vaultRows = useMemo(() => { + return Object.entries(vaults).flatMap(([assetIdString, metadata]) => { if (metadata.vaultId === null) { return []; } @@ -316,6 +295,8 @@ const AssetsAndBalancesTable: FC = () => { return []; } + const assetId = assertRestakeAssetId(assetIdString); + // APY in this case is always between 0 and 100%. const apyPercentage = config.apy.toNumber() / 100; @@ -324,13 +305,9 @@ const AssetsAndBalancesTable: FC = () => { ? undefined : new BN(config.depositCap.toString()); - // TODO: Avoid using `as` to force cast here. This is a temporary workaround until the type of `assetId` is updated to be `RestakeAssetId`. - const tvl = - assetsTvl === null - ? undefined - : assetsTvl.get(assetId as RestakeAssetId); + const tvl = assetsTvl === null ? undefined : assetsTvl.get(assetId); - const assetBalances: (typeof balances)[string] | undefined = + const assetBalances: (typeof balances)[RestakeAssetId] | undefined = balances[assetId]; const available = @@ -343,7 +320,7 @@ const AssetsAndBalancesTable: FC = () => { name: metadata.name, tvl, available, - locked: getTotalLockedInAsset(parseInt(assetId)), + locked: getTotalLockedInAsset(parseInt(assetIdString)), // TODO: This won't work because reward config is PER VAULT not PER ASSET. But isn't each asset its own vault? apyPercentage, tokenSymbol: metadata.symbol, @@ -351,15 +328,15 @@ const AssetsAndBalancesTable: FC = () => { depositCap, } satisfies Row; }); - }, [vaultAssets, assetsTvl, balances, getTotalLockedInAsset, rewardConfig]); + }, [vaults, assetsTvl, balances, getTotalLockedInAsset, rewardConfig]); // Combine all rows. const rows = useMemo(() => { - return [...assetRows].sort((a, b) => { + return [...vaultRows].sort((a, b) => { // Sort by available balance in descending order. return b.available.cmp(a.available); }); - }, [assetRows]); + }, [vaultRows]); const table = useReactTable({ data: rows, @@ -390,7 +367,7 @@ const AssetsAndBalancesTable: FC = () => { if (rows.length === 0) { return ( ); @@ -407,4 +384,4 @@ const AssetsAndBalancesTable: FC = () => { ); }; -export default AssetsAndBalancesTable; +export default VaultsAndBalancesTable; diff --git a/apps/tangle-dapp/src/containers/restaking/OperatorsTable.tsx b/apps/tangle-dapp/src/containers/restaking/OperatorsTable.tsx index a030d4fd32..d045ab49ff 100644 --- a/apps/tangle-dapp/src/containers/restaking/OperatorsTable.tsx +++ b/apps/tangle-dapp/src/containers/restaking/OperatorsTable.tsx @@ -29,7 +29,7 @@ const OperatorsTable: FC = ({ operatorTVL, }) => { const [globalFilter, setGlobalFilter] = useState(''); - const { assetMetadataMap } = useRestakeContext(); + const { vaults } = useRestakeContext(); const { result: identities } = useIdentities( useMemo(() => Object.keys(operatorMap), [operatorMap]), @@ -51,20 +51,11 @@ const OperatorsTable: FC = ({ identityName: identities[address]?.name ?? undefined, restakersCount, tvlInUsd, - vaultTokens: delegationsToVaultTokens( - delegations, - assetMetadataMap, - ), + vaultTokens: delegationsToVaultTokens(delegations, vaults), } satisfies RestakeOperator; }, ), - [ - assetMetadataMap, - identities, - operatorConcentration, - operatorMap, - operatorTVL, - ], + [vaults, identities, operatorConcentration, operatorMap, operatorTVL], ); return ( diff --git a/apps/tangle-dapp/src/containers/restaking/RestakeOverviewTabs.tsx b/apps/tangle-dapp/src/containers/restaking/RestakeOverviewTabs.tsx index 9e67ddbefd..7984282afc 100644 --- a/apps/tangle-dapp/src/containers/restaking/RestakeOverviewTabs.tsx +++ b/apps/tangle-dapp/src/containers/restaking/RestakeOverviewTabs.tsx @@ -61,14 +61,14 @@ const RestakeOverviewTabs: FC = ({ vaultTVL, action, }) => { - const { assetMetadataMap } = useRestakeContext(); + const { vaults: vaultsMetadataMap } = useRestakeContext(); const rewardConfig = useRestakeRewardConfig(); // Recalculate vaults data from assetMap const vaults = useMemo(() => { const vaults: Record = {}; - for (const { vaultId, name, symbol } of Object.values(assetMetadataMap)) { + for (const { vaultId, name, symbol } of Object.values(vaultsMetadataMap)) { if (vaultId === null) { continue; } else if (vaults[vaultId] === undefined) { @@ -93,7 +93,7 @@ const RestakeOverviewTabs: FC = ({ } return vaults; - }, [assetMetadataMap, rewardConfig, vaultTVL]); + }, [vaultsMetadataMap, rewardConfig, vaultTVL]); const delegatorTotalRestakedAssets = useMemo(() => { if (!delegatorInfo?.delegations) { @@ -122,16 +122,16 @@ const RestakeOverviewTabs: FC = ({ getExpandedRowContent(row) { const vaultId = row.original.id; - const vaultAssets = Object.values(assetMetadataMap) + const vaultAssets = Object.values(vaultsMetadataMap) .filter((asset) => asset.vaultId === vaultId) .map((asset) => { const selfStake = - delegatorTotalRestakedAssets[asset.id] ?? ZERO_BIG_INT; + delegatorTotalRestakedAssets[asset.assetId] ?? ZERO_BIG_INT; - const tvl = delegatorTVL?.[asset.id] ?? null; + const tvl = delegatorTVL?.[asset.assetId] ?? null; return { - id: asset.id, + id: asset.assetId, symbol: asset.symbol, decimals: asset.decimals, tvl, @@ -144,7 +144,7 @@ const RestakeOverviewTabs: FC = ({ ); }, }), - [assetMetadataMap, delegatorTVL, delegatorTotalRestakedAssets], + [vaultsMetadataMap, delegatorTVL, delegatorTotalRestakedAssets], ); return ( diff --git a/apps/tangle-dapp/src/containers/restaking/SelectOperatorModal.tsx b/apps/tangle-dapp/src/containers/restaking/SelectOperatorModal.tsx index 3c1a53ac7a..25c2d605ae 100644 --- a/apps/tangle-dapp/src/containers/restaking/SelectOperatorModal.tsx +++ b/apps/tangle-dapp/src/containers/restaking/SelectOperatorModal.tsx @@ -33,7 +33,7 @@ const SelectOperatorModal = ({ onItemSelected, operatorIdentities, }: Props) => { - const { assetMetadataMap } = useRestakeContext(); + const { vaults } = useRestakeContext(); // Aggregate the delegations based on the operator account ID and asset ID. const delegations = useMemo(() => { @@ -55,7 +55,7 @@ const SelectOperatorModal = ({ titleWhenEmpty="No Delegation Found" descriptionWhenEmpty="Have you deposited or delegated an asset to an operator yet?" onSelect={(item) => { - const asset = assetMetadataMap[item.assetId]; + const asset = vaults[item.assetId]; const decimals = asset?.decimals || DEFAULT_DECIMALS; const fmtAmount = formatUnits(item.amountBonded, decimals); @@ -65,7 +65,7 @@ const SelectOperatorModal = ({ }); }} filterItem={(delegation, query) => { - const metadata = assetMetadataMap[delegation.assetId]; + const metadata = vaults[delegation.assetId]; if (metadata === undefined) { return false; @@ -83,7 +83,7 @@ const SelectOperatorModal = ({ ]); }} renderItem={({ amountBonded, assetId, operatorAccountId }) => { - const asset = assetMetadataMap[assetId]; + const asset = vaults[assetId]; if (asset === undefined) { return null; diff --git a/apps/tangle-dapp/src/containers/restaking/UnstakeRequestTable.tsx b/apps/tangle-dapp/src/containers/restaking/UnstakeRequestTable.tsx index 795caa71e9..9f8476c043 100644 --- a/apps/tangle-dapp/src/containers/restaking/UnstakeRequestTable.tsx +++ b/apps/tangle-dapp/src/containers/restaking/UnstakeRequestTable.tsx @@ -10,15 +10,13 @@ import { import { CheckboxCircleFill } from '@webb-tools/icons/CheckboxCircleFill'; import { TimeFillIcon } from '@webb-tools/icons/TimeFillIcon'; import { useRestakeContext } from '@webb-tools/tangle-shared-ui/context/RestakeContext'; -import type { - RestakeVaultAssetMetadata, - DelegatorUnstakeRequest, -} from '@webb-tools/tangle-shared-ui/types/restake'; +import type { DelegatorUnstakeRequest } from '@webb-tools/tangle-shared-ui/types/restake'; import type { IdentityType } from '@webb-tools/tangle-shared-ui/utils/polkadot/identity'; import { AmountFormatStyle, - EMPTY_VALUE_PLACEHOLDER, formatDisplayAmount, + isEvmAddress, + Typography, } from '@webb-tools/webb-ui-components'; import { CheckBox } from '@webb-tools/webb-ui-components/components/CheckBox'; import { fuzzyFilter } from '@webb-tools/webb-ui-components/components/Filter/utils'; @@ -34,6 +32,9 @@ import pluralize from '@webb-tools/webb-ui-components/utils/pluralize'; import { BN } from '@polkadot/util'; import { SubstrateAddress } from '@webb-tools/webb-ui-components/types/address'; import { RestakeAssetId } from '@webb-tools/tangle-shared-ui/utils/createRestakeAssetId'; +import useSessionDurationMs from '../../data/useSessionDurationMs'; +import { findErc20Token } from '../../data/restake/useTangleEvmErc20Balances'; +import formatSessionDistance from '../../utils/formatSessionDistance'; export type UnstakeRequestTableRow = { amount: string; @@ -41,6 +42,7 @@ export type UnstakeRequestTableRow = { assetId: RestakeAssetId; assetSymbol: string; sessionsRemaining: number; + sessionDurationMs: number; operatorAccountId: SubstrateAddress; operatorIdentityName?: string; }; @@ -83,23 +85,26 @@ const COLUMNS = [ cell: (props) => { const sessionsRemaining = props.getValue(); + if (sessionsRemaining <= 0) { + return ( + + + Ready + + ); + } + + const timeRemaining = formatSessionDistance( + sessionsRemaining, + props.row.original.sessionDurationMs, + ); + return ( - - {sessionsRemaining === 0 ? ( - - - Ready - - ) : sessionsRemaining < 0 ? ( - EMPTY_VALUE_PLACEHOLDER - ) : ( - - - - {`${sessionsRemaining} ${pluralize('session', sessionsRemaining !== 1)}`} - - )} - + + + + {timeRemaining} + ); }, }), @@ -114,23 +119,25 @@ const UnstakeRequestTable: FC = ({ unstakeRequests, operatorIdentities, }) => { - const { assetMetadataMap } = useRestakeContext(); + const { vaults } = useRestakeContext(); const { delegationBondLessDelay } = useRestakeConsts(); const { result: currentRound } = useRestakeCurrentRound(); + const sessionDurationMs = useSessionDurationMs(); const requests = useMemo(() => { // Not yet ready. - if (currentRound === null) { + if (currentRound === null || sessionDurationMs == null) { return []; } return unstakeRequests.flatMap( ({ assetId, amount, requestedRound, operatorAccountId }) => { - const metadata: RestakeVaultAssetMetadata | undefined = - assetMetadataMap[assetId]; + const metadata = isEvmAddress(assetId) + ? findErc20Token(assetId) + : vaults[assetId]; - // Ignore entries without metadata. - if (!metadata) { + // Skip requests that are lacking metadata. + if (metadata === undefined || metadata === null) { return []; } @@ -152,6 +159,7 @@ const UnstakeRequestTable: FC = ({ assetId: assetId, assetSymbol: metadata.symbol, sessionsRemaining, + sessionDurationMs, operatorAccountId, operatorIdentityName: operatorIdentities?.[operatorAccountId]?.name ?? undefined, @@ -159,11 +167,12 @@ const UnstakeRequestTable: FC = ({ }, ); }, [ - assetMetadataMap, currentRound, + sessionDurationMs, + unstakeRequests, + vaults, delegationBondLessDelay, operatorIdentities, - unstakeRequests, ]); const table = useReactTable( diff --git a/apps/tangle-dapp/src/containers/restaking/WithdrawModal.tsx b/apps/tangle-dapp/src/containers/restaking/WithdrawModal.tsx index 55957508fe..d8f8078a39 100644 --- a/apps/tangle-dapp/src/containers/restaking/WithdrawModal.tsx +++ b/apps/tangle-dapp/src/containers/restaking/WithdrawModal.tsx @@ -7,9 +7,16 @@ import { formatUnits } from 'viem'; import ListModal from '@webb-tools/tangle-shared-ui/components/ListModal'; import filterBy from '../../utils/filterBy'; import LogoListItem from '../../components/Lists/LogoListItem'; -import addCommasToNumber from '@webb-tools/webb-ui-components/utils/addCommasToNumber'; import { RestakeAssetId } from '@webb-tools/tangle-shared-ui/utils/createRestakeAssetId'; import assertRestakeAssetId from '@webb-tools/tangle-shared-ui/utils/assertRestakeAssetId'; +import { + AmountFormatStyle, + formatDisplayAmount, + isEvmAddress, + shortenHex, +} from '@webb-tools/webb-ui-components'; +import { findErc20Token } from '../../data/restake/useTangleEvmErc20Balances'; +import { BN } from '@polkadot/util'; type Props = { delegatorInfo: DelegatorInfo | null; @@ -29,7 +36,7 @@ const WithdrawModal = ({ setIsOpen, onItemSelected, }: Props) => { - const { assetMetadataMap } = useRestakeContext(); + const { vaults } = useRestakeContext(); // Aggregate the delegations based on the operator account id and asset id const deposits = useMemo(() => { @@ -56,7 +63,7 @@ const WithdrawModal = ({ titleWhenEmpty="No Assets Found" descriptionWhenEmpty="This account has no assets available to withdraw." onSelect={(deposit) => { - const asset = assetMetadataMap[deposit.assetId]; + const asset = vaults[deposit.assetId]; const decimals = asset?.decimals || DEFAULT_DECIMALS; const fmtAmount = formatUnits(deposit.amount, decimals); @@ -66,31 +73,58 @@ const WithdrawModal = ({ }); }} filterItem={({ assetId }, query) => { - const asset = assetMetadataMap[assetId]; + const asset = vaults[assetId]; - return filterBy(query, [asset?.name, asset?.id, asset?.vaultId]); + return filterBy(query, [asset?.name, asset?.assetId, asset?.vaultId]); }} + // TODO: This can be cleaned up a bit. Seems like a bit of reused code. renderItem={({ amount, assetId }) => { - const metadata = assetMetadataMap[assetId]; + let name: string; + let symbol: string; + let decimals: number; + let vaultId: number | null = null; - if (metadata === undefined) { - return null; + if (isEvmAddress(assetId)) { + const erc20Token = findErc20Token(assetId); + + if (erc20Token === null) { + return null; + } + + name = erc20Token.name; + symbol = erc20Token.symbol; + decimals = erc20Token.decimals; + } else { + const metadata = vaults[assetId]; + + if (metadata === undefined) { + return null; + } + + name = metadata.name; + symbol = metadata.symbol; + decimals = metadata.decimals; + vaultId = metadata.vaultId; } - const fmtAmount = addCommasToNumber( - formatUnits(amount, metadata.decimals), + const fmtAmount = formatDisplayAmount( + new BN(amount.toString()), + decimals, + AmountFormatStyle.SHORT, ); + const idText = isEvmAddress(assetId) + ? `Address: ${shortenHex(assetId)}` + : `Asset ID: ${assetId}`; + return ( } - leftUpperContent={`${metadata.name} (${metadata.symbol})`} - leftBottomContent={`Asset ID: ${assetId}`} - rightUpperText={fmtAmount} + logo={} + leftUpperContent={`${name} (${symbol})`} + leftBottomContent={idText} + rightUpperText={`${fmtAmount} ${symbol}`} rightBottomText={ - metadata.vaultId !== null - ? `Vault ID: ${metadata.vaultId}` - : undefined + vaultId !== null ? `Vault ID: ${vaultId}` : undefined } /> ); diff --git a/apps/tangle-dapp/src/containers/restaking/WithdrawRequestTable.tsx b/apps/tangle-dapp/src/containers/restaking/WithdrawRequestTable.tsx index b2e9b68a1d..8430e79a4d 100644 --- a/apps/tangle-dapp/src/containers/restaking/WithdrawRequestTable.tsx +++ b/apps/tangle-dapp/src/containers/restaking/WithdrawRequestTable.tsx @@ -10,10 +10,7 @@ import { import { CheckboxCircleFill } from '@webb-tools/icons/CheckboxCircleFill'; import { TimeFillIcon } from '@webb-tools/icons/TimeFillIcon'; import { useRestakeContext } from '@webb-tools/tangle-shared-ui/context/RestakeContext'; -import type { - RestakeVaultAssetMetadata, - DelegatorWithdrawRequest, -} from '@webb-tools/tangle-shared-ui/types/restake'; +import type { DelegatorWithdrawRequest } from '@webb-tools/tangle-shared-ui/types/restake'; import { CheckBox } from '@webb-tools/webb-ui-components/components/CheckBox'; import { fuzzyFilter } from '@webb-tools/webb-ui-components/components/Filter/utils'; import { Table } from '@webb-tools/webb-ui-components/components/Table'; @@ -26,12 +23,15 @@ import { calculateTimeRemaining } from '../../pages/restake/utils'; import WithdrawRequestTableActions from './WithdrawRequestTableActions'; import { AmountFormatStyle, - EMPTY_VALUE_PLACEHOLDER, formatDisplayAmount, + isEvmAddress, } from '@webb-tools/webb-ui-components'; import { BN } from '@polkadot/util'; import pluralize from '@webb-tools/webb-ui-components/utils/pluralize'; import { RestakeAssetId } from '@webb-tools/tangle-shared-ui/utils/createRestakeAssetId'; +import { findErc20Token } from '../../data/restake/useTangleEvmErc20Balances'; +import useSessionDurationMs from '../../data/useSessionDurationMs'; +import formatSessionDistance from '../../utils/formatSessionDistance'; export type WithdrawRequestTableRow = { amount: string; @@ -39,6 +39,7 @@ export type WithdrawRequestTableRow = { assetId: RestakeAssetId; assetSymbol: string; sessionsRemaining: number; + sessionDurationMs: number; }; const COLUMN_HELPER = createColumnHelper(); @@ -72,29 +73,35 @@ const COLUMNS = [ ), }), COLUMN_HELPER.accessor('sessionsRemaining', { - header: () => Time Remaining, + header: () => 'Time Remaining', sortingFn: (a, b) => a.original.sessionsRemaining - b.original.sessionsRemaining, cell: (props) => { const sessionsRemaining = props.getValue(); + if (sessionsRemaining <= 0) { + return ( + + + Ready + + ); + } + + const timeRemaining = formatSessionDistance( + sessionsRemaining, + props.row.original.sessionDurationMs, + ); + return ( - - {sessionsRemaining === 0 ? ( - - - Ready - - ) : sessionsRemaining < 0 ? ( - EMPTY_VALUE_PLACEHOLDER - ) : ( - - - - {`${sessionsRemaining} ${pluralize('session', sessionsRemaining !== 1)}`} - - )} - + + + + {timeRemaining} + ); }, }), @@ -105,22 +112,24 @@ type Props = { }; const WithdrawRequestTable: FC = ({ withdrawRequests }) => { - const { assetMetadataMap } = useRestakeContext(); + const { vaults } = useRestakeContext(); const { leaveDelegatorsDelay } = useRestakeConsts(); const { result: currentRound } = useRestakeCurrentRound(); + const sessionDurationMs = useSessionDurationMs(); const requests = useMemo(() => { // Not yet ready. - if (currentRound === null) { + if (currentRound === null || sessionDurationMs === null) { return []; } return withdrawRequests.flatMap(({ assetId, amount, requestedRound }) => { - const metadata: RestakeVaultAssetMetadata | undefined = - assetMetadataMap[assetId]; + const metadata = isEvmAddress(assetId) + ? findErc20Token(assetId) + : vaults[assetId]; - // Ignore requests if the metadata is not available. - if (metadata === undefined) { + // Skip requests that are lacking metadata. + if (metadata === undefined || metadata === null) { return []; } @@ -139,12 +148,19 @@ const WithdrawRequestTable: FC = ({ withdrawRequests }) => { return { amount: fmtAmount, amountRaw: amount, - assetId: assetId, + assetId, assetSymbol: metadata.symbol, sessionsRemaining, + sessionDurationMs, } satisfies WithdrawRequestTableRow; }); - }, [assetMetadataMap, currentRound, leaveDelegatorsDelay, withdrawRequests]); + }, [ + currentRound, + sessionDurationMs, + withdrawRequests, + vaults, + leaveDelegatorsDelay, + ]); const table = useReactTable( useMemo>( diff --git a/apps/tangle-dapp/src/pages/bridge/context/BridgeTxQueueContext/BridgeTxQueueContext.ts b/apps/tangle-dapp/src/context/BridgeTxQueueContext/BridgeTxQueueContext.ts similarity index 100% rename from apps/tangle-dapp/src/pages/bridge/context/BridgeTxQueueContext/BridgeTxQueueContext.ts rename to apps/tangle-dapp/src/context/BridgeTxQueueContext/BridgeTxQueueContext.ts diff --git a/apps/tangle-dapp/src/pages/bridge/context/BridgeTxQueueContext/BridgeTxQueueProvider.tsx b/apps/tangle-dapp/src/context/BridgeTxQueueContext/BridgeTxQueueProvider.tsx similarity index 100% rename from apps/tangle-dapp/src/pages/bridge/context/BridgeTxQueueContext/BridgeTxQueueProvider.tsx rename to apps/tangle-dapp/src/context/BridgeTxQueueContext/BridgeTxQueueProvider.tsx diff --git a/apps/tangle-dapp/src/pages/bridge/context/BridgeTxQueueContext/index.ts b/apps/tangle-dapp/src/context/BridgeTxQueueContext/index.ts similarity index 100% rename from apps/tangle-dapp/src/pages/bridge/context/BridgeTxQueueContext/index.ts rename to apps/tangle-dapp/src/context/BridgeTxQueueContext/index.ts diff --git a/apps/tangle-dapp/src/pages/bridge/context/BridgeTxQueueContext/useBridgeTxQueue.ts b/apps/tangle-dapp/src/context/BridgeTxQueueContext/useBridgeTxQueue.ts similarity index 100% rename from apps/tangle-dapp/src/pages/bridge/context/BridgeTxQueueContext/useBridgeTxQueue.ts rename to apps/tangle-dapp/src/context/BridgeTxQueueContext/useBridgeTxQueue.ts diff --git a/apps/tangle-dapp/src/pages/bridge/context/HyperlaneWarpContext.tsx b/apps/tangle-dapp/src/context/HyperlaneWarpContext.tsx similarity index 92% rename from apps/tangle-dapp/src/pages/bridge/context/HyperlaneWarpContext.tsx rename to apps/tangle-dapp/src/context/HyperlaneWarpContext.tsx index 56d9bf889a..7c40ab793f 100644 --- a/apps/tangle-dapp/src/pages/bridge/context/HyperlaneWarpContext.tsx +++ b/apps/tangle-dapp/src/context/HyperlaneWarpContext.tsx @@ -3,7 +3,7 @@ import { FC, PropsWithChildren, useEffect } from 'react'; import { initHyperlaneWarpContext, removeHyperlaneWarpContext, -} from '../lib/hyperlane/context'; +} from '../pages/bridge/lib/hyperlane/context'; const HyperlaneWarpContext: FC = ({ children }) => { useEffect(() => { diff --git a/apps/tangle-dapp/src/context/RestakeContext/RestakeContext.ts b/apps/tangle-dapp/src/context/RestakeContext/RestakeContext.ts index b873609f55..7b2dd8509c 100644 --- a/apps/tangle-dapp/src/context/RestakeContext/RestakeContext.ts +++ b/apps/tangle-dapp/src/context/RestakeContext/RestakeContext.ts @@ -1,6 +1,6 @@ import { AssetBalanceMap, - RestakeVaultAssetMap, + RestakeVaultMap, } from '@webb-tools/tangle-shared-ui/types/restake'; import { createContext } from 'react'; import { of } from 'rxjs'; @@ -8,7 +8,7 @@ import { RestakeContextType } from './types'; const RestakeContext = createContext({ assetMap: {}, - assetMap$: of({}), + assetMap$: of({}), balances: {}, balances$: of({}), assetWithBalances: [], diff --git a/apps/tangle-dapp/src/context/RestakeContext/RestakeContextProvider.tsx b/apps/tangle-dapp/src/context/RestakeContext/RestakeContextProvider.tsx index 0e6c58ef6b..1fdad9aa4b 100644 --- a/apps/tangle-dapp/src/context/RestakeContext/RestakeContextProvider.tsx +++ b/apps/tangle-dapp/src/context/RestakeContext/RestakeContextProvider.tsx @@ -1,6 +1,6 @@ import { ZERO_BIG_INT } from '@webb-tools/dapp-config/constants'; import isDefined from '@webb-tools/dapp-types/utils/isDefined'; -import useRestakeVaultAssets from '@webb-tools/tangle-shared-ui/data/restake/useRestakeVaultAssets'; +import useRestakeVaults from '@webb-tools/tangle-shared-ui/data/restake/useRestakeVaults'; import useRestakeBalances from '@webb-tools/tangle-shared-ui/data/restake/useRestakeBalances'; import { AssetWithBalance } from '@webb-tools/tangle-shared-ui/types/restake'; import toPairs from 'lodash/toPairs'; @@ -8,17 +8,19 @@ import { useObservableState } from 'observable-hooks'; import { PropsWithChildren, useMemo } from 'react'; import { combineLatest, map } from 'rxjs'; import RestakeContext from './RestakeContext'; +import assertRestakeAssetId from '@webb-tools/tangle-shared-ui/utils/assertRestakeAssetId'; const RestakeContextProvider = (props: PropsWithChildren) => { - const { vaultAssets: assetMap, assetMap$ } = useRestakeVaultAssets(); + const { vaults, vaults$ } = useRestakeVaults(); const { balances, balances$ } = useRestakeBalances(); const assetWithBalances$ = useMemo( () => - combineLatest([assetMap$, balances$]).pipe( + combineLatest([vaults$, balances$]).pipe( map(([assetMap, balances]) => { const combined = toPairs(assetMap).reduce( - (assetWithBalances, [assetId, assetMetadata]) => { + (assetWithBalances, [assetIdString, assetMetadata]) => { + const assetId = assertRestakeAssetId(assetIdString); const balance = balances[assetId] ?? null; return assetWithBalances.concat({ @@ -45,7 +47,7 @@ const RestakeContextProvider = (props: PropsWithChildren) => { ]; }), ), - [assetMap$, balances$], + [vaults$, balances$], ); const assetWithBalances = useObservableState(assetWithBalances$, []); @@ -55,8 +57,8 @@ const RestakeContextProvider = (props: PropsWithChildren) => { value={{ assetWithBalances, assetWithBalances$, - assetMap, - assetMap$, + assetMap: vaults, + assetMap$: vaults$, balances, balances$, }} diff --git a/apps/tangle-dapp/src/context/RestakeContext/types.ts b/apps/tangle-dapp/src/context/RestakeContext/types.ts index 10b21f9aa7..989d2e4cf7 100644 --- a/apps/tangle-dapp/src/context/RestakeContext/types.ts +++ b/apps/tangle-dapp/src/context/RestakeContext/types.ts @@ -1,6 +1,6 @@ import { AssetBalanceMap, - RestakeVaultAssetMap, + RestakeVaultMap, AssetWithBalance, } from '@webb-tools/tangle-shared-ui/types/restake'; import { Observable } from 'rxjs'; @@ -9,12 +9,12 @@ export type RestakeContextType = { /** * The asset map for the current selected chain */ - assetMap: RestakeVaultAssetMap; + assetMap: RestakeVaultMap; /** * An observable of the asset map for the current selected chain */ - assetMap$: Observable; + assetMap$: Observable; /** * The balances of the current active account diff --git a/apps/tangle-dapp/src/pages/bridge/context/useBridgeStore.ts b/apps/tangle-dapp/src/context/useBridgeStore.ts similarity index 93% rename from apps/tangle-dapp/src/pages/bridge/context/useBridgeStore.ts rename to apps/tangle-dapp/src/context/useBridgeStore.ts index 1c7fab7b8d..408a0f6e0b 100644 --- a/apps/tangle-dapp/src/pages/bridge/context/useBridgeStore.ts +++ b/apps/tangle-dapp/src/context/useBridgeStore.ts @@ -7,7 +7,7 @@ import { Decimal } from 'decimal.js'; import { create } from 'zustand'; import { BridgeToken } from '@webb-tools/tangle-shared-ui/types'; -import { BRIDGE_CHAINS } from '../constants'; +import { BRIDGE_CHAINS } from '../constants/bridge'; const sortChainOptions = (chains: ChainConfig[]) => { return chains.sort((a, b) => a.name.localeCompare(b.name)); @@ -26,14 +26,14 @@ const DEFAULT_DESTINATION_CHAINS = sortChainOptions( ); const getDefaultTokens = (): BridgeToken[] => { - const firstSourceChain = chainsConfig[PresetTypedChainId.TangleMainnetEVM]; + const firstSourceChain = chainsConfig[PresetTypedChainId.Arbitrum]; const firstSourceChainId = calculateTypedChainId( firstSourceChain.chainType, firstSourceChain.id, ); - const firstDestChain = chainsConfig[PresetTypedChainId.EthereumMainNet]; + const firstDestChain = chainsConfig[PresetTypedChainId.TangleMainnetEVM]; const firstDestChainId = calculateTypedChainId( firstDestChain.chainType, @@ -91,8 +91,8 @@ const useBridgeStore = create((set) => ({ sourceChains: DEFAULT_SOURCE_CHAINS, destinationChains: DEFAULT_DESTINATION_CHAINS, - selectedSourceChain: chainsConfig[PresetTypedChainId.TangleMainnetEVM], - selectedDestinationChain: chainsConfig[PresetTypedChainId.EthereumMainNet], + selectedSourceChain: chainsConfig[PresetTypedChainId.Arbitrum], + selectedDestinationChain: chainsConfig[PresetTypedChainId.TangleMainnetEVM], setSelectedSourceChain: (chain) => set(() => { diff --git a/apps/tangle-dapp/src/data/bridge/useBridgeEvmBalances.ts b/apps/tangle-dapp/src/data/bridge/useBridgeEvmBalances.ts new file mode 100644 index 0000000000..5dbb16d199 --- /dev/null +++ b/apps/tangle-dapp/src/data/bridge/useBridgeEvmBalances.ts @@ -0,0 +1,110 @@ +import { PresetTypedChainId } from '@webb-tools/dapp-types'; +import { Decimal } from 'decimal.js'; +import { useCallback, useEffect, useState } from 'react'; + +import { + BridgeChainBalances, + BridgeToken, + BridgeTokenWithBalance, +} from '@webb-tools/tangle-shared-ui/types'; +import { BRIDGE_TOKENS } from '../../constants/bridge'; +import ensureError from '@webb-tools/tangle-shared-ui/utils/ensureError'; +import { EvmAddress } from '@webb-tools/webb-ui-components/types/address'; +import useEvmAddress20 from '../../hooks/useEvmAddress'; +import { isSolanaAddress } from '@webb-tools/webb-ui-components'; +import fetchErc20TokenBalance from '../../utils/fetchErc20TokenBalance'; +import useViemPublicClient from '../../hooks/useViemPublicClient'; + +export const useBridgeEvmBalances = ( + sourceChainId: number, + destinationChainId: number, +) => { + const accountEvmAddress = useEvmAddress20(); + + const [balances, setBalances] = useState({}); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const viemPublicClient = useViemPublicClient(); + + const fetchTokenBalance = useCallback( + async ( + token: BridgeToken, + chainId: PresetTypedChainId, + address: EvmAddress, + ): Promise => { + if (viemPublicClient === null || isSolanaAddress(token.address)) { + // TODO: Not all tokens are ERC20, ex. Solana. Handle the edge cases. For now, just return a balance of 0. + return { ...token, balance: new Decimal(0) }; + } + + const balance = await fetchErc20TokenBalance( + viemPublicClient, + address, + token.address, + token.abi, + token.decimals, + ); + + let syntheticBalance: Decimal | undefined; + + if (token.hyperlaneSyntheticAddress) { + syntheticBalance = await fetchErc20TokenBalance( + viemPublicClient, + address, + token.hyperlaneSyntheticAddress, + token.abi, + token.decimals, + ); + } + + return { ...token, balance, syntheticBalance }; + }, + [viemPublicClient], + ); + + const fetchAllBalances = useCallback(async () => { + if (accountEvmAddress === null || !sourceChainId) { + return; + } + + setIsLoading(true); + setError(null); + + try { + const newBalances: Record = + {}; + + let tokens = BRIDGE_TOKENS[sourceChainId]; + + if (!tokens || tokens.length === 0) { + tokens = BRIDGE_TOKENS[destinationChainId]; + } + + const tokenBalances = await Promise.all( + tokens.map((token) => + fetchTokenBalance(token, sourceChainId, accountEvmAddress), + ), + ); + + newBalances[sourceChainId] = tokenBalances; + + setBalances(newBalances); + } catch (possibleError) { + const error = ensureError(possibleError); + setError(`Failed to fetch token balances: ${error.message}`); + } finally { + setIsLoading(false); + } + }, [accountEvmAddress, destinationChainId, fetchTokenBalance, sourceChainId]); + + useEffect(() => { + fetchAllBalances(); + }, [fetchAllBalances]); + + return { + balances, + isLoading, + error, + refresh: fetchAllBalances, + }; +}; diff --git a/apps/tangle-dapp/src/pages/bridge/hooks/useBridgeRouterQuote.ts b/apps/tangle-dapp/src/data/bridge/useBridgeRouterQuote.ts similarity index 96% rename from apps/tangle-dapp/src/pages/bridge/hooks/useBridgeRouterQuote.ts rename to apps/tangle-dapp/src/data/bridge/useBridgeRouterQuote.ts index 2b04b2b32e..0d73c4a2f0 100644 --- a/apps/tangle-dapp/src/pages/bridge/hooks/useBridgeRouterQuote.ts +++ b/apps/tangle-dapp/src/data/bridge/useBridgeRouterQuote.ts @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; -import { ROUTER_PARTNER_ID, ROUTER_QUOTE_URL } from '../constants'; +import { ROUTER_PARTNER_ID, ROUTER_QUOTE_URL } from '../../constants/bridge'; import { EvmAddress } from '@webb-tools/webb-ui-components/types/address'; export type RouterQuoteParams = { diff --git a/apps/tangle-dapp/src/pages/bridge/hooks/useEtheresSigner.ts b/apps/tangle-dapp/src/data/bridge/useEthersSigner.ts similarity index 72% rename from apps/tangle-dapp/src/pages/bridge/hooks/useEtheresSigner.ts rename to apps/tangle-dapp/src/data/bridge/useEthersSigner.ts index 1c84268689..b6826a530d 100644 --- a/apps/tangle-dapp/src/pages/bridge/hooks/useEtheresSigner.ts +++ b/apps/tangle-dapp/src/data/bridge/useEthersSigner.ts @@ -2,17 +2,23 @@ import { useWebContext } from '@webb-tools/api-provider-environment'; import { WebbWeb3Provider } from '@webb-tools/web3-api-provider'; import { useMemo } from 'react'; -import viemConnectorClientToEthersSigner from '../../../utils/viemConnectorClientToEthersSigner'; +import viemConnectorClientToEthersSigner from '../../utils/viemConnectorClientToEthersSigner'; -export default function useEthersSigner() { +const useEthersSigner = () => { const { activeApi } = useWebContext(); const ethersSigner = useMemo(() => { - if (!activeApi || !(activeApi instanceof WebbWeb3Provider)) return null; + if (!activeApi || !(activeApi instanceof WebbWeb3Provider)) { + return null; + } + const walletClient = activeApi.walletClient; const ethersSigner = viemConnectorClientToEthersSigner(walletClient); + return ethersSigner; }, [activeApi]); return ethersSigner; -} +}; + +export default useEthersSigner; diff --git a/apps/tangle-dapp/src/pages/bridge/hooks/useHyperlaneQuote.ts b/apps/tangle-dapp/src/data/bridge/useHyperlaneQuote.ts similarity index 85% rename from apps/tangle-dapp/src/pages/bridge/hooks/useHyperlaneQuote.ts rename to apps/tangle-dapp/src/data/bridge/useHyperlaneQuote.ts index 970583497b..92e77a31c0 100644 --- a/apps/tangle-dapp/src/pages/bridge/hooks/useHyperlaneQuote.ts +++ b/apps/tangle-dapp/src/data/bridge/useHyperlaneQuote.ts @@ -1,8 +1,12 @@ import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; -import { getHyperlaneWarpCore } from '../lib/hyperlane/context'; +import { getHyperlaneWarpCore } from '../../pages/bridge/lib/hyperlane/context'; import { BridgeToken } from '@webb-tools/tangle-shared-ui/types'; -import { getHyperlaneChainName, tryFindToken } from '../lib/hyperlane/utils'; +import { + getHyperlaneChainName, + tryFindToken, +} from '../../pages/bridge/lib/hyperlane/utils'; +import { PresetTypedChainId } from '@webb-tools/dapp-types'; export type HyperlaneQuoteProps = { token: BridgeToken; @@ -29,13 +33,19 @@ export const getHyperlaneQuote = async (props: HyperlaneQuoteProps | null) => { } = props; const warpCore = getHyperlaneWarpCore(); + if (!warpCore) { throw new Error('Hyperlane warp core not found'); } + const tokenToBridge = + sourceTypedChainId === PresetTypedChainId.TangleMainnetEVM + ? token.hyperlaneSyntheticAddress + : token.address; + const hyperlaneToken = tryFindToken( getHyperlaneChainName(sourceTypedChainId), - token.hyperlaneRouteContractAddress, + tokenToBridge, ); if (!hyperlaneToken) { throw new Error('Hyperlane token not found'); diff --git a/apps/tangle-dapp/src/pages/bridge/hooks/useHyperlaneTransfer.ts b/apps/tangle-dapp/src/data/bridge/useHyperlaneTransfer.ts similarity index 84% rename from apps/tangle-dapp/src/pages/bridge/hooks/useHyperlaneTransfer.ts rename to apps/tangle-dapp/src/data/bridge/useHyperlaneTransfer.ts index 5f612fdff3..97a2a6353f 100644 --- a/apps/tangle-dapp/src/pages/bridge/hooks/useHyperlaneTransfer.ts +++ b/apps/tangle-dapp/src/data/bridge/useHyperlaneTransfer.ts @@ -4,9 +4,13 @@ import { useMutation } from '@tanstack/react-query'; import { providers, utils } from 'ethers'; import { BridgeToken } from '@webb-tools/tangle-shared-ui/types'; -import { getHyperlaneWarpCore } from '../lib/hyperlane/context'; -import { getHyperlaneChainName, tryFindToken } from '../lib/hyperlane/utils'; -import useEthersSigner from './useEtheresSigner'; +import { getHyperlaneWarpCore } from '../../pages/bridge/lib/hyperlane/context'; +import { + getHyperlaneChainName, + tryFindToken, +} from '../../pages/bridge/lib/hyperlane/utils'; +import useEthersSigner from './useEthersSigner'; +import { PresetTypedChainId } from '@webb-tools/dapp-types'; export type HyperlaneTransferProps = { token: BridgeToken; @@ -37,9 +41,14 @@ export const transferByHyperlane = async ({ throw new Error('Hyperlane warp core not found'); } + const tokenToBridge = + sourceTypedChainId === PresetTypedChainId.TangleMainnetEVM + ? token.hyperlaneSyntheticAddress + : token.address; + const hyperlaneToken = tryFindToken( getHyperlaneChainName(sourceTypedChainId), - token.hyperlaneRouteContractAddress, + tokenToBridge, ); if (!hyperlaneToken) { throw new Error('Hyperlane token not found'); @@ -86,7 +95,7 @@ export const transferByHyperlane = async ({ tx.transaction as providers.TransactionRequest, ); const receipt = await transaction.wait(); - console.log('✅ Transaction receipt:', receipt); + console.log('Transaction receipt:', receipt); receipts.push(receipt); } diff --git a/apps/tangle-dapp/src/pages/bridge/hooks/useRouterTransfer.ts b/apps/tangle-dapp/src/data/bridge/useRouterTransfer.ts similarity index 95% rename from apps/tangle-dapp/src/pages/bridge/hooks/useRouterTransfer.ts rename to apps/tangle-dapp/src/data/bridge/useRouterTransfer.ts index db375fd616..ebcf52d22f 100644 --- a/apps/tangle-dapp/src/pages/bridge/hooks/useRouterTransfer.ts +++ b/apps/tangle-dapp/src/data/bridge/useRouterTransfer.ts @@ -3,8 +3,8 @@ import { JsonRpcSigner } from '@ethersproject/providers'; import { useMutation } from '@tanstack/react-query'; import axios from 'axios'; -import { ROUTER_TRANSACTION_URL } from '../constants'; -import useEthersSigner from './useEtheresSigner'; +import { ROUTER_TRANSACTION_URL } from '../../constants/bridge'; +import useEthersSigner from './useEthersSigner'; import { isEvmAddress } from '@webb-tools/webb-ui-components'; interface RouterTransactionResponse { diff --git a/apps/tangle-dapp/src/data/restake/RestakeEvmApi.ts b/apps/tangle-dapp/src/data/restake/RestakeEvmApi.ts index 11f47d26b7..129fb3f42a 100644 --- a/apps/tangle-dapp/src/data/restake/RestakeEvmApi.ts +++ b/apps/tangle-dapp/src/data/restake/RestakeEvmApi.ts @@ -57,7 +57,9 @@ class RestakeEvmApi extends RestakeApiBase { ) { try { const connector = (() => { - if (this.provider.state.current === null) return; + if (this.provider.state.current === null) { + return; + } return this.provider.state.connections.get(this.provider.state.current) ?.connector; @@ -94,10 +96,12 @@ class RestakeEvmApi extends RestakeApiBase { return hash; } else { // TODO: Provide more context on what went wrong. - return new Error('EVM operation failed'); + const error = new Error('EVM operation failed'); + + this.onFailure(txName, error); } } catch (possibleError) { - return ensureError(possibleError); + this.onFailure(txName, ensureError(possibleError)); } } diff --git a/apps/tangle-dapp/src/data/restake/useRestakeAsset.ts b/apps/tangle-dapp/src/data/restake/useRestakeAsset.ts new file mode 100644 index 0000000000..11b89d000d --- /dev/null +++ b/apps/tangle-dapp/src/data/restake/useRestakeAsset.ts @@ -0,0 +1,58 @@ +import { useRestakeContext } from '@webb-tools/tangle-shared-ui/context/RestakeContext'; +import { RestakeAsset } from '@webb-tools/tangle-shared-ui/types/restake'; +import { isEvmAddress } from '@webb-tools/webb-ui-components'; +import { useMemo } from 'react'; +import useTangleEvmErc20Balances from './useTangleEvmErc20Balances'; +import { RestakeAssetId } from '@webb-tools/tangle-shared-ui/utils/createRestakeAssetId'; +import { BN, BN_ZERO } from '@polkadot/util'; + +const useRestakeAsset = (id: RestakeAssetId | null | undefined) => { + const { vaults, balances } = useRestakeContext(); + const erc20Balances = useTangleEvmErc20Balances(); + + const asset = useMemo(() => { + if (id === null || id === undefined) { + return null; + } else if (isEvmAddress(id)) { + if (erc20Balances === null) { + return null; + } + + const erc20Asset = erc20Balances.find( + (asset) => asset.contractAddress === id, + ); + + if (erc20Asset === undefined) { + return null; + } + + return { + ...erc20Asset, + id, + } satisfies RestakeAsset; + } + + const metadata = vaults[id]; + + if (metadata === undefined) { + return null; + } + + const balanceBigInt = balances[id]?.balance; + + const balanceBn = + balanceBigInt !== undefined ? new BN(balanceBigInt.toString()) : BN_ZERO; + + return { + id, + name: metadata.name, + symbol: metadata.symbol, + decimals: metadata.decimals, + balance: balanceBn, + } satisfies RestakeAsset; + }, [vaults, balances, erc20Balances, id]); + + return asset; +}; + +export default useRestakeAsset; diff --git a/apps/tangle-dapp/src/data/restake/useTangleEvmErc20Balances.ts b/apps/tangle-dapp/src/data/restake/useTangleEvmErc20Balances.ts new file mode 100644 index 0000000000..0a810bc83b --- /dev/null +++ b/apps/tangle-dapp/src/data/restake/useTangleEvmErc20Balances.ts @@ -0,0 +1,114 @@ +import { BN } from '@polkadot/util'; +import { assertEvmAddress } from '@webb-tools/webb-ui-components'; +import { EvmAddress } from '@webb-tools/webb-ui-components/types/address'; +import fetchErc20TokenBalance from '../../utils/fetchErc20TokenBalance'; +import useAgnosticAccountInfo from '../../hooks/useAgnosticAccountInfo'; +import { useCallback, useEffect, useState } from 'react'; +import ERC20_ABI from '../../abi/erc20'; +import { Decimal } from 'decimal.js'; +import useViemPublicClient from '../../hooks/useViemPublicClient'; + +type Erc20Token = { + contractAddress: EvmAddress; + name: string; + symbol: string; + decimals: number; +}; + +export type Erc20Balance = Erc20Token & { + balance: BN; +}; + +// TODO: Query from EVM instead of being hard-coded. Waiting for bridge to be implemented in order to do that. +export const ERC20_TEST_TOKENS: Erc20Token[] = [ + { + name: "Yuri's Local ERC-2 Dummy", + symbol: 'USDC', + decimals: 18, + contractAddress: assertEvmAddress( + '0x2af9b184d0d42cd8d3c4fd0c953a06b6838c9357', + ), + }, + { + name: 'Testnet ERC-20 Dummy', + symbol: 'USDC', + decimals: 18, + contractAddress: assertEvmAddress( + '0x9794e2f4edc455d1c31ad795d830c58e4c022475', + ), + }, +]; + +export const findErc20Token = (id: EvmAddress): Erc20Token | null => { + return ( + ERC20_TEST_TOKENS.find((token) => token.contractAddress === id) ?? null + ); +}; + +const useTangleEvmErc20Balances = (): Erc20Balance[] | null => { + const { evmAddress } = useAgnosticAccountInfo(); + const [balances, setBalances] = useState(null); + const viemPublicClient = useViemPublicClient(); + + const fetchBalance = useCallback( + async ( + evmAddress: EvmAddress, + token: Erc20Token, + ): Promise => { + if (viemPublicClient === null) { + return null; + } + + return fetchErc20TokenBalance( + viemPublicClient, + evmAddress, + token.contractAddress, + ERC20_ABI, + token.decimals, + ); + }, + [viemPublicClient], + ); + + // Fetch balances on mount and whenever the active account changes. + useEffect(() => { + // EVM account not connected. + if (evmAddress === null) { + setBalances(null); + + return; + } + + (async () => { + const newBalances: Erc20Balance[] = []; + + for (const asset of ERC20_TEST_TOKENS) { + const balance = await fetchBalance(evmAddress, asset); + + if (balance === null) { + continue; + } + + const scaledBalance = new BN(balance.toString()).mul( + new BN(10).pow(new BN(asset.decimals)), + ); + + // Ignore assets that have a zero balance. + if (scaledBalance.isZero()) { + continue; + } + + newBalances.push({ + ...asset, + balance: scaledBalance, + } satisfies Erc20Balance); + } + + setBalances(newBalances); + })(); + }, [evmAddress, fetchBalance]); + + return balances; +}; + +export default useTangleEvmErc20Balances; diff --git a/apps/tangle-dapp/src/data/useSessionDurationMs.ts b/apps/tangle-dapp/src/data/useSessionDurationMs.ts new file mode 100644 index 0000000000..75b49e85f1 --- /dev/null +++ b/apps/tangle-dapp/src/data/useSessionDurationMs.ts @@ -0,0 +1,25 @@ +import { useCallback, useMemo } from 'react'; +import useApi from '../hooks/useApi'; +import { BN } from '@polkadot/util'; + +const useSessionDurationMs = () => { + const { result: epochDuration } = useApi( + useCallback((api) => api.consts.babe.epochDuration, []), + ); + + const { result: babeExpectedBlockTime } = useApi( + useCallback((api) => api.consts.babe.expectedBlockTime, []), + ); + + const sessionTimeMs = useMemo(() => { + if (epochDuration == null || babeExpectedBlockTime === null) { + return null; + } + + return new BN(epochDuration).mul(new BN(babeExpectedBlockTime)).toNumber(); + }, [babeExpectedBlockTime, epochDuration]); + + return sessionTimeMs; +}; + +export default useSessionDurationMs; diff --git a/apps/tangle-dapp/src/pages/account.tsx b/apps/tangle-dapp/src/pages/account.tsx index 8c7ff41d24..584b25b9f4 100644 --- a/apps/tangle-dapp/src/pages/account.tsx +++ b/apps/tangle-dapp/src/pages/account.tsx @@ -2,7 +2,7 @@ import { Typography } from '@webb-tools/webb-ui-components/typography/Typography import { FC } from 'react'; import AccountSummaryCard from '../components/account/AccountSummaryCard'; -import AssetsAndBalancesTable from '../containers/AssetsAndBalancesTable'; +import VaultsAndBalancesTable from '../containers/VaultsAndBalancesTable'; import PointsReminder from '../components/account/PointsReminder'; const AccountPage: FC = () => { @@ -18,7 +18,7 @@ const AccountPage: FC = () => { Restake Assets - +
    ); }; diff --git a/apps/tangle-dapp/src/pages/bridge/containers/BridgeContainer.tsx b/apps/tangle-dapp/src/pages/bridge/BridgeContainer.tsx similarity index 86% rename from apps/tangle-dapp/src/pages/bridge/containers/BridgeContainer.tsx rename to apps/tangle-dapp/src/pages/bridge/BridgeContainer.tsx index 8be8654aa9..9848196b13 100644 --- a/apps/tangle-dapp/src/pages/bridge/containers/BridgeContainer.tsx +++ b/apps/tangle-dapp/src/pages/bridge/BridgeContainer.tsx @@ -7,7 +7,10 @@ import { makeExplorerUrl } from '@webb-tools/api-provider-environment/transactio import { useWebContext } from '@webb-tools/api-provider-environment/webb-context'; import chainsPopulated from '@webb-tools/dapp-config/chains/chainsPopulated'; import { PresetTypedChainId } from '@webb-tools/dapp-types'; -import { EVMTokenBridgeEnum } from '@webb-tools/evm-contract-metadata'; +import { + EVMTokenBridgeEnum, + EVMTokenEnum, +} from '@webb-tools/evm-contract-metadata'; import { calculateTypedChainId } from '@webb-tools/dapp-types/TypedChainId'; import useNetworkStore from '@webb-tools/tangle-shared-ui/context/useNetworkStore'; import { @@ -26,30 +29,30 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { twMerge } from 'tailwind-merge'; import { formatEther } from 'viem'; -import AddressInput from '../../../components/AddressInput'; -import AmountInput from '../../../components/AmountInput'; -import { BridgeConfirmationModal } from '../components/BridgeConfirmationModal'; -import { FeeDetail, FeeDetailProps } from '../components/FeeDetail'; -import { AssetConfig, AssetList } from '../../../components/Lists/AssetList'; -import { ChainList } from '../../../components/Lists/ChainList'; -import { ROUTER_NATIVE_TOKEN_ADDRESS } from '../constants'; -import useBridgeStore from '../context/useBridgeStore'; -import useBalances from '../../../data/balances/useBalances'; -import { useBridgeEvmBalances } from '../hooks/useBridgeEvmBalances'; +import AddressInput from '../../components/AddressInput'; +import AmountInput from '../../components/AmountInput'; +import { BridgeConfirmationModal } from '../../components/bridge/BridgeConfirmationModal'; +import { FeeDetail, FeeDetailProps } from '../../components/bridge/FeeDetail'; +import { AssetConfig, AssetList } from '../../components/Lists/AssetList'; +import { ChainList } from '../../components/Lists/ChainList'; +import { ROUTER_NATIVE_TOKEN_ADDRESS } from '../../constants/bridge'; +import useBridgeStore from '../../context/useBridgeStore'; +import useBalances from '../../data/balances/useBalances'; +import { useBridgeEvmBalances } from '../../data/bridge/useBridgeEvmBalances'; import { BridgeTokenWithBalance } from '@webb-tools/tangle-shared-ui/types'; import useBridgeRouterQuote, { RouterQuoteParams, -} from '../hooks/useBridgeRouterQuote'; -import convertDecimalToBn from '../../../utils/convertDecimalToBn'; -import formatTangleBalance from '../../../utils/formatTangleBalance'; +} from '../../data/bridge/useBridgeRouterQuote'; +import convertDecimalToBn from '../../utils/convertDecimalToBn'; +import formatTangleBalance from '../../utils/formatTangleBalance'; import { HyperlaneQuoteProps, useHyperlaneQuote, -} from '../hooks/useHyperlaneQuote'; -import { RouterTransferProps } from '../hooks/useRouterTransfer'; -import ErrorMessage from '../../../components/ErrorMessage'; +} from '../../data/bridge/useHyperlaneQuote'; +import { RouterTransferProps } from '../../data/bridge/useRouterTransfer'; +import ErrorMessage from '../../components/ErrorMessage'; import { WalletFillIcon } from '@webb-tools/icons'; -import { AddressType } from '../../../constants'; +import { AddressType } from '../../constants'; interface BridgeContainerProps { className?: string; @@ -70,7 +73,6 @@ export default function BridgeContainer({ className }: BridgeContainerProps) { : ''; }, [balance, nativeTokenSymbol]); - const { balances, refresh: refreshEvmBalances } = useBridgeEvmBalances(); const sourceChains = useBridgeStore((state) => state.sourceChains); const destinationChains = useBridgeStore((state) => state.destinationChains); @@ -90,12 +92,31 @@ export default function BridgeContainer({ className }: BridgeContainerProps) { (state) => state.setSelectedDestinationChain, ); + const sourceTypedChainId = useMemo(() => { + return calculateTypedChainId( + selectedSourceChain.chainType, + selectedSourceChain.id, + ); + }, [selectedSourceChain]); + + const destinationTypedChainId = useMemo(() => { + return calculateTypedChainId( + selectedDestinationChain.chainType, + selectedDestinationChain.id, + ); + }, [selectedDestinationChain]); + const tokens = useBridgeStore((state) => state.tokens); const selectedToken = useBridgeStore((state) => state.selectedToken); const setSelectedToken = useBridgeStore((state) => state.setSelectedToken); const amount = useBridgeStore((state) => state.amount); const setAmount = useBridgeStore((state) => state.setAmount); + const { balances, refresh: refreshEvmBalances } = useBridgeEvmBalances( + sourceTypedChainId, + destinationTypedChainId, + ); + const isAmountInputError = useBridgeStore( (state) => state.isAmountInputError, ); @@ -161,20 +182,6 @@ export default function BridgeContainer({ className }: BridgeContainerProps) { string | null >(null); - const sourceTypedChainId = useMemo(() => { - return calculateTypedChainId( - selectedSourceChain.chainType, - selectedSourceChain.id, - ); - }, [selectedSourceChain]); - - const destinationTypedChainId = useMemo(() => { - return calculateTypedChainId( - selectedDestinationChain.chainType, - selectedDestinationChain.id, - ); - }, [selectedDestinationChain]); - const isSolanaDestination = selectedDestinationChain.name === 'Solana'; const routerQuoteParams: RouterQuoteParams | null = useMemo(() => { @@ -291,6 +298,8 @@ export default function BridgeContainer({ className }: BridgeContainerProps) { }, estimatedTime: estimatedTime, bridgeFeeTokenType: routerQuote.bridgeFee.symbol, + sendingAmount: new Decimal(sendingAmount), + receivingAmount: new Decimal(receivingAmount), }; }, [ amount, @@ -305,12 +314,11 @@ export default function BridgeContainer({ className }: BridgeContainerProps) { return null; } - const sendingAmount = parseFloat( - formatEther(BigInt(amount?.toString() ?? '0')), - ); + const sendingAmount = new Decimal(amount?.toString() ?? '0') + .div(new Decimal(10).pow(selectedToken.decimals)) + .toString(); - const formattedSendingAmount = - sendingAmount.toString() + ' ' + selectedToken.tokenSymbol; + const formattedSendingAmount = `${sendingAmount} ${selectedToken.tokenSymbol}`; const formattedGasFee = formatEther(hyperlaneQuote.fees.local.amount) + @@ -336,6 +344,8 @@ export default function BridgeContainer({ className }: BridgeContainerProps) { estimatedTime: '', bridgeFeeTokenType: hyperlaneQuote.fees.local.symbol, gasFeeTokenType: hyperlaneQuote.fees.interchain.symbol, + sendingAmount: new Decimal(sendingAmount), + receivingAmount: new Decimal(sendingAmount), }; }, [ amount, @@ -360,6 +370,7 @@ export default function BridgeContainer({ className }: BridgeContainerProps) { const onSwitchChains = useCallback(() => { setSelectedSourceChain(selectedDestinationChain); setSelectedDestinationChain(selectedSourceChain); + refreshEvmBalances(); clearBridgeStore(); }, [ clearBridgeStore, @@ -367,17 +378,24 @@ export default function BridgeContainer({ className }: BridgeContainerProps) { selectedSourceChain, setSelectedDestinationChain, setSelectedSourceChain, + refreshEvmBalances, ]); const assets: AssetConfig[] = useMemo(() => { const tokenConfigs = tokens.map((token) => { const balance = - sourceTypedChainId === PresetTypedChainId.TangleMainnetEVM + sourceTypedChainId === PresetTypedChainId.TangleMainnetEVM && + token.tokenType === EVMTokenEnum.TNT ? fmtAccountBalance - : balances?.[sourceTypedChainId]?.find( - (tokenBalance: BridgeTokenWithBalance) => - tokenBalance.address === token.address, - )?.balance; + : sourceTypedChainId === PresetTypedChainId.TangleMainnetEVM + ? balances?.[sourceTypedChainId]?.find( + (tokenBalance: BridgeTokenWithBalance) => + tokenBalance.address === token.address, + )?.syntheticBalance + : balances?.[sourceTypedChainId]?.find( + (tokenBalance: BridgeTokenWithBalance) => + tokenBalance.address === token.address, + )?.balance; const selectedChainExplorerUrl = selectedSourceChain.blockExplorers?.default; @@ -386,7 +404,9 @@ export default function BridgeContainer({ className }: BridgeContainerProps) { selectedChainExplorerUrl?.url && makeExplorerUrl( selectedChainExplorerUrl.url, - token.address, + (sourceTypedChainId === PresetTypedChainId.TangleMainnetEVM + ? token.hyperlaneSyntheticAddress + : token.address) ?? '', 'address', 'web3', ); @@ -396,12 +416,13 @@ export default function BridgeContainer({ className }: BridgeContainerProps) { optionalSymbol: token.tokenSymbol, balance: activeAccount && balance - ? parseFloat(balance.toString()).toFixed(3) + ? parseFloat(balance.toString()).toFixed(6) : '', explorerUrl: - sourceTypedChainId !== PresetTypedChainId.TangleMainnetEVM - ? tokenExplorerUrl - : undefined, + sourceTypedChainId === PresetTypedChainId.TangleMainnetEVM && + token.tokenType === EVMTokenEnum.TNT + ? undefined + : tokenExplorerUrl, address: token.address as `0x${string}`, assetBridgeType: sourceTypedChainId === PresetTypedChainId.TangleMainnetEVM @@ -473,11 +494,7 @@ export default function BridgeContainer({ className }: BridgeContainerProps) { const isActionBtnLoading = isRouterQuoteLoading || isHyperlaneQuoteLoading; - const actionBtnLoadingText = isRouterQuoteLoading - ? 'Fetching Router quote' - : isHyperlaneQuoteLoading - ? 'Fetching Hyperlane bridge fee' - : ''; + const actionBtnLoadingText = isActionBtnLoading ? 'Preview Transaction' : ''; const actionButtonText = useMemo(() => { if (!activeAccount || !activeWallet || !activeChain) { @@ -496,7 +513,7 @@ export default function BridgeContainer({ className }: BridgeContainerProps) { return 'Confirm Bridge'; } - return 'Fetch Quote & Fees'; + return 'Preview Transaction'; }, [ activeAccount, activeWallet, @@ -596,9 +613,20 @@ export default function BridgeContainer({ className }: BridgeContainerProps) { setIsAmountInputError, ]); + const recipientExplorerUrl = useMemo(() => { + return activeAccount?.address + ? makeExplorerUrl( + selectedSourceChain.blockExplorers?.default.url ?? '', + activeAccount.address, + 'address', + 'web3', + ) + : undefined; + }, [activeAccount, selectedSourceChain.blockExplorers?.default.url]); + useEffect(() => { // Re-fetch every 30 seconds. - const interval = 30 * 1000; + const interval = 10 * 1000; const intervalId = setInterval(() => { refreshEvmBalances(); @@ -723,7 +751,7 @@ export default function BridgeContainer({ className }: BridgeContainerProps) { > Balance:{' '} {selectedTokenBalanceOnSourceChain !== null - ? `${Number(selectedTokenBalanceOnSourceChain).toFixed(3)} ${selectedToken.tokenType}` + ? `${Number(selectedTokenBalanceOnSourceChain).toFixed(6)} ${selectedToken.tokenType}` : EMPTY_VALUE_PLACEHOLDER} )} @@ -766,6 +794,9 @@ export default function BridgeContainer({ className }: BridgeContainerProps) { estimatedTime={routerFeeDetails.estimatedTime} amounts={routerFeeDetails.amounts} bridgeFeeTokenType={routerFeeDetails.bridgeFeeTokenType} + sendingAmount={routerFeeDetails.sendingAmount} + receivingAmount={routerFeeDetails.receivingAmount} + recipientExplorerUrl={recipientExplorerUrl} /> )} @@ -778,6 +809,9 @@ export default function BridgeContainer({ className }: BridgeContainerProps) { amounts={hyperlaneFeeDetails.amounts} bridgeFeeTokenType={hyperlaneFeeDetails.bridgeFeeTokenType} gasFeeTokenType={hyperlaneFeeDetails.gasFeeTokenType} + sendingAmount={hyperlaneFeeDetails.sendingAmount} + receivingAmount={hyperlaneFeeDetails.receivingAmount} + recipientExplorerUrl={recipientExplorerUrl} /> )} @@ -858,6 +892,8 @@ export default function BridgeContainer({ className }: BridgeContainerProps) { activeAccountAddress={activeAccount?.address ?? ''} destinationAddress={destinationAddress ?? ''} routerTransferData={routerTransferData} + sendingAmount={hyperlaneFeeDetails?.sendingAmount ?? null} + receivingAmount={hyperlaneFeeDetails?.receivingAmount ?? null} /> ); diff --git a/apps/tangle-dapp/src/pages/bridge/constants.ts b/apps/tangle-dapp/src/pages/bridge/constants.ts deleted file mode 100644 index f0ce51014c..0000000000 --- a/apps/tangle-dapp/src/pages/bridge/constants.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { - ChainMap, - ChainMetadata, - ExplorerFamily, - WarpCoreConfig, -} from '@hyperlane-xyz/sdk'; -import { ProtocolType } from '@hyperlane-xyz/utils'; -import { PresetTypedChainId } from '@webb-tools/dapp-types'; -import { - EVMTokenBridgeEnum, - EVMTokenEnum, - EVMTokens, - HyperlaneWarpRouteConfig, -} from '@webb-tools/evm-contract-metadata'; - -import { - BridgeChainsConfigType, - BridgeToken, -} from '@webb-tools/tangle-shared-ui/types'; -import { - assertAddressBy, - assertEvmAddress, - isSolanaAddress, -} from '@webb-tools/webb-ui-components'; -import { Abi } from 'viem'; - -// TODO: Include assertion logic, as the Abi type can't be directly imported from viem since the 'type' field clashes (string vs. 'function'). -const assertAbi = (abi: unknown): Abi => abi as Abi; - -export const BRIDGE_TOKENS: Record = { - [PresetTypedChainId.EthereumMainNet]: [ - { - tokenSymbol: 'routerTNT', - tokenType: EVMTokenEnum.TNT, - bridgeType: EVMTokenBridgeEnum.Router, - address: assertEvmAddress(EVMTokens.ethereum.router.TNT.address), - abi: assertAbi(EVMTokens.ethereum.router.TNT.abi), - decimals: EVMTokens.ethereum.router.TNT.decimals, - chainId: PresetTypedChainId.EthereumMainNet, - }, - ], - [PresetTypedChainId.Polygon]: [ - { - tokenSymbol: 'routerTNT', - tokenType: EVMTokenEnum.TNT, - bridgeType: EVMTokenBridgeEnum.Router, - address: assertEvmAddress(EVMTokens.polygon.router.TNT.address), - abi: assertAbi(EVMTokens.polygon.router.TNT.abi), - decimals: EVMTokens.polygon.router.TNT.decimals, - chainId: PresetTypedChainId.Polygon, - }, - ], - [PresetTypedChainId.Arbitrum]: [ - { - tokenSymbol: 'routerTNT', - tokenType: EVMTokenEnum.TNT, - bridgeType: EVMTokenBridgeEnum.Router, - address: assertEvmAddress(EVMTokens.arbitrum.router.TNT.address), - abi: assertAbi(EVMTokens.arbitrum.router.TNT.abi), - decimals: EVMTokens.arbitrum.router.TNT.decimals, - chainId: PresetTypedChainId.Arbitrum, - }, - ], - [PresetTypedChainId.Optimism]: [ - { - tokenSymbol: 'routerTNT', - tokenType: EVMTokenEnum.TNT, - bridgeType: EVMTokenBridgeEnum.Router, - address: assertEvmAddress(EVMTokens.optimism.router.TNT.address), - abi: assertAbi(EVMTokens.optimism.router.TNT.abi), - decimals: EVMTokens.optimism.router.TNT.decimals, - chainId: PresetTypedChainId.Optimism, - }, - ], - [PresetTypedChainId.Linea]: [ - { - tokenSymbol: 'routerTNT', - tokenType: EVMTokenEnum.TNT, - bridgeType: EVMTokenBridgeEnum.Router, - address: assertEvmAddress(EVMTokens.linea.router.TNT.address), - abi: assertAbi(EVMTokens.linea.router.TNT.abi), - decimals: EVMTokens.linea.router.TNT.decimals, - chainId: PresetTypedChainId.Linea, - }, - ], - [PresetTypedChainId.Base]: [ - { - tokenSymbol: 'routerTNT', - tokenType: EVMTokenEnum.TNT, - bridgeType: EVMTokenBridgeEnum.Router, - address: assertEvmAddress(EVMTokens.base.router.TNT.address), - abi: assertAbi(EVMTokens.base.router.TNT.abi), - decimals: EVMTokens.base.router.TNT.decimals, - chainId: PresetTypedChainId.Base, - }, - ], - [PresetTypedChainId.BSC]: [ - { - tokenSymbol: 'routerTNT', - tokenType: EVMTokenEnum.TNT, - bridgeType: EVMTokenBridgeEnum.Router, - address: assertEvmAddress(EVMTokens.bsc.router.TNT.address), - abi: assertAbi(EVMTokens.bsc.router.TNT.abi), - decimals: EVMTokens.bsc.router.TNT.decimals, - chainId: PresetTypedChainId.BSC, - }, - ], - [PresetTypedChainId.Holesky]: [ - { - tokenSymbol: 'WETH', - tokenType: EVMTokenEnum.WETH, - // This is a collateral token on Holesky but will be bridged using Hyperlane - bridgeType: EVMTokenBridgeEnum.Hyperlane, - address: assertEvmAddress(EVMTokens.holesky.none.WETH.address), - abi: assertAbi(EVMTokens.holesky.none.WETH.abi), - decimals: EVMTokens.holesky.none.WETH.decimals, - chainId: PresetTypedChainId.Holesky, - hyperlaneRouteContractAddress: assertEvmAddress( - EVMTokens.holesky.hyperlane.WETH.address, - ), - }, - ], - [PresetTypedChainId.TangleTestnetEVM]: [ - { - tokenSymbol: 'hypWETH', - tokenType: EVMTokenEnum.WETH, - bridgeType: EVMTokenBridgeEnum.Hyperlane, - address: assertEvmAddress(EVMTokens.tangletestnet.hyperlane.WETH.address), - abi: assertAbi(EVMTokens.tangletestnet.hyperlane.WETH.abi), - decimals: EVMTokens.tangletestnet.hyperlane.WETH.decimals, - chainId: PresetTypedChainId.TangleTestnetEVM, - }, - ], - [PresetTypedChainId.SolanaMainnet]: [ - { - tokenSymbol: 'routerTNT', - tokenType: EVMTokenEnum.TNT, - bridgeType: EVMTokenBridgeEnum.Router, - address: assertAddressBy( - 'FcermohxLgTo8xnJXpPyW6D2swUMepVjQVNiiNLw38pC', - isSolanaAddress, - ), - abi: [], - decimals: 18, - chainId: PresetTypedChainId.SolanaMainnet, - }, - ], -}; - -export const BRIDGE_CHAINS: BridgeChainsConfigType = { - [PresetTypedChainId.TangleMainnetEVM]: { - [PresetTypedChainId.EthereumMainNet]: { - supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.EthereumMainNet], - }, - [PresetTypedChainId.Polygon]: { - supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.Polygon], - }, - [PresetTypedChainId.Arbitrum]: { - supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.Arbitrum], - }, - [PresetTypedChainId.Optimism]: { - supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.Optimism], - }, - [PresetTypedChainId.Linea]: { - supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.Linea], - }, - [PresetTypedChainId.Base]: { - supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.Base], - }, - [PresetTypedChainId.BSC]: { - supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.BSC], - }, - [PresetTypedChainId.SolanaMainnet]: { - supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.SolanaMainnet], - }, - }, - [PresetTypedChainId.EthereumMainNet]: { - [PresetTypedChainId.TangleMainnetEVM]: { - supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.EthereumMainNet], - }, - }, - [PresetTypedChainId.Polygon]: { - [PresetTypedChainId.TangleMainnetEVM]: { - supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.Polygon], - }, - }, - [PresetTypedChainId.Arbitrum]: { - [PresetTypedChainId.TangleMainnetEVM]: { - supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.Arbitrum], - }, - }, - [PresetTypedChainId.Optimism]: { - [PresetTypedChainId.TangleMainnetEVM]: { - supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.Optimism], - }, - }, - [PresetTypedChainId.Linea]: { - [PresetTypedChainId.TangleMainnetEVM]: { - supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.Linea], - }, - }, - [PresetTypedChainId.Base]: { - [PresetTypedChainId.TangleMainnetEVM]: { - supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.Base], - }, - }, - [PresetTypedChainId.BSC]: { - [PresetTypedChainId.TangleMainnetEVM]: { - supportedTokens: BRIDGE_TOKENS[PresetTypedChainId.BSC], - }, - }, -}; - -export const ROUTER_QUOTE_URL = `https://api-beta.pathfinder.routerprotocol.com/api/v2/quote`; - -export const ROUTER_TRANSACTION_URL = `https://api-beta.pathfinder.routerprotocol.com/api/v2/transaction`; - -export const ROUTER_NATIVE_TOKEN_ADDRESS = - '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; - -export const ROUTER_PARTNER_ID = 252; - -export enum ROUTER_ERROR_CODE { - LOW_AMOUNT_INPUT = 'AMOUNT-LOW-W-VALUE', -} - -export const ROUTER_TX_EXPLORER_URL = 'https://explorer.routernitro.com/tx/'; - -export const HYPERLANE_REGISTRY_URL = - process.env.NEXT_PUBLIC_HYPERLANE_REGISTRY_URL || - 'https://github.com/hyperlane-xyz/hyperlane-registry'; - -export const HYPERLANE_CHAINS: ChainMap = { - holesky: { - blockExplorers: [ - { - apiUrl: 'https://api-holesky.etherscan.io/api', - family: ExplorerFamily.Etherscan, - name: 'Etherscan', - url: 'https://holesky.etherscan.io', - }, - ], - blocks: { - confirmations: 1, - estimateBlockTime: 13, - reorgPeriod: 2, - }, - chainId: 17000, - displayName: 'Holesky', - domainId: 17000, - isTestnet: true, - name: 'holesky', - nativeToken: { - decimals: 18, - name: 'Ether', - symbol: 'ETH', - }, - protocol: ProtocolType.Ethereum, - rpcUrls: [ - { - http: 'https://ethereum-holesky-rpc.publicnode.com', - }, - ], - }, - tangletestnet: { - blockExplorers: [ - { - apiUrl: 'https://testnet-explorer.tangle.tools/api', - family: ExplorerFamily.Blockscout, - name: 'Tangle Testnet Explorer', - url: 'https://testnet-explorer.tangle.tools', - }, - ], - blocks: { - confirmations: 4, - estimateBlockTime: 6, - reorgPeriod: 4, - }, - chainId: 3799, - displayName: 'Tangle Testnet', - domainId: 3799, - isTestnet: true, - name: 'tangletestnet', - nativeToken: { - decimals: 18, - name: 'Tangle Testnet Token', - symbol: 'tTNT', - }, - protocol: ProtocolType.Ethereum, - rpcUrls: [ - { - http: 'https://testnet-rpc.tangle.tools', - }, - ], - }, -}; - -export const HYPERLANE_WARP_ROUTE_CONFIGS: WarpCoreConfig = - HyperlaneWarpRouteConfig; - -export const HYPERLANE_WARP_ROUTE_WHITELIST: Array | null = [ - 'WETH/holesky-tangletestnet', -]; diff --git a/apps/tangle-dapp/src/pages/bridge/hooks/useBridgeEvmBalances.ts b/apps/tangle-dapp/src/pages/bridge/hooks/useBridgeEvmBalances.ts deleted file mode 100644 index 1e29afae57..0000000000 --- a/apps/tangle-dapp/src/pages/bridge/hooks/useBridgeEvmBalances.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { chainsConfig } from '@webb-tools/dapp-config/chains'; -import { PresetTypedChainId } from '@webb-tools/dapp-types'; -import { Decimal } from 'decimal.js'; -import { ethers } from 'ethers'; -import { useCallback, useState } from 'react'; -import { Abi, createPublicClient, getContract, http } from 'viem'; - -import { - BridgeChainBalances, - BridgeToken, - BridgeTokenWithBalance, -} from '@webb-tools/tangle-shared-ui/types'; -import { BRIDGE_TOKENS } from '../constants'; -import ensureError from '@webb-tools/tangle-shared-ui/utils/ensureError'; -import { EvmAddress } from '@webb-tools/webb-ui-components/types/address'; -import useEvmAddress20 from '../../../hooks/useEvmAddress'; -import { isSolanaAddress } from '@webb-tools/webb-ui-components'; -import assert from 'assert'; - -export const fetchEvmTokenBalance = async ( - accountAddress: string, - chainId: number, - erc20Address: EvmAddress, - tokenAbi: Abi, - decimals: number, -) => { - try { - const client = createPublicClient({ - chain: chainsConfig[chainId], - transport: http(), - }); - - const contract = getContract({ - address: erc20Address, - abi: tokenAbi, - client, - }); - - const balance = await contract.read.balanceOf([accountAddress]); - - assert( - typeof balance === 'bigint', - `Bridge failed to read ERC-20 token balance: Unexpected balance type returned, expected bigint but got ${typeof balance} (${balance})`, - ); - - return new Decimal(ethers.utils.formatUnits(balance, decimals)); - } catch (error) { - console.error('Bridge failed to fetch EVM token balance:', error); - - // Assume that the balance is 0 if fetching it failed. - return new Decimal(0); - } -}; - -export const useBridgeEvmBalances = () => { - const accountEvmAddress = useEvmAddress20(); - const [balances, setBalances] = useState({}); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const fetchTokenBalance = useCallback( - async ( - token: BridgeToken, - chainId: PresetTypedChainId, - address: EvmAddress, - ): Promise => { - // TODO: Not all tokens are ERC20, ex. Solana. Handle the edge cases. For now, just return a balance of 0. - if (isSolanaAddress(token.address)) { - return { ...token, balance: new Decimal(0) }; - } - - try { - const balance = await fetchEvmTokenBalance( - address, - chainId, - token.address, - token.abi, - token.decimals, - ); - - return { ...token, balance }; - } catch (possibleError) { - const error = ensureError(possibleError); - - console.error( - `Failed to fetch balance for token ${token.tokenSymbol}:`, - error, - ); - - return { ...token, balance: new Decimal(0) }; - } - }, - [], - ); - - const fetchBalances = useCallback(async () => { - // Can't fetch balances without an EVM account connected. - if (accountEvmAddress === null) { - return; - } - - setIsLoading(true); - setError(null); - - try { - const newBalances: BridgeChainBalances = {}; - const chainEntries = Object.entries(BRIDGE_TOKENS); - - // Fetch balances for all chains. - const results = await Promise.allSettled( - chainEntries.map(async ([chainIdStr, tokens]) => { - const chainId = Number(chainIdStr) as PresetTypedChainId; - - const tokenBalances = await Promise.all( - tokens.map((token) => - fetchTokenBalance(token, chainId, accountEvmAddress), - ), - ); - - return { chainId, tokenBalances }; - }), - ); - - results.forEach((result) => { - if (result.status === 'fulfilled') { - const { chainId, tokenBalances } = result.value; - - newBalances[chainId] = tokenBalances; - } - }); - - setBalances(newBalances); - } catch (possibleError) { - const error = ensureError(possibleError); - - console.error('Bridge failed to fetch EVM balances:', error); - setError(`Failed to fetch token balances: ${error.message}`); - } finally { - setIsLoading(false); - } - }, [accountEvmAddress, fetchTokenBalance]); - - return { - balances, - isLoading, - error, - refresh: fetchBalances, - }; -}; diff --git a/apps/tangle-dapp/src/pages/bridge/index.tsx b/apps/tangle-dapp/src/pages/bridge/index.tsx index 345e5fc09d..ad3766e4a1 100644 --- a/apps/tangle-dapp/src/pages/bridge/index.tsx +++ b/apps/tangle-dapp/src/pages/bridge/index.tsx @@ -1,6 +1,6 @@ import { FC } from 'react'; -import BridgeContainer from './containers/BridgeContainer'; +import BridgeContainer from './BridgeContainer'; const Bridge: FC = () => { return ; diff --git a/apps/tangle-dapp/src/pages/bridge/lib/hyperlane/assembleChainMetadata.ts b/apps/tangle-dapp/src/pages/bridge/lib/hyperlane/assembleChainMetadata.ts index cfa46b5602..9cda85e1f1 100644 --- a/apps/tangle-dapp/src/pages/bridge/lib/hyperlane/assembleChainMetadata.ts +++ b/apps/tangle-dapp/src/pages/bridge/lib/hyperlane/assembleChainMetadata.ts @@ -6,7 +6,10 @@ import { } from '@hyperlane-xyz/sdk'; import { z } from 'zod'; -import { HYPERLANE_CHAINS, HYPERLANE_REGISTRY_URL } from '../../constants'; +import { + HYPERLANE_CHAINS, + HYPERLANE_REGISTRY_URL, +} from '../../../../constants/bridge'; export default async function assembleChainMetadata() { const result = z.record(ChainMetadataSchema).safeParse({ diff --git a/apps/tangle-dapp/src/pages/bridge/lib/hyperlane/assembleWarpCoreConfig.ts b/apps/tangle-dapp/src/pages/bridge/lib/hyperlane/assembleWarpCoreConfig.ts index b4ee1c8946..cc7368bf41 100644 --- a/apps/tangle-dapp/src/pages/bridge/lib/hyperlane/assembleWarpCoreConfig.ts +++ b/apps/tangle-dapp/src/pages/bridge/lib/hyperlane/assembleWarpCoreConfig.ts @@ -5,7 +5,7 @@ import { objFilter, objMerge } from '@hyperlane-xyz/utils'; import { HYPERLANE_WARP_ROUTE_CONFIGS, HYPERLANE_WARP_ROUTE_WHITELIST, -} from '../../constants'; +} from '../../../../constants/bridge'; export default function assembleWarpCoreConfig(): WarpCoreConfig { const result = WarpCoreConfigSchema.safeParse(HYPERLANE_WARP_ROUTE_CONFIGS); diff --git a/apps/tangle-dapp/src/pages/bridge/lib/hyperlane/utils.ts b/apps/tangle-dapp/src/pages/bridge/lib/hyperlane/utils.ts index f3dc054501..75319c14c3 100644 --- a/apps/tangle-dapp/src/pages/bridge/lib/hyperlane/utils.ts +++ b/apps/tangle-dapp/src/pages/bridge/lib/hyperlane/utils.ts @@ -18,11 +18,20 @@ export function tryFindToken( } export function getHyperlaneChainName(typedChainId: number) { + console.log('typedChainId', typedChainId); switch (typedChainId) { - case PresetTypedChainId.TangleTestnetEVM: - return 'tangletestnet'; - case PresetTypedChainId.Holesky: - return 'holesky'; + case PresetTypedChainId.Arbitrum: + return 'arbitrum'; + case PresetTypedChainId.Optimism: + return 'optimism'; + case PresetTypedChainId.Base: + return 'base'; + case PresetTypedChainId.BSC: + return 'bsc'; + case PresetTypedChainId.Polygon: + return 'polygon'; + case PresetTypedChainId.TangleMainnetEVM: + return 'tangle'; default: throw new Error('Unknown chain'); } diff --git a/apps/tangle-dapp/src/pages/restake/delegate/Details.tsx b/apps/tangle-dapp/src/pages/restake/delegate/Details.tsx index 4824da0561..3ecb59b6e0 100644 --- a/apps/tangle-dapp/src/pages/restake/delegate/Details.tsx +++ b/apps/tangle-dapp/src/pages/restake/delegate/Details.tsx @@ -1,34 +1,29 @@ -import isDefined from '@webb-tools/dapp-types/utils/isDefined'; -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import DetailsContainer from '../../../components/DetailsContainer'; import DetailItem from '../../../components/LiquidStaking/stakeAndUnstake/DetailItem'; import useRestakeConsts from '../../../data/restake/useRestakeConsts'; -import pluralize from '@webb-tools/webb-ui-components/utils/pluralize'; +import useSessionDurationMs from '../../../data/useSessionDurationMs'; +import formatMsDuration from '../../../utils/formatMsDuration'; const Details = memo(() => { - const { leaveDelegatorsDelay, delegationBondLessDelay } = useRestakeConsts(); + const { delegationBondLessDelay } = useRestakeConsts(); + const sessionDurationMs = useSessionDurationMs(); + + const unstakePeriod = useMemo(() => { + if (sessionDurationMs === null || delegationBondLessDelay === null) { + return null; + } + + return formatMsDuration(sessionDurationMs * delegationBondLessDelay); + }, [delegationBondLessDelay, sessionDurationMs]); return ( - - ); diff --git a/apps/tangle-dapp/src/pages/restake/delegate/StakeInput.tsx b/apps/tangle-dapp/src/pages/restake/delegate/StakeInput.tsx index ab6357c5c7..53db94495b 100644 --- a/apps/tangle-dapp/src/pages/restake/delegate/StakeInput.tsx +++ b/apps/tangle-dapp/src/pages/restake/delegate/StakeInput.tsx @@ -1,12 +1,11 @@ import { ZERO_BIG_INT } from '@webb-tools/dapp-config/constants'; import isDefined from '@webb-tools/dapp-types/utils/isDefined'; import type { Noop } from '@webb-tools/dapp-types/utils/types'; -import { useRestakeContext } from '@webb-tools/tangle-shared-ui/context/RestakeContext'; import type { DelegatorInfo } from '@webb-tools/tangle-shared-ui/types/restake'; import type { IdentityType } from '@webb-tools/tangle-shared-ui/utils/polkadot/identity'; import type { TextFieldInputProps } from '@webb-tools/webb-ui-components/components/TextField/types'; import { TransactionInputCard } from '@webb-tools/webb-ui-components/components/TransactionInputCard'; -import { useCallback, useMemo } from 'react'; +import { FC, useCallback, useMemo } from 'react'; import type { UseFormRegister, UseFormSetValue, @@ -20,6 +19,7 @@ import type { DelegationFormFields } from '../../../types/restake'; import decimalsToStep from '../../../utils/decimalsToStep'; import { getAmountValidation } from '../../../utils/getAmountValidation'; import AssetPlaceholder from '../AssetPlaceholder'; +import useRestakeAsset from '../../../data/restake/useRestakeAsset'; type Props = { amountError: string | undefined; @@ -32,7 +32,7 @@ type Props = { operatorIdentities?: Record | null; }; -export default function StakeInput({ +const StakeInput: FC = ({ amountError, delegatorInfo, openAssetModal, @@ -41,17 +41,12 @@ export default function StakeInput({ setValue, watch, operatorIdentities, -}: Props) { +}) => { const selectedAssetId = watch('assetId'); const selectedOperatorAccountId = watch('operatorAccountId'); - const { assetMetadataMap } = useRestakeContext(); const { minDelegateAmount } = useRestakeConsts(); - - const selectedAsset = useMemo( - () => (selectedAssetId !== null ? assetMetadataMap[selectedAssetId] : null), - [assetMetadataMap, selectedAssetId], - ); + const selectedAsset = useRestakeAsset(selectedAssetId); const { max, maxFormatted } = useMemo(() => { if (!isDefined(selectedAsset) || !isDefined(delegatorInfo)) { @@ -60,6 +55,7 @@ export default function StakeInput({ const amountRaw = delegatorInfo.deposits[selectedAsset.id]?.amount ?? ZERO_BIG_INT; + const maxFormatted = +formatUnits(amountRaw, selectedAsset.decimals); return { @@ -86,29 +82,32 @@ export default function StakeInput({ [setValue], ); - const customAmountProps = useMemo( - () => { - const step = decimalsToStep(selectedAsset?.decimals); - - return { - type: 'number', - step, - ...register('amount', { - required: 'Amount is required', - validate: getAmountValidation( - step, - minFormatted, - min, - max, - selectedAsset?.decimals, - selectedAsset?.symbol, - ), - }), - }; - }, - // prettier-ignore - [max, min, minFormatted, register, selectedAsset?.decimals, selectedAsset?.symbol], - ); + const customAmountProps = useMemo(() => { + const step = decimalsToStep(selectedAsset?.decimals); + + return { + type: 'number', + step, + ...register('amount', { + required: 'Amount is required', + validate: getAmountValidation( + step, + minFormatted, + min, + max, + selectedAsset?.decimals, + selectedAsset?.symbol, + ), + }), + }; + }, [ + max, + min, + minFormatted, + register, + selectedAsset?.decimals, + selectedAsset?.symbol, + ]); return (
    @@ -155,4 +154,6 @@ export default function StakeInput({ {amountError}
    ); -} +}; + +export default StakeInput; diff --git a/apps/tangle-dapp/src/pages/restake/delegate/index.tsx b/apps/tangle-dapp/src/pages/restake/delegate/index.tsx index 1e5b59c094..7ac78ec5a0 100644 --- a/apps/tangle-dapp/src/pages/restake/delegate/index.tsx +++ b/apps/tangle-dapp/src/pages/restake/delegate/index.tsx @@ -8,19 +8,20 @@ import useRestakeDelegatorInfo from '@webb-tools/tangle-shared-ui/data/restake/u import useRestakeOperatorMap from '@webb-tools/tangle-shared-ui/data/restake/useRestakeOperatorMap'; import { useRpcSubscription } from '@webb-tools/tangle-shared-ui/hooks/usePolkadotApi'; import { + AmountFormatStyle, assertSubstrateAddress, Card, + formatDisplayAmount, + isEvmAddress, isSubstrateAddress, + shortenHex, } from '@webb-tools/webb-ui-components'; -import type { TokenListCardProps } from '@webb-tools/webb-ui-components/components/ListCard/types'; import { Modal } from '@webb-tools/webb-ui-components/components/Modal'; import { useModal } from '@webb-tools/webb-ui-components/hooks/useModal'; import { SubstrateAddress } from '@webb-tools/webb-ui-components/types/address'; -import addCommasToNumber from '@webb-tools/webb-ui-components/utils/addCommasToNumber'; import keys from 'lodash/keys'; import { FC, useCallback, useEffect, useMemo } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; -import { formatUnits } from 'viem'; import LogoListItem from '../../../components/Lists/LogoListItem'; import OperatorListItem from '../../../components/Lists/OperatorListItem'; import useIdentities from '../../../data/useIdentities'; @@ -40,6 +41,10 @@ import StakeInput from './StakeInput'; import parseChainUnits from '../../../utils/parseChainUnits'; import { BN } from '@polkadot/util'; import useRestakeApi from '../../../data/restake/useRestakeApi'; +import assertRestakeAssetId from '@webb-tools/tangle-shared-ui/utils/assertRestakeAssetId'; +import { RestakeAsset } from '@webb-tools/tangle-shared-ui/types/restake'; +import { findErc20Token } from '../../../data/restake/useTangleEvmErc20Balances'; +import useRestakeAsset from '../../../data/restake/useRestakeAsset'; type RestakeOperator = { accountId: SubstrateAddress; @@ -79,7 +84,7 @@ const RestakeDelegateForm: FC = () => { register('operatorAccountId', { required: 'Operator is required' }); }, [register]); - const { assetMetadataMap } = useRestakeContext(); + const { vaults } = useRestakeContext(); const restakeApi = useRestakeApi(); const { delegatorInfo } = useRestakeDelegatorInfo(); const { operatorMap } = useRestakeOperatorMap(); @@ -115,6 +120,7 @@ const RestakeDelegateForm: FC = () => { } }, [defaultAssetId, setValue]); + // Set the operatorAccountId from the URL param. useEffect(() => { if ( !operatorParam || @@ -152,35 +158,49 @@ const RestakeDelegateForm: FC = () => { update: updateOperatorModal, } = useModal(false); - const selectableTokens = useMemo(() => { + const depositedAssets = useMemo(() => { if (!isDefined(delegatorInfo)) { return []; } - return Object.entries(delegatorInfo.deposits) - .filter(([assetId]) => Object.hasOwn(assetMetadataMap, assetId)) - .map(([assetId, { amount }]) => { - const metadata = assetMetadataMap[assetId]; - - return { - id: metadata.id, - name: metadata.name, - symbol: metadata.symbol, - decimals: metadata.decimals, - assetBalanceProps: { - balance: +formatUnits(amount, metadata.decimals), - ...(metadata.vaultId - ? { - subContent: `Vault ID: ${metadata.vaultId}`, - } - : {}), - }, - }; - }); - }, [assetMetadataMap, delegatorInfo]); + return Object.entries(delegatorInfo.deposits).flatMap( + ([assetIdString, { amount }]) => { + const assetId = assertRestakeAssetId(assetIdString); + const balance = new BN(amount.toString()); + + if (!isEvmAddress(assetId)) { + const metadata = vaults[assetId]; + + if (metadata === undefined) { + return []; + } + + return { + id: metadata.assetId, + name: metadata.name, + symbol: metadata.symbol, + decimals: metadata.decimals, + balance, + } satisfies RestakeAsset; + } else { + const erc20Token = findErc20Token(assetId); + + if (erc20Token === null) { + return []; + } + + return { + ...erc20Token, + id: assetId, + balance, + } satisfies RestakeAsset; + } + }, + ); + }, [vaults, delegatorInfo]); - const handleAssetChange = useCallback( - (asset: TokenListCardProps['selectTokens'][number]) => { + const handleAssetSelect = useCallback( + (asset: RestakeAsset) => { setValue('assetId', asset.id); closeAssetModal(); }, @@ -197,16 +217,18 @@ const RestakeDelegateForm: FC = () => { [closeChainModal, switchChain], ); - const isReady = restakeApi !== null && !isSubmitting; + const selectedAsset = useRestakeAsset(watch('assetId')); + + const isReady = + restakeApi !== null && !isSubmitting && selectedAsset !== null; const onSubmit = useCallback>( ({ amount, assetId, operatorAccountId }) => { - if (!assetId || !isDefined(assetMetadataMap[assetId]) || !isReady) { + if (!isReady) { return; } - const assetMetadata = assetMetadataMap[assetId]; - const amountBn = parseChainUnits(amount, assetMetadata.decimals); + const amountBn = parseChainUnits(amount, selectedAsset.decimals); if (!(amountBn instanceof BN)) { return; @@ -214,7 +236,7 @@ const RestakeDelegateForm: FC = () => { return restakeApi.delegate(operatorAccountId, assetId, amountBn); }, - [assetMetadataMap, isReady, restakeApi], + [isReady, restakeApi, selectedAsset?.decimals], ); const operators = useMemo(() => { @@ -274,20 +296,28 @@ const RestakeDelegateForm: FC = () => { setIsOpen={updateAssetModal} titleWhenEmpty="No Assets Available" descriptionWhenEmpty="Have you made a deposit on this network yet?" - items={selectableTokens} + items={depositedAssets} searchInputId="restake-delegate-asset-search" searchPlaceholder="Search for asset or enter token address" getItemKey={(item) => item.id} - onSelect={handleAssetChange} + onSelect={handleAssetSelect} renderItem={(asset) => { - const fmtBalance = `${addCommasToNumber(asset.assetBalanceProps.balance)} ${asset.symbol}`; + const fmtBalance = formatDisplayAmount( + asset.balance, + asset.decimals, + AmountFormatStyle.SHORT, + ); + + const idText = isEvmAddress(asset.id) + ? `Address: ${shortenHex(asset.id)}` + : `Asset ID: ${asset.id}`; return ( } leftUpperContent={`${asset.name} (${asset.symbol})`} - leftBottomContent={`Asset ID: ${asset.id}`} - rightUpperText={fmtBalance} + leftBottomContent={idText} + rightUpperText={`${fmtBalance} ${asset.symbol}`} rightBottomText="Balance" /> ); diff --git a/apps/tangle-dapp/src/pages/restake/deposit/DepositForm.tsx b/apps/tangle-dapp/src/pages/restake/deposit/DepositForm.tsx index d5ab63f50e..113e1975e8 100644 --- a/apps/tangle-dapp/src/pages/restake/deposit/DepositForm.tsx +++ b/apps/tangle-dapp/src/pages/restake/deposit/DepositForm.tsx @@ -7,14 +7,18 @@ import { TokenIcon } from '@webb-tools/icons'; import ListModal from '@webb-tools/tangle-shared-ui/components/ListModal'; import { useRestakeContext } from '@webb-tools/tangle-shared-ui/context/RestakeContext'; import { useRpcSubscription } from '@webb-tools/tangle-shared-ui/hooks/usePolkadotApi'; -import { Card, EMPTY_VALUE_PLACEHOLDER } from '@webb-tools/webb-ui-components'; -import { type TokenListCardProps } from '@webb-tools/webb-ui-components/components/ListCard/types'; +import { + AmountFormatStyle, + Card, + formatDisplayAmount, + isEvmAddress, + shortenHex, +} from '@webb-tools/webb-ui-components'; import { Modal, ModalContent, } from '@webb-tools/webb-ui-components/components/Modal'; import { useModal } from '@webb-tools/webb-ui-components/hooks/useModal'; -import addCommasToNumber from '@webb-tools/webb-ui-components/utils/addCommasToNumber'; import { type ComponentProps, FC, @@ -42,6 +46,10 @@ import { BN } from '@polkadot/util'; import parseChainUnits from '../../../utils/parseChainUnits'; import { PresetTypedChainId } from '@webb-tools/dapp-types'; import useRestakeApi from '../../../data/restake/useRestakeApi'; +import assert from 'assert'; +import { RestakeAsset } from '@webb-tools/tangle-shared-ui/types/restake'; +import useTangleEvmErc20Balances from '../../../data/restake/useTangleEvmErc20Balances'; +import useRestakeAsset from '../../../data/restake/useRestakeAsset'; const getDefaultTypedChainId = ( activeTypedChainId: number | null, @@ -73,11 +81,13 @@ const DepositForm: FC = (props) => { }, }); + const depositAssetId = watch('depositAssetId'); + const [vaultIdParam, setVaultIdParam] = useQueryState( QueryParamKey.RESTAKE_VAULT, ); - const { assetMetadataMap, assetWithBalances } = useRestakeContext(); + const { assetWithBalances } = useRestakeContext(); const restakeApi = useRestakeApi(); const setValue = useCallback( @@ -170,71 +180,79 @@ const DepositForm: FC = (props) => { update: updateTokenModal, } = useModal(); - const selectableTokens = useMemo( - () => - assetWithBalances - .filter( - (asset) => - asset.balance?.balance !== undefined && - asset.balance?.balance !== BigInt(0), - ) - .map((asset) => { - const balance = asset.balance; - - return { - id: asset.assetId, - name: asset.metadata.name, - symbol: asset.metadata.symbol, - ...(balance !== null - ? { - assetBalanceProps: { - balance: +formatUnits( - balance.balance, - asset.metadata.decimals, - ), - ...(asset.metadata.vaultId - ? { - subContent: `Vault ID: ${asset.metadata.vaultId}`, - } - : {}), - }, - } - : {}), - } satisfies TokenListCardProps['selectTokens'][number]; - }), - [assetWithBalances], - ); + const nativeAssets = useMemo(() => { + const nativeAssetsWithBalances = assetWithBalances + .filter( + (asset) => + asset.balance?.balance !== undefined && + asset.balance.balance !== BigInt(0), + ) + .map((asset) => { + assert(asset.balance !== null); + + const balance = new BN(asset.balance.balance.toString()); + + return { + id: asset.assetId, + name: asset.metadata.name, + symbol: asset.metadata.symbol, + balance, + decimals: asset.metadata.decimals, + } satisfies RestakeAsset; + }); + + return nativeAssetsWithBalances; + }, [assetWithBalances]); + + const erc20Balances = useTangleEvmErc20Balances(); - const handleTokenChange = useCallback( - (token: TokenListCardProps['selectTokens'][number]) => { - setValue('depositAssetId', token.id); + const erc20Assets = useMemo(() => { + if (erc20Balances === null) { + return []; + } + + return erc20Balances.map( + (asset) => + ({ + name: asset.name, + symbol: asset.symbol, + balance: new BN(asset.balance.toString()), + decimals: asset.decimals, + id: asset.contractAddress, + }) satisfies RestakeAsset, + ); + }, [erc20Balances]); + + const allAssets = useMemo(() => { + return [...nativeAssets, ...erc20Assets]; + }, [erc20Assets, nativeAssets]); + + const handleAssetSelection = useCallback( + (asset: RestakeAsset) => { + setValue('depositAssetId', asset.id); closeTokenModal(); }, [closeTokenModal, setValue], ); - const isReady = restakeApi !== null && !isSubmitting; + const asset = useRestakeAsset(depositAssetId); + const isReady = restakeApi !== null && asset !== null && !isSubmitting; const onSubmit = useCallback>( - ({ amount, depositAssetId }) => { - if ( - depositAssetId === null || - assetMetadataMap[depositAssetId] === undefined || - !isReady - ) { + ({ amount }) => { + if (!isReady) { return; } - const asset = assetMetadataMap[depositAssetId]; const amountBn = parseChainUnits(amount, asset.decimals); if (!(amountBn instanceof BN)) { return; } - return restakeApi.deposit(depositAssetId, amountBn); + return restakeApi.deposit(asset.id, amountBn); }, - [assetMetadataMap, isReady, restakeApi], + [asset, isReady, restakeApi], ); const sourceChainOptions = useMemo(() => { @@ -292,7 +310,7 @@ const DepositForm: FC = (props) => { = (props) => { title="Select Asset" isOpen={tokenModalOpen} setIsOpen={updateTokenModal} + onSelect={handleAssetSelection} filterItem={(asset, query) => filterBy(query, [asset.id, asset.name, asset.symbol]) } @@ -312,24 +331,28 @@ const DepositForm: FC = (props) => { searchPlaceholder="Search assets..." titleWhenEmpty="No Assets Found" descriptionWhenEmpty="It seems that there are no available assets in this network yet. Please try again later." - items={selectableTokens} + items={allAssets} renderItem={(asset) => { - const fmtBalance = - asset.assetBalanceProps?.balance !== undefined - ? `${addCommasToNumber(asset.assetBalanceProps.balance)} ${asset.symbol}` - : EMPTY_VALUE_PLACEHOLDER; + const fmtBalance = formatDisplayAmount( + asset.balance, + asset.decimals, + AmountFormatStyle.SHORT, + ); + + const idText = isEvmAddress(asset.id) + ? `Address: ${shortenHex(asset.id)}` + : `Asset ID: ${asset.id}`; return ( } leftUpperContent={`${asset.name} (${asset.symbol})`} - leftBottomContent={`Asset ID: ${asset.id}`} + leftBottomContent={idText} rightBottomText="Balance" - rightUpperText={fmtBalance} + rightUpperText={`${fmtBalance} ${asset.symbol}`} /> ); }} - onSelect={handleTokenChange} /> diff --git a/apps/tangle-dapp/src/pages/restake/deposit/Details.tsx b/apps/tangle-dapp/src/pages/restake/deposit/Details.tsx index 5c8d33b562..f6aaba2d20 100644 --- a/apps/tangle-dapp/src/pages/restake/deposit/Details.tsx +++ b/apps/tangle-dapp/src/pages/restake/deposit/Details.tsx @@ -1,6 +1,5 @@ import { useRestakeContext } from '@webb-tools/tangle-shared-ui/context/RestakeContext'; import { EMPTY_VALUE_PLACEHOLDER } from '@webb-tools/webb-ui-components'; -import pluralize from '@webb-tools/webb-ui-components/utils/pluralize'; import { FC, useMemo } from 'react'; import { UseFormWatch } from 'react-hook-form'; import DetailsContainer from '../../../components/DetailsContainer'; @@ -8,15 +7,18 @@ import DetailItem from '../../../components/LiquidStaking/stakeAndUnstake/Detail import useRestakeConsts from '../../../data/restake/useRestakeConsts'; import useRestakeRewardConfig from '../../../data/restake/useRestakeRewardConfig'; import { DepositFormFields } from '../../../types/restake'; +import useSessionDurationMs from '../../../data/useSessionDurationMs'; +import formatMsDuration from '../../../utils/formatMsDuration'; type Props = { watch: UseFormWatch; }; const Details: FC = ({ watch }) => { - const { assetMetadataMap } = useRestakeContext(); + const { vaults } = useRestakeContext(); const { bondDuration } = useRestakeConsts(); const rewardConfig = useRestakeRewardConfig(); + const sessionDurationMs = useSessionDurationMs(); const assetId = watch('depositAssetId'); @@ -25,14 +27,22 @@ const Details: FC = ({ watch }) => { return null; } - const asset = assetMetadataMap[assetId]; + const asset = vaults[assetId]; if (asset === undefined || asset.vaultId === null) { return null; } return rewardConfig.get(asset.vaultId)?.apy ?? null; - }, [assetId, assetMetadataMap, rewardConfig]); + }, [assetId, vaults, rewardConfig]); + + const withdrawPeriod = useMemo(() => { + if (sessionDurationMs === null || bondDuration === null) { + return null; + } + + return formatMsDuration(sessionDurationMs * bondDuration); + }, [bondDuration, sessionDurationMs]); return ( @@ -42,13 +52,9 @@ const Details: FC = ({ watch }) => { /> ); diff --git a/apps/tangle-dapp/src/pages/restake/deposit/SourceChainInput.tsx b/apps/tangle-dapp/src/pages/restake/deposit/SourceChainInput.tsx index df6242d7a4..1a2026643a 100644 --- a/apps/tangle-dapp/src/pages/restake/deposit/SourceChainInput.tsx +++ b/apps/tangle-dapp/src/pages/restake/deposit/SourceChainInput.tsx @@ -1,10 +1,9 @@ import { ZERO_BIG_INT } from '@webb-tools/dapp-config/constants'; import type { Noop } from '@webb-tools/dapp-types/utils/types'; -import { useRestakeContext } from '@webb-tools/tangle-shared-ui/context/RestakeContext'; import type { TextFieldInputProps } from '@webb-tools/webb-ui-components/components/TextField/types'; import type { TokenSelectorProps } from '@webb-tools/webb-ui-components/components/TokenSelector/types'; import { TransactionInputCard } from '@webb-tools/webb-ui-components/components/TransactionInputCard'; -import { useCallback, useMemo } from 'react'; +import { FC, useCallback, useMemo } from 'react'; import type { UseFormRegister, UseFormSetValue, @@ -17,6 +16,7 @@ import { DepositFormFields } from '../../../types/restake'; import decimalsToStep from '../../../utils/decimalsToStep'; import { getAmountValidation } from '../../../utils/getAmountValidation'; import AssetPlaceholder from '../AssetPlaceholder'; +import useRestakeAsset from '../../../data/restake/useRestakeAsset'; type Props = { amountError?: string; @@ -27,42 +27,38 @@ type Props = { watch: UseFormWatch; }; -const SourceChainInput = ({ +const SourceChainInput: FC = ({ amountError, openChainModal, openTokenModal, register, setValue, watch, -}: Props) => { +}) => { // Selectors const sourceTypedChainId = watch('sourceTypedChainId'); const depositAssetId = watch('depositAssetId'); - const { assetMetadataMap, balances } = useRestakeContext(); const { minDelegateAmount } = useRestakeConsts(); - - const asset = useMemo(() => { - if (depositAssetId === null) { - return null; - } - - return assetMetadataMap[depositAssetId] ?? null; - }, [assetMetadataMap, depositAssetId]); + const asset = useRestakeAsset(depositAssetId); const { max, maxFormatted } = useMemo(() => { - if (asset === null) return {}; + if (asset === null) { + return {}; + } - const balance = balances[asset.id]?.balance ?? ZERO_BIG_INT; + const balanceBigInt = BigInt(asset.balance.toString()); return { - max: balance, - maxFormatted: formatUnits(balance, asset.decimals), + max: balanceBigInt, + maxFormatted: formatUnits(balanceBigInt, asset.decimals), }; - }, [asset, balances]); + }, [asset]); const { min, minFormatted } = useMemo(() => { - if (asset === null) return {}; + if (asset === null) { + return {}; + } return { min: minDelegateAmount ?? ZERO_BIG_INT, diff --git a/apps/tangle-dapp/src/pages/restake/operators/[address]/TVLTable.tsx b/apps/tangle-dapp/src/pages/restake/operators/[address]/TVLTable.tsx index a6e3ca8dc7..ac28bb3d36 100644 --- a/apps/tangle-dapp/src/pages/restake/operators/[address]/TVLTable.tsx +++ b/apps/tangle-dapp/src/pages/restake/operators/[address]/TVLTable.tsx @@ -24,7 +24,7 @@ const TVLTable: FC = ({ delegatorInfo, delegatorTVL, }) => { - const { assetMetadataMap } = useRestakeContext(); + const { vaults: vaultsMetadataMap } = useRestakeContext(); const rewardConfig = useRestakeRewardConfig(); const vaults = useMemo(() => { @@ -33,16 +33,16 @@ const TVLTable: FC = ({ const delegations = operatorData?.delegations ?? []; delegations.forEach(({ assetId }) => { - if (assetMetadataMap[assetId] === undefined) return; + if (vaultsMetadataMap[assetId] === undefined) return; - if (assetMetadataMap[assetId].vaultId === null) return; + if (vaultsMetadataMap[assetId].vaultId === null) return; - const vaultId = assetMetadataMap[assetId].vaultId; + const vaultId = vaultsMetadataMap[assetId].vaultId; if (vaults[vaultId] === undefined) { // TODO: Find out a proper way to get the vault name, now it's the first token name - const name = assetMetadataMap[assetId].name; + const name = vaultsMetadataMap[assetId].name; // TODO: Find out a proper way to get the vault symbol, now it's the first token symbol - const representToken = assetMetadataMap[assetId].symbol; + const representToken = vaultsMetadataMap[assetId].symbol; const apyPercentage = rewardConfig?.get(vaultId)?.apy.toNumber() ?? null; @@ -63,7 +63,7 @@ const TVLTable: FC = ({ }); return vaults; - }, [assetMetadataMap, operatorData?.delegations, rewardConfig, vaultTVL]); + }, [vaultsMetadataMap, operatorData?.delegations, rewardConfig, vaultTVL]); const delegatorTotalRestakedAssets = useMemo(() => { if (!delegatorInfo?.delegations) { @@ -91,16 +91,16 @@ const TVLTable: FC = ({ }, getExpandedRowContent(row) { const vaultId = row.original.id; - const vaultAssets = Object.values(assetMetadataMap) + const vaultAssets = Object.values(vaultsMetadataMap) .filter((asset) => asset.vaultId === vaultId) .map((asset) => { const selfStake = - delegatorTotalRestakedAssets[asset.id] ?? ZERO_BIG_INT; + delegatorTotalRestakedAssets[asset.assetId] ?? ZERO_BIG_INT; - const tvl = delegatorTVL?.[asset.id] ?? null; + const tvl = delegatorTVL?.[asset.assetId] ?? null; return { - id: asset.id, + id: asset.assetId, symbol: asset.symbol, decimals: asset.decimals, tvl, @@ -118,7 +118,7 @@ const TVLTable: FC = ({ ); }, }), - [assetMetadataMap, delegatorTVL, delegatorTotalRestakedAssets], + [vaultsMetadataMap, delegatorTVL, delegatorTotalRestakedAssets], ); return ( diff --git a/apps/tangle-dapp/src/pages/restake/unstake/Details.tsx b/apps/tangle-dapp/src/pages/restake/unstake/Details.tsx index 88099b8021..7c729fa07e 100644 --- a/apps/tangle-dapp/src/pages/restake/unstake/Details.tsx +++ b/apps/tangle-dapp/src/pages/restake/unstake/Details.tsx @@ -1,25 +1,28 @@ -import { EMPTY_VALUE_PLACEHOLDER } from '@webb-tools/webb-ui-components'; import DetailsContainer from '../../../components/DetailsContainer'; import DetailItem from '../../../components/LiquidStaking/stakeAndUnstake/DetailItem'; import useRestakeConsts from '../../../data/restake/useRestakeConsts'; -import pluralize from '@webb-tools/webb-ui-components/utils/pluralize'; -import { FC } from 'react'; +import { FC, useMemo } from 'react'; +import formatMsDuration from '../../../utils/formatMsDuration'; +import useSessionDurationMs from '../../../data/useSessionDurationMs'; const Details: FC = () => { const { delegationBondLessDelay } = useRestakeConsts(); + const sessionDurationMs = useSessionDurationMs(); + + const unstakePeriod = useMemo(() => { + if (sessionDurationMs === null || delegationBondLessDelay === null) { + return null; + } + + return formatMsDuration(sessionDurationMs * delegationBondLessDelay); + }, [delegationBondLessDelay, sessionDurationMs]); - // TODO: Add fee value. return ( - - ); diff --git a/apps/tangle-dapp/src/pages/restake/unstake/index.tsx b/apps/tangle-dapp/src/pages/restake/unstake/index.tsx index f032ed90e9..20f5522f6e 100644 --- a/apps/tangle-dapp/src/pages/restake/unstake/index.tsx +++ b/apps/tangle-dapp/src/pages/restake/unstake/index.tsx @@ -63,7 +63,7 @@ const RestakeUnstakeForm: FC = () => { const switchChain = useSwitchChain(); const activeTypedChainId = useActiveTypedChainId(); - const { assetMetadataMap } = useRestakeContext(); + const { vaults } = useRestakeContext(); const { status: isOperatorModalOpen, @@ -113,12 +113,12 @@ const RestakeUnstakeForm: FC = () => { }, [delegatorInfo?.unstakeRequests]); const selectedAsset = useMemo(() => { - if (!selectedAssetId || !assetMetadataMap[selectedAssetId]) { + if (!selectedAssetId || !vaults[selectedAssetId]) { return null; } - return assetMetadataMap[selectedAssetId]; - }, [assetMetadataMap, selectedAssetId]); + return vaults[selectedAssetId]; + }, [vaults, selectedAssetId]); const { maxAmount, formattedMaxAmount } = useMemo(() => { if (!Array.isArray(delegatorInfo?.delegations)) { @@ -131,17 +131,14 @@ const RestakeUnstakeForm: FC = () => { item.operatorAccountId === selectedOperatorAccountId, ); - if (!selectedDelegation || !assetMetadataMap[selectedDelegation.assetId]) { + if (!selectedDelegation || !vaults[selectedDelegation.assetId]) { return {}; } const maxAmount = selectedDelegation.amountBonded; const formattedMaxAmount = Number( - formatUnits( - maxAmount, - assetMetadataMap[selectedDelegation.assetId].decimals, - ), + formatUnits(maxAmount, vaults[selectedDelegation.assetId].decimals), ); return { @@ -150,7 +147,7 @@ const RestakeUnstakeForm: FC = () => { }; }, [ delegatorInfo?.delegations, - assetMetadataMap, + vaults, selectedAssetId, selectedOperatorAccountId, ]); @@ -193,11 +190,11 @@ const RestakeUnstakeForm: FC = () => { const onSubmit = useCallback>( async ({ amount, assetId, operatorAccountId }) => { - if (!assetId || !isDefined(assetMetadataMap[assetId]) || !isReady) { + if (!assetId || !isDefined(vaults[assetId]) || !isReady) { return; } - const assetMetadata = assetMetadataMap[assetId]; + const assetMetadata = vaults[assetId]; const amountBn = parseChainUnits(amount, assetMetadata.decimals); if (!(amountBn instanceof BN)) { @@ -206,7 +203,7 @@ const RestakeUnstakeForm: FC = () => { await restakeApi.undelegate(operatorAccountId, assetId, amountBn); }, - [assetMetadataMap, isReady, restakeApi], + [vaults, isReady, restakeApi], ); return ( diff --git a/apps/tangle-dapp/src/pages/restake/withdraw/Details.tsx b/apps/tangle-dapp/src/pages/restake/withdraw/Details.tsx index f074b69ab6..cad9c08151 100644 --- a/apps/tangle-dapp/src/pages/restake/withdraw/Details.tsx +++ b/apps/tangle-dapp/src/pages/restake/withdraw/Details.tsx @@ -1,27 +1,28 @@ -import isDefined from '@webb-tools/dapp-types/utils/isDefined'; -import { EMPTY_VALUE_PLACEHOLDER } from '@webb-tools/webb-ui-components/constants'; - import DetailsContainer from '../../../components/DetailsContainer'; import DetailItem from '../../../components/LiquidStaking/stakeAndUnstake/DetailItem'; import useRestakeConsts from '../../../data/restake/useRestakeConsts'; -import pluralize from '@webb-tools/webb-ui-components/utils/pluralize'; -import { FC } from 'react'; +import { FC, useMemo } from 'react'; +import useSessionDurationMs from '../../../data/useSessionDurationMs'; +import formatMsDuration from '../../../utils/formatMsDuration'; const Details: FC = () => { - const { leaveDelegatorsDelay } = useRestakeConsts(); + const { bondDuration } = useRestakeConsts(); + const sessionDurationMs = useSessionDurationMs(); + + const withdrawPeriod = useMemo(() => { + if (sessionDurationMs === null || bondDuration === null) { + return null; + } + + return formatMsDuration(sessionDurationMs * bondDuration); + }, [bondDuration, sessionDurationMs]); return ( - {/* TODO: Add fee value */} - - ); diff --git a/apps/tangle-dapp/src/pages/restake/withdraw/index.tsx b/apps/tangle-dapp/src/pages/restake/withdraw/index.tsx index a7d45e95ef..30e86b456f 100644 --- a/apps/tangle-dapp/src/pages/restake/withdraw/index.tsx +++ b/apps/tangle-dapp/src/pages/restake/withdraw/index.tsx @@ -6,7 +6,6 @@ import isDefined from '@webb-tools/dapp-types/utils/isDefined'; import { ChainIcon } from '@webb-tools/icons/ChainIcon'; import LockFillIcon from '@webb-tools/icons/LockFillIcon'; import { LockLineIcon } from '@webb-tools/icons/LockLineIcon'; -import { useRestakeContext } from '@webb-tools/tangle-shared-ui/context/RestakeContext'; import useRestakeDelegatorInfo from '@webb-tools/tangle-shared-ui/data/restake/useRestakeDelegatorInfo'; import { Card, @@ -43,6 +42,7 @@ import WithdrawRequestTable from '../../../containers/restaking/WithdrawRequestT import parseChainUnits from '../../../utils/parseChainUnits'; import { BN } from '@polkadot/util'; import useRestakeApi from '../../../data/restake/useRestakeApi'; +import useRestakeAsset from '../../../data/restake/useRestakeAsset'; const RestakeWithdrawForm: FC = () => { const { @@ -59,7 +59,6 @@ const RestakeWithdrawForm: FC = () => { const switchChain = useSwitchChain(); const activeTypedChainId = useActiveTypedChainId(); const { activeChain } = useWebContext(); - const { assetMetadataMap } = useRestakeContext(); const { status: isWithdrawModalOpen, @@ -103,39 +102,32 @@ const RestakeWithdrawForm: FC = () => { return delegatorInfo.withdrawRequests; }, [delegatorInfo?.withdrawRequests]); - const selectedAsset = useMemo(() => { - if (!selectedAssetId || !assetMetadataMap[selectedAssetId]) { - return null; - } + const selectedAsset = useRestakeAsset(selectedAssetId); - return assetMetadataMap[selectedAssetId]; - }, [assetMetadataMap, selectedAssetId]); + const { maxAmount, formattedMaxAmount } = useMemo(() => { + if (!delegatorInfo?.deposits) { + return {}; + } - const { maxAmount, formattedMaxAmount } = useMemo( - () => { - if (!delegatorInfo?.deposits) return {}; + const depositedAsset = Object.entries(delegatorInfo.deposits).find( + ([assetId]) => assetId === selectedAssetId, + ); - const depositedAsset = Object.entries(delegatorInfo.deposits).find( - ([assetId]) => assetId === selectedAssetId, - ); + if (!depositedAsset || selectedAsset === null) { + return {}; + } - if (!depositedAsset) return {}; - if (!assetMetadataMap[depositedAsset[0]]) return {}; + const maxAmount = depositedAsset[1].amount; - const assetId = depositedAsset[0]; - const maxAmount = depositedAsset[1].amount; - const formattedMaxAmount = Number( - formatUnits(maxAmount, assetMetadataMap[assetId].decimals), - ); + const formattedMaxAmount = Number( + formatUnits(maxAmount, selectedAsset.decimals), + ); - return { - maxAmount, - formattedMaxAmount, - }; - }, - // prettier-ignore - [assetMetadataMap, delegatorInfo?.deposits, selectedAssetId], - ); + return { + maxAmount, + formattedMaxAmount, + }; + }, [delegatorInfo?.deposits, selectedAsset, selectedAssetId]); const customAmountProps = useMemo(() => { const step = decimalsToStep(selectedAsset?.decimals); @@ -169,16 +161,16 @@ const RestakeWithdrawForm: FC = () => { const restakeApi = useRestakeApi(); - const isReady = restakeApi !== null && !isSubmitting; + const isReady = + restakeApi !== null && !isSubmitting && selectedAsset !== null; const onSubmit = useCallback>( ({ amount, assetId }) => { - if (!assetId || !isDefined(assetMetadataMap[assetId]) || !isReady) { + if (!isReady) { return; } - const assetMetadata = assetMetadataMap[assetId]; - const amountBn = parseChainUnits(amount, assetMetadata.decimals); + const amountBn = parseChainUnits(amount, selectedAsset.decimals); if (!(amountBn instanceof BN)) { return; @@ -186,7 +178,7 @@ const RestakeWithdrawForm: FC = () => { return restakeApi.withdraw(assetId, amountBn); }, - [assetMetadataMap, isReady, restakeApi], + [isReady, restakeApi, selectedAsset?.decimals], ); return ( @@ -211,7 +203,7 @@ const RestakeWithdrawForm: FC = () => { > { delegatorInfo={delegatorInfo} isOpen={isWithdrawModalOpen} setIsOpen={updateWithdrawModal} - onItemSelected={(item) => { + onItemSelected={({ formattedAmount, assetId }) => { closeWithdrawModal(); - const { formattedAmount, assetId } = item; - const commonOpts = { shouldDirty: true, shouldValidate: true, @@ -359,6 +349,7 @@ const RestakeWithdrawForm: FC = () => { chainConfig.chainType, chainConfig.id, ); + await switchChain(typedChainId); closeChainModal(); }} diff --git a/apps/tangle-dapp/src/utils/fetchErc20TokenBalance.ts b/apps/tangle-dapp/src/utils/fetchErc20TokenBalance.ts new file mode 100644 index 0000000000..25090b379f --- /dev/null +++ b/apps/tangle-dapp/src/utils/fetchErc20TokenBalance.ts @@ -0,0 +1,38 @@ +import { EvmAddress } from '@webb-tools/webb-ui-components/types/address'; +import assert from 'assert'; +import { Decimal } from 'decimal.js'; +import { ethers } from 'ethers'; +import { Abi, PublicClient } from 'viem'; + +const fetchErc20TokenBalance = async ( + viemPublicClient: PublicClient, + accountAddress: EvmAddress, + contractAddress: EvmAddress, + tokenAbi: Abi, + decimals: number, +): Promise => { + try { + const balance = await viemPublicClient.readContract({ + address: contractAddress, + abi: tokenAbi, + functionName: 'balanceOf', + args: [accountAddress], + }); + + assert( + typeof balance === 'bigint', + `Bridge failed to read ERC20 token balance: Unexpected balance type returned, expected bigint but got ${typeof balance} (${balance})`, + ); + + return new Decimal(ethers.utils.formatUnits(balance, decimals)); + } catch (e) { + console.warn( + 'Failed to fetch ERC20 token balance, assuming balance of 0:', + e, + ); + + return new Decimal(0); + } +}; + +export default fetchErc20TokenBalance; diff --git a/apps/tangle-dapp/src/utils/formatMsDuration.ts b/apps/tangle-dapp/src/utils/formatMsDuration.ts new file mode 100644 index 0000000000..801df7241b --- /dev/null +++ b/apps/tangle-dapp/src/utils/formatMsDuration.ts @@ -0,0 +1,10 @@ +import { formatDuration, intervalToDuration } from 'date-fns'; +import { capitalize } from 'lodash'; + +const formatMsDuration = (ms: number): string => { + const duration = formatDuration(intervalToDuration({ start: 0, end: ms })); + + return capitalize(duration); +}; + +export default formatMsDuration; diff --git a/apps/tangle-dapp/src/utils/formatSessionDistance.ts b/apps/tangle-dapp/src/utils/formatSessionDistance.ts new file mode 100644 index 0000000000..646da1dd5d --- /dev/null +++ b/apps/tangle-dapp/src/utils/formatSessionDistance.ts @@ -0,0 +1,14 @@ +import { formatDistance } from 'date-fns'; +import { capitalize } from 'lodash'; + +const formatSessionDistance = ( + sessionsRemaining: number, + sessionDurationMs: number, +): string => { + const now = Date.now(); + const futureDate = new Date(now + sessionsRemaining * sessionDurationMs); + + return capitalize(formatDistance(futureDate, now)); +}; + +export default formatSessionDistance; diff --git a/libs/api-provider-environment/src/WebbProvider/index.tsx b/libs/api-provider-environment/src/WebbProvider/index.tsx index 06ff3c641d..ba7e2a8169 100644 --- a/libs/api-provider-environment/src/WebbProvider/index.tsx +++ b/libs/api-provider-environment/src/WebbProvider/index.tsx @@ -32,11 +32,7 @@ import { ChainType, calculateTypedChainId, } from '@webb-tools/dapp-types/TypedChainId'; -import { - WebbWeb3Provider, - isErrorInstance, - isViemError, -} from '@webb-tools/web3-api-provider'; +import { WebbWeb3Provider } from '@webb-tools/web3-api-provider'; import { useWebbUI } from '@webb-tools/webb-ui-components'; import useWagmiHydration from '@webb-tools/webb-ui-components/hooks/useWagmiHydration'; import { useCallback, useEffect, useRef, useState, type FC } from 'react'; @@ -53,6 +49,7 @@ import { useActiveChain } from '../hooks/useActiveChain'; import { useActiveWallet } from '../hooks/useActiveWallet'; import waitForConfigReady from '../utils/waitForConfigReady'; import { WebbContext } from '../webb-context'; +import { BaseError } from 'viem'; interface WebbProviderInnerProps extends BareProps { appEvent: TAppEvent; @@ -486,7 +483,7 @@ const WebbProviderInner: FC = ({ ).message; // Libraries error check - if (isViemError(e) || isErrorInstance(e, WagmiBaseError)) { + if (e instanceof BaseError || e instanceof WagmiBaseError) { errorMessage = e.shortMessage; } else if (e instanceof Error) { errorMessage = e.message; @@ -633,7 +630,7 @@ const WebbProviderInner: FC = ({ } } else { // If the user did not want to switch to the previously stored chain, - // set the previosuly stored chain in the app for display only. + // set the previously stored chain in the app for display only. setActiveChain(chains[net]); } }; diff --git a/libs/dapp-config/src/utils/getPolkadotBasedWallet.ts b/libs/dapp-config/src/utils/getPolkadotBasedWallet.ts index 5394a43d79..a9c9573154 100644 --- a/libs/dapp-config/src/utils/getPolkadotBasedWallet.ts +++ b/libs/dapp-config/src/utils/getPolkadotBasedWallet.ts @@ -1,12 +1,13 @@ import type { InjectedExtension } from '@polkadot/extension-inject/types'; +import { web3Enable } from '@polkadot/extension-dapp'; async function getPolkadotBasedWallet( appName: string, extensionName: string, ): Promise { try { - const { web3Enable } = await import('@polkadot/extension-dapp'); const extensions = await web3Enable(appName); + return extensions.find((ex) => ex.name === extensionName); } catch (error) { console.error('Error getting polkadot based wallet', error); diff --git a/libs/dapp-config/src/wallets/wallets-config.tsx b/libs/dapp-config/src/wallets/wallets-config.tsx index 994c102b0d..d74c170c93 100644 --- a/libs/dapp-config/src/wallets/wallets-config.tsx +++ b/libs/dapp-config/src/wallets/wallets-config.tsx @@ -16,6 +16,13 @@ const ANY_EVM = [ PresetTypedChainId.EthereumMainNet, PresetTypedChainId.TangleMainnetEVM, + PresetTypedChainId.Arbitrum, + PresetTypedChainId.Base, + PresetTypedChainId.BSC, + PresetTypedChainId.Linea, + PresetTypedChainId.Optimism, + PresetTypedChainId.Polygon, + // Testnet PresetTypedChainId.Goerli, PresetTypedChainId.Sepolia, diff --git a/libs/icons/src/tokens/arb.svg b/libs/icons/src/tokens/arb.svg new file mode 100644 index 0000000000..90b40cd197 --- /dev/null +++ b/libs/icons/src/tokens/arb.svg @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/libs/icons/src/tokens/avail.svg b/libs/icons/src/tokens/avail.svg new file mode 100644 index 0000000000..e6bb95315e --- /dev/null +++ b/libs/icons/src/tokens/avail.svg @@ -0,0 +1,281 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/icons/src/tokens/bnb.svg b/libs/icons/src/tokens/bnb.svg new file mode 100644 index 0000000000..a6209d1809 --- /dev/null +++ b/libs/icons/src/tokens/bnb.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/icons/src/tokens/cbbtc.svg b/libs/icons/src/tokens/cbbtc.svg new file mode 100644 index 0000000000..0ed382dff6 --- /dev/null +++ b/libs/icons/src/tokens/cbbtc.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/libs/icons/src/tokens/wsteth.svg b/libs/icons/src/tokens/wsteth.svg new file mode 100644 index 0000000000..c0a0a57a86 --- /dev/null +++ b/libs/icons/src/tokens/wsteth.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/libs/tangle-shared-ui/src/components/ConnectWalletButton/WalletDropdown.tsx b/libs/tangle-shared-ui/src/components/ConnectWalletButton/WalletDropdown.tsx index d2711113aa..ac7c76ed62 100644 --- a/libs/tangle-shared-ui/src/components/ConnectWalletButton/WalletDropdown.tsx +++ b/libs/tangle-shared-ui/src/components/ConnectWalletButton/WalletDropdown.tsx @@ -7,7 +7,7 @@ import { useWallets } from '@webb-tools/api-provider-environment/hooks/useWallet import { ManagedWallet, WalletConfig } from '@webb-tools/dapp-config'; import { WebbError, WebbErrorCodes } from '@webb-tools/dapp-types'; import { LoginBoxLineIcon, WalletLineIcon } from '@webb-tools/icons'; -import { isViemError, WebbWeb3Provider } from '@webb-tools/web3-api-provider'; +import { WebbWeb3Provider } from '@webb-tools/web3-api-provider'; import { AccountDropdownBody, Button, @@ -25,6 +25,7 @@ import { FC, useCallback, useMemo } from 'react'; import useNetworkStore from '../../context/useNetworkStore'; import useSubstrateExplorerUrl from '../../hooks/useSubstrateExplorerUrl'; +import { BaseError } from 'viem'; const WalletDropdown: FC<{ accountName?: string; @@ -147,7 +148,7 @@ const SwitchAccountButton: FC = () => { WebbErrorCodes.SwitchAccountFailed, ).message; - if (isViemError(error)) { + if (error instanceof BaseError) { message = error.shortMessage; } diff --git a/libs/tangle-shared-ui/src/components/tables/Operators/VaultsDropdown.tsx b/libs/tangle-shared-ui/src/components/tables/Operators/VaultsDropdown.tsx index 92771d8a9b..d3219b7976 100644 --- a/libs/tangle-shared-ui/src/components/tables/Operators/VaultsDropdown.tsx +++ b/libs/tangle-shared-ui/src/components/tables/Operators/VaultsDropdown.tsx @@ -17,10 +17,10 @@ import { FC } from 'react'; import { VaultToken } from '../../../types'; import LsTokenIcon from '../../LsTokenIcon'; -const columnHelper = createColumnHelper(); +const COLUMN_HELPER = createColumnHelper(); -const columns = [ - columnHelper.accessor('name', { +const COLUMNS = [ + COLUMN_HELPER.accessor('name', { header: () => Token, cell: (props) => (
    @@ -30,7 +30,7 @@ const columns = [
    ), }), - columnHelper.accessor('amount', { + COLUMN_HELPER.accessor('amount', { header: () => ( Amount @@ -50,7 +50,7 @@ const columns = [ const VaultsDropdown: FC<{ vaultTokens: VaultToken[] }> = ({ vaultTokens }) => { const table = useReactTable({ - columns, + columns: COLUMNS, data: vaultTokens, getCoreRowModel: getCoreRowModel(), }); diff --git a/libs/tangle-shared-ui/src/context/RestakeContext/RestakeContext.ts b/libs/tangle-shared-ui/src/context/RestakeContext/RestakeContext.ts index e0c6dcbed0..baedf58e73 100644 --- a/libs/tangle-shared-ui/src/context/RestakeContext/RestakeContext.ts +++ b/libs/tangle-shared-ui/src/context/RestakeContext/RestakeContext.ts @@ -2,12 +2,12 @@ import { createContext } from 'react'; import { of } from 'rxjs'; -import { AssetBalanceMap, RestakeVaultAssetMap } from '../../types/restake'; +import { AssetBalanceMap, RestakeVaultMap } from '../../types/restake'; import { RestakeContextType } from './types'; const RestakeContext = createContext({ - assetMetadataMap: {}, - assetMap$: of({}), + vaults: {}, + vaults$: of({}), balances: {}, balances$: of({}), assetWithBalances: [], diff --git a/libs/tangle-shared-ui/src/context/RestakeContext/RestakeContextProvider.tsx b/libs/tangle-shared-ui/src/context/RestakeContext/RestakeContextProvider.tsx index 21e9ea7d7a..aeb61164c4 100644 --- a/libs/tangle-shared-ui/src/context/RestakeContext/RestakeContextProvider.tsx +++ b/libs/tangle-shared-ui/src/context/RestakeContext/RestakeContextProvider.tsx @@ -6,21 +6,23 @@ import toPairs from 'lodash/toPairs'; import { useObservableState } from 'observable-hooks'; import { PropsWithChildren, useMemo } from 'react'; import { combineLatest, map } from 'rxjs'; -import useRestakeVaultAssets from '../../data/restake/useRestakeVaultAssets'; +import useRestakeVaults from '../../data/restake/useRestakeVaults'; import useRestakeBalances from '../../data/restake/useRestakeBalances'; import { AssetWithBalance } from '../../types/restake'; import RestakeContext from './RestakeContext'; +import assertRestakeAssetId from '../../utils/assertRestakeAssetId'; const RestakeContextProvider = (props: PropsWithChildren) => { - const { vaultAssets: assetMap, assetMap$ } = useRestakeVaultAssets(); + const { vaults, vaults$ } = useRestakeVaults(); const { balances, balances$ } = useRestakeBalances(); const assetWithBalances$ = useMemo( () => - combineLatest([assetMap$, balances$]).pipe( + combineLatest([vaults$, balances$]).pipe( map(([assetMap, balances]) => { const combined = toPairs(assetMap).reduce( - (assetWithBalances, [assetId, assetMetadata]) => { + (assetWithBalances, [assetIdString, assetMetadata]) => { + const assetId = assertRestakeAssetId(assetIdString); const balance = balances[assetId] ?? null; return assetWithBalances.concat({ @@ -47,7 +49,7 @@ const RestakeContextProvider = (props: PropsWithChildren) => { ]; }), ), - [assetMap$, balances$], + [vaults$, balances$], ); const assetWithBalances = useObservableState(assetWithBalances$, []); @@ -57,8 +59,8 @@ const RestakeContextProvider = (props: PropsWithChildren) => { value={{ assetWithBalances, assetWithBalances$, - assetMetadataMap: assetMap, - assetMap$, + vaults, + vaults$, balances, balances$, }} diff --git a/libs/tangle-shared-ui/src/context/RestakeContext/types.ts b/libs/tangle-shared-ui/src/context/RestakeContext/types.ts index bc33503f1e..f2c370feb4 100644 --- a/libs/tangle-shared-ui/src/context/RestakeContext/types.ts +++ b/libs/tangle-shared-ui/src/context/RestakeContext/types.ts @@ -1,7 +1,7 @@ import { Observable } from 'rxjs'; import { AssetBalanceMap, - RestakeVaultAssetMap, + RestakeVaultMap, AssetWithBalance, } from '../../types/restake'; @@ -9,12 +9,12 @@ export type RestakeContextType = { /** * The asset map for the current selected chain */ - assetMetadataMap: RestakeVaultAssetMap; + vaults: RestakeVaultMap; /** * An observable of the asset map for the current selected chain */ - assetMap$: Observable; + vaults$: Observable; /** * The balances of the current active account diff --git a/libs/tangle-shared-ui/src/data/blueprints/useBlueprintListing.ts b/libs/tangle-shared-ui/src/data/blueprints/useBlueprintListing.ts index 4db51a50e3..95ca193f98 100644 --- a/libs/tangle-shared-ui/src/data/blueprints/useBlueprintListing.ts +++ b/libs/tangle-shared-ui/src/data/blueprints/useBlueprintListing.ts @@ -7,7 +7,7 @@ import useNetworkStore from '../../context/useNetworkStore'; import useApiRx from '../../hooks/useApiRx'; import { TangleError, TangleErrorCode } from '../../types/error'; import { useOperatorTVL } from '../restake/useOperatorTVL'; -import useRestakeVaultAssets from '../restake/useRestakeVaultAssets'; +import useRestakeVaults from '../restake/useRestakeVaults'; import useRestakeOperatorMap from '../restake/useRestakeOperatorMap'; import { createBlueprintObjects, @@ -19,8 +19,8 @@ import { export default function useBlueprintListing() { const { rpcEndpoint } = useNetworkStore(); const { operatorMap } = useRestakeOperatorMap(); - const { vaultAssets: assetMap } = useRestakeVaultAssets(); - const { operatorTVL } = useOperatorTVL(operatorMap, assetMap); + const { vaults } = useRestakeVaults(); + const { operatorTVL } = useOperatorTVL(operatorMap, vaults); const { result, ...rest } = useApiRx( useCallback( diff --git a/libs/tangle-shared-ui/src/data/restake/useBlueprintDetails.ts b/libs/tangle-shared-ui/src/data/restake/useBlueprintDetails.ts index a6c1de89df..35b9e37ae5 100644 --- a/libs/tangle-shared-ui/src/data/restake/useBlueprintDetails.ts +++ b/libs/tangle-shared-ui/src/data/restake/useBlueprintDetails.ts @@ -12,7 +12,7 @@ import useApiRx from '../../hooks/useApiRx'; import { RestakeOperator } from '../../types'; import type { Blueprint } from '../../types/blueprint'; import { TangleError, TangleErrorCode } from '../../types/error'; -import type { RestakeVaultAssetMap, OperatorMap } from '../../types/restake'; +import type { RestakeVaultMap, OperatorMap } from '../../types/restake'; import { getAccountInfo, getMultipleAccountInfo, @@ -20,12 +20,12 @@ import { import delegationsToVaultTokens from '../../utils/restake/delegationsToVaultTokens'; import { extractOperatorData } from '../blueprints/utils/blueprintHelpers'; import { toPrimitiveBlueprint } from '../blueprints/utils/toPrimitiveBlueprint'; -import useRestakeVaultAssets from './useRestakeVaultAssets'; +import useRestakeVaults from './useRestakeVaults'; import useRestakeOperatorMap from './useRestakeOperatorMap'; export default function useBlueprintDetails(id?: string) { const { rpcEndpoint } = useNetworkStore(); - const { vaultAssets: assetMap } = useRestakeVaultAssets(); + const { vaults } = useRestakeVaults(); const { operatorMap } = useRestakeOperatorMap(); const { delegatorInfo } = useRestakeDelegatorInfo(); @@ -101,7 +101,7 @@ export default function useBlueprintDetails(id?: string) { operatorsSet !== undefined ? await getBlueprintOperators( rpcEndpoint, - assetMap, + vaults, operatorsSet, operatorMap, operatorTVL, @@ -117,14 +117,14 @@ export default function useBlueprintDetails(id?: string) { ); }, // prettier-ignore - [assetMap, id, operatorConcentration, operatorMap, operatorTVL, rpcEndpoint], + [vaults, id, operatorConcentration, operatorMap, operatorTVL, rpcEndpoint], ), ); } async function getBlueprintOperators( rpcEndpoint: string, - assetMap: RestakeVaultAssetMap, + assetMap: RestakeVaultMap, operatorAccountSet: Set, operatorMap: OperatorMap, operatorTVL: Record, diff --git a/libs/tangle-shared-ui/src/data/restake/useDelegatorTVL.ts b/libs/tangle-shared-ui/src/data/restake/useDelegatorTVL.ts index 25597d5f51..dab541b64a 100644 --- a/libs/tangle-shared-ui/src/data/restake/useDelegatorTVL.ts +++ b/libs/tangle-shared-ui/src/data/restake/useDelegatorTVL.ts @@ -1,11 +1,11 @@ import { useObservable, useObservableState } from 'observable-hooks'; import { of, switchMap } from 'rxjs'; -import type { RestakeVaultAssetMap, DelegatorInfo } from '../../types/restake'; +import type { RestakeVaultMap, DelegatorInfo } from '../../types/restake'; import safeFormatUnits from '../../utils/safeFormatUnits'; export function useDelegatorTVL( delegatorInfo: DelegatorInfo | null, - assetMap: RestakeVaultAssetMap, + assetMap: RestakeVaultMap, ) { const tvl$ = useObservable( (input$) => diff --git a/libs/tangle-shared-ui/src/data/restake/useOperatorTVL.ts b/libs/tangle-shared-ui/src/data/restake/useOperatorTVL.ts index f989feb004..d36a672def 100644 --- a/libs/tangle-shared-ui/src/data/restake/useOperatorTVL.ts +++ b/libs/tangle-shared-ui/src/data/restake/useOperatorTVL.ts @@ -1,11 +1,11 @@ import { useObservable, useObservableState } from 'observable-hooks'; import { of, switchMap } from 'rxjs'; -import { RestakeVaultAssetMap, OperatorMap } from '../../types/restake'; +import { RestakeVaultMap, OperatorMap } from '../../types/restake'; import safeFormatUnits from '../../utils/safeFormatUnits'; export function useOperatorTVL( operatorMap: OperatorMap, - assetMap: RestakeVaultAssetMap, + assetMap: RestakeVaultMap, ) { const tvl$ = useObservable( (input$) => diff --git a/libs/tangle-shared-ui/src/data/restake/useRestakeAssetIds.ts b/libs/tangle-shared-ui/src/data/restake/useRestakeAssetIds.ts index 88d92648b3..b66cb4e56a 100644 --- a/libs/tangle-shared-ui/src/data/restake/useRestakeAssetIds.ts +++ b/libs/tangle-shared-ui/src/data/restake/useRestakeAssetIds.ts @@ -5,10 +5,11 @@ import { map, type Observable } from 'rxjs'; import usePolkadotApi from '../../hooks/usePolkadotApi'; import { assetIdsQuery } from '../../queries/restake/assetIds'; import { rewardVaultRxQuery } from '../../queries/restake/rewardVault'; +import { RestakeAssetId } from '../../utils/createRestakeAssetId'; export type UseRestakeAssetIdsReturnType = { - assetIds: string[]; - assetIds$: Observable; + assetIds: RestakeAssetId[]; + assetIds$: Observable; }; /** @@ -21,9 +22,7 @@ export default function useRestakeAssetIds(): Evaluate rewardVaultRxQuery(apiRx).pipe( - map((rewardVaults) => - assetIdsQuery(rewardVaults).map((id) => id.toString()), - ), + map((rewardVaults) => assetIdsQuery(rewardVaults)), ), [apiRx], ); diff --git a/libs/tangle-shared-ui/src/data/restake/useRestakeBalances.ts b/libs/tangle-shared-ui/src/data/restake/useRestakeBalances.ts index ccdf55d20e..7db4324b54 100644 --- a/libs/tangle-shared-ui/src/data/restake/useRestakeBalances.ts +++ b/libs/tangle-shared-ui/src/data/restake/useRestakeBalances.ts @@ -9,6 +9,7 @@ import useSubstrateAddress from '../../hooks/useSubstrateAddress'; import type { AssetBalance, AssetBalanceMap } from '../../types/restake'; import hasAssetsPallet from '../../utils/hasAssetsPallet'; import filterNativeAsset from '../../utils/restake/filterNativeAsset'; +import { RestakeAssetId } from '../../utils/createRestakeAssetId'; export default function useRestakeBalances() { const { apiRx } = usePolkadotApi(); @@ -30,6 +31,7 @@ export default function useRestakeBalances() { } const { hasNative, nonNativeAssetIds } = filterNativeAsset(assetIds); + if (nonNativeAssetIds.length === 0) { return hasNative ? getNativeBalance$(apiRx, activeAccount) @@ -89,7 +91,7 @@ export default function useRestakeBalances() { function assetBalancesReducer( assetBalances: Option[], initialValue: AssetBalanceMap, - nonNativeAssetIds: string[], + nonNativeAssetIds: RestakeAssetId[], ) { return assetBalances.reduce( (assetBalanceMap, accountBalance, idx) => { @@ -120,7 +122,7 @@ function assetBalancesReducer( return Object.assign(assetBalanceMap, { [assetId]: { - assetId: assetId, + assetId, balance: balance.toBigInt(), status: status.type, existenceReason: toPrimitiveReason(reason), diff --git a/libs/tangle-shared-ui/src/data/restake/useRestakeOperatorMap.ts b/libs/tangle-shared-ui/src/data/restake/useRestakeOperatorMap.ts index d0c17acbbc..36769c4039 100644 --- a/libs/tangle-shared-ui/src/data/restake/useRestakeOperatorMap.ts +++ b/libs/tangle-shared-ui/src/data/restake/useRestakeOperatorMap.ts @@ -10,6 +10,7 @@ import { useMemo } from 'react'; import { map, type Observable, of } from 'rxjs'; import usePolkadotApi from '../../hooks/usePolkadotApi'; import { OperatorMap, OperatorMetadata } from '../../types/restake'; +import createRestakeAssetId from '../../utils/createRestakeAssetId'; type UseRestakeOperatorMapReturnType = { operatorMap: OperatorMap; @@ -37,6 +38,7 @@ export default function useRestakeOperatorMap(): UseRestakeOperatorMapReturnType const accountId = accountStorage.args[0]; const operator = operatorMetadata.unwrap(); + const { delegations, restakersCount } = toPrimitiveDelegations( operator.delegations, ); @@ -118,7 +120,7 @@ function toPrimitiveDelegations( return { amount: amount.toBigInt(), delegatorAccountId, - assetId: assetId.toString(), + assetId: createRestakeAssetId(assetId), } satisfies OperatorMetadata['delegations'][number]; }, ); diff --git a/libs/tangle-shared-ui/src/data/restake/useRestakeTVL.ts b/libs/tangle-shared-ui/src/data/restake/useRestakeTVL.ts index 731e46dfa7..e2adc859e9 100644 --- a/libs/tangle-shared-ui/src/data/restake/useRestakeTVL.ts +++ b/libs/tangle-shared-ui/src/data/restake/useRestakeTVL.ts @@ -8,16 +8,12 @@ export default function useRestakeTVL( operatorMap: OperatorMap, delegatorInfo: DelegatorInfo | null, ) { - const { assetMetadataMap } = useRestakeContext(); - - const { operatorTVL, vaultTVL } = useOperatorTVL( - operatorMap, - assetMetadataMap, - ); + const { vaults } = useRestakeContext(); + const { operatorTVL, vaultTVL } = useOperatorTVL(operatorMap, vaults); const { delegatorTVL, totalDelegatorTVL } = useDelegatorTVL( delegatorInfo, - assetMetadataMap, + vaults, ); const totalNetworkTVL = Object.values(vaultTVL).reduce( diff --git a/libs/tangle-shared-ui/src/data/restake/useRestakeVaultAssets.ts b/libs/tangle-shared-ui/src/data/restake/useRestakeVaults.ts similarity index 65% rename from libs/tangle-shared-ui/src/data/restake/useRestakeVaultAssets.ts rename to libs/tangle-shared-ui/src/data/restake/useRestakeVaults.ts index 5763dc8052..9f7e78b956 100644 --- a/libs/tangle-shared-ui/src/data/restake/useRestakeVaultAssets.ts +++ b/libs/tangle-shared-ui/src/data/restake/useRestakeVaults.ts @@ -3,11 +3,11 @@ import { useObservableState } from 'observable-hooks'; import { useMemo } from 'react'; import { mergeMap } from 'rxjs'; import usePolkadotApi from '../../hooks/usePolkadotApi'; -import { assetDetailsRxQuery } from '../../queries/restake/assetDetails'; +import { queryVaultsRx } from '../../queries/restake/assetDetails'; import { assetIdsRxQuery } from '../../queries/restake/assetIds'; import { rewardVaultRxQuery } from '../../queries/restake/rewardVault'; -const useRestakeVaultAssets = () => { +const useRestakeVaults = () => { const { apiRx } = usePolkadotApi(); const { activeChain } = useWebContext(); @@ -18,26 +18,22 @@ const useRestakeVaultAssets = () => { [rewardVault$], ); - const assetMap$ = useMemo( + const vaults$ = useMemo( () => assetIds$.pipe( mergeMap((assetIds) => - assetDetailsRxQuery( - apiRx, - assetIds.map((assetId) => assetId.toString()), - activeChain?.nativeCurrency, - ), + queryVaultsRx(apiRx, assetIds, activeChain?.nativeCurrency), ), ), [activeChain?.nativeCurrency, apiRx, assetIds$], ); - const vaultAssets = useObservableState(assetMap$, {}); + const vaults = useObservableState(vaults$, {}); return { - vaultAssets, - assetMap$, + vaults, + vaults$, }; }; -export default useRestakeVaultAssets; +export default useRestakeVaults; diff --git a/libs/tangle-shared-ui/src/queries/restake/assetDetails.ts b/libs/tangle-shared-ui/src/queries/restake/assetDetails.ts index f8fa492b31..75f1161a3c 100644 --- a/libs/tangle-shared-ui/src/queries/restake/assetDetails.ts +++ b/libs/tangle-shared-ui/src/queries/restake/assetDetails.ts @@ -3,20 +3,18 @@ import type { Option, u32 } from '@polkadot/types'; import type { PalletAssetsAssetDetails, PalletAssetsAssetMetadata, + PalletAssetsAssetStatus, } from '@polkadot/types/lookup'; -import { formatBalance, hexToString } from '@polkadot/util'; +import { BN, formatBalance, hexToString } from '@polkadot/util'; import type { Chain } from '@webb-tools/dapp-config'; import { combineLatest, map, Observable, of, switchMap } from 'rxjs'; -import { - RestakeVaultAssetMap, - RestakeVaultAssetMetadata, -} from '../../types/restake'; +import { RestakeVaultMap, RestakeVaultMetadata } from '../../types/restake'; import filterNativeAsset from '../../utils/restake/filterNativeAsset'; -import { - fetchSingleTokenPriceBySymbol, - fetchTokenPricesBySymbols, -} from '../../utils/fetchTokenPrices'; +import { fetchTokenPriceBySymbol } from '../../utils/fetchTokenPrices'; import assertRestakeAssetId from '../../utils/assertRestakeAssetId'; +import { RestakeAssetId } from '../../utils/createRestakeAssetId'; +import createAssetIdEnum from '../../utils/createAssetIdEnum'; +import { assertEvmAddress, isEvmAddress } from '@webb-tools/webb-ui-components'; function createVaultId(u32: Option): number | null { if (u32.isNone) { @@ -49,60 +47,76 @@ const DEFAULT_NATIVE_CURRENCY = { // Combined process function for both regular and Rx versions function createAssetMetadata( assetId: string, - detail: PalletAssetsAssetDetails, metadata: PalletAssetsAssetMetadata, vaultId: Option, priceInUsd: number | null, -): RestakeVaultAssetMetadata { + status?: PalletAssetsAssetStatus['type'], +): RestakeVaultMetadata { const name = hexToString(metadata.name.toHex()) || `Asset ${assetId}`; const symbol = hexToString(metadata.symbol.toHex()) || `${assetId}`; const decimals = metadata.decimals.toNumber(); return { - id: assertRestakeAssetId(assetId), + assetId: assertRestakeAssetId(assetId), name, symbol, decimals, - status: detail.status.type, + status, vaultId: createVaultId(vaultId), priceInUsd, - details: detail, - } satisfies RestakeVaultAssetMetadata; -} - -function queryTokenPrices( - nonNativeAssetIds: string[], - assetMetadatas: PalletAssetsAssetMetadata[], -) { - const tokenSymbols = nonNativeAssetIds.map((_, idx) => - hexToString(assetMetadatas[idx].symbol.toHex()), - ); - - return fetchTokenPricesBySymbols(tokenSymbols); + } satisfies RestakeVaultMetadata; } function processAssetDetailsRx( api: ApiRx, - nonNativeAssetIds: string[], + nonNativeAssetIds: RestakeAssetId[], assetDetails: Option[], assetMetadatas: PalletAssetsAssetMetadata[], assetVaultIds: Option[], hasNative: boolean, nativeCurrency: Chain['nativeCurrency'], -): Observable { +): Observable { return hasNative ? getNativeAssetRx(nativeCurrency, api).pipe( - map((nativeAsset) => ({ [nativeAsset.id]: nativeAsset })), + map((nativeAsset) => ({ [nativeAsset.assetId]: nativeAsset })), ) - : of({}).pipe( + : of({}).pipe( switchMap(async (initialAssetMap) => { - const tokenPrices = await queryTokenPrices( - nonNativeAssetIds, - assetMetadatas, - ); - return nonNativeAssetIds.reduce((assetMap, assetId, idx) => { - if (assetDetails[idx].isNone) { + // TODO: Implement price fetching. + // const price = await fetchTokenPriceBySymbol(erc20Token.symbol); + const price = null; + + if (isEvmAddress(assetId)) { + const erc20Token = { + name: "Yuri's Local ERC-2 Dummy", + symbol: 'USDC', + decimals: 18, + contractAddress: assertEvmAddress( + '0x2af9b184d0d42cd8d3c4fd0c953a06b6838c9357', + ), + }; + + if (erc20Token === null) { + return assetMap; + } + + return { + ...assetMap, + [assetId]: { + assetId, + name: erc20Token.name, + symbol: erc20Token.symbol, + decimals: erc20Token.decimals, + status: 'Live' as const, + vaultId: assetVaultIds[idx].unwrap().toNumber(), + priceInUsd: price, + } satisfies RestakeVaultMetadata, + }; + } else if ( + assetDetails[idx] === undefined || + assetDetails[idx].isNone + ) { return assetMap; } @@ -110,10 +124,10 @@ function processAssetDetailsRx( ...assetMap, [assetId]: createAssetMetadata( assetId, - assetDetails[idx].unwrap(), assetMetadatas[idx], assetVaultIds[idx], - typeof tokenPrices[idx] === 'number' ? tokenPrices[idx] : null, + price, + assetDetails[idx].unwrap().status.type, ), }; }, initialAssetMap); @@ -124,29 +138,27 @@ function processAssetDetailsRx( function getNativeAssetRx( nativeCurrency: Chain['nativeCurrency'], api: ApiRx, -): Observable { +): Observable { const assetId = 0; return api.query.rewards.assetLookupRewardVaults({ Custom: assetId }).pipe( switchMap(async (vaultId) => { - const priceInUsd = await fetchSingleTokenPriceBySymbol( - nativeCurrency.symbol, - ); + const priceInUsd = await fetchTokenPriceBySymbol(nativeCurrency.symbol); return { ...nativeCurrency, - id: `${assetId}`, + assetId: `${assetId}`, status: 'Live' as const, vaultId: createVaultId(vaultId), priceInUsd: typeof priceInUsd === 'number' ? priceInUsd : null, - } satisfies RestakeVaultAssetMetadata; + } satisfies RestakeVaultMetadata; }), ); } -export const assetDetailsRxQuery = ( +export const queryVaultsRx = ( api: ApiRx, - assetIds: string[], + assetIds: RestakeAssetId[], nativeCurrency: Chain['nativeCurrency'] = DEFAULT_NATIVE_CURRENCY, ) => { const { hasNative, nonNativeAssetIds } = filterNativeAsset(assetIds); @@ -155,46 +167,56 @@ export const assetDetailsRxQuery = ( if (isNonNativeAssetsEmpty || !isApiSupported(api)) { if (hasNative) { return getNativeAssetRx(nativeCurrency, api).pipe( - map((nativeAsset) => ({ [nativeAsset.id]: nativeAsset })), + map((nativeAsset) => ({ [nativeAsset.assetId]: nativeAsset })), ); } else { return of<{ - [assetId: string]: RestakeVaultAssetMetadata; + [assetId: RestakeAssetId]: RestakeVaultMetadata; }>({}); } } // Batch queries for asset details const assetDetailQueries = nonNativeAssetIds.reduce( - (batchQueries, assetId) => - batchQueries.concat([ - [api.query.assets.asset, { Custom: assetId.toString() }] as const, - ]), - [] as [typeof api.query.assets.asset, { Custom: string }][], + (batchQueries, assetId) => { + if (isEvmAddress(assetId)) { + return batchQueries; + } + + return batchQueries.concat([[api.query.assets.asset, new BN(assetId)]]); + }, + [] as [typeof api.query.assets.asset, BN][], ); + type MetadataBatchQueries = [ + typeof api.query.assets.metadata, + Parameters[0], + ][]; + // Batch queries for asset metadata const assetMetadataQueries = nonNativeAssetIds.reduce( - (batchQueries, assetId) => - batchQueries.concat([ - [api.query.assets.metadata, { Custom: assetId.toString() }] as const, - ]), - [] as [typeof api.query.assets.metadata, { Custom: string }][], + (batchQueries: MetadataBatchQueries, assetId) => { + if (isEvmAddress(assetId)) { + return batchQueries; + } + + return batchQueries.concat([[api.query.assets.metadata, assetId]]); + }, + [], ); + type VaultIdQueries = [ + typeof api.query.rewards.assetLookupRewardVaults, + Parameters[0], + ][]; + // Batch queries for asset vault ID const assetVaultIdQueries = nonNativeAssetIds.reduce( - (batchQueries, assetId) => + (batchQueries: VaultIdQueries, assetId) => batchQueries.concat([ - [ - api.query.rewards.assetLookupRewardVaults, - { Custom: assetId }, - ] as const, + [api.query.rewards.assetLookupRewardVaults, createAssetIdEnum(assetId)], ]), - [] as [ - typeof api.query.rewards.assetLookupRewardVaults, - { Custom: string }, - ][], + [], ); const assetDetails$ = @@ -203,7 +225,6 @@ export const assetDetailsRxQuery = ( const assetMetadatas$ = api.queryMulti(assetMetadataQueries); - // TODO: Wrong type. Affected by {Custom:...} bug? const assetVaultIds$ = api.queryMulti[]>(assetVaultIdQueries); return combineLatest([assetDetails$, assetMetadatas$, assetVaultIds$]).pipe( diff --git a/libs/tangle-shared-ui/src/types/index.ts b/libs/tangle-shared-ui/src/types/index.ts index f1d5bc0a9e..1f522be2d3 100644 --- a/libs/tangle-shared-ui/src/types/index.ts +++ b/libs/tangle-shared-ui/src/types/index.ts @@ -99,7 +99,7 @@ export interface BridgeToken { abi: Abi; decimals: number; chainId: PresetTypedChainId; - hyperlaneRouteContractAddress?: EvmAddress; + hyperlaneSyntheticAddress?: EvmAddress; } export type BridgeChainsConfigType = Record< @@ -114,6 +114,7 @@ export type BridgeChainsConfigType = Record< export type BridgeTokenWithBalance = BridgeToken & { balance: Decimal; + syntheticBalance?: Decimal; }; export type BridgeChainBalances = Partial< diff --git a/libs/tangle-shared-ui/src/types/restake.ts b/libs/tangle-shared-ui/src/types/restake.ts index f22de811fb..66bae8181a 100644 --- a/libs/tangle-shared-ui/src/types/restake.ts +++ b/libs/tangle-shared-ui/src/types/restake.ts @@ -9,6 +9,7 @@ import { import { TransformEnum } from './utils'; import { SubstrateAddress } from '@webb-tools/webb-ui-components/types/address'; import { RestakeAssetId } from '../utils/createRestakeAssetId'; +import { BN } from '@polkadot/util'; /** * The activity status of the operator. @@ -24,7 +25,7 @@ export type OperatorStatus = export type OperatorDelegatorBond = { readonly delegatorAccountId: string; readonly amount: bigint; - readonly assetId: string; + readonly assetId: RestakeAssetId; }; export type OperatorBondLessRequest = { @@ -47,11 +48,11 @@ export type OperatorMetadata = { }; export type OperatorMap = { - readonly [accountId: SubstrateAddress]: OperatorMetadata; + readonly [accountAddress: SubstrateAddress]: OperatorMetadata; }; -export type RestakeVaultAssetMetadata = Readonly<{ - id: RestakeAssetId; +export type RestakeVaultMetadata = Readonly<{ + assetId: RestakeAssetId; name: string; symbol: string; decimals: number; @@ -66,11 +67,11 @@ export type RestakeVaultAssetMetadata = Readonly<{ * @field Frozen - The asset is frozen and cannot be staked. * @field Destroying - The asset is being destroyed and cannot be staked. */ - status: TransformEnum; + status?: PalletAssetsAssetStatus['type']; }>; -export type RestakeVaultAssetMap = { - readonly [assetId: string]: RestakeVaultAssetMetadata; +export type RestakeVaultMap = { + readonly [assetId: RestakeAssetId]: RestakeVaultMetadata; }; export type DelegatorWithdrawRequest = { @@ -110,11 +111,8 @@ export type DelegatorInfo = { }; readonly withdrawRequests: Array; - readonly delegations: Array; - readonly unstakeRequests: Array; - readonly status: DelegatorStatus; }; @@ -136,7 +134,7 @@ export type AssetAccountExistenceReason = * @name PalletAssetsAssetAccount */ export type AssetBalance = { - readonly assetId: string; + readonly assetId: RestakeAssetId; readonly balance: bigint; /** @@ -152,11 +150,19 @@ export type AssetBalance = { }; export type AssetBalanceMap = { - readonly [assetId: string]: AssetBalance; + readonly [assetId: RestakeAssetId]: AssetBalance; }; export type AssetWithBalance = { - assetId: string; - metadata: RestakeVaultAssetMetadata; + assetId: RestakeAssetId; + metadata: RestakeVaultMetadata; balance: AssetBalance | null; }; + +export type RestakeAsset = { + id: RestakeAssetId; + name: string; + symbol: string; + balance: BN; + decimals: number; +}; diff --git a/libs/tangle-shared-ui/src/utils/createAssetIdEnum.ts b/libs/tangle-shared-ui/src/utils/createAssetIdEnum.ts new file mode 100644 index 0000000000..2411e2d923 --- /dev/null +++ b/libs/tangle-shared-ui/src/utils/createAssetIdEnum.ts @@ -0,0 +1,17 @@ +import { EvmAddress } from '@webb-tools/webb-ui-components/types/address'; +import { RestakeAssetId } from './createRestakeAssetId'; +import { isEvmAddress } from '@webb-tools/webb-ui-components'; + +export type AssetIdEnum = + | { + Custom: `${number}`; + } + | { + Erc20: EvmAddress; + }; + +const createAssetIdEnum = (assetId: RestakeAssetId): AssetIdEnum => { + return isEvmAddress(assetId) ? { Erc20: assetId } : { Custom: assetId }; +}; + +export default createAssetIdEnum; diff --git a/libs/tangle-shared-ui/src/utils/createRestakeAssetId.ts b/libs/tangle-shared-ui/src/utils/createRestakeAssetId.ts index 59d35b7c2f..59d59996fc 100644 --- a/libs/tangle-shared-ui/src/utils/createRestakeAssetId.ts +++ b/libs/tangle-shared-ui/src/utils/createRestakeAssetId.ts @@ -2,7 +2,7 @@ import { u128 } from '@polkadot/types'; import { TanglePrimitivesServicesAsset } from '@polkadot/types/lookup'; import { assertEvmAddress } from '@webb-tools/webb-ui-components'; import { EvmAddress } from '@webb-tools/webb-ui-components/types/address'; -import { Address } from 'viem'; +import { Address, checksumAddress } from 'viem'; export type RestakeAssetId = `${number}` | EvmAddress; @@ -15,7 +15,7 @@ const createRestakeAssetId = ( case 'Custom': return `${tangleAssetId.asCustom.toNumber()}`; case 'Erc20': - return assertEvmAddress(tangleAssetId.asErc20.toHex()); + return assertEvmAddress(checksumAddress(tangleAssetId.asErc20.toHex())); } }; diff --git a/libs/tangle-shared-ui/src/utils/ensureError.ts b/libs/tangle-shared-ui/src/utils/ensureError.ts index 2dcb566678..6228a3c795 100644 --- a/libs/tangle-shared-ui/src/utils/ensureError.ts +++ b/libs/tangle-shared-ui/src/utils/ensureError.ts @@ -1,9 +1,11 @@ -import { isErrorInstance } from '@webb-tools/web3-api-provider'; -import isViemError from '@webb-tools/web3-api-provider/utils/isViemError'; -import { BaseError } from 'wagmi'; +import { BaseError as ViemError } from 'viem'; +import { BaseError as WagmiError } from 'wagmi'; function ensureError(possibleError: unknown): Error { - if (isViemError(possibleError) || isErrorInstance(possibleError, BaseError)) { + if ( + possibleError instanceof ViemError || + possibleError instanceof WagmiError + ) { return new Error(possibleError.shortMessage); } else if (typeof possibleError === 'string') { return new Error(possibleError); diff --git a/libs/tangle-shared-ui/src/utils/fetchTokenPrices.ts b/libs/tangle-shared-ui/src/utils/fetchTokenPrices.ts index 33837403c5..a0f3296880 100644 --- a/libs/tangle-shared-ui/src/utils/fetchTokenPrices.ts +++ b/libs/tangle-shared-ui/src/utils/fetchTokenPrices.ts @@ -4,81 +4,52 @@ import axios from 'axios'; const COINGECKO_API_BASE_URL = 'https://api.coingecko.com/api/v3/simple/price'; const CURRENCY = 'usd'; -export enum CoingeckoTokenId { +export enum CoinGeckoTokenId { ETH = 'ethereum', BTC = 'bitcoin', USDT = 'tether', } -const SYMBOL_MAP: Record = { +const SYMBOL_MAP: Record = { // TODO: Add a list of token symbols which will be available on testnet/mainnet - eth: CoingeckoTokenId.ETH, + eth: CoinGeckoTokenId.ETH, }; -// TODO: Properly implement this function, right now it's a bit wacky, and the logic doesn't work as expected. -export const fetchTokenPricesBySymbols = async ( - tokenSymbols: string[], -): Promise<(number | Error)[]> => { - const coingeckoTokenIds: (CoingeckoTokenId | null)[] = tokenSymbols.map( - (symbol) => SYMBOL_MAP[symbol] ?? null, - ); - - const errors = coingeckoTokenIds - .map((result, index) => { - if (result === null) { - return new Error( - `No Coingecko token ID found for symbol: ${tokenSymbols[index]}`, - ); - } - - return null; - }) - .filter((error): error is Error => error !== null); - - if (errors.length > 0) { - return errors; - } - - return fetchTokenPrices( - coingeckoTokenIds.filter((id): id is CoingeckoTokenId => id !== null), - ); -}; - -export const fetchTokenPrices = async ( - tokenIds: CoingeckoTokenId[], -): Promise<(number | Error)[]> => { +export const fetchTokenPrice = async ( + tokenId: CoinGeckoTokenId, +): Promise => { try { const endpointUrl = new URL(COINGECKO_API_BASE_URL); - endpointUrl.searchParams.append('ids', tokenIds.join(',')); + endpointUrl.searchParams.append('ids', tokenId); endpointUrl.searchParams.append('vs_currencies', CURRENCY); const response = await axios.get< - Record + Record >(endpointUrl.toString()); - return tokenIds.map((requestedTokenId) => { - const prices = response.data[requestedTokenId]; + const prices = response.data[tokenId]; - if (prices === undefined) { - return new Error( - `Token "${requestedTokenId}" not found in the response`, - ); - } else if (!(CURRENCY in prices)) { - return new Error(`Currency "${CURRENCY}" not found in the response`); - } + if (prices === undefined) { + return new Error(`Token "${tokenId}" not found in the response`); + } else if (!(CURRENCY in prices)) { + return new Error(`Currency "${CURRENCY}" not found in the response`); + } - return prices[CURRENCY]; - }); + return prices[CURRENCY]; } catch (possibleError) { - return tokenIds.map(() => ensureError(possibleError)); + return ensureError(possibleError); } }; -export const fetchSingleTokenPriceBySymbol = async ( +export const fetchTokenPriceBySymbol = async ( tokenSymbol: string, ): Promise => { - const [result] = await fetchTokenPricesBySymbols([tokenSymbol]); + const coingeckoTokenId = SYMBOL_MAP[tokenSymbol] ?? null; + + if (coingeckoTokenId === null) { + return new Error(`No CoinGecko token ID found for symbol: ${tokenSymbol}`); + } - return result; + return fetchTokenPrice(coingeckoTokenId); }; diff --git a/libs/tangle-shared-ui/src/utils/restake/delegationsToVaultTokens.ts b/libs/tangle-shared-ui/src/utils/restake/delegationsToVaultTokens.ts index 3e1ef0801a..5e38f0db21 100644 --- a/libs/tangle-shared-ui/src/utils/restake/delegationsToVaultTokens.ts +++ b/libs/tangle-shared-ui/src/utils/restake/delegationsToVaultTokens.ts @@ -1,13 +1,10 @@ import { VaultToken } from '../../types'; -import { - RestakeVaultAssetMap, - OperatorDelegatorBond, -} from '../../types/restake'; +import { RestakeVaultMap, OperatorDelegatorBond } from '../../types/restake'; import safeFormatUnits from '../safeFormatUnits'; export default function delegationsToVaultTokens( delegations: OperatorDelegatorBond[], - assetMap: RestakeVaultAssetMap, + assetMap: RestakeVaultMap, ) { return delegations.reduce( (vaultTokenArr, { assetId, amount }) => { diff --git a/libs/tangle-shared-ui/src/utils/restake/filterNativeAsset.ts b/libs/tangle-shared-ui/src/utils/restake/filterNativeAsset.ts index 0cab5a816d..4e662994d1 100644 --- a/libs/tangle-shared-ui/src/utils/restake/filterNativeAsset.ts +++ b/libs/tangle-shared-ui/src/utils/restake/filterNativeAsset.ts @@ -1,3 +1,5 @@ +import { RestakeAssetId } from '../createRestakeAssetId'; + /** * By convention, the native asset ID is `0`. * This function filters out the native asset ID from the list of asset IDs. @@ -7,13 +9,14 @@ * - `hasNative`: Whether the native asset ID is present. * - `nonNativeIds`: The non-native asset IDs. */ -export default function filterNativeAsset(assetIds: string[]) { +export default function filterNativeAsset(assetIds: RestakeAssetId[]) { let hasNative = false; // Filter out the native asset ID const nonNativeAssetIds = assetIds.filter((assetId) => { if (assetId === '0') { hasNative = true; + return false; } diff --git a/libs/web3-api-provider/src/index.ts b/libs/web3-api-provider/src/index.ts index b88c1be4e3..be8a794def 100644 --- a/libs/web3-api-provider/src/index.ts +++ b/libs/web3-api-provider/src/index.ts @@ -2,5 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 export * from './ext-provider'; -export * from './utils'; export * from './webb-provider'; diff --git a/libs/web3-api-provider/src/utils/getViemChain.ts b/libs/web3-api-provider/src/utils/getViemChain.ts deleted file mode 100644 index b41b923a59..0000000000 --- a/libs/web3-api-provider/src/utils/getViemChain.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { - LOCALNET_CHAIN_IDS, - chainsConfig, -} from '@webb-tools/dapp-config/chains'; -import { DEFAULT_EVM_CURRENCY } from '@webb-tools/dapp-config/constants'; -import * as chains from 'viem/chains'; - -// At the time of writing, Viem does not support multicall for these chains. -const VIEM_NOT_SUPPORTED_MULTICALL_CHAINS = LOCALNET_CHAIN_IDS; - -/** - * Gets the chain object for the given chain id. - * @param chainId - Chain id of the target EVM chain. - * @returns Viem's chain object. - */ -function getViemChain(chainId: number): chains.Chain | undefined { - for (const chain of Object.values(chains)) { - if (chain.id === chainId) { - return chain; - } - } -} - -/** - * Defines the Viem chain object from the org chain config. - * @returns Viem's chain object. - */ -function defineViemChain(typedChainId: number): chains.Chain { - const chain = chainsConfig[typedChainId]; - if (!chain) { - throw new Error('Chain not found in the chainsConfig'); - } - - if (!chain.group) { - throw new Error(`Chain ${chain.name} does not have a base network`); - } - - if (!chain.rpcUrls) { - throw new Error(`Chain ${chain.name} does not have rpc urls`); - } - - return { - id: chain.id, - name: chain.name, - testnet: true, - nativeCurrency: DEFAULT_EVM_CURRENCY, - rpcUrls: chain.rpcUrls, - blockExplorers: chain.blockExplorers, - contracts: chain.contracts, - } as const satisfies chains.Chain; -} - -export { VIEM_NOT_SUPPORTED_MULTICALL_CHAINS, defineViemChain, getViemChain }; diff --git a/libs/web3-api-provider/src/utils/getViemClient.ts b/libs/web3-api-provider/src/utils/getViemClient.ts deleted file mode 100644 index d76166a1ec..0000000000 --- a/libs/web3-api-provider/src/utils/getViemClient.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { chainsConfig } from '@webb-tools/dapp-config/chains/evm'; -import { parseTypedChainId } from '@webb-tools/dapp-types/TypedChainId'; -import { - createPublicClient, - fallback, - http, - type Chain, - type FallbackTransport, - type PublicClient, -} from 'viem'; -import { - VIEM_NOT_SUPPORTED_MULTICALL_CHAINS, - defineViemChain, - getViemChain, -} from './getViemChain'; - -function getViemClient( - typedChainId: number, -): PublicClient { - const { chainId } = parseTypedChainId(typedChainId); - - let chain: Chain | undefined = chainsConfig[typedChainId]; - - if (!chain) { - chain = getViemChain(typedChainId); - } - - if (!chain || VIEM_NOT_SUPPORTED_MULTICALL_CHAINS.includes(chainId)) { - chain = defineViemChain(typedChainId); - } - - return createPublicClient({ - chain: chain, - batch: { - multicall: !!chain.contracts?.multicall3, - }, - transport: fallback( - chain.rpcUrls.default.http.map((url) => http(url, { timeout: 60_000 })), - ), - }); -} - -export default getViemClient; diff --git a/libs/web3-api-provider/src/utils/index.ts b/libs/web3-api-provider/src/utils/index.ts deleted file mode 100644 index e63ffeb5bb..0000000000 --- a/libs/web3-api-provider/src/utils/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as getViemClient } from './getViemClient'; -export { default as isViemError } from './isViemError'; -export { default as isErrorInstance } from './isErrorInstance'; diff --git a/libs/web3-api-provider/src/utils/isErrorInstance.ts b/libs/web3-api-provider/src/utils/isErrorInstance.ts deleted file mode 100644 index 52c9d07267..0000000000 --- a/libs/web3-api-provider/src/utils/isErrorInstance.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Check if an unknown error is an instance of the provided error class - * with type assertion - * @param error the error to check - * @param ErrorClass the error class to check against and perform type assertion - * @returns whether the error is an instance of the provided error class with type assertion - */ -function isErrorInstance( - error: unknown, - ErrorClass: new (...args: any[]) => T, -): error is T { - return error instanceof ErrorClass; -} - -export default isErrorInstance; diff --git a/libs/web3-api-provider/src/utils/isViemError.ts b/libs/web3-api-provider/src/utils/isViemError.ts deleted file mode 100644 index 7e9e06ec6d..0000000000 --- a/libs/web3-api-provider/src/utils/isViemError.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { BaseError } from 'viem'; - -/** - * Check if an unknown error is a Viem error - * with type assertion - * @param error the error to check - * @param checkFn a function to check the error type more specifically - * @returns true if the error is a Viem error - */ -const isViemError = (error: unknown): error is BaseError => { - return error instanceof BaseError; -}; - -export default isViemError; diff --git a/libs/webb-ui-components/src/utils/assertEvmAddress.ts b/libs/webb-ui-components/src/utils/assertEvmAddress.ts index 390838071e..9e4aec21a5 100644 --- a/libs/webb-ui-components/src/utils/assertEvmAddress.ts +++ b/libs/webb-ui-components/src/utils/assertEvmAddress.ts @@ -1,6 +1,7 @@ import assert from 'assert'; import { EvmAddress } from '../types/address'; import { isEvmAddress } from './isEvmAddress20'; +import { checksumAddress } from 'viem'; export const assertEvmAddress = (address: string): EvmAddress => { assert( @@ -8,5 +9,7 @@ export const assertEvmAddress = (address: string): EvmAddress => { `Address should be a valid EVM address, but got ${address}`, ); - return address as EvmAddress; + // Normalize the address to checksum format. This helps with + // consistency and possible issues when comparing addresses. + return checksumAddress(address) as EvmAddress; };