Skip to content

Commit

Permalink
refactor(tangle-dapp): Fetch all NFTs at once
Browse files Browse the repository at this point in the history
  • Loading branch information
yurixander committed Aug 25, 2024
1 parent bfe3cd1 commit 84a52ce
Show file tree
Hide file tree
Showing 14 changed files with 158 additions and 120 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const useAgnosticLsBalance = (isNative: boolean, protocolId: LsProtocolId) => {
});
}, [evmAddress20, isNative, protocol, readErc20, readLiquidErc20]);

usePolling({ fetcher: erc20BalanceFetcher });
usePolling({ effect: erc20BalanceFetcher });

useEffect(() => {
if (protocol.type !== 'parachain' || parachainBalances === null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ const RebondLstUnstakeRequestButton: FC<RebondLstUnstakeRequestButtonProps> = ({
}
onClick={() => setIsConfirmationModalOpen(true)}
isFullWidth
isLoading={rebondTxStatus === TxStatus.PROCESSING}
loadingText="Processing"
>
Cancel Unstake
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ type UnstakeRequestTableRow =
| LiquifierUnlockNftMetadata
| ParachainUnstakeRequest;

const columnHelper = createColumnHelper<UnstakeRequestTableRow>();
const COLUMN_HELPER = createColumnHelper<UnstakeRequestTableRow>();

const columns = [
columnHelper.accessor('unlockId', {
const COLUMNS = [
COLUMN_HELPER.accessor('unlockId', {
header: () => <HeaderCell title="Unlock ID" className="justify-start" />,
cell: (props) => {
return (
Expand All @@ -98,7 +98,7 @@ const columns = [
);
},
}),
columnHelper.accessor('progress', {
COLUMN_HELPER.accessor('progress', {
header: () => <HeaderCell title="Status" className="justify-center" />,
cell: (props) => {
const progress = props.getValue();
Expand Down Expand Up @@ -135,7 +135,7 @@ const columns = [
return <div className="flex items-center justify-start">{content}</div>;
},
}),
columnHelper.accessor('amount', {
COLUMN_HELPER.accessor('amount', {
header: () => <HeaderCell title="Amount" className="justify-center" />,
cell: (props) => {
const unstakeRequest = props.row.original;
Expand All @@ -162,9 +162,7 @@ const UnstakeRequestsTable: FC = () => {
const { selectedProtocolId } = useLiquidStakingStore();
const substrateAddress = useSubstrateAddress();
const parachainRows = useLstUnlockRequestTableRows();

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

// Select the table rows based on whether the selected protocol
// is an EVM-based chain or a parachain-based Substrate chain.
Expand All @@ -175,17 +173,18 @@ const UnstakeRequestsTable: FC = () => {
// In case that the data is not loaded yet, use an empty array
// to avoid TypeScript errors.
data: rows ?? [],
columns,
columns: COLUMNS,
filterFns: {
fuzzy: fuzzyFilter,
},
// getRowId: (row) => row.unlockId.toString(),
globalFilterFn: fuzzyFilter,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
enableRowSelection: true,
autoResetPageIndex: false,
getRowId: (row) => row.unlockId.toString(),
}),
[rows],
);
Expand Down Expand Up @@ -237,6 +236,7 @@ const UnstakeRequestsTable: FC = () => {
tdClassName="!bg-inherit !px-3 !py-2 whitespace-nowrap"
tableProps={tableProps}
totalRecords={rows.length}
isPaginated
/>
);
})();
Expand Down Expand Up @@ -265,7 +265,7 @@ const UnstakeRequestsTable: FC = () => {
// request has completed its unlocking period.
return request.type === 'parachainUnstakeRequest'
? request.progress === undefined
: request.progress === 100;
: request.progress === 1;
});
}, [selectedRowsUnlockIds, rows]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,16 @@ const WithdrawLstUnstakeRequestButton: FC<
return (
<>
<Button
variant="secondary"
variant="primary"
isDisabled={
!canWithdraw ||
executeWithdrawRedeemTx === null ||
currenciesAndUnlockIds.length === 0
}
onClick={handleClick}
isFullWidth
isLoading={withdrawRedeemTxStatus === TxStatus.PROCESSING}
loadingText="Processing"
>
Withdraw
</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Button } from '@webb-tools/webb-ui-components';
import { FC, useCallback } from 'react';
import { FC, useCallback, useState } from 'react';

import { LsErc20TokenId } from '../../../constants/liquidStaking/types';
import useLiquifierWithdraw from '../../../data/liquifier/useLiquifierWithdraw';
Expand All @@ -15,24 +15,42 @@ const WithdrawUnlockNftButton: FC<WithdrawUnlockNftButtonProps> = ({
canWithdraw,
unlockIds,
}) => {
const [isProcessing, setIsProcessing] = useState(false);
const withdraw = useLiquifierWithdraw();

const handleClick = useCallback(() => {
const handleClick = useCallback(async () => {
if (withdraw === null) {
return;
}

for (const unlockId of unlockIds) {
withdraw(tokenId, unlockId);
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 (
<Button
variant="secondary"
variant="primary"
isDisabled={!canWithdraw || unlockIds.length === 0 || withdraw === null}
onClick={handleClick}
isFullWidth
isLoading={isProcessing}
loadingText="Processing"
>
Withdraw
</Button>
Expand Down
4 changes: 2 additions & 2 deletions apps/tangle-dapp/constants/liquidStaking/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ import {
* use dummy data.
*/
const SEPOLIA_TESTNET_CONTRACTS = {
LIQUIFIER: '0xCE0148DbA55c9719B59642f5cBf4F26e67F44E70',
LIQUIFIER: '0x55D942dC55b8b3bEE51C964c4985C46C7DF98Be0',
ERC20: '0x2eE951c2d215ba1b3E0DF20764c96a0bC7809F41',
// Use the same address as the dummy ERC20 contract.
TG_TOKEN: '0x2eE951c2d215ba1b3E0DF20764c96a0bC7809F41',
UNLOCKS: '0x4EFd70b31c3bfC1824eD081AFFE8b107BcDf456A',
UNLOCKS: '0x32d70bC73d0965209Cf175711b010dE6A7650c2B',
} as const satisfies Record<string, HexString>;

const CHAINLINK: LsErc20TokenDef = {
Expand Down
17 changes: 11 additions & 6 deletions apps/tangle-dapp/data/liquidStaking/useExchangeRate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TANGLE_RESTAKING_PARACHAIN_LOCAL_DEV_NETWORK } from '@webb-tools/webb-ui-components/constants/networks';
import { useCallback, useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { erc20Abi } from 'viem';

import {
Expand All @@ -21,6 +21,8 @@ export enum ExchangeRateType {

// 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 [exchangeRate, setExchangeRate] = useState<number | null>(null);

const protocol = getLsProtocolDef(protocolId);

const { result: tokenPoolAmount } = useApiRx((api) => {
Expand Down Expand Up @@ -76,10 +78,13 @@ const useExchangeRate = (type: ExchangeRateType, protocolId: LsProtocolId) => {
[],
);

const fetcher = useCallback(() => {
return protocol.type === 'parachain'
? parachainExchangeRate
: fetchErc20ExchangeRate(protocol);
const fetcher = useCallback(async () => {
const promise =
protocol.type === 'parachain'
? parachainExchangeRate
: fetchErc20ExchangeRate(protocol);

setExchangeRate(await promise);
}, [fetchErc20ExchangeRate, parachainExchangeRate, protocol]);

const totalSupplyFetcher = useCallback((): ContractReadOptions<
Expand Down Expand Up @@ -113,7 +118,7 @@ const useExchangeRate = (type: ExchangeRateType, protocolId: LsProtocolId) => {
}, [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 });
const isRefreshing = usePolling({ effect: fetcher });

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

export enum PollingPrimaryCacheKey {
EXCHANGE_RATE,
CONTRACT_READ_SUBSCRIPTION,
LS_ERC20_BALANCE,
}
import { useCallback, useEffect, useState } from 'react';

export type PollingOptions<T> = {
fetcher: (() => Promise<T> | T) | null;
effect: (() => Promise<T> | T) | null;
refreshInterval?: number;
};

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

useEffect(() => {
const intervalHandle = setInterval(async () => {
// Fetcher isn't ready to be called yet.
if (fetcher === null) {
return;
}
const refresh = useCallback(async () => {
// Fetcher isn't ready to be called yet.
if (effect === null) {
return;
}

const now = Date.now();
const difference = now - lastUpdatedTimestampRef.current;
setIsRefreshing(true);
await effect();
setIsRefreshing(false);
}, [effect]);

// 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;
}
useEffect(() => {
let intervalHandle: ReturnType<typeof setInterval> | null = null;

(async () => {
// Call it immediately to avoid initial delay.
await refresh();

setIsRefreshing(true);
setValue(await fetcher());
lastUpdatedTimestampRef.current = now;
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;
27 changes: 22 additions & 5 deletions apps/tangle-dapp/data/liquifier/useContractRead.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { PromiseOrT } from '@webb-tools/abstract-api-provider';
import { useCallback, useState } from 'react';
import { Abi as ViemAbi, ContractFunctionName } from 'viem';
import {
Abi as ViemAbi,
ContractFunctionArgs,
ContractFunctionName,
ContractFunctionReturnType,
} from 'viem';

import usePolling from '../liquidStaking/usePolling';
import useContractReadOnce, {
Expand All @@ -22,6 +27,18 @@ const useContractRead = <
// This is useful for when the options are dependent on some state.
| (() => PromiseOrT<ContractReadOptions<Abi, FunctionName> | null>),
) => {
type ReturnType =
| Error
| Awaited<
ContractFunctionReturnType<
Abi,
'pure' | 'view',
FunctionName,
ContractFunctionArgs<Abi, 'pure' | 'view', FunctionName>
>
>;

const [value, setValue] = useState<ReturnType | null>(null);
const [isPaused, setIsPaused] = useState(false);
const readOnce = useContractReadOnce(abi);

Expand All @@ -39,16 +56,16 @@ const useContractRead = <
return null;
}

return readOnce(options_);
setValue(await readOnce(options_));
}, [isPaused, options, readOnce]);

const { value, isRefreshing } = usePolling({
usePolling({
// By providing null, it signals to the hook to maintain
// its current value and not refresh.
fetcher: isPaused ? null : fetcher,
effect: isPaused ? null : fetcher,
});

return { value, isRefreshing, isPaused, setIsPaused };
return { value, isPaused, setIsPaused };
};

export default useContractRead;
Loading

0 comments on commit 84a52ce

Please sign in to comment.