Skip to content

Commit

Permalink
feat(tangle-dapp): Fetch all unlock NFTs
Browse files Browse the repository at this point in the history
  • Loading branch information
yurixander committed Aug 24, 2024
1 parent 3c21add commit dd3220c
Show file tree
Hide file tree
Showing 12 changed files with 348 additions and 230 deletions.
13 changes: 1 addition & 12 deletions apps/tangle-dapp/app/liquid-staking/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@ import { FC } from 'react';
import { LiquidStakingSelectionTable } from '../../components/LiquidStaking/LiquidStakingSelectionTable';
import LiquidStakeCard from '../../components/LiquidStaking/stakeAndUnstake/LiquidStakeCard';
import LiquidUnstakeCard from '../../components/LiquidStaking/stakeAndUnstake/LiquidUnstakeCard';
import UnlockNftsTable from '../../components/LiquidStaking/unlockNftsTable/UnlockNftsTable';
import UnstakeRequestsTable from '../../components/LiquidStaking/unstakeRequestsTable/UnstakeRequestsTable';
import { LsSearchParamKey } from '../../constants/liquidStaking/types';
import { useLiquidStakingStore } from '../../data/liquidStaking/useLiquidStakingStore';
import useSearchParamState from '../../hooks/useSearchParamState';
import isLsParachainChainId from '../../utils/liquidStaking/isLsParachainChainId';
import TabListItem from '../restake/TabListItem';
import TabsList from '../restake/TabsList';

Expand All @@ -28,8 +25,6 @@ const LiquidStakingTokenPage: FC = () => {
value ? SearchParamAction.STAKE : SearchParamAction.UNSTAKE,
});

const { selectedProtocolId } = useLiquidStakingStore();

