0 &&
- 'flex flex-col justify-between min-h-[500px]',
+ isDataState && 'flex flex-col justify-between min-h-[500px]',
)}
>
{table}
- {rows !== null && rows.length > 0 && (
+ {isDataState && (
-
-
-
+ {isLsParachainChainId(selectedProtocolId) && (
+
+ )}
+
+ {isLsParachainChainId(selectedProtocolId) ? (
+
+ ) : (
+
+ )}
)}
diff --git a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/WithdrawLstUnstakeRequestButton.tsx b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/WithdrawLstUnstakeRequestButton.tsx
index 095c290a3a..bd1f8a8eab 100644
--- a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/WithdrawLstUnstakeRequestButton.tsx
+++ b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/WithdrawLstUnstakeRequestButton.tsx
@@ -30,7 +30,7 @@ const WithdrawLstUnstakeRequestButton: FC<
}
}, [withdrawRedeemTxStatus]);
- const handleConfirmation = useCallback(() => {
+ const handleClick = useCallback(() => {
// The button should have been disabled if this was null.
assert(
executeWithdrawRedeemTx !== null,
@@ -43,10 +43,16 @@ const WithdrawLstUnstakeRequestButton: FC<
return (
<>
diff --git a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/WithdrawUnlockNftButton.tsx b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/WithdrawUnlockNftButton.tsx
new file mode 100644
index 0000000000..03ea896616
--- /dev/null
+++ b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/WithdrawUnlockNftButton.tsx
@@ -0,0 +1,60 @@
+import { Button } from '@webb-tools/webb-ui-components';
+import { FC, useCallback, useState } from 'react';
+
+import { LsLiquifierProtocolId } from '../../../constants/liquidStaking/types';
+import useLiquifierWithdraw from '../../../data/liquifier/useLiquifierWithdraw';
+
+type WithdrawUnlockNftButtonProps = {
+ tokenId: LsLiquifierProtocolId;
+ canWithdraw: boolean;
+ unlockIds: number[];
+};
+
+const WithdrawUnlockNftButton: FC
= ({
+ tokenId,
+ canWithdraw,
+ unlockIds,
+}) => {
+ const [isProcessing, setIsProcessing] = useState(false);
+ const withdraw = useLiquifierWithdraw();
+
+ const handleClick = useCallback(async () => {
+ if (withdraw === null) {
+ return;
+ }
+
+ setIsProcessing(true);
+
+ for (const [index, unlockId] of unlockIds.entries()) {
+ const success = await withdraw(tokenId, unlockId, {
+ current: index + 1,
+ total: unlockIds.length,
+ });
+
+ if (!success) {
+ console.error(
+ 'Liquifier withdraw batch was aborted because one request failed',
+ );
+
+ break;
+ }
+ }
+
+ setIsProcessing(false);
+ }, [tokenId, unlockIds, withdraw]);
+
+ return (
+
+ );
+};
+
+export default WithdrawUnlockNftButton;
diff --git a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/useLstUnlockRequestTableRows.ts b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/useLstUnlockRequestTableRows.ts
index 0d32a5fdda..3534fae535 100644
--- a/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/useLstUnlockRequestTableRows.ts
+++ b/apps/tangle-dapp/components/LiquidStaking/unstakeRequestsTable/useLstUnlockRequestTableRows.ts
@@ -2,17 +2,16 @@ import assert from 'assert';
import { useMemo } from 'react';
import { LS_PARACHAIN_CHAIN_MAP } from '../../../constants/liquidStaking/constants';
-import { LsSimpleParachainTimeUnit } from '../../../constants/liquidStaking/types';
+import { LsParachainSimpleTimeUnit } from '../../../constants/liquidStaking/types';
import useLstUnlockRequests from '../../../data/liquidStaking/useLstUnlockRequests';
import useOngoingTimeUnits from '../../../data/liquidStaking/useOngoingTimeUnits';
-import { AnySubstrateAddress } from '../../../types/utils';
-import { UnstakeRequestTableRow } from './UnstakeRequestsTable';
+import { ParachainUnstakeRequest } from './UnstakeRequestsTable';
const useLstUnlockRequestTableRows = () => {
const tokenUnlockLedger = useLstUnlockRequests();
const ongoingTimeUnits = useOngoingTimeUnits();
- const rows = useMemo(() => {
+ const rows = useMemo(() => {
// Data not loaded yet.
if (tokenUnlockLedger === null || ongoingTimeUnits === null) {
return null;
@@ -54,7 +53,7 @@ const useLstUnlockRequestTableRows = () => {
const remainingTimeUnitValue =
request.unlockTimeUnit.value - ongoingTimeUnitEntry.timeUnit.value;
- const remainingTimeUnit: LsSimpleParachainTimeUnit | undefined =
+ const remainingTimeUnit: LsParachainSimpleTimeUnit | undefined =
remainingTimeUnitValue <= 0
? undefined
: {
@@ -63,14 +62,13 @@ const useLstUnlockRequestTableRows = () => {
};
return {
+ type: 'parachainUnstakeRequest',
unlockId: request.unlockId,
amount: request.amount,
- // TODO: Using dummy address for now.
- address: '0xef1234567890abcdef123456789' as AnySubstrateAddress,
currency: request.currency,
decimals: chain.decimals,
- remainingTimeUnit,
- } satisfies UnstakeRequestTableRow;
+ progress: remainingTimeUnit,
+ } satisfies ParachainUnstakeRequest;
});
}, [ongoingTimeUnits, tokenUnlockLedger]);
diff --git a/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx b/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx
index fbb07992ed..2fc775a9ce 100644
--- a/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx
+++ b/apps/tangle-dapp/components/NetworkSelector/NetworkSelectionButton.tsx
@@ -23,11 +23,11 @@ import { twMerge } from 'tailwind-merge';
import { IS_PRODUCTION_ENV } from '../../constants/env';
import useNetworkStore from '../../context/useNetworkStore';
-import { useLiquidStakingStore } from '../../data/liquidStaking/useLiquidStakingStore';
+import { useLsStore } from '../../data/liquidStaking/useLsStore';
import useNetworkSwitcher from '../../hooks/useNetworkSwitcher';
import { PagePath } from '../../types';
import createCustomNetwork from '../../utils/createCustomNetwork';
-import isLsErc20TokenId from '../../utils/liquidStaking/isLsErc20TokenId';
+import isLiquifierProtocolId from '../../utils/liquidStaking/isLiquifierProtocolId';
import { NetworkSelectorDropdown } from './NetworkSelectorDropdown';
// TODO: Currently hard-coded, but shouldn't it always be the Tangle icon, since it's not switching chains but rather networks within Tangle? If so, find some constant somewhere instead of having it hard-coded here.
@@ -40,7 +40,7 @@ const NetworkSelectionButton: FC = () => {
const { network } = useNetworkStore();
const { switchNetwork, isCustom } = useNetworkSwitcher();
const pathname = usePathname();
- const { selectedProtocolId } = useLiquidStakingStore();
+ const { selectedProtocolId } = useLsStore();
// TODO: Handle switching network on EVM wallet here.
const switchToCustomNetwork = useCallback(
@@ -99,13 +99,13 @@ const NetworkSelectionButton: FC = () => {
// Network can't be switched from the Tangle Restaking Parachain while
// on liquid staking page.
else if (isInLiquidStakingPath) {
- const liquidStakingNetworkName = isLsErc20TokenId(selectedProtocolId)
+ const liquidStakingNetworkName = isLiquifierProtocolId(selectedProtocolId)
? IS_PRODUCTION_ENV
? 'Ethereum Mainnet'
: 'Sepolia Testnet'
: TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK.name;
- const chainIconName = isLsErc20TokenId(selectedProtocolId)
+ const chainIconName = isLiquifierProtocolId(selectedProtocolId)
? 'ethereum'
: TANGLE_TESTNET_CHAIN_NAME;
diff --git a/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx b/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx
index 40bbca1501..7ed0a40ed1 100644
--- a/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx
+++ b/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx
@@ -37,7 +37,7 @@ import { ValidatorTableProps } from './types';
const columnHelper = createColumnHelper();
-const getStaticColumns = (isWaiting?: boolean) => [
+const getTableColumns = (isWaiting?: boolean) => [
// Hide the effective amount staked and self-staked columns on waiting validators tab
// as they don't have values for these columns
...(isWaiting
@@ -167,7 +167,7 @@ const ValidatorTable: FC = ({
);
},
}),
- ...getStaticColumns(isWaiting),
+ ...getTableColumns(isWaiting),
],
[isWaiting, network.nativeExplorerUrl, network.polkadotJsDashboardUrl],
);
diff --git a/apps/tangle-dapp/constants/index.ts b/apps/tangle-dapp/constants/index.ts
index 29b912984a..62c5d10722 100644
--- a/apps/tangle-dapp/constants/index.ts
+++ b/apps/tangle-dapp/constants/index.ts
@@ -60,6 +60,7 @@ export enum TxName {
LS_LIQUIFIER_APPROVE = 'approve spending for liquifier',
LS_LIQUIFIER_DEPOSIT = 'liquifier deposit',
LS_LIQUIFIER_UNLOCK = 'liquifier unlock',
+ LS_LIQUIFIER_WITHDRAW = 'liquifier withdraw',
}
export const PAYMENT_DESTINATION_OPTIONS: StakingRewardsDestinationDisplayText[] =
diff --git a/apps/tangle-dapp/constants/liquidStaking/constants.ts b/apps/tangle-dapp/constants/liquidStaking/constants.ts
index b1236ed4a9..7b8c1c6ab8 100644
--- a/apps/tangle-dapp/constants/liquidStaking/constants.ts
+++ b/apps/tangle-dapp/constants/liquidStaking/constants.ts
@@ -1,12 +1,18 @@
-import { HexString } from '@polkadot/util/types';
-import { TANGLE_TOKEN_DECIMALS } from '@webb-tools/dapp-config';
-import { TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK } from '@webb-tools/webb-ui-components/constants/networks';
-
-import { CrossChainTimeUnit } from '../../utils/CrossChainTime';
+import ASTAR from '../../data/liquidStaking/adapters/astar';
+import CHAINLINK from '../../data/liquidStaking/adapters/chainlink';
+import LIVEPEER from '../../data/liquidStaking/adapters/livepeer';
+import MANTA from '../../data/liquidStaking/adapters/manta';
+import MOONBEAM from '../../data/liquidStaking/adapters/moonbeam';
+import PHALA from '../../data/liquidStaking/adapters/phala';
+import POLKADOT from '../../data/liquidStaking/adapters/polkadot';
+import POLYGON from '../../data/liquidStaking/adapters/polygon';
+import THE_GRAPH from '../../data/liquidStaking/adapters/theGraph';
import { IS_PRODUCTION_ENV } from '../env';
import {
- LsErc20TokenDef,
- LsErc20TokenId,
+ LsLiquifierProtocolDef,
+ LsLiquifierProtocolId,
+ LsNetwork,
+ LsNetworkId,
LsParachainChainDef,
LsParachainChainId,
LsParachainToken,
@@ -15,185 +21,11 @@ import {
LsToken,
} from './types';
-/**
- * Development only. Sepolia testnet contracts that were
- * deployed to test the liquifier functionality. These contracts
- * use dummy data.
- */
-const SEPOLIA_TESTNET_CONTRACTS = {
- LIQUIFIER: '0xCE0148DbA55c9719B59642f5cBf4F26e67F44E70',
- ERC20: '0x2eE951c2d215ba1b3E0DF20764c96a0bC7809F41',
- // Use the same address as the dummy ERC20 contract.
- TG_TOKEN: '0x2eE951c2d215ba1b3E0DF20764c96a0bC7809F41',
-} as const satisfies Record;
-
-const CHAINLINK: LsErc20TokenDef = {
- type: 'erc20',
- id: LsProtocolId.CHAINLINK,
- name: 'Chainlink',
- chainIconFileName: 'chainlink',
- token: LsToken.LINK,
- decimals: 18,
- address: IS_PRODUCTION_ENV
- ? '0x514910771AF9Ca656af840dff83E8264EcF986CA'
- : SEPOLIA_TESTNET_CONTRACTS.ERC20,
- // TODO: Use the actual Chainlink Liquifier Adapter address. This is likely deployed to a testnet (Tenderly?).
- liquifierAdapterAddress: IS_PRODUCTION_ENV
- ? '0x'
- : SEPOLIA_TESTNET_CONTRACTS.LIQUIFIER,
- liquifierTgTokenAddress: IS_PRODUCTION_ENV
- ? '0x'
- : SEPOLIA_TESTNET_CONTRACTS.TG_TOKEN,
- timeUnit: CrossChainTimeUnit.DAY,
- unstakingPeriod: 7,
-};
-
-const THE_GRAPH: LsErc20TokenDef = {
- type: 'erc20',
- id: LsProtocolId.THE_GRAPH,
- name: 'The Graph',
- chainIconFileName: 'the-graph',
- token: LsToken.GRT,
- decimals: 18,
- address: IS_PRODUCTION_ENV
- ? '0xc944E90C64B2c07662A292be6244BDf05Cda44a7'
- : SEPOLIA_TESTNET_CONTRACTS.ERC20,
- // TODO: Use the actual Chainlink Liquifier Adapter address. This is likely deployed to a testnet (Tenderly?).
- liquifierAdapterAddress: IS_PRODUCTION_ENV
- ? '0x'
- : SEPOLIA_TESTNET_CONTRACTS.LIQUIFIER,
- liquifierTgTokenAddress: IS_PRODUCTION_ENV
- ? '0x'
- : SEPOLIA_TESTNET_CONTRACTS.TG_TOKEN,
- timeUnit: CrossChainTimeUnit.DAY,
- unstakingPeriod: 28,
-};
-
-const LIVEPEER: LsErc20TokenDef = {
- type: 'erc20',
- id: LsProtocolId.LIVEPEER,
- name: 'Livepeer',
- chainIconFileName: 'livepeer',
- token: LsToken.LPT,
- decimals: 18,
- address: IS_PRODUCTION_ENV
- ? '0x58b6A8A3302369DAEc383334672404Ee733aB239'
- : SEPOLIA_TESTNET_CONTRACTS.ERC20,
- // TODO: Use the actual Chainlink Liquifier Adapter address. This is likely deployed to a testnet (Tenderly?).
- liquifierAdapterAddress: IS_PRODUCTION_ENV
- ? '0x'
- : SEPOLIA_TESTNET_CONTRACTS.LIQUIFIER,
- liquifierTgTokenAddress: IS_PRODUCTION_ENV
- ? '0x'
- : SEPOLIA_TESTNET_CONTRACTS.TG_TOKEN,
- timeUnit: CrossChainTimeUnit.LIVEPEER_ROUND,
- unstakingPeriod: 7,
-};
-
-const POLYGON: LsErc20TokenDef = {
- type: 'erc20',
- id: LsProtocolId.POLYGON,
- name: 'Polygon',
- chainIconFileName: 'polygon',
- token: LsToken.POL,
- decimals: 18,
- // TODO: Use Liquifier's testnet address if the environment is development.
- address: IS_PRODUCTION_ENV
- ? '0x0D500B1d8E8eF31E21C99d1Db9A6444d3ADf1270'
- : SEPOLIA_TESTNET_CONTRACTS.ERC20,
- // TODO: Use the actual Chainlink Liquifier Adapter address. This is likely deployed to a testnet (Tenderly?).
- liquifierAdapterAddress: IS_PRODUCTION_ENV
- ? '0x'
- : SEPOLIA_TESTNET_CONTRACTS.LIQUIFIER,
- liquifierTgTokenAddress: IS_PRODUCTION_ENV
- ? '0x'
- : SEPOLIA_TESTNET_CONTRACTS.TG_TOKEN,
- timeUnit: CrossChainTimeUnit.POLYGON_CHECKPOINT,
- unstakingPeriod: 82,
-};
-
-const POLKADOT: LsParachainChainDef = {
- type: 'parachain',
- id: LsProtocolId.POLKADOT,
- name: 'Polkadot',
- token: LsToken.DOT,
- chainIconFileName: 'polkadot',
- currency: 'Dot',
- decimals: 10,
- rpcEndpoint: 'wss://polkadot-rpc.dwellir.com',
- timeUnit: CrossChainTimeUnit.POLKADOT_ERA,
- unstakingPeriod: 28,
-};
-
-const PHALA: LsParachainChainDef = {
- type: 'parachain',
- id: LsProtocolId.PHALA,
- name: 'Phala',
- token: LsToken.PHALA,
- chainIconFileName: 'phala',
- currency: 'Pha',
- decimals: 18,
- rpcEndpoint: 'wss://api.phala.network/ws',
- timeUnit: CrossChainTimeUnit.DAY,
- unstakingPeriod: 7,
-};
-
-const MOONBEAM: LsParachainChainDef = {
- type: 'parachain',
- id: LsProtocolId.MOONBEAM,
- name: 'Moonbeam',
- token: LsToken.GLMR,
- chainIconFileName: 'moonbeam',
- // TODO: No currency entry for GLMR in the Tangle Primitives?
- currency: 'Dot',
- decimals: 18,
- rpcEndpoint: 'wss://moonbeam.api.onfinality.io/public-ws',
- timeUnit: CrossChainTimeUnit.MOONBEAM_ROUND,
- unstakingPeriod: 28,
-};
-
-const ASTAR: LsParachainChainDef = {
- type: 'parachain',
- id: LsProtocolId.ASTAR,
- name: 'Astar',
- token: LsToken.ASTAR,
- chainIconFileName: 'astar',
- // TODO: No currency entry for ASTAR in the Tangle Primitives?
- currency: 'Dot',
- decimals: 18,
- rpcEndpoint: 'wss://astar.api.onfinality.io/public-ws',
- timeUnit: CrossChainTimeUnit.ASTAR_ERA,
- unstakingPeriod: 7,
-};
-
-const MANTA: LsParachainChainDef = {
- type: 'parachain',
- id: LsProtocolId.MANTA,
- name: 'Manta',
- token: LsToken.MANTA,
- chainIconFileName: 'manta',
- // TODO: No currency entry for ASTAR in the Tangle Primitives?
- currency: 'Dot',
- decimals: 18,
- rpcEndpoint: 'wss://ws.manta.systems',
- timeUnit: CrossChainTimeUnit.DAY,
- unstakingPeriod: 7,
-};
-
-const TANGLE_RESTAKING_PARACHAIN: LsParachainChainDef = {
- type: 'parachain',
- id: LsProtocolId.TANGLE_RESTAKING_PARACHAIN,
- name: 'Tangle Parachain',
- token: LsToken.TNT,
- chainIconFileName: 'tangle',
- currency: 'Bnc',
- decimals: TANGLE_TOKEN_DECIMALS,
- rpcEndpoint: TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK.wsRpcEndpoint,
- timeUnit: CrossChainTimeUnit.TANGLE_RESTAKING_PARACHAIN_ERA,
- // TODO: The Tangle Restaking Parachain is a special case.
- unstakingPeriod: 0,
-};
+export const LS_REGISTRY_ADDRESS = IS_PRODUCTION_ENV
+ ? '0x'
+ : '0xCFF6785AC20878250b3FE70152905cEed38cf554';
+// TODO: Find a way to avoid casting. Some type errors are being pesky.
export const LS_PARACHAIN_CHAIN_MAP: Record<
LsParachainChainId,
LsParachainChainDef
@@ -203,10 +35,12 @@ export const LS_PARACHAIN_CHAIN_MAP: Record<
[LsProtocolId.MOONBEAM]: MOONBEAM,
[LsProtocolId.ASTAR]: ASTAR,
[LsProtocolId.MANTA]: MANTA,
- [LsProtocolId.TANGLE_RESTAKING_PARACHAIN]: TANGLE_RESTAKING_PARACHAIN,
-};
+} as Record;
-export const LS_ERC20_TOKEN_MAP: Record = {
+export const LS_LIQUIFIER_PROTOCOL_MAP: Record<
+ LsLiquifierProtocolId,
+ LsLiquifierProtocolDef
+> = {
[LsProtocolId.CHAINLINK]: CHAINLINK,
[LsProtocolId.THE_GRAPH]: THE_GRAPH,
[LsProtocolId.LIVEPEER]: LIVEPEER,
@@ -215,20 +49,20 @@ export const LS_ERC20_TOKEN_MAP: Record = {
export const LS_PROTOCOLS: LsProtocolDef[] = [
...Object.values(LS_PARACHAIN_CHAIN_MAP),
- ...Object.values(LS_ERC20_TOKEN_MAP),
+ ...Object.values(LS_LIQUIFIER_PROTOCOL_MAP),
];
-export const LS_ERC20_TOKEN_IDS = [
+export const LS_LIQUIFIER_PROTOCOL_IDS = [
LsProtocolId.CHAINLINK,
LsProtocolId.THE_GRAPH,
LsProtocolId.LIVEPEER,
LsProtocolId.POLYGON,
-] as const satisfies LsErc20TokenId[];
+] as const satisfies LsLiquifierProtocolId[];
export const LS_PARACHAIN_CHAIN_IDS = Object.values(LsProtocolId).filter(
(value): value is LsParachainChainId =>
typeof value !== 'string' &&
- !LS_ERC20_TOKEN_IDS.includes(value as LsErc20TokenId),
+ !LS_LIQUIFIER_PROTOCOL_IDS.includes(value as LsLiquifierProtocolId),
) satisfies LsParachainChainId[];
export const LS_PARACHAIN_TOKENS = [
@@ -243,4 +77,26 @@ export const LS_PARACHAIN_TOKENS = [
export const TVS_TOOLTIP =
"Total Value Staked (TVS) refers to the total value of assets that are currently staked for this network in fiat currency. Generally used as an indicator of a network's security and trustworthiness.";
-export const LST_PREFIX = 'tg';
+export const LS_DERIVATIVE_TOKEN_PREFIX = 'tg';
+
+export const LS_ETHEREUM_MAINNET_LIQUIFIER: LsNetwork = {
+ type: LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER,
+ networkName: IS_PRODUCTION_ENV ? 'Ethereum Mainnet' : 'Sepolia Testnet',
+ chainIconFileName: 'ethereum',
+ defaultProtocolId: LsProtocolId.CHAINLINK,
+ protocols: [CHAINLINK, THE_GRAPH, LIVEPEER, POLYGON],
+};
+
+export const LS_TANGLE_RESTAKING_PARACHAIN: LsNetwork = {
+ type: LsNetworkId.TANGLE_RESTAKING_PARACHAIN,
+ networkName: 'Tangle Parachain',
+ chainIconFileName: 'tangle',
+ defaultProtocolId: LsProtocolId.POLKADOT,
+ // TODO: Find a way to avoid casting. Some type errors are being pesky.
+ protocols: [POLKADOT, PHALA, MOONBEAM, ASTAR, MANTA] as LsProtocolDef[],
+};
+
+export const LS_NETWORKS: LsNetwork[] = [
+ LS_ETHEREUM_MAINNET_LIQUIFIER,
+ LS_TANGLE_RESTAKING_PARACHAIN,
+];
diff --git a/apps/tangle-dapp/constants/liquidStaking/devConstants.ts b/apps/tangle-dapp/constants/liquidStaking/devConstants.ts
new file mode 100644
index 0000000000..c966626e65
--- /dev/null
+++ b/apps/tangle-dapp/constants/liquidStaking/devConstants.ts
@@ -0,0 +1,14 @@
+import { HexString } from '@polkadot/util/types';
+
+/**
+ * Development only. Sepolia testnet contracts that were
+ * deployed to test the liquifier functionality. These contracts
+ * use dummy data.
+ */
+export const SEPOLIA_TESTNET_CONTRACTS = {
+ LIQUIFIER: '0x3D65A0aae9c42A26814C4F4cF7C92dC37114a476',
+ ERC20: '0x2eE951c2d215ba1b3E0DF20764c96a0bC7809F41',
+ // Use the same address as the dummy ERC20 contract.
+ TG_TOKEN: '0x2eE951c2d215ba1b3E0DF20764c96a0bC7809F41',
+ UNLOCKS: '0x32d70bC73d0965209Cf175711b010dE6A7650c2B',
+} as const satisfies Record;
diff --git a/apps/tangle-dapp/constants/liquidStaking/liquifierAdapterAbi.ts b/apps/tangle-dapp/constants/liquidStaking/liquifierAdapterAbi.ts
index cca7edef0d..92e37c7bae 100644
--- a/apps/tangle-dapp/constants/liquidStaking/liquifierAdapterAbi.ts
+++ b/apps/tangle-dapp/constants/liquidStaking/liquifierAdapterAbi.ts
@@ -341,6 +341,18 @@ const LIQUIFIER_ADAPTER_ABI = [
name: 'DepositBufferedTokens',
type: 'event',
},
+ {
+ name: 'totalShares',
+ type: 'function',
+ inputs: [],
+ stateMutability: 'view',
+ outputs: [
+ {
+ name: '',
+ type: 'uint256',
+ },
+ ],
+ },
] as const satisfies Abi;
export default LIQUIFIER_ADAPTER_ABI;
diff --git a/apps/tangle-dapp/constants/liquidStaking/liquifierRegistryAbi.ts b/apps/tangle-dapp/constants/liquidStaking/liquifierRegistryAbi.ts
new file mode 100644
index 0000000000..4844bc35d0
--- /dev/null
+++ b/apps/tangle-dapp/constants/liquidStaking/liquifierRegistryAbi.ts
@@ -0,0 +1,211 @@
+import { Abi } from 'viem';
+
+const LIQUIFIER_REGISTRY_ABI = [
+ // Constructor
+ {
+ type: 'constructor',
+ stateMutability: 'nonpayable',
+ inputs: [],
+ },
+
+ // Initialize function
+ {
+ type: 'function',
+ name: 'initialize',
+ inputs: [
+ { internalType: 'address', name: '_liquifier', type: 'address' },
+ { internalType: 'address', name: '_unlocks', type: 'address' },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+
+ // Getters
+ {
+ type: 'function',
+ name: 'adapter',
+ inputs: [{ internalType: 'address', name: 'asset', type: 'address' }],
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'liquifier',
+ inputs: [],
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'treasury',
+ inputs: [],
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'unlocks',
+ inputs: [],
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'fee',
+ inputs: [{ internalType: 'address', name: 'asset', type: 'address' }],
+ outputs: [{ internalType: 'uint96', name: '', type: 'uint96' }],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'isLiquifier',
+ inputs: [{ internalType: 'address', name: 'liquifier', type: 'address' }],
+ outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'getLiquifier',
+ inputs: [
+ { internalType: 'address', name: 'asset', type: 'address' },
+ { internalType: 'address', name: 'validator', type: 'address' },
+ ],
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ },
+
+ // Setters
+ {
+ type: 'function',
+ name: 'registerAdapter',
+ inputs: [
+ { internalType: 'address', name: 'asset', type: 'address' },
+ { internalType: 'address', name: 'adapter', type: 'address' },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'registerLiquifier',
+ inputs: [
+ { internalType: 'address', name: 'asset', type: 'address' },
+ { internalType: 'address', name: 'validator', type: 'address' },
+ { internalType: 'address', name: 'liquifier', type: 'address' },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'setFee',
+ inputs: [
+ { internalType: 'address', name: 'asset', type: 'address' },
+ { internalType: 'uint96', name: 'fee', type: 'uint96' },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'setTreasury',
+ inputs: [{ internalType: 'address', name: 'treasury', type: 'address' }],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+
+ // Required by UUPSUpgradeable
+ {
+ type: 'function',
+ name: '_authorizeUpgrade',
+ inputs: [{ internalType: 'address', name: '', type: 'address' }],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+
+ // Events
+ {
+ type: 'event',
+ name: 'AdapterRegistered',
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'asset',
+ type: 'address',
+ },
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'adapter',
+ type: 'address',
+ },
+ ],
+ anonymous: false,
+ },
+ {
+ type: 'event',
+ name: 'NewLiquifier',
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'asset',
+ type: 'address',
+ },
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'validator',
+ type: 'address',
+ },
+ {
+ indexed: false,
+ internalType: 'address',
+ name: 'liquifier',
+ type: 'address',
+ },
+ ],
+ anonymous: false,
+ },
+ {
+ type: 'event',
+ name: 'FeeAdjusted',
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'asset',
+ type: 'address',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'newFee',
+ type: 'uint256',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'oldFee',
+ type: 'uint256',
+ },
+ ],
+ anonymous: false,
+ },
+ {
+ type: 'event',
+ name: 'TreasurySet',
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'treasury',
+ type: 'address',
+ },
+ ],
+ anonymous: false,
+ },
+] as const satisfies Abi;
+
+export default LIQUIFIER_REGISTRY_ABI;
diff --git a/apps/tangle-dapp/constants/liquidStaking/types.ts b/apps/tangle-dapp/constants/liquidStaking/types.ts
index 514232fed5..e9353e2a80 100644
--- a/apps/tangle-dapp/constants/liquidStaking/types.ts
+++ b/apps/tangle-dapp/constants/liquidStaking/types.ts
@@ -5,6 +5,10 @@ import {
import { BN } from '@polkadot/util';
import { HexString } from '@polkadot/util/types';
+import {
+ LsNetworkEntityAdapter,
+ ProtocolEntity,
+} from '../../data/liquidStaking/adapter';
import { CrossChainTimeUnit } from '../../utils/CrossChainTime';
export enum LsProtocolId {
@@ -13,20 +17,19 @@ export enum LsProtocolId {
MOONBEAM,
ASTAR,
MANTA,
- TANGLE_RESTAKING_PARACHAIN,
CHAINLINK,
THE_GRAPH,
LIVEPEER,
POLYGON,
}
-export type LsErc20TokenId =
+export type LsLiquifierProtocolId =
| LsProtocolId.CHAINLINK
| LsProtocolId.THE_GRAPH
| LsProtocolId.LIVEPEER
| LsProtocolId.POLYGON;
-export type LsParachainChainId = Exclude;
+export type LsParachainChainId = Exclude;
export enum LsToken {
DOT = 'DOT',
@@ -41,13 +44,13 @@ export enum LsToken {
POL = 'POL',
}
-export type LsErc20Token =
+export type LsLiquifierProtocolToken =
| LsToken.LINK
| LsToken.GRT
| LsToken.LPT
| LsToken.POL;
-export type LsParachainToken = Exclude;
+export type LsParachainToken = Exclude;
type ProtocolDefCommon = {
name: string;
@@ -57,35 +60,45 @@ type ProtocolDefCommon = {
chainIconFileName: string;
};
-export interface LsParachainChainDef extends ProtocolDefCommon {
- type: 'parachain';
+export enum LsNetworkId {
+ TANGLE_RESTAKING_PARACHAIN,
+ ETHEREUM_MAINNET_LIQUIFIER,
+}
+
+export interface LsParachainChainDef
+ extends ProtocolDefCommon {
+ networkId: LsNetworkId.TANGLE_RESTAKING_PARACHAIN;
id: LsParachainChainId;
name: string;
token: LsParachainToken;
currency: ParachainCurrency;
rpcEndpoint: string;
+ ss58Prefix: number;
+ adapter: LsNetworkEntityAdapter;
}
-export interface LsErc20TokenDef extends ProtocolDefCommon {
- type: 'erc20';
- id: LsErc20TokenId;
- token: LsErc20Token;
- address: HexString;
- liquifierAdapterAddress: HexString;
- liquifierTgTokenAddress: HexString;
+export interface LsLiquifierProtocolDef extends ProtocolDefCommon {
+ networkId: LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER;
+ id: LsLiquifierProtocolId;
+ token: LsLiquifierProtocolToken;
+ erc20TokenAddress: HexString;
+ liquifierContractAddress: HexString;
+ tgTokenContractAddress: HexString;
+ unlocksContractAddress: HexString;
}
-export type LsProtocolDef = LsParachainChainDef | LsErc20TokenDef;
+export type LsProtocolDef = LsParachainChainDef | LsLiquifierProtocolDef;
export type LsCardSearchParams = {
amount: BN;
- chainId: LsProtocolId;
+ protocolId: LsProtocolId;
};
export enum LsSearchParamKey {
AMOUNT = 'amount',
PROTOCOL_ID = 'protocol',
ACTION = 'action',
+ NETWORK_ID = 'network',
}
// TODO: These should be moved/managed in libs/webb-ui-components/src/constants/networks.ts and not here. This is just a temporary solution.
@@ -112,7 +125,15 @@ export type LsParachainCurrencyKey =
export type LsParachainTimeUnit = TanglePrimitivesTimeUnit['type'];
-export type LsSimpleParachainTimeUnit = {
+export type LsParachainSimpleTimeUnit = {
value: number;
unit: LsParachainTimeUnit;
};
+
+export type LsNetwork = {
+ type: LsNetworkId;
+ networkName: string;
+ chainIconFileName: string;
+ defaultProtocolId: LsProtocolId;
+ protocols: LsProtocolDef[];
+};
diff --git a/apps/tangle-dapp/containers/ApiDevStatsContainer/index.ts b/apps/tangle-dapp/containers/ApiDevStatsContainer/index.ts
deleted file mode 100644
index c5ce29e044..0000000000
--- a/apps/tangle-dapp/containers/ApiDevStatsContainer/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './ApiDevStatsContainer';
diff --git a/apps/tangle-dapp/containers/ApiDevStatsContainer/ApiDevStatsContainer.tsx b/apps/tangle-dapp/containers/DebugMetricsContainer/DebugMetrics.tsx
similarity index 75%
rename from apps/tangle-dapp/containers/ApiDevStatsContainer/ApiDevStatsContainer.tsx
rename to apps/tangle-dapp/containers/DebugMetricsContainer/DebugMetrics.tsx
index 17a8a4538d..47b73ac9af 100644
--- a/apps/tangle-dapp/containers/ApiDevStatsContainer/ApiDevStatsContainer.tsx
+++ b/apps/tangle-dapp/containers/DebugMetricsContainer/DebugMetrics.tsx
@@ -4,6 +4,7 @@ import { Expand } from '@webb-tools/icons';
import { SkeletonLoader, Typography } from '@webb-tools/webb-ui-components';
import { FC, useCallback, useEffect, useState } from 'react';
+import useDebugMetricsStore from '../../context/useDebugMetricsStore';
import useNetworkStore from '../../context/useNetworkStore';
import usePromise from '../../hooks/usePromise';
import { getApiPromise, getApiRx } from '../../utils/polkadot';
@@ -19,9 +20,10 @@ function formatBytes(bytes: number): string {
return Math.round(bytes * MEGABYTE_FACTOR * 100) / 100 + 'mb';
}
-const ApiDevStats: FC = () => {
+const DebugMetrics: FC = () => {
const [isCollapsed, setIsCollapsed] = useState(true);
const { rpcEndpoint } = useNetworkStore();
+ const { requestCount, subscriptionCount } = useDebugMetricsStore();
const { result: api } = usePromise(
useCallback(() => getApiPromise(rpcEndpoint), [rpcEndpoint]),
@@ -37,11 +39,9 @@ const ApiDevStats: FC = () => {
const [tick, setTick] = useState(0);
const totalRequests =
- (api?.stats?.total.requests ?? 0) + (apiRx?.stats?.total.requests ?? 0);
-
- const totalSubscriptions =
- (api?.stats?.total.subscriptions ?? 0) +
- (apiRx?.stats?.total.subscriptions ?? 0);
+ (api?.stats?.total.requests ?? 0) +
+ (apiRx?.stats?.total.requests ?? 0) +
+ requestCount;
const totalBytesReceived =
(api?.stats?.total.bytesRecv ?? 0) + (apiRx?.stats?.total.bytesRecv ?? 0);
@@ -52,12 +52,10 @@ const ApiDevStats: FC = () => {
const totalErrors =
(api?.stats?.total.errors ?? 0) + (apiRx?.stats?.total.errors ?? 0);
- const totalActiveRequests =
- (api?.stats?.active.requests ?? 0) + (apiRx?.stats?.active.requests ?? 0);
-
const totalActiveSubscriptions =
(api?.stats?.active.subscriptions ?? 0) +
- (apiRx?.stats?.active.subscriptions ?? 0);
+ (apiRx?.stats?.active.subscriptions ?? 0) +
+ subscriptionCount;
// Manually trigger a re-render every second, since the stats
// are not automatically updated.
@@ -80,33 +78,13 @@ const ApiDevStats: FC = () => {
-
-
-
-
-
-
@@ -148,7 +126,7 @@ const Metric: FC<{
: '';
return (
-
+
{title}
@@ -156,7 +134,7 @@ const Metric: FC<{
{isApiLoading ? (
) : (
-
+
{value}
)}
@@ -164,4 +142,4 @@ const Metric: FC<{
);
};
-export default ApiDevStats;
+export default DebugMetrics;
diff --git a/apps/tangle-dapp/containers/DebugMetricsContainer/index.ts b/apps/tangle-dapp/containers/DebugMetricsContainer/index.ts
new file mode 100644
index 0000000000..8405ad051a
--- /dev/null
+++ b/apps/tangle-dapp/containers/DebugMetricsContainer/index.ts
@@ -0,0 +1 @@
+export { default } from './DebugMetrics';
diff --git a/apps/tangle-dapp/containers/Layout/Layout.tsx b/apps/tangle-dapp/containers/Layout/Layout.tsx
index 067143e941..38abe88bdc 100644
--- a/apps/tangle-dapp/containers/Layout/Layout.tsx
+++ b/apps/tangle-dapp/containers/Layout/Layout.tsx
@@ -18,7 +18,7 @@ import {
SidebarMenu,
} from '../../components';
import { IS_PRODUCTION_ENV } from '../../constants/env';
-import ApiDevStatsContainer from '../ApiDevStatsContainer';
+import ApiDevStatsContainer from '../DebugMetricsContainer';
import WalletAndChainContainer from '../WalletAndChainContainer/WalletAndChainContainer';
import { WalletModalContainer } from '../WalletModalContainer';
import FeedbackBanner from './FeedbackBanner';
diff --git a/apps/tangle-dapp/context/useDebugMetricsStore.ts b/apps/tangle-dapp/context/useDebugMetricsStore.ts
new file mode 100644
index 0000000000..408f07d2cf
--- /dev/null
+++ b/apps/tangle-dapp/context/useDebugMetricsStore.ts
@@ -0,0 +1,22 @@
+'use client';
+
+import { create } from 'zustand';
+
+const useDebugMetricsStore = create<{
+ requestCount: number;
+ subscriptionCount: number;
+ incrementRequestCount: () => void;
+ incrementSubscriptionCount: () => void;
+ decrementSubscriptionCount: () => void;
+}>((set) => ({
+ requestCount: 0,
+ subscriptionCount: 0,
+ incrementRequestCount: () =>
+ set((state) => ({ requestCount: state.requestCount + 1 })),
+ incrementSubscriptionCount: () =>
+ set((state) => ({ subscriptionCount: state.subscriptionCount + 1 })),
+ decrementSubscriptionCount: () =>
+ set((state) => ({ subscriptionCount: state.subscriptionCount - 1 })),
+}));
+
+export default useDebugMetricsStore;
diff --git a/apps/tangle-dapp/context/useSearchParamsStore.ts b/apps/tangle-dapp/context/useSearchParamsStore.ts
index 10c1bbeb92..c23e7953d3 100644
--- a/apps/tangle-dapp/context/useSearchParamsStore.ts
+++ b/apps/tangle-dapp/context/useSearchParamsStore.ts
@@ -34,6 +34,7 @@ const useSearchParamsStore = () => {
setBufferSearchParams(routerSearchParams);
}, [routerSearchParams, setBufferSearchParams]);
+ // TODO: Sort params by name so that after each update, the URL search params don't "flicker" due to their positions changing.
const updateSearchParam = useCallback(
(key: string, value: string | undefined) => {
if (bufferSearchParams === null) {
diff --git a/apps/tangle-dapp/data/liquidStaking/adapter.ts b/apps/tangle-dapp/data/liquidStaking/adapter.ts
new file mode 100644
index 0000000000..c95d39f5fa
--- /dev/null
+++ b/apps/tangle-dapp/data/liquidStaking/adapter.ts
@@ -0,0 +1,39 @@
+import { ColumnDef } from '@tanstack/react-table';
+import { PromiseOrT } from '@webb-tools/abstract-api-provider';
+import { MutableRefObject } from 'react';
+
+import { AstarDapp } from './adapters/astar';
+import { MoonbeamCollator } from './adapters/moonbeam';
+import { PhalaVaultOrStakePool } from './adapters/phala';
+import { PolkadotValidator } from './adapters/polkadot';
+
+export type ProtocolEntity =
+ | PolkadotValidator
+ | PhalaVaultOrStakePool
+ | AstarDapp
+ | MoonbeamCollator;
+
+export enum NetworkEntityType {
+ POLKADOT_VALIDATOR,
+ PHALA_VAULT_OR_STAKE_POOL,
+}
+
+export type FetchProtocolEntitiesFn = (
+ rpcEndpoint: string,
+) => PromiseOrT;
+
+// Note that it's acceptable to use `any` for the return type here,
+// since it won't affect our type safety of our logic, only TanStack's.
+// In fact, they also use `any` internally, likely because of the complexity
+// of the different possible column types.
+export type GetTableColumnsFn = (
+ toggleSortSelectionHandlerRef: MutableRefObject<
+ ((desc?: boolean | undefined, isMulti?: boolean | undefined) => void) | null
+ >,
+) => ColumnDef[];
+
+export type LsNetworkEntityAdapter =
+ {
+ fetchProtocolEntities: FetchProtocolEntitiesFn;
+ getTableColumns: GetTableColumnsFn;
+ };
diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/astar.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/astar.tsx
new file mode 100644
index 0000000000..f2cc9c07d3
--- /dev/null
+++ b/apps/tangle-dapp/data/liquidStaking/adapters/astar.tsx
@@ -0,0 +1,199 @@
+import { BN, BN_ZERO } from '@polkadot/util';
+import { createColumnHelper, SortingFnOption } from '@tanstack/react-table';
+import {
+ Avatar,
+ CopyWithTooltip,
+ shortenString,
+ Typography,
+} from '@webb-tools/webb-ui-components';
+
+import { StakingItemExternalLinkButton } from '../../../components/LiquidStaking/StakingItemExternalLinkButton';
+import {
+ LsNetworkId,
+ LsParachainChainDef,
+ LsProtocolId,
+ LsToken,
+} from '../../../constants/liquidStaking/types';
+import { LiquidStakingItem } from '../../../types/liquidStaking';
+import { CrossChainTimeUnit } from '../../../utils/CrossChainTime';
+import formatBn from '../../../utils/formatBn';
+import { GetTableColumnsFn } from '../adapter';
+import { sortSelected, sortValueStaked } from '../columnSorting';
+import { fetchMappedDappsTotalValueStaked } from '../fetchHelpers';
+import RadioInput from '../useLsValidatorSelectionTableColumns';
+
+const DECIMALS = 18;
+
+export type AstarDapp = {
+ id: string;
+ name: string;
+ // TODO: Is this a Substrate address? If so, use `SubstrateAddress` type.
+ contractAddress: string;
+ identity?: string;
+ totalValueStaked: BN;
+};
+
+const fetchDapps = async (rpcEndpoint: string): Promise => {
+ const [dapps, mappedTotalValueStaked, stakingDappsResponse] =
+ await Promise.all([
+ fetchDapps(rpcEndpoint),
+ fetchMappedDappsTotalValueStaked(rpcEndpoint),
+ fetch('https://api.astar.network/api/v1/astar/dapps-staking/dapps'),
+ ]);
+
+ if (!stakingDappsResponse.ok) {
+ throw new Error('Failed to fetch staking dapps');
+ }
+
+ // TODO: This is typed as 'any'.
+ const dappInfosArray = await stakingDappsResponse.json();
+
+ const dappInfosMap = new Map(
+ // TODO: Avoid using `any`.
+ dappInfosArray.map((dappInfo: any) => [dappInfo.address, dappInfo]),
+ );
+
+ return dapps.map((dapp) => {
+ const totalValueStaked = mappedTotalValueStaked.get(dapp.id);
+
+ // TODO: Avoid casting.
+ const dappInfo = dappInfosMap.get(dapp.contractAddress) as
+ | {
+ name?: string;
+ contractType?: string;
+ }
+ | undefined;
+
+ return {
+ id: dapp.id,
+ contractAddress: dapp.contractAddress,
+ name:
+ dappInfo !== undefined
+ ? dappInfo.name || dapp.contractAddress
+ : dapp.contractAddress,
+ dappContractType:
+ dappInfo !== undefined ? dappInfo.contractType || '' : '',
+ commission: BN_ZERO,
+ totalValueStaked: totalValueStaked ?? BN_ZERO,
+ itemType: LiquidStakingItem.DAPP,
+ };
+ });
+};
+
+const getTableColumns: GetTableColumnsFn = (
+ toggleSortSelectionHandlerRef,
+) => {
+ const dappColumnHelper = createColumnHelper();
+
+ return [
+ dappColumnHelper.accessor('contractAddress', {
+ header: ({ header }) => {
+ toggleSortSelectionHandlerRef.current = header.column.toggleSorting;
+ return (
+
+ dApp
+
+ );
+ },
+ cell: (props) => {
+ const address = props.getValue();
+ const isEthAddress = address.startsWith('0x');
+ const dappName = props.row.original.name;
+
+ return (
+
+
+
+
+
+
+
+ {dappName === address ? shortenString(address, 8) : dappName}
+
+
+
+
+
+ );
+ },
+ sortingFn: sortSelected as SortingFnOption,
+ }),
+ dappColumnHelper.accessor('totalValueStaked', {
+ header: ({ header }) => (
+
+
+ Total Staked
+
+
+ ),
+ cell: (props) => (
+
+
+ {formatBn(props.getValue(), DECIMALS) + ` ${LsToken.ASTAR}`}
+
+
+ ),
+ sortingFn: sortValueStaked,
+ }),
+ dappColumnHelper.display({
+ id: 'href',
+ header: () => ,
+ cell: (props) => {
+ const href = `https://portal.astar.network/astar/dapp-staking/dapp?dapp=${props.row.original.contractAddress}`;
+
+ return ;
+ },
+ }),
+ ];
+};
+
+const ASTAR: LsParachainChainDef = {
+ networkId: LsNetworkId.TANGLE_RESTAKING_PARACHAIN,
+ id: LsProtocolId.ASTAR,
+ name: 'Astar',
+ token: LsToken.ASTAR,
+ chainIconFileName: 'astar',
+ // TODO: No currency entry for ASTAR in the Tangle Primitives?
+ currency: 'Dot',
+ decimals: DECIMALS,
+ rpcEndpoint: 'wss://astar.api.onfinality.io/public-ws',
+ timeUnit: CrossChainTimeUnit.ASTAR_ERA,
+ unstakingPeriod: 7,
+ ss58Prefix: 5,
+ adapter: {
+ fetchProtocolEntities: fetchDapps,
+ getTableColumns,
+ },
+} as const satisfies LsParachainChainDef;
+
+export default ASTAR;
diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/chainlink.ts b/apps/tangle-dapp/data/liquidStaking/adapters/chainlink.ts
new file mode 100644
index 0000000000..5da8814f9e
--- /dev/null
+++ b/apps/tangle-dapp/data/liquidStaking/adapters/chainlink.ts
@@ -0,0 +1,34 @@
+import { IS_PRODUCTION_ENV } from '../../../constants/env';
+import { SEPOLIA_TESTNET_CONTRACTS } from '../../../constants/liquidStaking/devConstants';
+import {
+ LsLiquifierProtocolDef,
+ LsNetworkId,
+ LsProtocolId,
+ LsToken,
+} from '../../../constants/liquidStaking/types';
+import { CrossChainTimeUnit } from '../../../utils/CrossChainTime';
+
+const CHAINLINK = {
+ networkId: LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER,
+ id: LsProtocolId.CHAINLINK,
+ name: 'Chainlink',
+ chainIconFileName: 'chainlink',
+ token: LsToken.LINK,
+ decimals: 18,
+ erc20TokenAddress: IS_PRODUCTION_ENV
+ ? '0x514910771AF9Ca656af840dff83E8264EcF986CA'
+ : SEPOLIA_TESTNET_CONTRACTS.ERC20,
+ liquifierContractAddress: IS_PRODUCTION_ENV
+ ? '0x'
+ : SEPOLIA_TESTNET_CONTRACTS.LIQUIFIER,
+ tgTokenContractAddress: IS_PRODUCTION_ENV
+ ? '0x'
+ : SEPOLIA_TESTNET_CONTRACTS.TG_TOKEN,
+ unlocksContractAddress: IS_PRODUCTION_ENV
+ ? '0x'
+ : SEPOLIA_TESTNET_CONTRACTS.UNLOCKS,
+ timeUnit: CrossChainTimeUnit.DAY,
+ unstakingPeriod: 7,
+} as const satisfies LsLiquifierProtocolDef;
+
+export default CHAINLINK;
diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/livepeer.ts b/apps/tangle-dapp/data/liquidStaking/adapters/livepeer.ts
new file mode 100644
index 0000000000..4797c86527
--- /dev/null
+++ b/apps/tangle-dapp/data/liquidStaking/adapters/livepeer.ts
@@ -0,0 +1,34 @@
+import { IS_PRODUCTION_ENV } from '../../../constants/env';
+import { SEPOLIA_TESTNET_CONTRACTS } from '../../../constants/liquidStaking/devConstants';
+import {
+ LsLiquifierProtocolDef,
+ LsNetworkId,
+ LsProtocolId,
+ LsToken,
+} from '../../../constants/liquidStaking/types';
+import { CrossChainTimeUnit } from '../../../utils/CrossChainTime';
+
+const LIVEPEER = {
+ networkId: LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER,
+ id: LsProtocolId.LIVEPEER,
+ name: 'Livepeer',
+ chainIconFileName: 'livepeer',
+ token: LsToken.LPT,
+ decimals: 18,
+ erc20TokenAddress: IS_PRODUCTION_ENV
+ ? '0x58b6A8A3302369DAEc383334672404Ee733aB239'
+ : SEPOLIA_TESTNET_CONTRACTS.ERC20,
+ liquifierContractAddress: IS_PRODUCTION_ENV
+ ? '0x'
+ : SEPOLIA_TESTNET_CONTRACTS.LIQUIFIER,
+ tgTokenContractAddress: IS_PRODUCTION_ENV
+ ? '0x'
+ : SEPOLIA_TESTNET_CONTRACTS.TG_TOKEN,
+ unlocksContractAddress: IS_PRODUCTION_ENV
+ ? '0x'
+ : SEPOLIA_TESTNET_CONTRACTS.UNLOCKS,
+ timeUnit: CrossChainTimeUnit.LIVEPEER_ROUND,
+ unstakingPeriod: 7,
+} as const satisfies LsLiquifierProtocolDef;
+
+export default LIVEPEER;
diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/manta.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/manta.tsx
new file mode 100644
index 0000000000..d36f43fce7
--- /dev/null
+++ b/apps/tangle-dapp/data/liquidStaking/adapters/manta.tsx
@@ -0,0 +1,227 @@
+import { BN, BN_ZERO } from '@polkadot/util';
+import { createColumnHelper, SortingFnOption } from '@tanstack/react-table';
+import {
+ Avatar,
+ CheckBox,
+ CopyWithTooltip,
+ shortenString,
+ Typography,
+} from '@webb-tools/webb-ui-components';
+
+import { StakingItemExternalLinkButton } from '../../../components/LiquidStaking/StakingItemExternalLinkButton';
+import {
+ LsNetworkId,
+ LsParachainChainDef,
+ LsProtocolId,
+ LsToken,
+} from '../../../constants/liquidStaking/types';
+import { LiquidStakingItem } from '../../../types/liquidStaking';
+import { CrossChainTimeUnit } from '../../../utils/CrossChainTime';
+import formatBn from '../../../utils/formatBn';
+import { getApiPromise } from '../../../utils/polkadot';
+import { FetchProtocolEntitiesFn, GetTableColumnsFn } from '../adapter';
+import {
+ sortDelegationCount,
+ sortSelected,
+ sortValueStaked,
+} from '../columnSorting';
+import {
+ fetchMappedCollatorInfo,
+ fetchMappedIdentityNames,
+} from '../fetchHelpers';
+
+const DECIMALS = 18;
+
+export type MantaCollator = {
+ // TODO: Is `collatorAddress` a Substrate address? If so, use `SubstrateAddress` type.
+ address: string;
+ identity: string;
+ delegationCount: number;
+ totalValueStaked: BN;
+};
+
+const fetchAllCollators = async (rpcEndpoint: string) => {
+ const api = await getApiPromise(rpcEndpoint);
+
+ if (!api.query.parachainStaking) {
+ throw new Error('parachainStaking pallet is not available in the runtime');
+ }
+
+ const selectedCollators =
+ await api.query.parachainStaking.selectedCandidates();
+
+ return selectedCollators.map((collator) => collator.toString());
+};
+
+const fetchCollators: FetchProtocolEntitiesFn = async (
+ rpcEndpoint,
+) => {
+ const [collators, mappedIdentityNames, mappedCollatorInfo] =
+ await Promise.all([
+ fetchAllCollators(rpcEndpoint),
+ fetchMappedIdentityNames(rpcEndpoint),
+ fetchMappedCollatorInfo(rpcEndpoint),
+ ]);
+
+ return collators.map((collator) => {
+ const identityName = mappedIdentityNames.get(collator);
+ const collatorInfo = mappedCollatorInfo.get(collator);
+
+ return {
+ id: collator,
+ address: collator,
+ identity: identityName || collator,
+ delegationCount: collatorInfo?.delegationCount ?? 0,
+ totalValueStaked: collatorInfo?.totalStaked ?? BN_ZERO,
+ itemType: LiquidStakingItem.COLLATOR,
+ };
+ });
+};
+
+const getTableColumns: GetTableColumnsFn = (
+ toggleSortSelectionHandlerRef,
+) => {
+ const collatorColumnHelper = createColumnHelper();
+
+ return [
+ collatorColumnHelper.accessor('address', {
+ header: ({ header }) => {
+ toggleSortSelectionHandlerRef.current = header.column.toggleSorting;
+ return (
+
+ Collator
+
+ );
+ },
+ cell: (props) => {
+ const address = props.getValue();
+ const isEthAddress = address.startsWith('0x');
+ const identity = props.row.original.identity ?? address;
+
+ return (
+
+
+
+
+
+
+
+ {identity === address ? shortenString(address, 8) : identity}
+
+
+
+
+
+ );
+ },
+ sortingFn: sortSelected as SortingFnOption,
+ }),
+ collatorColumnHelper.accessor('totalValueStaked', {
+ header: ({ header }) => (
+
+
+ Total Staked
+
+
+ ),
+ cell: (props) => (
+
+
+ {formatBn(props.getValue(), DECIMALS) + ` ${LsToken.GLMR}`}
+
+
+ ),
+ sortingFn: sortValueStaked,
+ }),
+ collatorColumnHelper.accessor('delegationCount', {
+ header: ({ header }) => (
+
+
+ Delegations
+
+
+ ),
+ cell: (props) => (
+
+
+ {props.getValue()}
+
+
+ ),
+ sortingFn: sortDelegationCount as SortingFnOption,
+ }),
+ collatorColumnHelper.display({
+ id: 'identity',
+ header: () => ,
+ cell: () => {
+ // Note that Moonbeam collators don't have a direct link on
+ // stakeglmr.com.
+ return ;
+ },
+ }),
+ ];
+};
+
+const MANTA: LsParachainChainDef = {
+ networkId: LsNetworkId.TANGLE_RESTAKING_PARACHAIN,
+ id: LsProtocolId.MANTA,
+ name: 'Manta',
+ token: LsToken.MANTA,
+ chainIconFileName: 'manta',
+ // TODO: No currency entry for ASTAR in the Tangle Primitives?
+ currency: 'Dot',
+ decimals: 18,
+ rpcEndpoint: 'wss://ws.manta.systems',
+ timeUnit: CrossChainTimeUnit.DAY,
+ unstakingPeriod: 7,
+ ss58Prefix: 77,
+ adapter: {
+ fetchProtocolEntities: fetchCollators,
+ getTableColumns,
+ },
+} as const satisfies LsParachainChainDef;
+
+export default MANTA;
diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/moonbeam.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/moonbeam.tsx
new file mode 100644
index 0000000000..0c580b84d5
--- /dev/null
+++ b/apps/tangle-dapp/data/liquidStaking/adapters/moonbeam.tsx
@@ -0,0 +1,227 @@
+import { BN, BN_ZERO } from '@polkadot/util';
+import { createColumnHelper, SortingFnOption } from '@tanstack/react-table';
+import {
+ Avatar,
+ CheckBox,
+ CopyWithTooltip,
+ shortenString,
+ Typography,
+} from '@webb-tools/webb-ui-components';
+
+import { StakingItemExternalLinkButton } from '../../../components/LiquidStaking/StakingItemExternalLinkButton';
+import {
+ LsNetworkId,
+ LsParachainChainDef,
+ LsProtocolId,
+ LsToken,
+} from '../../../constants/liquidStaking/types';
+import { LiquidStakingItem } from '../../../types/liquidStaking';
+import { CrossChainTimeUnit } from '../../../utils/CrossChainTime';
+import formatBn from '../../../utils/formatBn';
+import { getApiPromise } from '../../../utils/polkadot';
+import { FetchProtocolEntitiesFn, GetTableColumnsFn } from '../adapter';
+import {
+ sortDelegationCount,
+ sortSelected,
+ sortValueStaked,
+} from '../columnSorting';
+import {
+ fetchMappedCollatorInfo,
+ fetchMappedIdentityNames,
+} from '../fetchHelpers';
+
+const DECIMALS = 18;
+
+export type MoonbeamCollator = {
+ // TODO: Is `collatorAddress` a Substrate address? If so, use `SubstrateAddress` type.
+ address: string;
+ identity: string;
+ delegationCount: number;
+ totalValueStaked: BN;
+};
+
+const fetchAllCollators = async (rpcEndpoint: string) => {
+ const api = await getApiPromise(rpcEndpoint);
+
+ if (!api.query.parachainStaking) {
+ throw new Error('parachainStaking pallet is not available in the runtime');
+ }
+
+ const selectedCollators =
+ await api.query.parachainStaking.selectedCandidates();
+
+ return selectedCollators.map((collator) => collator.toString());
+};
+
+const fetchCollators: FetchProtocolEntitiesFn = async (
+ rpcEndpoint,
+) => {
+ const [collators, mappedIdentityNames, mappedCollatorInfo] =
+ await Promise.all([
+ fetchAllCollators(rpcEndpoint),
+ fetchMappedIdentityNames(rpcEndpoint),
+ fetchMappedCollatorInfo(rpcEndpoint),
+ ]);
+
+ return collators.map((collator) => {
+ const identityName = mappedIdentityNames.get(collator);
+ const collatorInfo = mappedCollatorInfo.get(collator);
+
+ return {
+ id: collator,
+ address: collator,
+ identity: identityName || collator,
+ delegationCount: collatorInfo?.delegationCount ?? 0,
+ totalValueStaked: collatorInfo?.totalStaked ?? BN_ZERO,
+ itemType: LiquidStakingItem.COLLATOR,
+ };
+ });
+};
+
+const getTableColumns: GetTableColumnsFn = (
+ toggleSortSelectionHandlerRef,
+) => {
+ const collatorColumnHelper = createColumnHelper();
+
+ return [
+ collatorColumnHelper.accessor('address', {
+ header: ({ header }) => {
+ toggleSortSelectionHandlerRef.current = header.column.toggleSorting;
+ return (
+
+ Collator
+
+ );
+ },
+ cell: (props) => {
+ const address = props.getValue();
+ const isEthAddress = address.startsWith('0x');
+ const identity = props.row.original.identity ?? address;
+
+ return (
+
+
+
+
+
+
+
+ {identity === address ? shortenString(address, 8) : identity}
+
+
+
+
+
+ );
+ },
+ sortingFn: sortSelected as SortingFnOption,
+ }),
+ collatorColumnHelper.accessor('totalValueStaked', {
+ header: ({ header }) => (
+
+
+ Total Staked
+
+
+ ),
+ cell: (props) => (
+
+
+ {formatBn(props.getValue(), DECIMALS) + ` ${LsToken.GLMR}`}
+
+
+ ),
+ sortingFn: sortValueStaked,
+ }),
+ collatorColumnHelper.accessor('delegationCount', {
+ header: ({ header }) => (
+
+
+ Delegations
+
+
+ ),
+ cell: (props) => (
+
+
+ {props.getValue()}
+
+
+ ),
+ sortingFn: sortDelegationCount as SortingFnOption,
+ }),
+ collatorColumnHelper.display({
+ id: 'identity',
+ header: () => ,
+ cell: () => {
+ // Note that Moonbeam collators don't have a direct link on
+ // stakeglmr.com.
+ return ;
+ },
+ }),
+ ];
+};
+
+const MOONBEAM: LsParachainChainDef = {
+ networkId: LsNetworkId.TANGLE_RESTAKING_PARACHAIN,
+ id: LsProtocolId.MOONBEAM,
+ name: 'Moonbeam',
+ token: LsToken.GLMR,
+ chainIconFileName: 'moonbeam',
+ // TODO: No currency entry for GLMR in the Tangle Primitives?
+ currency: 'Dot',
+ decimals: DECIMALS,
+ rpcEndpoint: 'wss://moonbeam.api.onfinality.io/public-ws',
+ timeUnit: CrossChainTimeUnit.MOONBEAM_ROUND,
+ unstakingPeriod: 28,
+ ss58Prefix: 1284,
+ adapter: {
+ fetchProtocolEntities: fetchCollators,
+ getTableColumns,
+ },
+} as const satisfies LsParachainChainDef;
+
+export default MOONBEAM;
diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/phala.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/phala.tsx
new file mode 100644
index 0000000000..e48a1142b5
--- /dev/null
+++ b/apps/tangle-dapp/data/liquidStaking/adapters/phala.tsx
@@ -0,0 +1,242 @@
+import { BN } from '@polkadot/util';
+import {
+ createColumnHelper,
+ Row,
+ SortingFnOption,
+} from '@tanstack/react-table';
+import {
+ Avatar,
+ Chip,
+ CopyWithTooltip,
+ Typography,
+} from '@webb-tools/webb-ui-components';
+
+import { StakingItemExternalLinkButton } from '../../../components/LiquidStaking/StakingItemExternalLinkButton';
+import {
+ LsNetworkId,
+ LsParachainChainDef,
+ LsProtocolId,
+ LsToken,
+} from '../../../constants/liquidStaking/types';
+import { LiquidStakingItem } from '../../../types/liquidStaking';
+import { CrossChainTimeUnit } from '../../../utils/CrossChainTime';
+import formatBn from '../../../utils/formatBn';
+import { FetchProtocolEntitiesFn, GetTableColumnsFn } from '../adapter';
+import {
+ sortCommission,
+ sortSelected,
+ sortValueStaked,
+} from '../columnSorting';
+import { fetchVaultsAndStakePools } from '../fetchHelpers';
+import RadioInput from '../useLsValidatorSelectionTableColumns';
+
+const DECIMALS = 18;
+
+export type PhalaVaultOrStakePool = {
+ // TODO: Is `id` or `accountId` a Substrate address? If so, use `SubstrateAddress` type.
+ id: string;
+ accountId: string;
+ commission: BN;
+ totalValueStaked: BN;
+ type: 'vault' | 'stake-pool';
+};
+
+const fetchVaultOrStakePools: FetchProtocolEntitiesFn<
+ PhalaVaultOrStakePool
+> = async (rpcEndpoint) => {
+ const vaultsAndStakePools = await fetchVaultsAndStakePools(rpcEndpoint);
+
+ return vaultsAndStakePools.map((entity) => {
+ const type = entity.type === 'vault' ? 'vault' : 'stake-pool';
+
+ return {
+ id: entity.id,
+ accountId: entity.accountId,
+ commission: entity.commission,
+ totalValueStaked: entity.totalValueStaked,
+ type,
+ itemType: LiquidStakingItem.VAULT_OR_STAKE_POOL,
+ };
+ });
+};
+
+const getTableColumns: GetTableColumnsFn = (
+ toggleSortSelectionHandlerRef,
+) => {
+ const vaultOrStakePoolColumnHelper =
+ createColumnHelper();
+
+ // Uses localeCompare for string comparison to ensure
+ // proper alphabetical order.
+ const sortType = (rowA: Row, rowB: Row) => {
+ const rowAType = rowA.original.type;
+ const rowBType = rowB.original.type;
+
+ return rowAType.localeCompare(rowBType);
+ };
+
+ return [
+ vaultOrStakePoolColumnHelper.accessor('id', {
+ header: ({ header }) => {
+ toggleSortSelectionHandlerRef.current = header.column.toggleSorting;
+ return (
+
+ Vault/Pool ID
+
+ );
+ },
+ cell: (props) => {
+ const id = props.getValue();
+
+ return (
+
+ );
+ },
+ // TODO: Avoid casting sorting function.
+ sortingFn: sortSelected as SortingFnOption,
+ }),
+ vaultOrStakePoolColumnHelper.accessor('type', {
+ header: ({ header }) => (
+
+
+ Type
+
+
+ ),
+ cell: (props) => {
+ const type = props.getValue();
+
+ return (
+
+ {type}
+
+ );
+ },
+ sortingFn: sortType,
+ }),
+ vaultOrStakePoolColumnHelper.accessor('totalValueStaked', {
+ header: ({ header }) => (
+
+
+ Total Staked
+
+
+ ),
+ cell: (props) => (
+
+
+ {formatBn(props.getValue(), DECIMALS) + ` ${LsToken.PHALA}`}
+
+
+ ),
+ sortingFn: sortValueStaked,
+ }),
+ vaultOrStakePoolColumnHelper.accessor('commission', {
+ header: ({ header }) => (
+
+
+ Commission
+
+
+ ),
+ cell: (props) => (
+
+
+ {(Number(props.getValue().toString()) / 10000).toFixed(2)}%
+
+
+ ),
+ // TODO: Avoid casting sorting function.
+ sortingFn: sortCommission as SortingFnOption,
+ }),
+ vaultOrStakePoolColumnHelper.display({
+ id: 'href',
+ header: () => ,
+ cell: (props) => {
+ const href = `https://app.phala.network/phala/${props.row.original.type}/${props.row.original.id}`;
+
+ return ;
+ },
+ }),
+ ];
+};
+
+const PHALA: LsParachainChainDef = {
+ networkId: LsNetworkId.TANGLE_RESTAKING_PARACHAIN,
+ id: LsProtocolId.PHALA,
+ name: 'Phala',
+ token: LsToken.PHALA,
+ chainIconFileName: 'phala',
+ currency: 'Pha',
+ decimals: DECIMALS,
+ rpcEndpoint: 'wss://api.phala.network/ws',
+ timeUnit: CrossChainTimeUnit.DAY,
+ unstakingPeriod: 7,
+ ss58Prefix: 30,
+ adapter: {
+ fetchProtocolEntities: fetchVaultOrStakePools,
+ getTableColumns,
+ },
+} as const satisfies LsParachainChainDef;
+
+export default PHALA;
diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/polkadot.tsx b/apps/tangle-dapp/data/liquidStaking/adapters/polkadot.tsx
new file mode 100644
index 0000000000..2ceb9186dc
--- /dev/null
+++ b/apps/tangle-dapp/data/liquidStaking/adapters/polkadot.tsx
@@ -0,0 +1,229 @@
+import { BN, BN_ZERO } from '@polkadot/util';
+import { createColumnHelper, SortingFnOption } from '@tanstack/react-table';
+import {
+ Avatar,
+ CheckBox,
+ CopyWithTooltip,
+ shortenString,
+ Typography,
+} from '@webb-tools/webb-ui-components';
+
+import { StakingItemExternalLinkButton } from '../../../components/LiquidStaking/StakingItemExternalLinkButton';
+import {
+ LsNetworkId,
+ LsParachainChainDef,
+ LsProtocolId,
+ LsToken,
+} from '../../../constants/liquidStaking/types';
+import { LiquidStakingItem } from '../../../types/liquidStaking';
+import { SubstrateAddress } from '../../../types/utils';
+import assertSubstrateAddress from '../../../utils/assertSubstrateAddress';
+import calculateCommission from '../../../utils/calculateCommission';
+import { CrossChainTimeUnit } from '../../../utils/CrossChainTime';
+import formatBn from '../../../utils/formatBn';
+import { GetTableColumnsFn } from '../adapter';
+import {
+ sortCommission,
+ sortSelected,
+ sortValueStaked,
+} from '../columnSorting';
+import {
+ fetchChainDecimals,
+ fetchMappedIdentityNames,
+ fetchMappedValidatorsCommission,
+ fetchMappedValidatorsTotalValueStaked,
+ fetchTokenSymbol,
+} from '../fetchHelpers';
+
+const SS58_PREFIX = 0;
+const DECIMALS = 18;
+
+export type PolkadotValidator = {
+ address: SubstrateAddress;
+ identity: string;
+ commission: BN;
+ apy?: number;
+ totalValueStaked: BN;
+};
+
+const fetchValidators = async (
+ rpcEndpoint: string,
+): Promise => {
+ const [
+ validators,
+ mappedIdentityNames,
+ mappedTotalValueStaked,
+ mappedCommission,
+ ] = await Promise.all([
+ fetchValidators(rpcEndpoint),
+ fetchMappedIdentityNames(rpcEndpoint),
+ fetchMappedValidatorsTotalValueStaked(rpcEndpoint),
+ fetchMappedValidatorsCommission(rpcEndpoint),
+ fetchChainDecimals(rpcEndpoint),
+ fetchTokenSymbol(rpcEndpoint),
+ ]);
+
+ return validators.map((address) => {
+ const identityName = mappedIdentityNames.get(address.toString());
+ const totalValueStaked = mappedTotalValueStaked.get(address.toString());
+ const commission = mappedCommission.get(address.toString());
+
+ return {
+ id: address.toString(),
+ address: assertSubstrateAddress(address.toString(), SS58_PREFIX),
+ identity: identityName ?? address.toString(),
+ totalValueStaked: totalValueStaked ?? BN_ZERO,
+ apy: 0,
+ commission: commission ?? BN_ZERO,
+ itemType: LiquidStakingItem.VALIDATOR,
+ };
+ });
+};
+
+const getTableColumns: GetTableColumnsFn = (
+ toggleSortSelectionHandlerRef,
+) => {
+ const validatorColumnHelper = createColumnHelper();
+
+ return [
+ validatorColumnHelper.accessor('address', {
+ header: ({ header }) => {
+ toggleSortSelectionHandlerRef.current = header.column.toggleSorting;
+ return (
+
+ Validator
+
+ );
+ },
+ cell: (props) => {
+ const address = props.getValue();
+ const identity = props.row.original.identity ?? address;
+
+ return (
+
+
+
+
+
+
+
+ {identity === address ? shortenString(address, 8) : identity}
+
+
+
+
+
+ );
+ },
+ // TODO: Avoid casting sorting function.
+ sortingFn: sortSelected as SortingFnOption,
+ }),
+ validatorColumnHelper.accessor('totalValueStaked', {
+ header: ({ header }) => (
+
+
+ Total Staked
+
+
+ ),
+ cell: (props) => (
+
+
+ {formatBn(props.getValue(), DECIMALS) + ` ${LsToken.DOT}`}
+
+
+ ),
+ sortingFn: sortValueStaked,
+ }),
+ validatorColumnHelper.accessor('commission', {
+ header: ({ header }) => (
+
+
+ Commission
+
+
+ ),
+ cell: (props) => (
+
+
+ {calculateCommission(props.getValue()).toFixed(2) + '%'}
+
+
+ ),
+ // TODO: Avoid casting sorting function.
+ sortingFn: sortCommission as SortingFnOption,
+ }),
+ validatorColumnHelper.display({
+ id: 'href',
+ header: () => ,
+ cell: (props) => {
+ const href = `https://polkadot.subscan.io/account/${props.getValue()}`;
+
+ return ;
+ },
+ }),
+ ];
+};
+
+const POLKADOT = {
+ networkId: LsNetworkId.TANGLE_RESTAKING_PARACHAIN,
+ id: LsProtocolId.POLKADOT,
+ name: 'Polkadot',
+ token: LsToken.DOT,
+ chainIconFileName: 'polkadot',
+ currency: 'Dot',
+ decimals: DECIMALS,
+ rpcEndpoint: 'wss://polkadot-rpc.dwellir.com',
+ timeUnit: CrossChainTimeUnit.POLKADOT_ERA,
+ unstakingPeriod: 28,
+ ss58Prefix: 0,
+ adapter: {
+ fetchProtocolEntities: fetchValidators,
+ getTableColumns,
+ },
+} as const satisfies LsParachainChainDef;
+
+export default POLKADOT;
diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/polygon.ts b/apps/tangle-dapp/data/liquidStaking/adapters/polygon.ts
new file mode 100644
index 0000000000..574442fa66
--- /dev/null
+++ b/apps/tangle-dapp/data/liquidStaking/adapters/polygon.ts
@@ -0,0 +1,34 @@
+import { IS_PRODUCTION_ENV } from '../../../constants/env';
+import { SEPOLIA_TESTNET_CONTRACTS } from '../../../constants/liquidStaking/devConstants';
+import {
+ LsLiquifierProtocolDef,
+ LsNetworkId,
+ LsProtocolId,
+ LsToken,
+} from '../../../constants/liquidStaking/types';
+import { CrossChainTimeUnit } from '../../../utils/CrossChainTime';
+
+const POLYGON = {
+ networkId: LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER,
+ id: LsProtocolId.POLYGON,
+ name: 'Polygon',
+ chainIconFileName: 'polygon',
+ token: LsToken.POL,
+ decimals: 18,
+ erc20TokenAddress: IS_PRODUCTION_ENV
+ ? '0x0D500B1d8E8eF31E21C99d1Db9A6444d3ADf1270'
+ : SEPOLIA_TESTNET_CONTRACTS.ERC20,
+ liquifierContractAddress: IS_PRODUCTION_ENV
+ ? '0x'
+ : SEPOLIA_TESTNET_CONTRACTS.LIQUIFIER,
+ tgTokenContractAddress: IS_PRODUCTION_ENV
+ ? '0x'
+ : SEPOLIA_TESTNET_CONTRACTS.TG_TOKEN,
+ unlocksContractAddress: IS_PRODUCTION_ENV
+ ? '0x'
+ : SEPOLIA_TESTNET_CONTRACTS.UNLOCKS,
+ timeUnit: CrossChainTimeUnit.POLYGON_CHECKPOINT,
+ unstakingPeriod: 82,
+} as const satisfies LsLiquifierProtocolDef;
+
+export default POLYGON;
diff --git a/apps/tangle-dapp/data/liquidStaking/adapters/theGraph.ts b/apps/tangle-dapp/data/liquidStaking/adapters/theGraph.ts
new file mode 100644
index 0000000000..b5a126efec
--- /dev/null
+++ b/apps/tangle-dapp/data/liquidStaking/adapters/theGraph.ts
@@ -0,0 +1,34 @@
+import { IS_PRODUCTION_ENV } from '../../../constants/env';
+import { SEPOLIA_TESTNET_CONTRACTS } from '../../../constants/liquidStaking/devConstants';
+import {
+ LsLiquifierProtocolDef,
+ LsNetworkId,
+ LsProtocolId,
+ LsToken,
+} from '../../../constants/liquidStaking/types';
+import { CrossChainTimeUnit } from '../../../utils/CrossChainTime';
+
+const THE_GRAPH = {
+ networkId: LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER,
+ id: LsProtocolId.THE_GRAPH,
+ name: 'The Graph',
+ chainIconFileName: 'the-graph',
+ token: LsToken.GRT,
+ decimals: 18,
+ erc20TokenAddress: IS_PRODUCTION_ENV
+ ? '0xc944E90C64B2c07662A292be6244BDf05Cda44a7'
+ : SEPOLIA_TESTNET_CONTRACTS.ERC20,
+ liquifierContractAddress: IS_PRODUCTION_ENV
+ ? '0x'
+ : SEPOLIA_TESTNET_CONTRACTS.LIQUIFIER,
+ tgTokenContractAddress: IS_PRODUCTION_ENV
+ ? '0x'
+ : SEPOLIA_TESTNET_CONTRACTS.TG_TOKEN,
+ unlocksContractAddress: IS_PRODUCTION_ENV
+ ? '0x'
+ : SEPOLIA_TESTNET_CONTRACTS.UNLOCKS,
+ timeUnit: CrossChainTimeUnit.DAY,
+ unstakingPeriod: 28,
+} as const satisfies LsLiquifierProtocolDef;
+
+export default THE_GRAPH;
diff --git a/apps/tangle-dapp/data/liquidStaking/columnSorting.ts b/apps/tangle-dapp/data/liquidStaking/columnSorting.ts
new file mode 100644
index 0000000000..3dc9401e28
--- /dev/null
+++ b/apps/tangle-dapp/data/liquidStaking/columnSorting.ts
@@ -0,0 +1,76 @@
+/**
+ * Function to sort rows based on their selected status.
+ * Selected rows are sorted to appear before non-selected rows.
+ */
+
+import { BN } from '@polkadot/util';
+import { Row } from '@tanstack/react-table';
+
+export const sortSelected = boolean }>(
+ rowA: Row,
+ rowB: Row,
+) => {
+ const rowASelected = rowA.getIsSelected();
+ const rowBSelected = rowB.getIsSelected();
+ return rowASelected === rowBSelected ? 0 : rowASelected ? -1 : 1;
+};
+
+/**
+ * Function to sort rows based on the total value staked.
+ * Rows with higher staked values are sorted before those with lower values.
+ */
+export const sortValueStaked = (
+ rowA: Row,
+ rowB: Row,
+) => {
+ const rowAValue = rowA.original.totalValueStaked;
+ const rowBValue = rowB.original.totalValueStaked;
+ return Number(rowAValue.sub(rowBValue).toString());
+};
+
+/**
+ * Function to sort rows based on commission values.
+ * Rows with lower commission values are sorted before those with higher values.
+ */
+export const sortCommission = <
+ T extends { validatorCommission?: BN; commission?: BN },
+>(
+ rowA: Row,
+ rowB: Row,
+) => {
+ const rowAValue =
+ Number(rowA.original.validatorCommission) ||
+ Number(rowA.original.commission);
+ const rowBValue =
+ Number(rowB.original.validatorCommission) ||
+ Number(rowB.original.commission);
+ return rowAValue - rowBValue;
+};
+
+/**
+ * Function to sort rows based on the number of delegations for collators.
+ * Rows with fewer delegations are sorted before those with more delegations.
+ */
+export const sortDelegationCount = <
+ T extends { collatorDelegationCount: number },
+>(
+ rowA: Row,
+ rowB: Row,
+) => {
+ const rowAValue = rowA.original.collatorDelegationCount;
+ const rowBValue = rowB.original.collatorDelegationCount;
+ return rowAValue - rowBValue;
+};
+
+/**
+ * Function to sort rows based on type.
+ * Uses localeCompare for string comparison to ensure proper alphabetical order.
+ */
+export const sortType = (
+ rowA: Row,
+ rowB: Row,
+) => {
+ const rowAType = rowA.original.type;
+ const rowBType = rowB.original.type;
+ return rowAType.localeCompare(rowBType);
+};
diff --git a/apps/tangle-dapp/data/liquidStaking/helper.ts b/apps/tangle-dapp/data/liquidStaking/fetchHelpers.ts
similarity index 95%
rename from apps/tangle-dapp/data/liquidStaking/helper.ts
rename to apps/tangle-dapp/data/liquidStaking/fetchHelpers.ts
index 08bbe4f8f0..0267c9a980 100644
--- a/apps/tangle-dapp/data/liquidStaking/helper.ts
+++ b/apps/tangle-dapp/data/liquidStaking/fetchHelpers.ts
@@ -18,6 +18,7 @@ export const fetchValidators = async (
): Promise => {
const api = await getApiPromise(rpcEndpoint);
const validators = (await api.query.session.validators()).map((val) => val);
+
return validators;
};
@@ -40,11 +41,13 @@ export const fetchMappedIdentityNames = async (
const validator = identity[0].args[0];
const info = identity[1].unwrap()[0].info;
+
const displayName =
extractDataFromIdentityInfo(info, IdentityDataType.NAME) ||
validator.toString();
map.set(validator.toString(), displayName);
});
+
return map;
};
@@ -62,6 +65,7 @@ export const fetchMappedValidatorsTotalValueStaked = async (
const validators = await fetchValidators(rpcEndpoint);
const erasStakersOverviewMap = new Map();
+
const erasStakersOverviewEntries =
await api.query.staking.erasStakersOverview.entries();
@@ -76,6 +80,7 @@ export const fetchMappedValidatorsTotalValueStaked = async (
if (eraIndex === activeEraIndex.toNumber()) {
const totalStaked =
validatorExposure.unwrap().total.unwrap().toBn() || BN_ZERO;
+
erasStakersOverviewMap.set(validator, totalStaked);
}
});
@@ -84,8 +89,10 @@ export const fetchMappedValidatorsTotalValueStaked = async (
validators.forEach((validator) => {
const totalStaked = erasStakersOverviewMap.get(validator.toString());
+
map.set(validator.toString(), totalStaked || BN_ZERO);
});
+
return map;
};
@@ -124,7 +131,8 @@ export const fetchChainDecimals = async (
rpcEndpoint: string,
): Promise => {
const api = await getApiPromise(rpcEndpoint);
- const chainDecimals = await api.registry.chainDecimals;
+ const chainDecimals = api.registry.chainDecimals;
+
return chainDecimals.length > 0 ? chainDecimals[0] : 18;
};
@@ -153,11 +161,11 @@ export const fetchDapps = async (rpcEndpoint: string): Promise => {
const dapps = integratedDapps.map((dapp) => {
const dappAddress = JSON.parse(dapp[0].args[0].toString());
const dappIdOption = dapp[1] as Option;
- let dappId = '';
- if (dappIdOption.isSome) {
- dappId = dappIdOption.unwrap().id.toString();
- }
+ const dappId = dappIdOption.isSome
+ ? dappIdOption.unwrap().id.toString()
+ : '';
+
return {
id: dappId,
address: dappAddress.evm || dappAddress.wasm,
@@ -178,10 +186,13 @@ export const fetchMappedDappsTotalValueStaked = async (
const contractStakes = await api.query.dappStaking.contractStake.entries();
const map = new Map();
+
contractStakes.forEach((contractStake) => {
const dappId = contractStake[0].args[0].toString();
+
const stakeAmount =
contractStake[1] as PalletDappStakingV3ContractStakeAmount;
+
let totalStaked = BN_ZERO;
if (!stakeAmount.isEmpty) {
@@ -246,7 +257,7 @@ export const fetchVaultsAndStakePools = async (
if (poolInfoObj[type].basepool.totalValue !== undefined) {
totalValueStaked = new BN(
- cleanHexString(poolInfoObj[type].basepool.totalValue.toString()),
+ remove0xPrefix(poolInfoObj[type].basepool.totalValue.toString()),
16,
);
}
@@ -307,6 +318,7 @@ export const fetchMappedCollatorInfo = async (
if (info.isSome) {
const infoObj = info.unwrap();
+
totalStaked = infoObj.totalCounted.toBn() || BN_ZERO;
delegationCount = infoObj.delegationCount.toNumber();
}
@@ -321,12 +333,8 @@ export const fetchMappedCollatorInfo = async (
};
/** @internal */
-const cleanHexString = (hex: string) => {
- if (!hex) return '';
-
- if (hex.startsWith('0x')) {
- hex = hex.slice(2);
- }
-
- return hex;
+const remove0xPrefix = (possibleHexString: string): string => {
+ return possibleHexString.startsWith('0x')
+ ? possibleHexString.slice(2)
+ : possibleHexString;
};
diff --git a/apps/tangle-dapp/data/liquidStaking/useExchangeRate.ts b/apps/tangle-dapp/data/liquidStaking/useExchangeRate.ts
deleted file mode 100644
index 97c0e37d8e..0000000000
--- a/apps/tangle-dapp/data/liquidStaking/useExchangeRate.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import { TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK } from '@webb-tools/webb-ui-components/constants/networks';
-import { useCallback, useEffect, useMemo } from 'react';
-import { erc20Abi } from 'viem';
-
-import {
- LsErc20TokenDef,
- LsParachainCurrencyKey,
- LsProtocolId,
-} from '../../constants/liquidStaking/types';
-import useApiRx from '../../hooks/useApiRx';
-import calculateBnRatio from '../../utils/calculateBnRatio';
-import getLsProtocolDef from '../../utils/liquidStaking/getLsProtocolDef';
-import { ContractReadOptions } from '../liquifier/useContractRead';
-import useContractReadSubscription from '../liquifier/useContractReadSubscription';
-import usePolling, { PollingPrimaryCacheKey } from './usePolling';
-
-export enum ExchangeRateType {
- NativeToLiquid,
- LiquidToNative,
-}
-
-// TODO: This NEEDS to be based on subscription for sure, since exchange rates are always changing. Perhaps make it return whether it is re-fetching, so that an effect can be shown on the UI to indicate that it is fetching the latest exchange rate, and also have it be ran in a 3 or 5 second interval. Will also need de-duping logic, error handling, and also prevent spamming requests when the parent component is re-rendered many times (e.g. by using a ref to store the latest fetch timestamp). Might want to extract this pattern into its own hook, similar to a subscription. Also consider having a global store (Zustand) for that custom hook that uses caching to prevent spamming requests when the same hook is used in multiple components, might need to accept a custom 'key' parameter to use as the cache key.
-const useExchangeRate = (type: ExchangeRateType, protocolId: LsProtocolId) => {
- const protocol = getLsProtocolDef(protocolId);
-
- const { result: tokenPoolAmount } = useApiRx((api) => {
- if (protocol.type !== 'parachain') {
- return null;
- }
-
- const key: LsParachainCurrencyKey =
- type === ExchangeRateType.NativeToLiquid
- ? { Native: protocol.currency }
- : { lst: protocol.currency };
-
- return api.query.lstMinting.tokenPool(key);
- }, TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK.wsRpcEndpoint);
-
- const { result: lstTotalIssuance } = useApiRx((api) => {
- if (protocol.type !== 'parachain') {
- return null;
- }
-
- return api.query.tokens.totalIssuance({ lst: protocol.currency });
- }, TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK.wsRpcEndpoint);
-
- const parachainExchangeRate = useMemo(async () => {
- // Not yet ready.
- if (tokenPoolAmount === null || lstTotalIssuance === null) {
- return null;
- }
-
- const isEitherZero = tokenPoolAmount.isZero() || lstTotalIssuance.isZero();
-
- // TODO: Need to review whether this is the right way to handle this edge case.
- // Special case: No native tokens or liquidity available for conversion.
- // Default to 1:1 exchange rate. This also helps prevent division by zero.
- if (isEitherZero) {
- return 1;
- }
-
- const ratio =
- type === ExchangeRateType.NativeToLiquid
- ? calculateBnRatio(lstTotalIssuance, tokenPoolAmount)
- : calculateBnRatio(tokenPoolAmount, lstTotalIssuance);
-
- return ratio;
- }, [lstTotalIssuance, tokenPoolAmount, type]);
-
- const fetchErc20ExchangeRate = useCallback(
- async (_erc20TokenDef: LsErc20TokenDef) => {
- // TODO: Implement.
-
- return 0;
- },
- [],
- );
-
- const fetcher = useCallback(() => {
- return protocol.type === 'parachain'
- ? parachainExchangeRate
- : fetchErc20ExchangeRate(protocol);
- }, [fetchErc20ExchangeRate, parachainExchangeRate, protocol]);
-
- const totalSupplyFetcher = useCallback((): ContractReadOptions<
- typeof erc20Abi,
- 'totalSupply'
- > | null => {
- if (protocol.type !== 'erc20') {
- return null;
- }
-
- return {
- address: protocol.address,
- functionName: 'totalSupply',
- args: [],
- };
- }, [protocol]);
-
- // TODO: Will need one for the LST total issuance, and another for the token pool amount.
- const {
- value: _erc20TotalIssuance,
- setIsPaused: setIsErc20TotalIssuancePaused,
- } = useContractReadSubscription(erc20Abi, totalSupplyFetcher);
-
- // Pause or resume ERC20-based exchange rate fetching based
- // on whether the requested protocol is a parachain or an ERC20 token.
- // This helps prevent unnecessary requests.
- useEffect(() => {
- const isPaused = protocol.type === 'parachain';
-
- setIsErc20TotalIssuancePaused(isPaused);
- }, [protocol.type, setIsErc20TotalIssuancePaused]);
-
- // TODO: Use polling for the ERC20 exchange rate, NOT the parachain exchange rate which is already based on a subscription. Might need a mechanism to 'pause' polling when the selected protocol is a parachain chain, that way it doesn't make unnecessary requests until an ERC20 token is selected.
- const { value: exchangeRate, isRefreshing } = usePolling({
- fetcher,
- // Refresh every 5 seconds.
- refreshInterval: 5_000,
- primaryCacheKey: PollingPrimaryCacheKey.EXCHANGE_RATE,
- cacheKey: ['exchangeRate', protocolId],
- });
-
- return { exchangeRate, isRefreshing };
-};
-
-export default useExchangeRate;
diff --git a/apps/tangle-dapp/data/liquidStaking/useLiquidStakingStore.ts b/apps/tangle-dapp/data/liquidStaking/useLiquidStakingStore.ts
deleted file mode 100644
index 6f5da45f3a..0000000000
--- a/apps/tangle-dapp/data/liquidStaking/useLiquidStakingStore.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { create } from 'zustand';
-
-import { LsProtocolId } from '../../constants/liquidStaking/types';
-
-type State = {
- selectedProtocolId: LsProtocolId;
- selectedItems: Set;
-};
-
-type Actions = {
- setSelectedProtocolId: (selectedChainId: State['selectedProtocolId']) => void;
- setSelectedItems: (selectedItems: State['selectedItems']) => void;
-};
-
-type Store = State & Actions;
-
-export const useLiquidStakingStore = create((set) => ({
- selectedProtocolId: LsProtocolId.TANGLE_RESTAKING_PARACHAIN,
- selectedItems: new Set(),
- setSelectedProtocolId: (selectedChainId) =>
- set({ selectedProtocolId: selectedChainId }),
- setSelectedItems: (selectedItems) => set({ selectedItems }),
-}));
diff --git a/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts b/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts
new file mode 100644
index 0000000000..334434f8a4
--- /dev/null
+++ b/apps/tangle-dapp/data/liquidStaking/useLsExchangeRate.ts
@@ -0,0 +1,191 @@
+import { BN } from '@polkadot/util';
+import { TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK } from '@webb-tools/webb-ui-components/constants/networks';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { erc20Abi } from 'viem';
+
+import LIQUIFIER_ADAPTER_ABI from '../../constants/liquidStaking/liquifierAdapterAbi';
+import LIQUIFIER_TG_TOKEN_ABI from '../../constants/liquidStaking/liquifierTgTokenAbi';
+import {
+ LsNetworkId,
+ LsParachainCurrencyKey,
+ LsProtocolId,
+} from '../../constants/liquidStaking/types';
+import useApiRx from '../../hooks/useApiRx';
+import calculateBnRatio from '../../utils/calculateBnRatio';
+import getLsProtocolDef from '../../utils/liquidStaking/getLsProtocolDef';
+import useContractRead from '../liquifier/useContractRead';
+import { ContractReadOptions } from '../liquifier/useContractReadOnce';
+import usePolling from './usePolling';
+
+export enum ExchangeRateType {
+ NativeToDerivative,
+ DerivativeToNative,
+}
+
+const computeExchangeRate = (
+ type: ExchangeRateType,
+ totalNativeSupply: BN,
+ totalDerivativeSupply: BN,
+) => {
+ const isEitherZero =
+ totalNativeSupply.isZero() || totalDerivativeSupply.isZero();
+
+ // TODO: Need to review whether this is the right way to handle this edge case.
+ // Special case: No native tokens or liquidity available for conversion.
+ // Default to 1:1 exchange rate. This also helps prevent division by zero.
+ if (isEitherZero) {
+ return 1;
+ }
+
+ const ratio =
+ type === ExchangeRateType.NativeToDerivative
+ ? calculateBnRatio(totalDerivativeSupply, totalNativeSupply)
+ : calculateBnRatio(totalNativeSupply, totalDerivativeSupply);
+
+ return ratio;
+};
+
+const MAX_BN_OPERATION_NUMBER = 2 ** 26 - 1;
+
+const useLsExchangeRate = (
+ type: ExchangeRateType,
+ protocolId: LsProtocolId,
+) => {
+ const [exchangeRate, setExchangeRate] = useState(null);
+
+ const protocol = getLsProtocolDef(protocolId);
+
+ const { result: tokenPoolAmount } = useApiRx((api) => {
+ if (protocol.networkId !== LsNetworkId.TANGLE_RESTAKING_PARACHAIN) {
+ return null;
+ }
+
+ const key: LsParachainCurrencyKey =
+ type === ExchangeRateType.NativeToDerivative
+ ? { Native: protocol.currency }
+ : { lst: protocol.currency };
+
+ return api.query.lstMinting.tokenPool(key);
+ }, TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK.wsRpcEndpoint);
+
+ const { result: lstTotalIssuance } = useApiRx((api) => {
+ if (protocol.networkId !== LsNetworkId.TANGLE_RESTAKING_PARACHAIN) {
+ return null;
+ }
+
+ return api.query.tokens.totalIssuance({ lst: protocol.currency });
+ }, TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK.wsRpcEndpoint);
+
+ const parachainExchangeRate = useMemo(async () => {
+ // Not yet ready.
+ if (tokenPoolAmount === null || lstTotalIssuance === null) {
+ return null;
+ }
+
+ return computeExchangeRate(type, tokenPoolAmount, lstTotalIssuance);
+ }, [lstTotalIssuance, tokenPoolAmount, type]);
+
+ const getTgTokenTotalSupplyOptions = useCallback((): ContractReadOptions<
+ typeof LIQUIFIER_TG_TOKEN_ABI,
+ 'totalSupply'
+ > | null => {
+ if (protocol.networkId !== LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER) {
+ return null;
+ }
+
+ return {
+ address: protocol.erc20TokenAddress,
+ functionName: 'totalSupply',
+ args: [],
+ };
+ }, [protocol]);
+
+ const getLiquifierTotalSharesOptions = useCallback((): ContractReadOptions<
+ typeof LIQUIFIER_ADAPTER_ABI,
+ 'totalShares'
+ > | null => {
+ if (protocol.networkId !== LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER) {
+ return null;
+ }
+
+ return {
+ address: protocol.liquifierContractAddress,
+ functionName: 'totalShares',
+ args: [],
+ };
+ }, [protocol]);
+
+ const {
+ value: tgTokenTotalSupply,
+ setIsPaused: setIsTgTokenTotalSupplyPaused,
+ } = useContractRead(erc20Abi, getTgTokenTotalSupplyOptions);
+
+ const {
+ value: liquifierTotalShares,
+ setIsPaused: setIsLiquifierTotalSharesPaused,
+ } = useContractRead(LIQUIFIER_ADAPTER_ABI, getLiquifierTotalSharesOptions);
+
+ const fetchLiquifierExchangeRate = useCallback(async () => {
+ // Propagate error or loading states.
+ if (typeof tgTokenTotalSupply !== 'bigint') {
+ return tgTokenTotalSupply;
+ } else if (typeof liquifierTotalShares !== 'bigint') {
+ return liquifierTotalShares;
+ }
+
+ const tgTokenTotalSupplyBn = new BN(tgTokenTotalSupply.toString());
+ const liquifierTotalSharesBn = new BN(liquifierTotalShares.toString());
+
+ return computeExchangeRate(
+ type,
+ tgTokenTotalSupplyBn,
+ liquifierTotalSharesBn,
+ );
+ }, [liquifierTotalShares, tgTokenTotalSupply, type]);
+
+ const fetch = useCallback(async () => {
+ const promise =
+ protocol.networkId === LsNetworkId.TANGLE_RESTAKING_PARACHAIN
+ ? parachainExchangeRate
+ : fetchLiquifierExchangeRate();
+
+ const newExchangeRate = await promise;
+
+ // Still loading. Do not update the value. Display the stale
+ // value.
+ if (newExchangeRate === null) {
+ return;
+ }
+
+ setExchangeRate(newExchangeRate);
+ }, [fetchLiquifierExchangeRate, parachainExchangeRate, protocol]);
+
+ // Pause or resume ERC20-based exchange rate fetching based
+ // on whether the requested protocol is a parachain or an ERC20 token.
+ // This helps prevent unnecessary requests.
+ useEffect(() => {
+ const isPaused =
+ protocol.networkId === LsNetworkId.TANGLE_RESTAKING_PARACHAIN;
+
+ setIsTgTokenTotalSupplyPaused(isPaused);
+ setIsLiquifierTotalSharesPaused(isPaused);
+ }, [
+ protocol.networkId,
+ setIsLiquifierTotalSharesPaused,
+ setIsTgTokenTotalSupplyPaused,
+ ]);
+
+ const isRefreshing = usePolling({ effect: fetch });
+
+ return {
+ exchangeRate:
+ // For some undocumented reason, BN.js can perform number operations
+ // on BN instances that are up to 2^26 - 1.
+ typeof exchangeRate === 'number'
+ ? Math.min(MAX_BN_OPERATION_NUMBER, exchangeRate)
+ : exchangeRate,
+ isRefreshing,
+ };
+};
+
+export default useLsExchangeRate;
diff --git a/apps/tangle-dapp/data/liquidStaking/useLsProtocolEntities.ts b/apps/tangle-dapp/data/liquidStaking/useLsProtocolEntities.ts
new file mode 100644
index 0000000000..1bddab5a30
--- /dev/null
+++ b/apps/tangle-dapp/data/liquidStaking/useLsProtocolEntities.ts
@@ -0,0 +1,81 @@
+import { useCallback, useEffect, useState } from 'react';
+
+import { LsNetworkId, LsProtocolId } from '../../constants/liquidStaking/types';
+import useLocalStorage, { LocalStorageKey } from '../../hooks/useLocalStorage';
+import { LiquidStakingItem } from '../../types/liquidStaking';
+import getLsProtocolDef from '../../utils/liquidStaking/getLsProtocolDef';
+import { ProtocolEntity } from './adapter';
+
+const useLsProtocolEntities = (protocolId: LsProtocolId) => {
+ const { setWithPreviousValue: setLiquidStakingTableData } = useLocalStorage(
+ LocalStorageKey.LIQUID_STAKING_TABLE_DATA,
+ );
+
+ const [isLoading, setIsLoading] = useState(false);
+ const [entities, setEntities] = useState([]);
+
+ const dataType = getDataType(protocolId);
+
+ const fetchData = useCallback(
+ async (protocolId: LsProtocolId) => {
+ const protocol = getLsProtocolDef(protocolId);
+
+ if (protocol.networkId !== LsNetworkId.TANGLE_RESTAKING_PARACHAIN) {
+ setEntities([]);
+ setIsLoading(false);
+
+ return;
+ }
+
+ const newEntities = await protocol.adapter.fetchProtocolEntities(
+ protocol.rpcEndpoint,
+ );
+
+ setEntities(newEntities);
+
+ setLiquidStakingTableData((prev) => ({
+ ...prev?.value,
+ [protocolId]: newEntities,
+ }));
+
+ setIsLoading(false);
+ },
+ [setLiquidStakingTableData],
+ );
+
+ // Whenever the selected protocol changes, fetch the data for
+ // the new protocol.
+ useEffect(() => {
+ setEntities([]);
+ setIsLoading(true);
+ fetchData(protocolId);
+ }, [protocolId, fetchData]);
+
+ return {
+ isLoading,
+ data: entities,
+ dataType,
+ };
+};
+
+const getDataType = (chain: LsProtocolId): LiquidStakingItem | null => {
+ switch (chain) {
+ case LsProtocolId.MANTA:
+ return LiquidStakingItem.COLLATOR;
+ case LsProtocolId.MOONBEAM:
+ return LiquidStakingItem.COLLATOR;
+ case LsProtocolId.POLKADOT:
+ return LiquidStakingItem.VALIDATOR;
+ case LsProtocolId.PHALA:
+ return LiquidStakingItem.VAULT_OR_STAKE_POOL;
+ case LsProtocolId.ASTAR:
+ return LiquidStakingItem.DAPP;
+ case LsProtocolId.CHAINLINK:
+ case LsProtocolId.LIVEPEER:
+ case LsProtocolId.POLYGON:
+ case LsProtocolId.THE_GRAPH:
+ return null;
+ }
+};
+
+export default useLsProtocolEntities;
diff --git a/apps/tangle-dapp/data/liquidStaking/useLsStore.ts b/apps/tangle-dapp/data/liquidStaking/useLsStore.ts
new file mode 100644
index 0000000000..eb47616b3c
--- /dev/null
+++ b/apps/tangle-dapp/data/liquidStaking/useLsStore.ts
@@ -0,0 +1,33 @@
+import { create } from 'zustand';
+
+import { LsNetworkId, LsProtocolId } from '../../constants/liquidStaking/types';
+import getLsNetwork from '../../utils/liquidStaking/getLsNetwork';
+
+type State = {
+ selectedNetworkId: LsNetworkId;
+ selectedProtocolId: LsProtocolId;
+ selectedItems: Set;
+};
+
+type Actions = {
+ setSelectedProtocolId: (newProtocolId: State['selectedProtocolId']) => void;
+ setSelectedItems: (selectedItems: State['selectedItems']) => void;
+ setSelectedNetworkId: (newNetworkId: State['selectedNetworkId']) => void;
+};
+
+type Store = State & Actions;
+
+export const useLsStore = create((set) => ({
+ selectedNetworkId: LsNetworkId.TANGLE_RESTAKING_PARACHAIN,
+ selectedProtocolId: LsProtocolId.POLKADOT,
+ selectedItems: new Set(),
+ setSelectedProtocolId: (selectedChainId) =>
+ set({ selectedProtocolId: selectedChainId }),
+ setSelectedItems: (selectedItems) => set({ selectedItems }),
+ setSelectedNetworkId: (selectedNetworkId) => {
+ const network = getLsNetwork(selectedNetworkId);
+ const defaultProtocolId = network.defaultProtocolId;
+
+ set({ selectedNetworkId, selectedProtocolId: defaultProtocolId });
+ },
+}));
diff --git a/apps/tangle-dapp/hooks/LiquidStaking/useLiquidStakingSelectionTableColumns.tsx b/apps/tangle-dapp/data/liquidStaking/useLsValidatorSelectionTableColumns.tsx
similarity index 98%
rename from apps/tangle-dapp/hooks/LiquidStaking/useLiquidStakingSelectionTableColumns.tsx
rename to apps/tangle-dapp/data/liquidStaking/useLsValidatorSelectionTableColumns.tsx
index 8cb87f82b5..99961bf68c 100644
--- a/apps/tangle-dapp/hooks/LiquidStaking/useLiquidStakingSelectionTableColumns.tsx
+++ b/apps/tangle-dapp/data/liquidStaking/useLsValidatorSelectionTableColumns.tsx
@@ -21,18 +21,21 @@ import {
Collator,
Dapp,
LiquidStakingItem,
+ PhalaVaultOrStakePool,
Validator,
- VaultOrStakePool,
} from '../../types/liquidStaking';
import calculateCommission from '../../utils/calculateCommission';
import formatBn from '../../utils/formatBn';
const validatorColumnHelper = createColumnHelper();
const dappColumnHelper = createColumnHelper();
-const vaultOrStakePoolColumnHelper = createColumnHelper();
+
+const vaultOrStakePoolColumnHelper =
+ createColumnHelper();
+
const collatorColumnHelper = createColumnHelper();
-export const useLiquidStakingSelectionTableColumns = (
+export const useLsValidatorSelectionTableColumns = (
toggleSortSelectionHandlerRef: React.MutableRefObject<
((desc?: boolean | undefined, isMulti?: boolean | undefined) => void) | null
>,
@@ -299,7 +302,7 @@ export const useLiquidStakingSelectionTableColumns = (
);
},
- sortingFn: sortSelected as SortingFnOption
,
+ sortingFn: sortSelected as SortingFnOption,
}),
vaultOrStakePoolColumnHelper.accessor('type', {
header: ({ header }) => (
@@ -382,7 +385,7 @@ export const useLiquidStakingSelectionTableColumns = (
),
- sortingFn: sortCommission as SortingFnOption,
+ sortingFn: sortCommission as SortingFnOption,
}),
vaultOrStakePoolColumnHelper.accessor('href', {
header: () => ,
diff --git a/apps/tangle-dapp/data/liquidStaking/useLiquidStakingItems.ts b/apps/tangle-dapp/data/liquidStaking/useLsValidators.ts
similarity index 93%
rename from apps/tangle-dapp/data/liquidStaking/useLiquidStakingItems.ts
rename to apps/tangle-dapp/data/liquidStaking/useLsValidators.ts
index 6cde28d83f..6462b7b9e9 100644
--- a/apps/tangle-dapp/data/liquidStaking/useLiquidStakingItems.ts
+++ b/apps/tangle-dapp/data/liquidStaking/useLsValidators.ts
@@ -1,14 +1,14 @@
import { BN_ZERO } from '@polkadot/util';
import { useCallback, useEffect, useMemo, useState } from 'react';
-import { LsProtocolId } from '../../constants/liquidStaking/types';
+import { LsNetworkId, LsProtocolId } from '../../constants/liquidStaking/types';
import useLocalStorage, { LocalStorageKey } from '../../hooks/useLocalStorage';
import {
Collator,
Dapp,
LiquidStakingItem,
+ PhalaVaultOrStakePool,
Validator,
- VaultOrStakePool,
} from '../../types/liquidStaking';
import getLsProtocolDef from '../../utils/liquidStaking/getLsProtocolDef';
import {
@@ -23,9 +23,9 @@ import {
fetchTokenSymbol,
fetchValidators,
fetchVaultsAndStakePools,
-} from './helper';
+} from './fetchHelpers';
-const useLiquidStakingItems = (selectedChain: LsProtocolId) => {
+const useLsValidators = (selectedChain: LsProtocolId) => {
const { setWithPreviousValue: setLiquidStakingTableData } = useLocalStorage(
LocalStorageKey.LIQUID_STAKING_TABLE_DATA,
);
@@ -33,7 +33,7 @@ const useLiquidStakingItems = (selectedChain: LsProtocolId) => {
const [isLoading, setIsLoading] = useState(false);
const [items, setItems] = useState<
- Validator[] | VaultOrStakePool[] | Dapp[] | Collator[]
+ Validator[] | PhalaVaultOrStakePool[] | Dapp[] | Collator[]
>([]);
const dataType = useMemo(() => getDataType(selectedChain), [selectedChain]);
@@ -42,15 +42,18 @@ const useLiquidStakingItems = (selectedChain: LsProtocolId) => {
async (protocolId: LsProtocolId) => {
const protocol = getLsProtocolDef(protocolId);
- if (protocol.type !== 'parachain') {
+ if (protocol.networkId !== LsNetworkId.TANGLE_RESTAKING_PARACHAIN) {
setItems([]);
setIsLoading(false);
return;
}
- let fetchedItems: Validator[] | VaultOrStakePool[] | Dapp[] | Collator[] =
- [];
+ let fetchedItems:
+ | Validator[]
+ | PhalaVaultOrStakePool[]
+ | Dapp[]
+ | Collator[] = [];
switch (protocolId) {
case LsProtocolId.POLKADOT:
@@ -112,7 +115,7 @@ const useLiquidStakingItems = (selectedChain: LsProtocolId) => {
};
};
-export default useLiquidStakingItems;
+export default useLsValidators;
const getDataType = (chain: LsProtocolId) => {
switch (chain) {
@@ -120,7 +123,6 @@ const getDataType = (chain: LsProtocolId) => {
return LiquidStakingItem.COLLATOR;
case LsProtocolId.MOONBEAM:
return LiquidStakingItem.COLLATOR;
- case LsProtocolId.TANGLE_RESTAKING_PARACHAIN:
case LsProtocolId.POLKADOT:
return LiquidStakingItem.VALIDATOR;
case LsProtocolId.PHALA:
@@ -219,7 +221,7 @@ const getDapps = async (endpoint: string): Promise => {
const getVaultsAndStakePools = async (
endpoint: string,
-): Promise => {
+): Promise => {
const [vaultsAndStakePools, chainDecimals, chainTokenSymbol] =
await Promise.all([
fetchVaultsAndStakePools(endpoint),
diff --git a/apps/tangle-dapp/data/liquidStaking/useLstUnlockRequests.ts b/apps/tangle-dapp/data/liquidStaking/useLstUnlockRequests.ts
index 8411451ec1..f7d6c98ac3 100644
--- a/apps/tangle-dapp/data/liquidStaking/useLstUnlockRequests.ts
+++ b/apps/tangle-dapp/data/liquidStaking/useLstUnlockRequests.ts
@@ -12,7 +12,7 @@ import { useCallback, useMemo } from 'react';
import { map } from 'rxjs';
import {
- LsSimpleParachainTimeUnit,
+ LsParachainSimpleTimeUnit,
ParachainCurrency,
} from '../../constants/liquidStaking/types';
import useApiRx from '../../hooks/useApiRx';
@@ -25,7 +25,7 @@ export type LstUnlockRequest = {
unlockId: number;
currencyType: TanglePrimitivesCurrencyCurrencyId['type'];
currency: ParachainCurrency;
- unlockTimeUnit: LsSimpleParachainTimeUnit;
+ unlockTimeUnit: LsParachainSimpleTimeUnit;
amount: BN;
};
diff --git a/apps/tangle-dapp/data/liquidStaking/useOngoingTimeUnits.ts b/apps/tangle-dapp/data/liquidStaking/useOngoingTimeUnits.ts
index 02b14d2a2d..6326a8d238 100644
--- a/apps/tangle-dapp/data/liquidStaking/useOngoingTimeUnits.ts
+++ b/apps/tangle-dapp/data/liquidStaking/useOngoingTimeUnits.ts
@@ -3,7 +3,7 @@ import { TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK } from '@webb-tools/webb-u
import { useCallback, useMemo } from 'react';
import {
- LsSimpleParachainTimeUnit,
+ LsParachainSimpleTimeUnit,
ParachainCurrency,
} from '../../constants/liquidStaking/types';
import useApiRx from '../../hooks/useApiRx';
@@ -13,7 +13,7 @@ import getValueOfTangleCurrency from './getValueOfTangleCurrency';
export type OngoingTimeUnitEntry = {
currencyType: TanglePrimitivesCurrencyCurrencyId['type'];
currency: ParachainCurrency;
- timeUnit: LsSimpleParachainTimeUnit;
+ timeUnit: LsParachainSimpleTimeUnit;
};
const useOngoingTimeUnits = () => {
diff --git a/apps/tangle-dapp/data/liquidStaking/useMintAndRedeemFees.ts b/apps/tangle-dapp/data/liquidStaking/useParachainLsFees.ts
similarity index 90%
rename from apps/tangle-dapp/data/liquidStaking/useMintAndRedeemFees.ts
rename to apps/tangle-dapp/data/liquidStaking/useParachainLsFees.ts
index 7a65dc678b..a724c715d9 100644
--- a/apps/tangle-dapp/data/liquidStaking/useMintAndRedeemFees.ts
+++ b/apps/tangle-dapp/data/liquidStaking/useParachainLsFees.ts
@@ -5,7 +5,7 @@ import { map } from 'rxjs';
import useApiRx from '../../hooks/useApiRx';
import permillToPercentage from '../../utils/permillToPercentage';
-const useMintAndRedeemFees = () => {
+const useParachainLsFees = () => {
return useApiRx(
useCallback((api) => {
return api.query.lstMinting.fees().pipe(
@@ -24,4 +24,4 @@ const useMintAndRedeemFees = () => {
);
};
-export default useMintAndRedeemFees;
+export default useParachainLsFees;
diff --git a/apps/tangle-dapp/data/liquidStaking/usePolling.ts b/apps/tangle-dapp/data/liquidStaking/usePolling.ts
index e6fb1ac56e..0c23d96a4b 100644
--- a/apps/tangle-dapp/data/liquidStaking/usePolling.ts
+++ b/apps/tangle-dapp/data/liquidStaking/usePolling.ts
@@ -1,48 +1,48 @@
-import { useEffect, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
-export enum PollingPrimaryCacheKey {
- EXCHANGE_RATE,
- CONTRACT_READ_SUBSCRIPTION,
- LS_ERC20_BALANCE,
-}
-
-export type PollingOptions = {
- fetcher: (() => Promise | T) | null;
+export type PollingOptions = {
+ effect: (() => Promise | unknown) | null;
refreshInterval?: number;
- primaryCacheKey: PollingPrimaryCacheKey;
- cacheKey?: unknown[];
};
-// TODO: Use Zustand global store for caching.
-
-const usePolling = ({
- fetcher,
- // Default to a 3 second refresh interval.
- refreshInterval = 3_000,
- primaryCacheKey: _primaryCacheKey,
- cacheKey: _cacheKey,
-}: PollingOptions) => {
- const [value, setValue] = useState(null);
+const usePolling = ({
+ effect,
+ // Default to a 12 second refresh interval. This default is also
+ // convenient since it matches the expected block time of Ethereum
+ // as well as some Substrate-based chains.
+ refreshInterval = 12_000,
+}: PollingOptions) => {
const [isRefreshing, setIsRefreshing] = useState(false);
+ const refresh = useCallback(async () => {
+ // Fetcher isn't ready to be called yet.
+ if (effect === null) {
+ return;
+ }
+
+ setIsRefreshing(true);
+ await effect();
+ setIsRefreshing(false);
+ }, [effect]);
+
useEffect(() => {
- const intervalHandle = setInterval(async () => {
- // Fetcher isn't ready to be called yet.
- if (fetcher === null) {
- return;
- }
+ let intervalHandle: ReturnType | null = null;
+
+ (async () => {
+ // Call it immediately to avoid initial delay.
+ await refresh();
- setIsRefreshing(true);
- setValue(await fetcher());
- setIsRefreshing(false);
- }, refreshInterval);
+ intervalHandle = setInterval(refresh, refreshInterval);
+ })();
return () => {
- clearInterval(intervalHandle);
+ if (intervalHandle !== null) {
+ clearInterval(intervalHandle);
+ }
};
- }, [fetcher, refreshInterval]);
+ }, [effect, refresh, refreshInterval]);
- return { value, isRefreshing };
+ return isRefreshing;
};
export default usePolling;
diff --git a/apps/tangle-dapp/data/liquidStaking/useTokenUnlockDurations.ts b/apps/tangle-dapp/data/liquidStaking/useTokenUnlockDurations.ts
index 658fc993ad..ab5194cb4e 100644
--- a/apps/tangle-dapp/data/liquidStaking/useTokenUnlockDurations.ts
+++ b/apps/tangle-dapp/data/liquidStaking/useTokenUnlockDurations.ts
@@ -6,7 +6,7 @@ import { TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK } from '@webb-tools/webb-u
import { useCallback, useMemo } from 'react';
import {
- LsSimpleParachainTimeUnit,
+ LsParachainSimpleTimeUnit,
ParachainCurrency,
} from '../../constants/liquidStaking/types';
import useApiRx from '../../hooks/useApiRx';
@@ -16,7 +16,7 @@ import getValueOfTangleCurrency from './getValueOfTangleCurrency';
export type TokenUnlockDurationEntry = {
isNative: boolean;
currency: ParachainCurrency;
- timeUnit: LsSimpleParachainTimeUnit;
+ timeUnit: LsParachainSimpleTimeUnit;
};
const useTokenUnlockDurations = () => {
diff --git a/apps/tangle-dapp/data/liquifier/useContractRead.ts b/apps/tangle-dapp/data/liquifier/useContractRead.ts
index dcc15856d2..de0c8da32f 100644
--- a/apps/tangle-dapp/data/liquifier/useContractRead.ts
+++ b/apps/tangle-dapp/data/liquifier/useContractRead.ts
@@ -1,77 +1,87 @@
-import { HexString } from '@polkadot/util/types';
-import assert from 'assert';
-import { useCallback } from 'react';
+import { PromiseOrT } from '@webb-tools/abstract-api-provider';
+import { useCallback, useEffect, useState } from 'react';
import {
Abi as ViemAbi,
ContractFunctionArgs,
ContractFunctionName,
+ ContractFunctionReturnType,
} from 'viem';
-import { mainnet, sepolia } from 'viem/chains';
-import { ReadContractReturnType } from 'wagmi/actions';
-import { IS_PRODUCTION_ENV } from '../../constants/env';
-import ensureError from '../../utils/ensureError';
-import useViemPublicClientWithChain from './useViemPublicClientWithChain';
+import useDebugMetricsStore from '../../context/useDebugMetricsStore';
+import usePolling from '../liquidStaking/usePolling';
+import useContractReadOnce, {
+ ContractReadOptions,
+} from './useContractReadOnce';
-export type ContractReadOptions<
+/**
+ * Continuously reads a contract function, refreshing the value
+ * at a specified interval.
+ */
+const useContractRead = <
Abi extends ViemAbi,
FunctionName extends ContractFunctionName,
-> = {
- address: HexString;
- functionName: FunctionName;
- args: ContractFunctionArgs;
-};
-
-const useContractRead = (abi: Abi) => {
- // Use Sepolia testnet for development, and mainnet for production.
- // Some dummy contracts were deployed on Sepolia for testing purposes.
- const chain = IS_PRODUCTION_ENV ? mainnet : sepolia;
-
- const publicClient = useViemPublicClientWithChain(chain);
-
- const read = useCallback(
- async >({
- address,
- functionName,
- args,
- }: ContractReadOptions): Promise<
- | ReadContractReturnType<
+>(
+ abi: Abi,
+ options:
+ | ContractReadOptions
+ // Allow consumers to provide a function that returns the options.
+ // This is useful for when the options are dependent on some state.
+ | (() => PromiseOrT | null>),
+) => {
+ type ReturnType =
+ | Error
+ | Awaited<
+ ContractFunctionReturnType<
Abi,
+ 'pure' | 'view',
FunctionName,
ContractFunctionArgs
>
- | Error
- > => {
- assert(
- publicClient !== null,
- "Should not be able to call this function if the client isn't ready yet",
- );
+ >;
+
+ const [value, setValue] = useState(null);
+ const [isPaused, setIsPaused] = useState(false);
+ const readOnce = useContractReadOnce(abi);
- try {
- return await publicClient.readContract({
- address,
- abi,
- functionName,
- args,
- });
- } catch (possibleError) {
- const error = ensureError(possibleError);
+ const { incrementSubscriptionCount, decrementSubscriptionCount } =
+ useDebugMetricsStore();
- console.error(
- `Error reading contract ${address} function ${functionName}:`,
- error,
- );
+ // Register and deregister subscription count entry to keep track
+ // of performance metrics.
+ useEffect(() => {
+ isPaused ? decrementSubscriptionCount() : incrementSubscriptionCount();
- return error;
+ return () => {
+ if (!isPaused) {
+ decrementSubscriptionCount();
}
- },
- [abi, publicClient],
- );
+ };
+ }, [decrementSubscriptionCount, incrementSubscriptionCount, isPaused]);
+
+ const fetcher = useCallback(async () => {
+ // Not yet ready to fetch.
+ if (isPaused || readOnce === null) {
+ return null;
+ }
+
+ const options_ = typeof options === 'function' ? await options() : options;
+
+ // If the options are null, it means that the consumer
+ // of this hook does not want to fetch the data at this time.
+ if (options_ === null) {
+ return null;
+ }
+
+ setValue(await readOnce(options_));
+ }, [isPaused, options, readOnce]);
- // TODO: This only loads the balance once. Make it so it updates every few seconds that way the it responds to any balance changes that may occur, not just when loading the site initially. Like a subscription. So, add an extra function like `readStream` or `subscribe` that will allow the user to subscribe to the contract and get updates whenever the contract changes.
+ usePolling({
+ // By providing null, it signals to the hook to maintain
+ // its current value and not refresh.
+ effect: isPaused ? null : fetcher,
+ });
- // Only provide the read functions once the public client is ready.
- return publicClient === null ? null : read;
+ return { value, isPaused, setIsPaused };
};
export default useContractRead;
diff --git a/apps/tangle-dapp/data/liquifier/useContractReadBatch.ts b/apps/tangle-dapp/data/liquifier/useContractReadBatch.ts
new file mode 100644
index 0000000000..5e41f2cece
--- /dev/null
+++ b/apps/tangle-dapp/data/liquifier/useContractReadBatch.ts
@@ -0,0 +1,104 @@
+import { PromiseOrT } from '@webb-tools/abstract-api-provider';
+import { useCallback, useState } from 'react';
+import {
+ Abi as ViemAbi,
+ ContractFunctionArgs,
+ ContractFunctionName,
+ ContractFunctionReturnType,
+} from 'viem';
+import { mainnet, sepolia } from 'viem/chains';
+
+import { IS_PRODUCTION_ENV } from '../../constants/env';
+import ensureError from '../../utils/ensureError';
+import usePolling from '../liquidStaking/usePolling';
+import { ContractReadOptions } from './useContractReadOnce';
+import useViemPublicClientWithChain from './useViemPublicClientWithChain';
+
+export type ContractReadOptionsBatch<
+ Abi extends ViemAbi,
+ FunctionName extends ContractFunctionName,
+> = Omit, 'args'> & {
+ args: ContractReadOptions['args'][];
+};
+
+const useContractReadBatch = <
+ Abi extends ViemAbi,
+ FunctionName extends ContractFunctionName,
+>(
+ abi: Abi,
+ options:
+ | ContractReadOptionsBatch
+ // Allow consumers to provide a function that returns the options.
+ // This is useful for when the options are dependent on some state.
+ | (() => PromiseOrT | null>),
+) => {
+ type ValueType = ContractFunctionReturnType<
+ Abi,
+ 'pure' | 'view',
+ FunctionName,
+ ContractFunctionArgs
+ >;
+
+ type ReturnType = (Error | Awaited)[];
+
+ const [results, setResults] = useState(null);
+
+ // Use Sepolia testnet for development, and mainnet for production.
+ // Some dummy contracts were deployed on Sepolia for testing purposes.
+ const chain = IS_PRODUCTION_ENV ? mainnet : sepolia;
+
+ const publicClient = useViemPublicClientWithChain(chain);
+
+ const refresh = useCallback(async () => {
+ // Not yet ready to fetch.
+ if (publicClient === null) {
+ return null;
+ }
+
+ const options_ = typeof options === 'function' ? await options() : options;
+
+ // If the options are null, it means that the consumer
+ // of this hook does not want to fetch the data at this time.
+ if (options_ === null) {
+ return null;
+ }
+ // Remind developer about possible performance impact.
+ else if (!IS_PRODUCTION_ENV && options_.args.length >= 50) {
+ console.warn(
+ 'Reading a large amount of contracts simultaneously may affect performance, please consider utilizing pagination',
+ );
+ }
+
+ const targets = options_.args.map((args) => ({
+ abi: abi,
+ ...options_,
+ args,
+ }));
+
+ // See: https://viem.sh/docs/contract/multicall.html
+ const promiseOfAll = await publicClient.multicall({
+ // TODO: Viem is complaining about the type of `contracts` here.
+ contracts: targets as any,
+ });
+
+ // TODO: Avoid casting to ReturnType. Viem is complaining, likely because of the complexity of the types involved.
+ const results = promiseOfAll.map((promise) => {
+ if (promise.result === undefined) {
+ return ensureError(promise.error);
+ }
+
+ return promise.result;
+ }) as ReturnType;
+
+ setResults(results);
+ }, [abi, options, publicClient]);
+
+ usePolling({ effect: refresh });
+
+ return {
+ results,
+ refresh,
+ };
+};
+
+export default useContractReadBatch;
diff --git a/apps/tangle-dapp/data/liquifier/useContractReadOnce.ts b/apps/tangle-dapp/data/liquifier/useContractReadOnce.ts
new file mode 100644
index 0000000000..4182a1c74f
--- /dev/null
+++ b/apps/tangle-dapp/data/liquifier/useContractReadOnce.ts
@@ -0,0 +1,80 @@
+import { HexString } from '@polkadot/util/types';
+import assert from 'assert';
+import { useCallback } from 'react';
+import {
+ Abi as ViemAbi,
+ ContractFunctionArgs,
+ ContractFunctionName,
+} from 'viem';
+import { mainnet, sepolia } from 'viem/chains';
+import { ReadContractReturnType } from 'wagmi/actions';
+
+import { IS_PRODUCTION_ENV } from '../../constants/env';
+import useDebugMetricsStore from '../../context/useDebugMetricsStore';
+import ensureError from '../../utils/ensureError';
+import useViemPublicClientWithChain from './useViemPublicClientWithChain';
+
+export type ContractReadOptions<
+ Abi extends ViemAbi,
+ FunctionName extends ContractFunctionName,
+> = {
+ address: HexString;
+ functionName: FunctionName;
+ args: ContractFunctionArgs;
+};
+
+const useContractReadOnce = (abi: Abi) => {
+ const { incrementRequestCount } = useDebugMetricsStore();
+
+ // Use Sepolia testnet for development, and mainnet for production.
+ // Some dummy contracts were deployed on Sepolia for testing purposes.
+ const chain = IS_PRODUCTION_ENV ? mainnet : sepolia;
+
+ const publicClient = useViemPublicClientWithChain(chain);
+
+ const read = useCallback(
+ async >({
+ address,
+ functionName,
+ args,
+ }: ContractReadOptions): Promise<
+ | ReadContractReturnType<
+ Abi,
+ FunctionName,
+ ContractFunctionArgs
+ >
+ | Error
+ > => {
+ assert(
+ publicClient !== null,
+ "Should not be able to call this function if the client isn't ready yet",
+ );
+
+ incrementRequestCount();
+
+ try {
+ return await publicClient.readContract({
+ address,
+ abi,
+ functionName,
+ args,
+ });
+ } catch (possibleError) {
+ const error = ensureError(possibleError);
+
+ console.error(
+ `Error reading contract ${address} function ${functionName}:`,
+ error,
+ );
+
+ return error;
+ }
+ },
+ [abi, incrementRequestCount, publicClient],
+ );
+
+ // Only provide the read functions once the public client is ready.
+ return publicClient === null ? null : read;
+};
+
+export default useContractReadOnce;
diff --git a/apps/tangle-dapp/data/liquifier/useContractReadSubscription.ts b/apps/tangle-dapp/data/liquifier/useContractReadSubscription.ts
deleted file mode 100644
index 44ac4b7311..0000000000
--- a/apps/tangle-dapp/data/liquifier/useContractReadSubscription.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { PromiseOrT } from '@webb-tools/abstract-api-provider';
-import { useCallback, useState } from 'react';
-import { Abi as ViemAbi, ContractFunctionName } from 'viem';
-
-import usePolling, {
- PollingPrimaryCacheKey,
-} from '../liquidStaking/usePolling';
-import useContractRead, { ContractReadOptions } from './useContractRead';
-
-const useContractReadSubscription = <
- Abi extends ViemAbi,
- FunctionName extends ContractFunctionName,
->(
- abi: Abi,
- options:
- | ContractReadOptions
- // Allow consumers to provide a function that returns the options.
- // This is useful for when the options are dependent on some state.
- | (() => PromiseOrT | null>),
-) => {
- const [isPaused, setIsPaused] = useState(false);
- const read = useContractRead(abi);
-
- const fetcher = useCallback(async () => {
- if (isPaused || read === null) {
- return null;
- }
-
- const options_ = typeof options === 'function' ? await options() : options;
-
- if (options_ === null) {
- return null;
- }
-
- const result = await read(options_);
-
- return result instanceof Error ? null : result;
- }, [isPaused, options, read]);
-
- const { value, isRefreshing } = usePolling({
- // By providing null, it signals to the hook to maintain
- // its current value and not refresh.
- fetcher: isPaused ? null : fetcher,
- primaryCacheKey: PollingPrimaryCacheKey.CONTRACT_READ_SUBSCRIPTION,
- // TODO: Options include args, which may be objects. Having these objects be stable across renders is important for the intended caching behavior. Instead of using the options object directly, consider providing the user a way to specify a cache key.
- cacheKey: Object.values(options),
- });
-
- return { value, isRefreshing, isPaused, setIsPaused };
-};
-
-export default useContractReadSubscription;
diff --git a/apps/tangle-dapp/data/liquifier/useLiquifierDeposit.ts b/apps/tangle-dapp/data/liquifier/useLiquifierDeposit.ts
index 0550aba5fe..ed1aea4f33 100644
--- a/apps/tangle-dapp/data/liquifier/useLiquifierDeposit.ts
+++ b/apps/tangle-dapp/data/liquifier/useLiquifierDeposit.ts
@@ -4,9 +4,9 @@ import { useCallback } from 'react';
import { erc20Abi } from 'viem';
import { TxName } from '../../constants';
-import { LS_ERC20_TOKEN_MAP } from '../../constants/liquidStaking/constants';
+import { LS_LIQUIFIER_PROTOCOL_MAP } from '../../constants/liquidStaking/constants';
import LIQUIFIER_ABI from '../../constants/liquidStaking/liquifierAbi';
-import { LsErc20TokenId } from '../../constants/liquidStaking/types';
+import { LsLiquifierProtocolId } from '../../constants/liquidStaking/types';
import useEvmAddress20 from '../../hooks/useEvmAddress';
import useContractWrite from './useContractWrite';
@@ -38,7 +38,7 @@ const useLiquifierDeposit = () => {
activeEvmAddress20 !== null;
const deposit = useCallback(
- async (tokenId: LsErc20TokenId, amount: BN) => {
+ async (tokenId: LsLiquifierProtocolId, amount: BN) => {
// TODO: Should the user balance check be done here or assume that the consumer of the hook will handle that?
assert(
@@ -46,16 +46,16 @@ const useLiquifierDeposit = () => {
'Should not be able to call this function if the requirements are not ready yet',
);
- const tokenDef = LS_ERC20_TOKEN_MAP[tokenId];
+ const tokenDef = LS_LIQUIFIER_PROTOCOL_MAP[tokenId];
// TODO: Check for approval first, in case that it has already been granted. This prevents another unnecessary approval transaction (ex. if the transaction fails after the approval but before the deposit).
// Approve spending the token amount by the Liquifier contract.
const approveTxSucceeded = await writeChainlinkErc20({
txName: TxName.LS_LIQUIFIER_APPROVE,
- address: tokenDef.address,
+ address: tokenDef.erc20TokenAddress,
functionName: 'approve',
- args: [tokenDef.liquifierAdapterAddress, BigInt(amount.toString())],
- notificationStep: { current: 1, max: 2 },
+ args: [tokenDef.liquifierContractAddress, BigInt(amount.toString())],
+ notificationStep: { current: 1, total: 2 },
});
if (!approveTxSucceeded) {
@@ -65,11 +65,11 @@ const useLiquifierDeposit = () => {
const depositTxSucceeded = await writeLiquifier({
txName: TxName.LS_LIQUIFIER_DEPOSIT,
// TODO: Does the adapter contract have a deposit function? It doesn't seem like so. In that case, will need to update the way that Liquifier contract's address is handled.
- address: tokenDef.liquifierAdapterAddress,
+ address: tokenDef.liquifierContractAddress,
functionName: 'deposit',
// TODO: Provide the first arg. (validator). Need to figure out how it works on Chainlink (vaults? single address?). See: https://github.com/webb-tools/tnt-core/blob/21c158d6cb11e2b5f50409d377431e7cd51ff72f/src/lst/adapters/ChainlinkAdapter.sol#L187
args: [activeEvmAddress20, BigInt(amount.toString())],
- notificationStep: { current: 2, max: 2 },
+ notificationStep: { current: 2, total: 2 },
});
return depositTxSucceeded;
diff --git a/apps/tangle-dapp/data/liquifier/useLiquifierNftUnlocks.ts b/apps/tangle-dapp/data/liquifier/useLiquifierNftUnlocks.ts
index 98e4bee3bf..90fb0e76c7 100644
--- a/apps/tangle-dapp/data/liquifier/useLiquifierNftUnlocks.ts
+++ b/apps/tangle-dapp/data/liquifier/useLiquifierNftUnlocks.ts
@@ -1,21 +1,27 @@
-import { BN } from '@polkadot/util';
+import { assert, BN } from '@polkadot/util';
import { useCallback, useMemo } from 'react';
import { Address } from 'viem';
+import { BaseUnstakeRequest } from '../../components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable';
+import { IS_PRODUCTION_ENV } from '../../constants/env';
import LIQUIFIER_UNLOCKS_ABI from '../../constants/liquidStaking/liquifierUnlocksAbi';
-import { LsErc20TokenId } from '../../constants/liquidStaking/types';
+import { LsNetworkId } from '../../constants/liquidStaking/types';
import useEvmAddress20 from '../../hooks/useEvmAddress';
import getLsProtocolDef from '../../utils/liquidStaking/getLsProtocolDef';
-import { ContractReadOptions } from './useContractRead';
-import useContractReadSubscription from './useContractReadSubscription';
+import { useLsStore } from '../liquidStaking/useLsStore';
+import useContractRead from './useContractRead';
+import useContractReadBatch, {
+ ContractReadOptionsBatch,
+} from './useContractReadBatch';
+import { ContractReadOptions } from './useContractReadOnce';
/**
* Represents the metadata of an ERC-721 NFT liquifier unlock request.
*
* See: https://github.com/webb-tools/tnt-core/blob/21c158d6cb11e2b5f50409d377431e7cd51ff72f/src/lst/unlocks/Unlocks.sol#L21
*/
-export type LiquifierUnlockNft = {
- unlockId: number;
+export type LiquifierUnlockNftMetadata = BaseUnstakeRequest & {
+ type: 'liquifierUnlockNft';
symbol: string;
name: string;
validator: Address;
@@ -26,12 +32,6 @@ export type LiquifierUnlockNft = {
*/
progress: number;
- /**
- * The underlying stake tokens amount represented by the unlock
- * request.
- */
- amount: BN;
-
/**
* A timestamp representing the date at which the unlock request
* can be fulfilled.
@@ -47,95 +47,119 @@ export type LiquifierUnlockNft = {
* including the progress of the unlock request, the amount of underlying
* stake tokens, and the maturity timestamp.
*/
-const useLiquifierNftUnlocks = (
- tokenId: LsErc20TokenId,
-): LiquifierUnlockNft[] | null => {
+const useLiquifierNftUnlocks = (): LiquifierUnlockNftMetadata[] | null => {
+ const { selectedProtocolId } = useLsStore();
const activeEvmAddress20 = useEvmAddress20();
- // const getUnlockIdCountOptions = useCallback((): ContractReadOptions<
- // typeof LIQUIFIER_UNLOCKS_ABI,
- // 'balanceOf'
- // > | null => {
- // if (activeEvmAddress20 === null) {
- // return null;
- // }
-
- // const protocol = getLsProtocolDef(tokenId);
-
- // return {
- // // TODO: This should be something like 'unlockAddress', defined per-protocol. For now, just use the protocol address as a placeholder.
- // address: protocol.address,
- // functionName: 'balanceOf',
- // args: [activeEvmAddress20],
- // };
- // }, [activeEvmAddress20, tokenId]);
-
- // const { value: rawUnlockIdCount } = useContractReadSubscription(
- // LIQUIFIER_UNLOCKS_ABI,
- // getUnlockIdCountOptions,
- // );
-
- // const _unlockIds = useMemo(() => {
- // if (rawUnlockIdCount === null) {
- // return null;
- // }
-
- // const ids = [];
-
- // // TODO: Since this is a `balanceOf` operation, might need to shrink it down to base unit, since it's likely in the underlying token's decimals, which is very big, causing JavaScript to throw an `invalid array length` error. Also, for now made the upper bound be `0`, it should be `rawUnlockIdCount`, but it was erroring since it's not yet implemented.
- // for (let i = 0; i < 0; i++) {
- // ids.push(i);
- // }
-
- // return ids;
- // }, [rawUnlockIdCount]);
-
- // TODO: Need to page/lazy load this, since there could be many unlock requests. Then, paging would be handled by the parent table component. Perhaps try to add the lazy loading functionality directly into the `useContractReadSubscription` hook (e.g. multi-arg fetch capability + paging options & state).
- const getMetadataOptions = useCallback((): ContractReadOptions<
+ const protocol = getLsProtocolDef(selectedProtocolId);
+
+ const getUnlockIdCountOptions = useCallback((): ContractReadOptions<
+ typeof LIQUIFIER_UNLOCKS_ABI,
+ 'balanceOf'
+ > | null => {
+ if (
+ activeEvmAddress20 === null ||
+ protocol.networkId !== LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER
+ ) {
+ return null;
+ }
+
+ return {
+ address: protocol.unlocksContractAddress,
+ functionName: 'balanceOf',
+ args: [activeEvmAddress20],
+ };
+ }, [activeEvmAddress20, protocol]);
+
+ const { value: rawUnlockIdCount } = useContractRead(
+ LIQUIFIER_UNLOCKS_ABI,
+ getUnlockIdCountOptions,
+ );
+
+ const unlockIds = useMemo(() => {
+ if (rawUnlockIdCount === null || rawUnlockIdCount instanceof Error) {
+ return null;
+ }
+
+ // Extremely unlikely that the user would have this many unlock
+ // requests, but just in case.
+ assert(
+ rawUnlockIdCount <= Number.MAX_SAFE_INTEGER,
+ 'Unlock ID count exceeds maximum safe integer, user seems to have an unreasonable amount of unlock requests',
+ );
+
+ const unlockIdCount = Number(rawUnlockIdCount);
+
+ return Array.from({
+ length: unlockIdCount,
+ }).map((_, i) => BigInt(i));
+ }, [rawUnlockIdCount]);
+
+ const getMetadataOptions = useCallback((): ContractReadOptionsBatch<
typeof LIQUIFIER_UNLOCKS_ABI,
'getMetadata'
> | null => {
- // Do not fetch if there's no active EVM account.
- if (activeEvmAddress20 === null) {
+ if (
+ // Do not fetch if there's no active EVM account.
+ activeEvmAddress20 === null ||
+ protocol.networkId !== LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER ||
+ unlockIds === null
+ ) {
return null;
}
- const protocol = getLsProtocolDef(tokenId);
+ const batchArgs = unlockIds.map((unlockId) => [BigInt(unlockId)] as const);
return {
- // TODO: This should be something like 'unlockAddress', defined per-protocol. For now, just use the protocol address as a placeholder.
- address: protocol.address,
+ address: protocol.unlocksContractAddress,
functionName: 'getMetadata',
- // TODO: Using index 0 for now, until paging is implemented.
- // TODO: Consider adding support for an array of args, which would be interpreted as a multi-fetch by `useContractReadSubscription`.
- args: [BigInt(0)],
+ args: batchArgs,
};
- }, [activeEvmAddress20, tokenId]);
+ }, [activeEvmAddress20, protocol, unlockIds]);
- const { value: rawMetadata } = useContractReadSubscription(
+ const { results: rawMetadatas } = useContractReadBatch(
LIQUIFIER_UNLOCKS_ABI,
getMetadataOptions,
);
- const metadata = useMemo(() => {
- if (rawMetadata === null) {
+ const metadatas = useMemo(() => {
+ if (rawMetadatas === null) {
return null;
}
- return [
- {
- unlockId: Number(rawMetadata.unlockId),
- symbol: rawMetadata.symbol,
- name: rawMetadata.name,
- validator: rawMetadata.validator,
- progress: Number(rawMetadata.progress) / 100,
- amount: new BN(rawMetadata.amount.toString()),
- maturityTimestamp: Number(rawMetadata.maturity),
- },
- ];
- }, [rawMetadata]);
-
- return metadata;
+ return rawMetadatas.flatMap((metadata, index) => {
+ // Ignore failed metadata fetches and those that are still loading.
+ if (metadata === null || metadata instanceof Error) {
+ return [];
+ }
+
+ // The Sepolia development contract always returns 0 for the
+ // unlock ID. Use the index number to differentiate between
+ // different unlock requests.
+ const unlockId = IS_PRODUCTION_ENV ? Number(metadata.unlockId) : index;
+
+ // On development, mark some as completed for testing purposes.
+ const progress = IS_PRODUCTION_ENV
+ ? Number(metadata.progress) / 100
+ : index < 10
+ ? 1
+ : 0.6123;
+
+ return {
+ type: 'liquifierUnlockNft',
+ decimals: protocol.decimals,
+ unlockId,
+ symbol: metadata.symbol,
+ name: metadata.name,
+ validator: metadata.validator,
+ progress,
+ amount: new BN(metadata.amount.toString()),
+ maturityTimestamp: Number(metadata.maturity),
+ } satisfies LiquifierUnlockNftMetadata;
+ });
+ }, [protocol.decimals, rawMetadatas]);
+
+ return metadatas;
};
export default useLiquifierNftUnlocks;
diff --git a/apps/tangle-dapp/data/liquifier/useLiquifierUnlock.ts b/apps/tangle-dapp/data/liquifier/useLiquifierUnlock.ts
index 43d3ec7c52..cf7dcb3171 100644
--- a/apps/tangle-dapp/data/liquifier/useLiquifierUnlock.ts
+++ b/apps/tangle-dapp/data/liquifier/useLiquifierUnlock.ts
@@ -3,9 +3,9 @@ import assert from 'assert';
import { useCallback } from 'react';
import { TxName } from '../../constants';
-import { LS_ERC20_TOKEN_MAP } from '../../constants/liquidStaking/constants';
+import { LS_LIQUIFIER_PROTOCOL_MAP } from '../../constants/liquidStaking/constants';
import LIQUIFIER_ABI from '../../constants/liquidStaking/liquifierAbi';
-import { LsErc20TokenId } from '../../constants/liquidStaking/types';
+import { LsLiquifierProtocolId } from '../../constants/liquidStaking/types';
import useEvmAddress20 from '../../hooks/useEvmAddress';
import useContractWrite from './useContractWrite';
@@ -16,7 +16,7 @@ const useLiquifierUnlock = () => {
const isReady = writeLiquifier !== null && activeEvmAddress20 !== null;
const unlock = useCallback(
- async (tokenId: LsErc20TokenId, amount: BN): Promise => {
+ async (tokenId: LsLiquifierProtocolId, amount: BN): Promise => {
// TODO: Should the user balance check be done here or assume that the consumer of the hook will handle that?
assert(
@@ -24,12 +24,12 @@ const useLiquifierUnlock = () => {
'Should not be able to call this function if the requirements are not ready yet',
);
- const tokenDef = LS_ERC20_TOKEN_MAP[tokenId];
+ const tokenDef = LS_LIQUIFIER_PROTOCOL_MAP[tokenId];
return writeLiquifier({
txName: TxName.LS_LIQUIFIER_UNLOCK,
// TODO: Does the adapter contract have a unlock function? It doesn't seem like so. In that case, will need to update the way that Liquifier contract's address is handled.
- address: tokenDef.liquifierAdapterAddress,
+ address: tokenDef.liquifierContractAddress,
functionName: 'unlock',
args: [BigInt(amount.toString())],
});
diff --git a/apps/tangle-dapp/data/liquifier/useLiquifierWithdraw.ts b/apps/tangle-dapp/data/liquifier/useLiquifierWithdraw.ts
new file mode 100644
index 0000000000..452b4b813b
--- /dev/null
+++ b/apps/tangle-dapp/data/liquifier/useLiquifierWithdraw.ts
@@ -0,0 +1,52 @@
+import assert from 'assert';
+import { useCallback } from 'react';
+
+import { TxName } from '../../constants';
+import { LS_LIQUIFIER_PROTOCOL_MAP } from '../../constants/liquidStaking/constants';
+import LIQUIFIER_ABI from '../../constants/liquidStaking/liquifierAbi';
+import { LsLiquifierProtocolId } from '../../constants/liquidStaking/types';
+import useEvmAddress20 from '../../hooks/useEvmAddress';
+import { NotificationSteps } from '../../hooks/useTxNotification';
+import useContractWrite from './useContractWrite';
+
+const useLiquifierWithdraw = () => {
+ const activeEvmAddress20 = useEvmAddress20();
+ const writeLiquifier = useContractWrite(LIQUIFIER_ABI);
+
+ const isReady = writeLiquifier !== null && activeEvmAddress20 !== null;
+
+ const withdraw = useCallback(
+ async (
+ tokenId: LsLiquifierProtocolId,
+ unlockId: number,
+ notificationStep?: NotificationSteps,
+ ) => {
+ // TODO: Should the user balance check be done here or assume that the consumer of the hook will handle that?
+
+ assert(
+ isReady,
+ 'Should not be able to call this function if the requirements are not ready yet',
+ );
+
+ const tokenDef = LS_LIQUIFIER_PROTOCOL_MAP[tokenId];
+
+ const withdrawTxSucceeded = await writeLiquifier({
+ txName: TxName.LS_LIQUIFIER_WITHDRAW,
+ // TODO: Does the adapter contract have a deposit function? It doesn't seem like so. In that case, will need to update the way that Liquifier contract's address is handled.
+ address: tokenDef.liquifierContractAddress,
+ functionName: 'withdraw',
+ args: [activeEvmAddress20, BigInt(unlockId)],
+ notificationStep,
+ });
+
+ return withdrawTxSucceeded;
+ },
+ [activeEvmAddress20, isReady, writeLiquifier],
+ );
+
+ // Wait for the requirements to be ready before
+ // returning the withdraw function.
+ return !isReady ? null : withdraw;
+};
+
+export default useLiquifierWithdraw;
diff --git a/apps/tangle-dapp/data/payouts/usePayoutAllTx.ts b/apps/tangle-dapp/data/payouts/usePayoutAllTx.ts
index df91bbcc88..5b8f061598 100644
--- a/apps/tangle-dapp/data/payouts/usePayoutAllTx.ts
+++ b/apps/tangle-dapp/data/payouts/usePayoutAllTx.ts
@@ -49,6 +49,7 @@ const usePayoutAllTx = () => {
},
);
+ // TODO: Will need to split tx into multiple batch calls if there are too many, this is because it will otherwise fail with "1010: Invalid Transaction: Transaction would exhaust the block limits," due to the block weight limit.
return optimizeTxBatch(api, txs);
}, []);
diff --git a/apps/tangle-dapp/hooks/useInputAmount.ts b/apps/tangle-dapp/hooks/useInputAmount.ts
index 00c015b291..90fac09daf 100644
--- a/apps/tangle-dapp/hooks/useInputAmount.ts
+++ b/apps/tangle-dapp/hooks/useInputAmount.ts
@@ -94,6 +94,7 @@ const useInputAmount = ({
return;
}
+ // TODO: Format the new amount string to include commas. Use `INPUT_AMOUNT_FORMAT`.
setDisplayAmount(cleanAmountString);
const amountOrError = safeParseInputAmount({
@@ -131,7 +132,12 @@ const useInputAmount = ({
const setDisplayAmount_ = useCallback(
(amount: BN) => {
- setDisplayAmount(formatBn(amount, decimals, INPUT_AMOUNT_FORMAT));
+ setDisplayAmount(
+ formatBn(amount, decimals, {
+ ...INPUT_AMOUNT_FORMAT,
+ includeCommas: false,
+ }),
+ );
},
[decimals],
);
diff --git a/apps/tangle-dapp/hooks/useLocalStorage.ts b/apps/tangle-dapp/hooks/useLocalStorage.ts
index 024c3fb670..ae4b9e110e 100644
--- a/apps/tangle-dapp/hooks/useLocalStorage.ts
+++ b/apps/tangle-dapp/hooks/useLocalStorage.ts
@@ -8,8 +8,8 @@ import { BridgeQueueTxItem } from '../types/bridge';
import {
Collator,
Dapp,
+ PhalaVaultOrStakePool,
Validator,
- VaultOrStakePool,
} from '../types/liquidStaking';
import Optional from '../utils/Optional';
@@ -36,7 +36,7 @@ export type PayoutsCache = {
};
export type LiquidStakingTableData = {
- [chain: string]: Validator[] | VaultOrStakePool[] | Dapp[] | Collator[];
+ [chain: string]: Validator[] | PhalaVaultOrStakePool[] | Dapp[] | Collator[];
};
export type SubstrateWalletsMetadataEntry = {
diff --git a/apps/tangle-dapp/hooks/useTxNotification.tsx b/apps/tangle-dapp/hooks/useTxNotification.tsx
index bd787978c7..1096540343 100644
--- a/apps/tangle-dapp/hooks/useTxNotification.tsx
+++ b/apps/tangle-dapp/hooks/useTxNotification.tsx
@@ -35,6 +35,7 @@ const SUCCESS_MESSAGES: Record = {
[TxName.LS_LIQUIFIER_DEPOSIT]: 'Liquifier deposit successful',
[TxName.LS_LIQUIFIER_APPROVE]: 'Liquifier approval successful',
[TxName.LS_LIQUIFIER_UNLOCK]: 'Liquifier unlock successful',
+ [TxName.LS_LIQUIFIER_WITHDRAW]: 'Liquifier withdrawal successful',
};
const makeKey = (txName: TxName): `${TxName}-tx-notification` =>
@@ -42,7 +43,7 @@ const makeKey = (txName: TxName): `${TxName}-tx-notification` =>
export type NotificationSteps = {
current: number;
- max: number;
+ total: number;
};
// TODO: Use a ref for the key to permit multiple rapid fire transactions from stacking under the same key. Otherwise, use a global state counter via Zustand.
@@ -133,7 +134,7 @@ const useTxNotification = (explorerUrl?: string) => {
const notifyProcessing = useCallback(
(txName: TxName, steps?: NotificationSteps) => {
// Sanity check.
- if (steps !== undefined && steps.current > steps.max) {
+ if (steps !== undefined && steps.current > steps.total) {
console.warn(
'Current transaction notification steps exceed the maximum steps (check for off-by-one errors)',
);
@@ -145,8 +146,8 @@ const useTxNotification = (explorerUrl?: string) => {
enqueueSnackbar(
- {steps !== undefined && `(${steps.current}/${steps.max}) `}Processing{' '}
- {txName}
+ {steps !== undefined && `(${steps.current}/${steps.total}) `}
+ Processing {txName}
,
{
key,
diff --git a/apps/tangle-dapp/types/liquidStaking.ts b/apps/tangle-dapp/types/liquidStaking.ts
index 4a086feae9..7e48666029 100644
--- a/apps/tangle-dapp/types/liquidStaking.ts
+++ b/apps/tangle-dapp/types/liquidStaking.ts
@@ -23,7 +23,7 @@ export type Validator = {
} & StakingItem;
// Chain - Phala Network (Stake on Vaults or Stake Pools)
-export type VaultOrStakePool = {
+export type PhalaVaultOrStakePool = {
vaultOrStakePoolID: string;
vaultOrStakePoolName: string;
vaultOrStakePoolAccountID: string;
@@ -55,7 +55,7 @@ export enum LiquidStakingItem {
export type LiquidStakingItemType =
| Validator
- | VaultOrStakePool
+ | PhalaVaultOrStakePool
| Dapp
| Collator;
diff --git a/apps/tangle-dapp/utils/assertSubstrateAddress.ts b/apps/tangle-dapp/utils/assertSubstrateAddress.ts
new file mode 100644
index 0000000000..615daedf5c
--- /dev/null
+++ b/apps/tangle-dapp/utils/assertSubstrateAddress.ts
@@ -0,0 +1,18 @@
+import { isAddress } from '@polkadot/util-crypto';
+import assert from 'assert';
+
+import { SubstrateAddress } from '../types/utils';
+
+const assertSubstrateAddress = (
+ address: string,
+ ss58Prefix: SS58,
+): SubstrateAddress => {
+ assert(
+ isAddress(address, undefined, ss58Prefix),
+ 'Address should be a valid Substrate address',
+ );
+
+ return address as SubstrateAddress;
+};
+
+export default assertSubstrateAddress;
diff --git a/apps/tangle-dapp/utils/formatBn.ts b/apps/tangle-dapp/utils/formatBn.ts
index 5a32174257..a6c233a744 100644
--- a/apps/tangle-dapp/utils/formatBn.ts
+++ b/apps/tangle-dapp/utils/formatBn.ts
@@ -34,14 +34,18 @@ function formatBn(
decimals: number,
options?: Partial,
): string {
- const finalOptions = { ...DEFAULT_FORMAT_OPTIONS, ...options };
+ const finalOptions: FormatOptions = { ...DEFAULT_FORMAT_OPTIONS, ...options };
const chainUnitFactorBn = getChainUnitFactor(decimals);
+ const isNegative = amount.isNeg();
+
+ // There's a weird bug with BN.js, so need to create a new BN
+ // instance here for the amount, to avoid a strange error.
+ const integerPartBn = new BN(amount.toString()).div(chainUnitFactorBn);
- const integerPartBn = new BN(amount).div(chainUnitFactorBn);
const remainderBn = amount.mod(chainUnitFactorBn);
- let integerPart = integerPartBn.toString(10);
- let fractionPart = remainderBn.toString(10).padStart(decimals, '0');
+ let integerPart = integerPartBn.abs().toString(10);
+ let fractionPart = remainderBn.abs().toString(10).padStart(decimals, '0');
const amountStringLength = amount.toString().length;
const partsLength = integerPart.length + fractionPart.length;
@@ -87,9 +91,13 @@ function formatBn(
integerPart = addCommasToNumber(integerPart);
}
+ const polarity = isNegative ? '-' : '';
+
// Combine the integer and fraction parts. Only include the fraction
// part if it's available.
- return fractionPart !== '' ? `${integerPart}.${fractionPart}` : integerPart;
+ return fractionPart !== ''
+ ? `${polarity}${integerPart}.${fractionPart}`
+ : `${polarity}${integerPart}`;
}
export default formatBn;
diff --git a/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts b/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts
new file mode 100644
index 0000000000..811ebc96a3
--- /dev/null
+++ b/apps/tangle-dapp/utils/liquidStaking/getLsNetwork.ts
@@ -0,0 +1,16 @@
+import {
+ LS_ETHEREUM_MAINNET_LIQUIFIER,
+ LS_TANGLE_RESTAKING_PARACHAIN,
+} from '../../constants/liquidStaking/constants';
+import { LsNetwork, LsNetworkId } from '../../constants/liquidStaking/types';
+
+const getLsNetwork = (networkId: LsNetworkId): LsNetwork => {
+ switch (networkId) {
+ case LsNetworkId.ETHEREUM_MAINNET_LIQUIFIER:
+ return LS_ETHEREUM_MAINNET_LIQUIFIER;
+ case LsNetworkId.TANGLE_RESTAKING_PARACHAIN:
+ return LS_TANGLE_RESTAKING_PARACHAIN;
+ }
+};
+
+export default getLsNetwork;
diff --git a/apps/tangle-dapp/utils/liquidStaking/getLsProtocolDef.ts b/apps/tangle-dapp/utils/liquidStaking/getLsProtocolDef.ts
index 63d4d90201..685d7f8520 100644
--- a/apps/tangle-dapp/utils/liquidStaking/getLsProtocolDef.ts
+++ b/apps/tangle-dapp/utils/liquidStaking/getLsProtocolDef.ts
@@ -2,7 +2,7 @@ import assert from 'assert';
import { LS_PROTOCOLS } from '../../constants/liquidStaking/constants';
import {
- LsErc20TokenDef,
+ LsLiquifierProtocolDef,
LsParachainChainDef,
LsParachainChainId,
LsProtocolId,
@@ -10,7 +10,7 @@ import {
type IdToDefMap = T extends LsParachainChainId
? LsParachainChainDef
- : LsErc20TokenDef;
+ : LsLiquifierProtocolDef;
const getLsProtocolDef = (id: T): IdToDefMap => {
const result = LS_PROTOCOLS.find((def) => def.id === id);
diff --git a/apps/tangle-dapp/utils/liquidStaking/isLiquifierProtocolId.ts b/apps/tangle-dapp/utils/liquidStaking/isLiquifierProtocolId.ts
new file mode 100644
index 0000000000..1917fbf6b6
--- /dev/null
+++ b/apps/tangle-dapp/utils/liquidStaking/isLiquifierProtocolId.ts
@@ -0,0 +1,10 @@
+import { LS_LIQUIFIER_PROTOCOL_IDS } from '../../constants/liquidStaking/constants';
+import { LsLiquifierProtocolId } from '../../constants/liquidStaking/types';
+
+function isLiquifierProtocolId(
+ protocolId: number,
+): protocolId is LsLiquifierProtocolId {
+ return LS_LIQUIFIER_PROTOCOL_IDS.includes(protocolId);
+}
+
+export default isLiquifierProtocolId;
diff --git a/apps/tangle-dapp/utils/liquidStaking/isLsErc20TokenId.ts b/apps/tangle-dapp/utils/liquidStaking/isLsErc20TokenId.ts
deleted file mode 100644
index da6c31f566..0000000000
--- a/apps/tangle-dapp/utils/liquidStaking/isLsErc20TokenId.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { LS_ERC20_TOKEN_IDS } from '../../constants/liquidStaking/constants';
-import { LsErc20TokenId } from '../../constants/liquidStaking/types';
-
-function isLsErc20TokenId(tokenId: number): tokenId is LsErc20TokenId {
- return LS_ERC20_TOKEN_IDS.includes(tokenId);
-}
-
-export default isLsErc20TokenId;
diff --git a/apps/tangle-dapp/utils/liquidStaking/stringifyTimeUnit.ts b/apps/tangle-dapp/utils/liquidStaking/stringifyTimeUnit.ts
index 5acddb8efd..212761036c 100644
--- a/apps/tangle-dapp/utils/liquidStaking/stringifyTimeUnit.ts
+++ b/apps/tangle-dapp/utils/liquidStaking/stringifyTimeUnit.ts
@@ -1,7 +1,7 @@
-import { LsSimpleParachainTimeUnit } from '../../constants/liquidStaking/types';
+import { LsParachainSimpleTimeUnit } from '../../constants/liquidStaking/types';
const stringifyTimeUnit = (
- timeUnit: LsSimpleParachainTimeUnit,
+ timeUnit: LsParachainSimpleTimeUnit,
): [number, string] => {
const plurality = timeUnit.value === 1 ? '' : 's';
diff --git a/apps/tangle-dapp/utils/liquidStaking/tangleTimeUnitToSimpleInstance.ts b/apps/tangle-dapp/utils/liquidStaking/tangleTimeUnitToSimpleInstance.ts
index 819139d7c9..2dfedc47a5 100644
--- a/apps/tangle-dapp/utils/liquidStaking/tangleTimeUnitToSimpleInstance.ts
+++ b/apps/tangle-dapp/utils/liquidStaking/tangleTimeUnitToSimpleInstance.ts
@@ -1,11 +1,11 @@
import { TanglePrimitivesTimeUnit } from '@polkadot/types/lookup';
-import { LsSimpleParachainTimeUnit } from '../../constants/liquidStaking/types';
+import { LsParachainSimpleTimeUnit } from '../../constants/liquidStaking/types';
import getValueOfTangleTimeUnit from './getValueOfTangleTimeUnit';
const tangleTimeUnitToSimpleInstance = (
tangleTimeUnit: TanglePrimitivesTimeUnit,
-): LsSimpleParachainTimeUnit => {
+): LsParachainSimpleTimeUnit => {
return {
unit: tangleTimeUnit.type,
value: getValueOfTangleTimeUnit(tangleTimeUnit),