diff --git a/libs/ui-toolkit/src/components/toast/toast.tsx b/libs/ui-toolkit/src/components/toast/toast.tsx index e035379a3..16b8a9a3c 100644 --- a/libs/ui-toolkit/src/components/toast/toast.tsx +++ b/libs/ui-toolkit/src/components/toast/toast.tsx @@ -18,6 +18,8 @@ export type ToastContent = JSX.Element | undefined; type ToastState = 'initial' | 'showing' | 'expired'; +type WithdrawalInfoMeta = { withdrawalId: string | undefined }; + export type Toast = { id: string; intent: Intent; @@ -27,6 +29,8 @@ export type Toast = { signal?: 'close'; loader?: boolean; hidden?: boolean; + // meta information + meta?: WithdrawalInfoMeta | undefined; }; type ToastProps = Toast & { diff --git a/libs/ui-toolkit/src/components/toast/toasts-container.tsx b/libs/ui-toolkit/src/components/toast/toasts-container.tsx index aff2c2ca3..ca2e06818 100644 --- a/libs/ui-toolkit/src/components/toast/toasts-container.tsx +++ b/libs/ui-toolkit/src/components/toast/toasts-container.tsx @@ -43,6 +43,10 @@ export const ToastsContainer = ({ }; }, [count, order, toasts]); + const validToasts = Object.values(toasts).filter( + (t) => !t.hidden || showHidden + ); + return ( } @@ -73,8 +77,8 @@ export const ToastsContainer = ({ 'flex-col-reverse': order === 'desc', })} > - {toasts && - Object.values(toasts) + {validToasts.length > 0 && + validToasts .filter((t) => !t.hidden || showHidden) .map((toast) => { return ( @@ -93,7 +97,7 @@ export const ToastsContainer = ({ 'opacity-0 group-hover:opacity-50 hover:!opacity-100', 'text-sm text-black dark:text-white bg-white dark:bg-black hover:!bg-white hover:dark:!bg-black', { - hidden: Object.keys(toasts).length === 0, + hidden: validToasts.length === 0, } )} onClick={() => { diff --git a/libs/ui-toolkit/src/components/toast/use-toasts.ts b/libs/ui-toolkit/src/components/toast/use-toasts.ts index 08424c5a5..f06ddb64e 100644 --- a/libs/ui-toolkit/src/components/toast/use-toasts.ts +++ b/libs/ui-toolkit/src/components/toast/use-toasts.ts @@ -50,6 +50,10 @@ type Actions = { * Checks if a given toasts exists in the collection */ hasToast: (id: string) => boolean; + /** + * Closes toast by meta + */ + closeBy: (meta: Toast['meta']) => void; }; type ToastsStore = State & Actions; @@ -102,6 +106,17 @@ export const useToasts = create()( }), removeAll: () => set({ toasts: {}, count: 0 }), hasToast: (id) => get().toasts[id] != null, + closeBy: (meta) => { + if (!meta) return; + set((state) => { + const found = Object.values(state.toasts).find((t) => + isEqual(t.meta, meta) + ); + if (found) { + found.signal = 'close'; + } + }); + }, })) ); diff --git a/libs/web3/src/index.ts b/libs/web3/src/index.ts index 8134e8660..91bbf497b 100644 --- a/libs/web3/src/index.ts +++ b/libs/web3/src/index.ts @@ -28,3 +28,4 @@ export * from './lib/web3-provider'; export * from './lib/withdrawal-approval-dialog'; export * from './lib/withdrawal-approval-status'; export * from './lib/default-web3-provider'; +export * from './lib/use-withdraw-data-store'; diff --git a/libs/web3/src/lib/use-ethereum-transaction-store.tsx b/libs/web3/src/lib/use-ethereum-transaction-store.tsx index 8d0d887b4..2647d4619 100644 --- a/libs/web3/src/lib/use-ethereum-transaction-store.tsx +++ b/libs/web3/src/lib/use-ethereum-transaction-store.tsx @@ -4,7 +4,10 @@ import type { MultisigControl } from '@vegaprotocol/smart-contracts'; import type { CollateralBridge } from '@vegaprotocol/smart-contracts'; import type { Token } from '@vegaprotocol/smart-contracts'; -import type { DepositBusEventFieldsFragment } from '@vegaprotocol/wallet'; +import type { + DepositBusEventFieldsFragment, + WithdrawalBusEventFieldsFragment, +} from '@vegaprotocol/wallet'; import type { EthTxState } from './use-ethereum-transaction'; import { EthTxStatus } from './use-ethereum-transaction'; @@ -27,6 +30,7 @@ export interface EthStoredTxState extends EthTxState { requiresConfirmation: boolean; // whether or not the tx needs external confirmation (IE from a subscription even) assetId?: string; deposit?: DepositBusEventFieldsFragment; + withdrawal?: WithdrawalBusEventFieldsFragment; } export interface EthTransactionStore { @@ -37,7 +41,8 @@ export interface EthTransactionStore { args: string[], assetId?: string, requiredConfirmations?: number, - requiresConfirmation?: boolean + requiresConfirmation?: boolean, + withdrawal?: EthStoredTxState['withdrawal'] ) => number; update: ( id: EthStoredTxState['id'], @@ -62,7 +67,8 @@ export const useEthTransactionStore = create()( args: string[] = [], assetId?: string, requiredConfirmations = 1, - requiresConfirmation = false + requiresConfirmation = false, + withdrawal = undefined ) => { const transactions = get().transactions; const now = new Date(); @@ -82,6 +88,7 @@ export const useEthTransactionStore = create()( requiredConfirmations, requiresConfirmation, assetId, + withdrawal, }; set({ transactions: transactions.concat(transaction) }); return transaction.id; diff --git a/libs/web3/src/lib/use-ethereum-transaction-toasts.tsx b/libs/web3/src/lib/use-ethereum-transaction-toasts.tsx index 5a9235f23..f0e49b482 100644 --- a/libs/web3/src/lib/use-ethereum-transaction-toasts.tsx +++ b/libs/web3/src/lib/use-ethereum-transaction-toasts.tsx @@ -162,9 +162,10 @@ const isFinal = (tx: EthStoredTxState) => [EthTxStatus.Confirmed, EthTxStatus.Error].includes(tx.status); export const useEthereumTransactionToasts = () => { - const [setToast, removeToast] = useToasts((store) => [ + const [setToast, removeToast, closeToastBy] = useToasts((store) => [ store.setToast, store.remove, + store.closeBy, ]); const dismissTx = useEthTransactionStore((state) => state.dismiss); @@ -173,8 +174,16 @@ export const useEthereumTransactionToasts = () => { (tx: EthStoredTxState) => () => { dismissTx(tx.id); removeToast(`eth-${tx.id}`); + // closes related "Funds released" toast after successful withdrawal + if ( + isWithdrawTransaction(tx) && + tx.status === EthTxStatus.Confirmed && + tx.withdrawal + ) { + closeToastBy({ withdrawalId: tx.withdrawal.id }); + } }, - [dismissTx, removeToast] + [closeToastBy, dismissTx, removeToast] ); const fromEthTransaction = useCallback( diff --git a/libs/web3/src/lib/use-ethereum-withdraw-approvals-manager.tsx b/libs/web3/src/lib/use-ethereum-withdraw-approvals-manager.tsx index d06abcddf..4f00e6abb 100644 --- a/libs/web3/src/lib/use-ethereum-withdraw-approvals-manager.tsx +++ b/libs/web3/src/lib/use-ethereum-withdraw-approvals-manager.tsx @@ -91,7 +91,8 @@ export const useEthWithdrawApprovalsManager = () => { if (threshold && amount.isGreaterThan(threshold)) { const delaySecs = await getDelay(); const completeTimestamp = - new Date(withdrawal.createdTimestamp).getTime() + delaySecs * 1000; + new Date(withdrawal.createdTimestamp).getTime() + + (delaySecs as number) * 1000; const now = Date.now(); if (now < completeTimestamp) { update(transaction.id, { @@ -139,7 +140,11 @@ export const useEthWithdrawApprovalsManager = () => { approval.creation, approval.nonce, approval.signatures, - ] + ], + undefined, + undefined, + undefined, + transaction.withdrawal || undefined ); })().catch((err) => { localLoggerFactory({ application: 'web3' }).error( diff --git a/libs/web3/src/lib/use-get-withdraw-delay.ts b/libs/web3/src/lib/use-get-withdraw-delay.ts index 62f061dff..4652d1fbd 100644 --- a/libs/web3/src/lib/use-get-withdraw-delay.ts +++ b/libs/web3/src/lib/use-get-withdraw-delay.ts @@ -1,32 +1,7 @@ import { useBridgeContract } from './use-bridge-contract'; -import { useCallback, useEffect } from 'react'; +import { useCallback } from 'react'; import { localLoggerFactory } from '@vegaprotocol/logger'; -import { create } from 'zustand'; - -/** - * Returns a function that gets the delay in seconds that's required if the - * withdrawal amount is over the withdrawal threshold - * (contract.get_withdraw_threshold) - */ -export const useGetWithdrawDelay = () => { - const contract = useBridgeContract(true); - const getDelay = useCallback(async () => { - const logger = localLoggerFactory({ application: 'web3' }); - if (!contract) { - logger.error('get withdraw delay: no bridge contract'); - return; - } - try { - logger.info('get withdraw delay', { contract }); - const res = await contract?.default_withdraw_delay(); - return res.toNumber(); - } catch (err) { - logger.error('get withdraw delay', err); - } - }, [contract]); - - return getDelay; -}; +import { useWithdrawDataStore } from './use-withdraw-data-store'; /** * The withdraw delay is a global value set on the contract bridge which may be @@ -36,34 +11,36 @@ export const useGetWithdrawDelay = () => { const MAX_AGE = 5 * 60 * 1000; // 5 minutes -type WithdrawDelayStore = { - delay: number | null; - ts: number; - setDelay: (delay: number) => void; -}; -const useWithdrawDelayStore = create()((set) => ({ - delay: null, - ts: 0, - setDelay: (delay) => set({ delay }), -})); - -export const useWithdrawDelay = () => { - const getDelay = useGetWithdrawDelay(); +/** + * Returns a function that gets the delay in seconds that's required if the + * withdrawal amount is over the withdrawal threshold + * (contract.get_withdraw_threshold) + */ +export const useGetWithdrawDelay = () => { + const delay = useWithdrawDataStore((state) => state.delay); + const setDelay = useWithdrawDataStore((state) => state.setDelay); + const contract = useBridgeContract(true); const logger = localLoggerFactory({ application: 'web3' }); - const [delay, ts] = useWithdrawDelayStore((state) => [state.delay, state.ts]); - const setDelay = useWithdrawDelayStore((state) => state.setDelay); - useEffect(() => { - if (delay && Date.now() - ts <= MAX_AGE) return; - getDelay() - .then((d) => { - if (typeof d === 'number') { - logger.info(`retrieved withdraw delay: ${d} seconds`); - setDelay(d); - } - }) - .catch((err) => logger.error('could not get the withdraw delay', err)); - }, [delay, getDelay, logger, setDelay, ts]); + const getDelay = useCallback(async () => { + // return cached value if still valid + if (delay && Date.now() - delay.ts <= MAX_AGE) { + return delay.value; + } + if (!contract) { + logger.info('could not get withdraw delay: no bridge contract'); + return undefined; + } + try { + const res = await contract?.default_withdraw_delay(); + logger.info(`retrieved withdraw delay: ${res} seconds`); + setDelay(res.toNumber()); + return res.toNumber() as number; + } catch (err) { + logger.error('could not get withdraw delay', err); + return undefined; + } + }, [contract, delay, logger, setDelay]); - return delay; + return getDelay; }; diff --git a/libs/web3/src/lib/use-get-withdraw-threshold.tsx b/libs/web3/src/lib/use-get-withdraw-threshold.tsx index 2ab416e8f..b8c2089b8 100644 --- a/libs/web3/src/lib/use-get-withdraw-threshold.tsx +++ b/libs/web3/src/lib/use-get-withdraw-threshold.tsx @@ -1,44 +1,20 @@ -import { useCallback, useEffect } from 'react'; +import { useCallback } from 'react'; import { useBridgeContract } from './use-bridge-contract'; import BigNumber from 'bignumber.js'; import { addDecimal } from '@vegaprotocol/utils'; import type { WithdrawalBusEventFieldsFragment } from '@vegaprotocol/wallet'; import { localLoggerFactory } from '@vegaprotocol/logger'; -import { create } from 'zustand'; +import { + BUILTIN_ASSET_ADDRESS, + BUILTIN_ASSET_THRESHOLD, + useWithdrawDataStore, +} from './use-withdraw-data-store'; type Asset = Pick< WithdrawalBusEventFieldsFragment['asset'], 'source' | 'decimals' >; -/** - * Returns a function to get the threshold amount for a withdrawal. - * If a withdrawal amount is greater than this value it will incur a delay - * before being able to be completed. The delay is set on the smart contract and - * can be retrieved using contract.default_withdraw_delay - */ -export const useGetWithdrawThreshold = () => { - const contract = useBridgeContract(true); - const getThreshold = useCallback( - async (asset: Asset | undefined) => { - if (!contract || asset?.source.__typename !== 'ERC20') { - return new BigNumber(Infinity); - } - const res = await contract.get_withdraw_threshold( - asset.source.contractAddress - ); - const value = new BigNumber(addDecimal(res.toString(), asset.decimals)); - const threshold = value.isEqualTo(0) - ? new BigNumber(Infinity) - : value.minus(new BigNumber(addDecimal('1', asset.decimals))); - return threshold; - }, - [contract] - ); - - return getThreshold; -}; - /** * The withdraw threshold is a value set on the contract bridge per asset which * may be changed via a proposal therefore it can be cached for a set amount of @@ -46,57 +22,55 @@ export const useGetWithdrawThreshold = () => { */ const MAX_AGE = 5 * 60 * 1000; // 5 minutes -const BUILTIN_ASSET_ADDRESS = 'builtin'; -const BUILTIN_ASSET_THRESHOLD = new BigNumber(Infinity); -type WithdrawThresholdsStore = { - thresholds: Record; - setThreshold: (contractAddress: string, threshold: BigNumber) => void; -}; - -const useWithdrawThresholdsStore = create()((set) => ({ - thresholds: { - [BUILTIN_ASSET_ADDRESS]: { value: BUILTIN_ASSET_THRESHOLD, ts: 0 }, - }, - setThreshold: (contractAddress, threshold) => - set((state) => { - state.thresholds[contractAddress] = { value: threshold, ts: Date.now() }; - return state; - }), -})); - -const addr = (asset: Asset) => - asset?.source.__typename === 'ERC20' +export const addr = (asset: Asset | undefined) => + asset && asset.source.__typename === 'ERC20' ? asset.source.contractAddress : BUILTIN_ASSET_ADDRESS; -export const useWithdrawThresholds = (assets: Asset[] | undefined) => { - const getThreshold = useGetWithdrawThreshold(); +/** + * Returns a function to get the threshold amount for a withdrawal. + * If a withdrawal amount is greater than this value it will incur a delay + * before being able to be completed. The delay is set on the smart contract and + * can be retrieved using contract.default_withdraw_delay + */ +export const useGetWithdrawThreshold = () => { const logger = localLoggerFactory({ application: 'web3' }); - const thresholds = useWithdrawThresholdsStore((state) => state.thresholds); - const setThreshold = useWithdrawThresholdsStore( - (state) => state.setThreshold + const contract = useBridgeContract(true); + const thresholds = useWithdrawDataStore((state) => state.thresholds); + const setThreshold = useWithdrawDataStore((state) => state.setThreshold); + const getThreshold = useCallback( + async (asset: Asset | undefined) => { + const contractAddress = addr(asset); + // return cached value if still valid + const thr = thresholds[contractAddress]; + if (thr && Date.now() - thr.ts <= MAX_AGE) { + return thr.value; + } + if (!contract || !asset || contractAddress === BUILTIN_ASSET_ADDRESS) { + setThreshold(BUILTIN_ASSET_ADDRESS, BUILTIN_ASSET_THRESHOLD); + return BUILTIN_ASSET_THRESHOLD; + } + try { + const res = await contract.get_withdraw_threshold(contractAddress); + const value = new BigNumber(addDecimal(res.toString(), asset.decimals)); + const threshold = value.isEqualTo(0) + ? new BigNumber(Infinity) + : value.minus(new BigNumber(addDecimal('1', asset.decimals))); + logger.info( + `retrieved withdraw threshold for ${addr( + asset + )}: ${threshold.toString()}` + ); + setThreshold(contractAddress, threshold); + return threshold; + } catch (err) { + logger.error('could not get the withdraw thresholds', err); + return undefined; + } + }, + [contract, logger, setThreshold, thresholds] ); - useEffect(() => { - if (!assets || assets.length === 0) return; - for (const asset of assets) { - const threshold = thresholds[addr(asset)]; - if (threshold && Date.now() - threshold.ts <= MAX_AGE) { - return; - } - getThreshold(asset) - .then((t) => { - logger.info( - `retrieved withdraw threshold for ${addr(asset)}: ${t.toString()}` - ); - setThreshold(addr(asset), t); - }) - .catch((err) => - logger.error('could not get the withdraw threshold', err) - ); - } - }, [assets, getThreshold, logger, setThreshold, thresholds]); - - return thresholds; + return getThreshold; }; diff --git a/libs/web3/src/lib/use-vega-transaction-toasts.tsx b/libs/web3/src/lib/use-vega-transaction-toasts.tsx index 0eb8383a4..1fb4ad9b1 100644 --- a/libs/web3/src/lib/use-vega-transaction-toasts.tsx +++ b/libs/web3/src/lib/use-vega-transaction-toasts.tsx @@ -666,6 +666,12 @@ export const useVegaTransactionToasts = () => { const closeAfter = isFinal(tx) && !isWithdrawTransaction(tx.body) ? CLOSE_AFTER : undefined; + // marks "Funds unlocked" toast so it can be found in eth toasts + const meta = + isFinal(tx) && isWithdrawTransaction(tx.body) + ? { withdrawalId: tx.withdrawal?.id } + : undefined; + return { id: `vega-${tx.id}`, intent, @@ -673,6 +679,7 @@ export const useVegaTransactionToasts = () => { loader: tx.status === VegaTxStatus.Pending, content, closeAfter, + meta, }; }; diff --git a/libs/web3/src/lib/use-withdraw-data-store.ts b/libs/web3/src/lib/use-withdraw-data-store.ts new file mode 100644 index 000000000..6433860e9 --- /dev/null +++ b/libs/web3/src/lib/use-withdraw-data-store.ts @@ -0,0 +1,33 @@ +import BigNumber from 'bignumber.js'; +import { create } from 'zustand'; + +export const BUILTIN_ASSET_ADDRESS = 'builtin'; +export const BUILTIN_ASSET_THRESHOLD = new BigNumber(Infinity); + +type TimestampedThreshold = { value: BigNumber; ts: number }; +type TimestampedDelay = { value: number | undefined; ts: number }; +type WithdrawDataStore = { + thresholds: Record; + delay: TimestampedDelay; + setThreshold: (contractAddress: string, threshold: BigNumber) => void; + setDelay: (delay: number) => void; +}; + +export const useWithdrawDataStore = create()((set) => ({ + thresholds: { + [BUILTIN_ASSET_ADDRESS]: { value: BUILTIN_ASSET_THRESHOLD, ts: 0 }, + }, + delay: { + value: undefined, + ts: 0, + }, + setThreshold: (contractAddress, threshold) => + set((state) => { + state.thresholds[contractAddress] = { + value: threshold, + ts: Date.now(), + }; + return state; + }), + setDelay: (delay) => set({ delay: { value: delay, ts: Date.now() } }), +})); diff --git a/libs/withdraws/src/lib/use-ready-to-complete-withdrawals-toast.tsx b/libs/withdraws/src/lib/use-ready-to-complete-withdrawals-toast.tsx index 0380ed256..1f1b0028a 100644 --- a/libs/withdraws/src/lib/use-ready-to-complete-withdrawals-toast.tsx +++ b/libs/withdraws/src/lib/use-ready-to-complete-withdrawals-toast.tsx @@ -5,21 +5,26 @@ import { } from '@vegaprotocol/wallet'; import BigNumber from 'bignumber.js'; import type { Toast } from '@vegaprotocol/ui-toolkit'; +import { CLOSE_AFTER } from '@vegaprotocol/ui-toolkit'; import { Button, Intent, Panel, ToastHeading } from '@vegaprotocol/ui-toolkit'; import { useToasts } from '@vegaprotocol/ui-toolkit'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useEffect } from 'react'; import { t } from '@vegaprotocol/i18n'; import { formatNumber, toBigNum } from '@vegaprotocol/utils'; import { useNavigate } from 'react-router-dom'; import { + addr, useEthWithdrawApprovalsStore, - useWithdrawDelay, - useWithdrawThresholds, + useGetWithdrawDelay, + useGetWithdrawThreshold, + useWithdrawDataStore, } from '@vegaprotocol/web3'; import { withdrawalProvider } from './withdrawals-provider'; import type { WithdrawalFieldsFragment } from './__generated__/Withdrawal'; +import uniqBy from 'lodash/uniqBy'; -const TOAST_ID = `ready-to-withdraw`; +const CHECK_INTERVAL = 1000; +const ON_APP_START_TOAST_ID = `ready-to-withdraw`; type UseReadyToWithdrawalToastsOptions = { withdrawalsLink: string; }; @@ -27,10 +32,15 @@ type UseReadyToWithdrawalToastsOptions = { export const useReadyToWithdrawalToasts = ({ withdrawalsLink, }: UseReadyToWithdrawalToastsOptions) => { - const [setToast, hasToast, updateToast] = useToasts((store) => [ + const [setToast, hasToast, updateToast, removeToast] = useToasts((store) => [ store.setToast, store.hasToast, store.update, + store.remove, + ]); + const [thresholds, delay] = useWithdrawDataStore((state) => [ + state.thresholds, + state.delay, ]); const { pubKey, isReadOnly } = useVegaWallet(); @@ -39,60 +49,125 @@ export const useReadyToWithdrawalToasts = ({ variables: { partyId: pubKey || '' }, skip: !pubKey || isReadOnly, }); - const delay = useWithdrawDelay(); // seconds - const incompleteWithdrawals = useMemo( - () => data?.filter((w) => !w.txHash), - [data] - ); + const getDelay = useGetWithdrawDelay(); // seconds + const incompleteWithdrawals = data?.filter((w) => !w.txHash); - const assets = incompleteWithdrawals?.map((w) => w.asset); - const thresholds = useWithdrawThresholds(assets); - const readyToComplete = incompleteWithdrawals?.filter((w) => { - const address = - w.asset?.source.__typename === 'ERC20' - ? w.asset.source.contractAddress - : 'builtin'; - const threshold = thresholds[address]; - if (threshold && delay) { - if (!new BigNumber(w.amount).isGreaterThan(threshold.value)) { - // there's no delay time for withdrawals below the threshold - return true; - } - const completeTimestamp = - new Date(w.createdTimestamp).getTime() + delay * 1000; - if (Date.now() >= completeTimestamp) { - // after delay - return true; - } - } - return false; - }); + const assets = uniqBy( + incompleteWithdrawals?.map((w) => w.asset), + (a) => addr(a) + ); + const getThreshold = useGetWithdrawThreshold(); + + const checkWithdraws = useCallback(async () => { + if (assets.length === 0) return; + // trigger delay + // trigger thresholds + return await Promise.all([ + getDelay(), + ...assets.map((asset) => getThreshold(asset)), + ]); + }, [assets, getDelay, getThreshold]); const onClose = useCallback(() => { - updateToast(TOAST_ID, { hidden: true }); + updateToast(ON_APP_START_TOAST_ID, { hidden: true }); }, [updateToast]); useEffect(() => { - if (!readyToComplete || readyToComplete?.length === 0) { - return; - } - const toast: Toast = { - id: TOAST_ID, - intent: Intent.Warning, - content: - readyToComplete.length === 1 ? ( - - ) : ( - - ), - onClose, - }; - // set only once, unless removed - if (!hasToast(TOAST_ID)) setToast(toast); - }, [hasToast, onClose, readyToComplete, setToast, withdrawalsLink]); + checkWithdraws().then((retrieved) => { + if (!retrieved || delay.value === undefined || !incompleteWithdrawals) { + return; + } + const timestamped = incompleteWithdrawals.map((w) => { + let timestamp = undefined; + const threshold = thresholds[addr(w.asset)]; // { value: new BigNumber(0) }; + if (threshold) { + timestamp = 0; + if (new BigNumber(w.amount).isGreaterThan(threshold.value)) { + const created = w.createdTimestamp; + timestamp = + new Date(created).getTime() + (delay.value as number) * 1000; + } + } + return { + data: w, + timestamp, + }; + }); + const delayed = timestamped?.filter( + (item) => item.timestamp != null && Date.now() < item.timestamp + ); + + const readyToComplete = timestamped?.filter( + (item) => item.timestamp != null && Date.now() >= item.timestamp + ); + + // set on app start toast if there are withdrawals ready to complete + if (readyToComplete && readyToComplete.length > 0) { + // set only once, unless removed + if (!hasToast(ON_APP_START_TOAST_ID)) { + const appStartToast: Toast = { + id: ON_APP_START_TOAST_ID, + intent: Intent.Warning, + content: + readyToComplete.length === 1 ? ( + + ) : ( + + ), + onClose, + }; + setToast(appStartToast); + } + } + + // set toast whenever a withdrawal delay is passed + let interval: NodeJS.Timer; + if (delayed && delayed.length > 0) { + interval = setInterval(() => { + const ready = delayed.filter( + (item) => item.timestamp && Date.now() >= item.timestamp + ); + for (const withdrawal of ready) { + const id = `complete-withdrawal-${withdrawal.data.id}`; + const toast: Toast = { + id, + intent: Intent.Warning, + content: ( + + ), + onClose: () => { + // updateToast(id, { hidden: true }); + removeToast(id); + }, + // closeAfter: CLOSE_AFTER, + }; + if (!hasToast(id)) setToast(toast); + } + }, CHECK_INTERVAL); + } + + return () => { + clearInterval(interval); + }; + }); + }, [ + checkWithdraws, + delay, + hasToast, + incompleteWithdrawals, + onClose, + removeToast, + setToast, + thresholds, + withdrawalsLink, + ]); }; const MultipleReadyToWithdrawToastContent = ({