return (
<div className="flex flex-wrap gap-12">
<div className="flex flex-col gap-4 w-full min-w-[450px] max-w-[600px]">
Expand All @@ -50,13 +45,7 @@ const LiquidStakingTokenPage: FC = () => {
</div>

<div className="flex flex-col flex-grow w-min gap-4 min-w-[370px]">
{isStaking ? (
<LiquidStakingSelectionTable />
) : isLsParachainChainId(selectedProtocolId) ? (
<UnstakeRequestsTable />
) : (
<UnlockNftsTable tokenId={selectedProtocolId} />
)}
{isStaking ? <LiquidStakingSelectionTable /> : <UnstakeRequestsTable />}
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ import { EMPTY_VALUE_PLACEHOLDER } from '../../../constants';
import LIQUIFIER_TG_TOKEN_ABI from '../../../constants/liquidStaking/liquifierTgTokenAbi';
import { LsProtocolId } from '../../../constants/liquidStaking/types';
import useParachainBalances from '../../../data/liquidStaking/useParachainBalances';
import usePolling, {
PollingPrimaryCacheKey,
} from '../../../data/liquidStaking/usePolling';
import useContractRead from '../../../data/liquifier/useContractRead';
import usePolling from '../../../data/liquidStaking/usePolling';
import useContractReadOnce from '../../../data/liquifier/useContractReadOnce';
import useEvmAddress20 from '../../../hooks/useEvmAddress';
import useSubstrateAddress from '../../../hooks/useSubstrateAddress';
import getLsProtocolDef from '../../../utils/liquidStaking/getLsProtocolDef';
Expand All @@ -18,8 +16,10 @@ const useAgnosticLsBalance = (isNative: boolean, protocolId: LsProtocolId) => {
const substrateAddress = useSubstrateAddress();
const evmAddress20 = useEvmAddress20();
const { nativeBalances, liquidBalances } = useParachainBalances();
const readErc20 = useContractRead(erc20Abi);
const readLiquidErc20 = useContractRead(LIQUIFIER_TG_TOKEN_ABI);

// TODO: Why not use the subscription hook variants (useContractRead) instead of manually utilizing usePolling?
const readErc20 = useContractReadOnce(erc20Abi);
const readLiquidErc20 = useContractReadOnce(LIQUIFIER_TG_TOKEN_ABI);

const [balance, setBalance] = useState<
BN | null | typeof EMPTY_VALUE_PLACEHOLDER
Expand Down Expand Up @@ -80,7 +80,6 @@ const useAgnosticLsBalance = (isNative: boolean, protocolId: LsProtocolId) => {
usePolling({
fetcher: erc20BalanceFetcher,
refreshInterval: 5_000,
primaryCacheKey: PollingPrimaryCacheKey.LS_ERC20_BALANCE,
});

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import useLiquifierNftUnlocks, {
import useSubstrateAddress from '../../../hooks/useSubstrateAddress';
import addCommasToNumber from '../../../utils/addCommasToNumber';
import isLsErc20TokenId from '../../../utils/liquidStaking/isLsErc20TokenId';
import isLsParachainChainId from '../../../utils/liquidStaking/isLsParachainChainId';
import stringifyTimeUnit from '../../../utils/liquidStaking/stringifyTimeUnit';
import GlassCard from '../../GlassCard';
import { HeaderCell } from '../../tableCells';
Expand Down Expand Up @@ -160,7 +161,9 @@ const UnstakeRequestsTable: FC = () => {
const { selectedProtocolId } = useLiquidStakingStore();
const substrateAddress = useSubstrateAddress();
const parachainRows = useLstUnlockRequestTableRows();
const evmRows = useLiquifierNftUnlocks();

// TODO: Link table paging with paging options here.
const evmRows = useLiquifierNftUnlocks({ page: 1, pageSize: 5 });

// Select the table rows based on whether the selected protocol
// is an EVM-based chain or a parachain-based Substrate chain.
Expand All @@ -187,13 +190,13 @@ const UnstakeRequestsTable: FC = () => {
);

const tableProps = useReactTable(tableOptions);
const tablePropsRows = tableProps.getSelectedRowModel().rows;
const selectedRows = tableProps.getSelectedRowModel().rows;

const selectedRowsUnlockIds = useMemo<Set<number>>(() => {
const selectedRows = tablePropsRows.map((row) => row.original.unlockId);
const selectedRowsIds = selectedRows.map((row) => row.original.unlockId);

return new Set(selectedRows);
}, [tablePropsRows]);
return new Set(selectedRowsIds);
}, [selectedRows]);

// Note that memoizing this will cause the table to not update
// when the selected rows change.
Expand Down Expand Up @@ -241,7 +244,7 @@ const UnstakeRequestsTable: FC = () => {
// have all completed their unlocking period.
const canWithdrawAllSelected = useMemo(() => {
// No rows selected or not loaded yet.
if (selectedRowsUnlockIds.size === 0 || parachainRows === null) {
if (selectedRowsUnlockIds.size === 0 || rows === null) {
return false;
}

Expand All @@ -250,9 +253,7 @@ const UnstakeRequestsTable: FC = () => {
// Check that all selected rows have completed their unlocking
// period.
return unlockIds.every((unlockId) => {
const request = parachainRows.find(
(request) => request.unlockId === unlockId,
);
const request = rows.find((request) => request.unlockId === unlockId);

assert(
request !== undefined,
Expand All @@ -265,15 +266,17 @@ const UnstakeRequestsTable: FC = () => {
? request.progress === undefined
: request.progress === 100;
});
}, [selectedRowsUnlockIds, parachainRows]);
}, [selectedRowsUnlockIds, rows]);

const currenciesAndUnlockIds = useMemo<[ParachainCurrency, number][]>(() => {
return tablePropsRows.map((row) => {
const { currency, unlockId } = row.original;
return selectedRows.flatMap((row) => {
if (row.original.type !== 'parachainUnstakeRequest') {
return [];
}

return [currency, unlockId];
return [[row.original.currency, row.original.unlockId]];
});
}, [tablePropsRows]);
}, [selectedRows]);

return (
<div className="space-y-4 flex-grow max-w-[700px]">
Expand All @@ -288,16 +291,20 @@ const UnstakeRequestsTable: FC = () => {

{parachainRows !== null && parachainRows.length > 0 && (
<div className="flex gap-3 items-center justify-center">
<RebondLstUnstakeRequestButton
// Can only rebond if there are selected rows.
isDisabled={selectedRowsUnlockIds.size === 0}
currenciesAndUnlockIds={currenciesAndUnlockIds}
/>

<WithdrawLstUnstakeRequestButton
canWithdraw={canWithdrawAllSelected}
currenciesAndUnlockIds={currenciesAndUnlockIds}
/>
{isLsParachainChainId(selectedProtocolId) && (
<RebondLstUnstakeRequestButton
// Can only rebond if there are selected rows.
isDisabled={selectedRowsUnlockIds.size === 0}
currenciesAndUnlockIds={currenciesAndUnlockIds}
/>
)}

{isLsParachainChainId(selectedProtocolId) && (
<WithdrawLstUnstakeRequestButton
canWithdraw={canWithdrawAllSelected}
currenciesAndUnlockIds={currenciesAndUnlockIds}
/>
)}
</div>
)}
</GlassCard>
Expand Down
14 changes: 14 additions & 0 deletions apps/tangle-dapp/constants/liquidStaking/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const SEPOLIA_TESTNET_CONTRACTS = {
ERC20: '0x2eE951c2d215ba1b3E0DF20764c96a0bC7809F41',
// Use the same address as the dummy ERC20 contract.
TG_TOKEN: '0x2eE951c2d215ba1b3E0DF20764c96a0bC7809F41',
// TODO: Deploy a development/testing `Unlocks.sol` contract on Sepolia and put its address here.
UNLOCKS: '0x',
} as const satisfies Record<string, HexString>;

const CHAINLINK: LsErc20TokenDef = {
Expand All @@ -44,6 +46,9 @@ const CHAINLINK: LsErc20TokenDef = {
liquifierTgTokenAddress: IS_PRODUCTION_ENV
? '0x'
: SEPOLIA_TESTNET_CONTRACTS.TG_TOKEN,
liquifierUnlocksAddress: IS_PRODUCTION_ENV
? '0x'
: SEPOLIA_TESTNET_CONTRACTS.UNLOCKS,
timeUnit: CrossChainTimeUnit.DAY,
unstakingPeriod: 7,
};
Expand All @@ -65,6 +70,9 @@ const THE_GRAPH: LsErc20TokenDef = {
liquifierTgTokenAddress: IS_PRODUCTION_ENV
? '0x'
: SEPOLIA_TESTNET_CONTRACTS.TG_TOKEN,
liquifierUnlocksAddress: IS_PRODUCTION_ENV
? '0x'
: SEPOLIA_TESTNET_CONTRACTS.UNLOCKS,
timeUnit: CrossChainTimeUnit.DAY,
unstakingPeriod: 28,
};
Expand All @@ -86,6 +94,9 @@ const LIVEPEER: LsErc20TokenDef = {
liquifierTgTokenAddress: IS_PRODUCTION_ENV
? '0x'
: SEPOLIA_TESTNET_CONTRACTS.TG_TOKEN,
liquifierUnlocksAddress: IS_PRODUCTION_ENV
? '0x'
: SEPOLIA_TESTNET_CONTRACTS.UNLOCKS,
timeUnit: CrossChainTimeUnit.LIVEPEER_ROUND,
unstakingPeriod: 7,
};
Expand All @@ -108,6 +119,9 @@ const POLYGON: LsErc20TokenDef = {
liquifierTgTokenAddress: IS_PRODUCTION_ENV
? '0x'
: SEPOLIA_TESTNET_CONTRACTS.TG_TOKEN,
liquifierUnlocksAddress: IS_PRODUCTION_ENV
? '0x'
: SEPOLIA_TESTNET_CONTRACTS.UNLOCKS,
timeUnit: CrossChainTimeUnit.POLYGON_CHECKPOINT,
unstakingPeriod: 82,
};
Expand Down
1 change: 1 addition & 0 deletions apps/tangle-dapp/constants/liquidStaking/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export interface LsErc20TokenDef extends ProtocolDefCommon {
address: HexString;
liquifierAdapterAddress: HexString;
liquifierTgTokenAddress: HexString;
liquifierUnlocksAddress: HexString;
}

export type LsProtocolDef = LsParachainChainDef | LsErc20TokenDef;
Expand Down
10 changes: 4 additions & 6 deletions apps/tangle-dapp/data/liquidStaking/useExchangeRate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import {
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';
import useContractRead from '../liquifier/useContractRead';
import { ContractReadOptions } from '../liquifier/useContractReadOnce';
import usePolling from './usePolling';

export enum ExchangeRateType {
NativeToLiquid,
Expand Down Expand Up @@ -101,7 +101,7 @@ const useExchangeRate = (type: ExchangeRateType, protocolId: LsProtocolId) => {
const {
value: _erc20TotalIssuance,
setIsPaused: setIsErc20TotalIssuancePaused,
} = useContractReadSubscription(erc20Abi, totalSupplyFetcher);
} = useContractRead(erc20Abi, totalSupplyFetcher);

// Pause or resume ERC20-based exchange rate fetching based
// on whether the requested protocol is a parachain or an ERC20 token.
Expand All @@ -117,8 +117,6 @@ const useExchangeRate = (type: ExchangeRateType, protocolId: LsProtocolId) => {
fetcher,
// Refresh every 5 seconds.
refreshInterval: 5_000,
primaryCacheKey: PollingPrimaryCacheKey.EXCHANGE_RATE,
cacheKey: ['exchangeRate', protocolId],
});

return { exchangeRate, isRefreshing };
Expand Down
21 changes: 14 additions & 7 deletions apps/tangle-dapp/data/liquidStaking/usePolling.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';

export enum PollingPrimaryCacheKey {
EXCHANGE_RATE,
Expand All @@ -9,21 +9,16 @@ export enum PollingPrimaryCacheKey {
export type PollingOptions<T> = {
fetcher: (() => Promise<T> | T) | null;
refreshInterval?: number;
primaryCacheKey: PollingPrimaryCacheKey;
cacheKey?: unknown[];
};

// TODO: Use Zustand global store for caching.

const usePolling = <T>({
fetcher,
// Default to a 3 second refresh interval.
refreshInterval = 3_000,
primaryCacheKey: _primaryCacheKey,
cacheKey: _cacheKey,
}: PollingOptions<T>) => {
const [value, setValue] = useState<T | null>(null);
const [isRefreshing, setIsRefreshing] = useState(false);
const lastUpdatedTimestampRef = useRef(Date.now());

useEffect(() => {
const intervalHandle = setInterval(async () => {
Expand All @@ -32,8 +27,20 @@ const usePolling = <T>({
return;
}

const now = Date.now();
const difference = now - lastUpdatedTimestampRef.current;

// Don't refresh if the last refresh was less than
// the refresh interval ago. This prevents issues where
// the fetcher is unstable and the setInterval gets called
// multiple times before the fetcher resolves.
if (difference < refreshInterval) {
return;
}

setIsRefreshing(true);
setValue(await fetcher());
lastUpdatedTimestampRef.current = now;
setIsRefreshing(false);
}, refreshInterval);

Expand Down
Loading

0 comments on commit dd3220c

Please sign in to comment.