feat(web3): ready to withdraw toast
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
import type { DefaultWeb3ProviderContextShape } from '@vegaprotocol/web3';
|
||||
import {
|
||||
useEthereumConfig,
|
||||
createConnectors,
|
||||
Web3Provider as Web3ProviderInternal,
|
||||
useWeb3ConnectStore,
|
||||
createDefaultProvider,
|
||||
} from '@vegaprotocol/web3';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const Web3Provider = ({ children }: { children: ReactNode }) => {
|
||||
@@ -17,10 +20,13 @@ export const Web3Provider = ({ children }: { children: ReactNode }) => {
|
||||
|
||||
const connectors = useWeb3ConnectStore((store) => store.connectors);
|
||||
const initializeConnectors = useWeb3ConnectStore((store) => store.initialize);
|
||||
const [defaultProvider, setDefaultProvider] = useState<
|
||||
DefaultWeb3ProviderContextShape['provider'] | undefined
|
||||
>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (config?.chain_id) {
|
||||
return initializeConnectors(
|
||||
initializeConnectors(
|
||||
createConnectors(
|
||||
ETHEREUM_PROVIDER_URL,
|
||||
Number(config?.chain_id),
|
||||
@@ -29,6 +35,11 @@ export const Web3Provider = ({ children }: { children: ReactNode }) => {
|
||||
),
|
||||
Number(config.chain_id)
|
||||
);
|
||||
const defaultProvider = createDefaultProvider(
|
||||
ETHEREUM_PROVIDER_URL,
|
||||
Number(config?.chain_id)
|
||||
);
|
||||
setDefaultProvider(defaultProvider);
|
||||
}
|
||||
}, [
|
||||
config?.chain_id,
|
||||
@@ -49,7 +60,10 @@ export const Web3Provider = ({ children }: { children: ReactNode }) => {
|
||||
}}
|
||||
noDataMessage={t('Could not fetch Ethereum configuration')}
|
||||
>
|
||||
<Web3ProviderInternal connectors={connectors}>
|
||||
<Web3ProviderInternal
|
||||
connectors={connectors}
|
||||
defaultProvider={defaultProvider}
|
||||
>
|
||||
<>{children}</>
|
||||
</Web3ProviderInternal>
|
||||
</AsyncRenderer>
|
||||
|
||||
@@ -3,12 +3,17 @@ import { useUpdateNetworkParametersToasts } from '@vegaprotocol/proposals';
|
||||
import { useVegaTransactionToasts } from '@vegaprotocol/web3';
|
||||
import { useEthereumTransactionToasts } from '@vegaprotocol/web3';
|
||||
import { useEthereumWithdrawApprovalsToasts } from '@vegaprotocol/web3';
|
||||
import { Routes } from './client-router';
|
||||
import { useReadyToWithdrawalToasts } from '@vegaprotocol/withdraws';
|
||||
|
||||
export const ToastsManager = () => {
|
||||
useUpdateNetworkParametersToasts();
|
||||
useVegaTransactionToasts();
|
||||
useEthereumTransactionToasts();
|
||||
useEthereumWithdrawApprovalsToasts();
|
||||
useReadyToWithdrawalToasts({
|
||||
withdrawalsLink: `${Routes.PORTFOLIO}#withdrawals`,
|
||||
});
|
||||
|
||||
const toasts = useToasts((store) => store.toasts);
|
||||
return <ToastsContainer order="desc" toasts={toasts} />;
|
||||
|
||||
@@ -26,6 +26,7 @@ export type Toast = {
|
||||
onClose?: () => void;
|
||||
signal?: 'close';
|
||||
loader?: boolean;
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
type ToastProps = Toast & {
|
||||
|
||||
@@ -13,11 +13,13 @@ import { Portal } from '@radix-ui/react-portal';
|
||||
type ToastsContainerProps = {
|
||||
toasts: Toasts;
|
||||
order: 'asc' | 'desc';
|
||||
showHidden?: boolean;
|
||||
};
|
||||
|
||||
export const ToastsContainer = ({
|
||||
toasts,
|
||||
order = 'asc',
|
||||
showHidden = false,
|
||||
}: ToastsContainerProps) => {
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
const closeAll = useToasts((store) => store.closeAll);
|
||||
@@ -72,13 +74,15 @@ export const ToastsContainer = ({
|
||||
})}
|
||||
>
|
||||
{toasts &&
|
||||
Object.values(toasts).map((toast) => {
|
||||
return (
|
||||
<li key={toast.id}>
|
||||
<Toast {...toast} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
Object.values(toasts)
|
||||
.filter((t) => !t.hidden || showHidden)
|
||||
.map((toast) => {
|
||||
return (
|
||||
<li key={toast.id}>
|
||||
<Toast {...toast} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
title={t('Dismiss all toasts')}
|
||||
size="sm"
|
||||
|
||||
@@ -46,12 +46,16 @@ type Actions = {
|
||||
* Arbitrary removes all toasts
|
||||
*/
|
||||
removeAll: () => void;
|
||||
/**
|
||||
* Checks if a given toasts exists in the collection
|
||||
*/
|
||||
hasToast: (id: string) => boolean;
|
||||
};
|
||||
|
||||
type ToastsStore = State & Actions;
|
||||
|
||||
export const useToasts = create<ToastsStore>()(
|
||||
immer((set) => ({
|
||||
immer((set, get) => ({
|
||||
toasts: {},
|
||||
count: 0,
|
||||
add: (toast) =>
|
||||
@@ -97,6 +101,7 @@ export const useToasts = create<ToastsStore>()(
|
||||
}
|
||||
}),
|
||||
removeAll: () => set({ toasts: {}, count: 0 }),
|
||||
hasToast: (id) => get().toasts[id] != null,
|
||||
}))
|
||||
);
|
||||
|
||||
|
||||
@@ -27,3 +27,4 @@ export * from './lib/web3-connectors';
|
||||
export * from './lib/web3-provider';
|
||||
export * from './lib/withdrawal-approval-dialog';
|
||||
export * from './lib/withdrawal-approval-status';
|
||||
export * from './lib/default-web3-provider';
|
||||
|
||||
@@ -16,3 +16,6 @@ export const getChainName = (chainId: number | null | undefined) => {
|
||||
*/
|
||||
export const WALLETCONNECT_PROJECT_ID =
|
||||
process.env['NX_WALLETCONNECT_PROJECT_ID'] || '';
|
||||
|
||||
export const ETHEREUM_PROVIDER_URL =
|
||||
process.env['NX_ETHEREUM_PROVIDER_URL'] || '';
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { ethers } from 'ethers';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export type DefaultWeb3ProviderContextShape = {
|
||||
provider?: ethers.providers.JsonRpcProvider;
|
||||
};
|
||||
export const DefaultWeb3ProviderContext = createContext<
|
||||
DefaultWeb3ProviderContextShape | undefined
|
||||
>(undefined);
|
||||
|
||||
export const useDefaultWeb3Provider = () => {
|
||||
const context = useContext(DefaultWeb3ProviderContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
'useDefaultWeb3Provider must be used within DefaultWeb3ProviderContext'
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -2,23 +2,33 @@ import { CollateralBridge } from '@vegaprotocol/smart-contracts';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import { useMemo } from 'react';
|
||||
import { useEthereumConfig } from './use-ethereum-config';
|
||||
import { useDefaultWeb3Provider } from './default-web3-provider';
|
||||
import { localLoggerFactory } from '@vegaprotocol/logger';
|
||||
|
||||
export const useBridgeContract = () => {
|
||||
const { provider } = useWeb3React();
|
||||
export const useBridgeContract = (allowDefaultProvider = false) => {
|
||||
const { provider: activeProvider } = useWeb3React();
|
||||
const { provider: defaultProvider } = useDefaultWeb3Provider();
|
||||
const { config } = useEthereumConfig();
|
||||
const logger = localLoggerFactory({ application: 'web3' });
|
||||
|
||||
let provider: typeof activeProvider | typeof defaultProvider = activeProvider;
|
||||
let signer = activeProvider?.getSigner();
|
||||
if (!activeProvider && allowDefaultProvider) {
|
||||
logger.info('bridge contract will use default provider');
|
||||
provider = defaultProvider;
|
||||
signer = undefined;
|
||||
}
|
||||
|
||||
const contract = useMemo(() => {
|
||||
if (!provider || !config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const signer = provider.getSigner();
|
||||
|
||||
return new CollateralBridge(
|
||||
config.collateral_bridge_contract.address,
|
||||
signer || provider
|
||||
);
|
||||
}, [provider, config]);
|
||||
}, [provider, signer, config]);
|
||||
|
||||
return contract;
|
||||
};
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import { useBridgeContract } from './use-bridge-contract';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { localLoggerFactory } from '@vegaprotocol/logger';
|
||||
import { create } from 'zustand';
|
||||
|
||||
/**
|
||||
* Gets the delay in seconds thats required if the withdrawal amount is
|
||||
* over the withdrawal threshold (contract.get_withdraw_threshold)
|
||||
* 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();
|
||||
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: contract?.toString() });
|
||||
logger.info('get withdraw delay', { contract });
|
||||
const res = await contract?.default_withdraw_delay();
|
||||
return res.toNumber();
|
||||
} catch (err) {
|
||||
@@ -21,3 +27,43 @@ export const useGetWithdrawDelay = () => {
|
||||
|
||||
return getDelay;
|
||||
};
|
||||
|
||||
/**
|
||||
* The withdraw delay is a global value set on the contract bridge which may be
|
||||
* changed via a proposal therefore it can be cached for a set amount of time
|
||||
* (MAX_AGE) and re-retrieved when necessary.
|
||||
*/
|
||||
|
||||
const MAX_AGE = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
type WithdrawDelayStore = {
|
||||
delay: number | null;
|
||||
ts: number;
|
||||
setDelay: (delay: number) => void;
|
||||
};
|
||||
const useWithdrawDelayStore = create<WithdrawDelayStore>()((set) => ({
|
||||
delay: null,
|
||||
ts: 0,
|
||||
setDelay: (delay) => set({ delay }),
|
||||
}));
|
||||
|
||||
export const useWithdrawDelay = () => {
|
||||
const getDelay = useGetWithdrawDelay();
|
||||
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]);
|
||||
|
||||
return delay;
|
||||
};
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useEffect } 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';
|
||||
|
||||
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
|
||||
* 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();
|
||||
const contract = useBridgeContract(true);
|
||||
const getThreshold = useCallback(
|
||||
async (
|
||||
asset:
|
||||
| Pick<WithdrawalBusEventFieldsFragment['asset'], 'source' | 'decimals'>
|
||||
| undefined
|
||||
) => {
|
||||
async (asset: Asset | undefined) => {
|
||||
if (!contract || asset?.source.__typename !== 'ERC20') {
|
||||
return new BigNumber(Infinity);
|
||||
}
|
||||
@@ -34,3 +38,65 @@ export const useGetWithdrawThreshold = () => {
|
||||
|
||||
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
|
||||
* time (MAX_AGE) and re-retrieved when necessary.
|
||||
*/
|
||||
|
||||
const MAX_AGE = 5 * 60 * 1000; // 5 minutes
|
||||
const BUILTIN_ASSET_ADDRESS = 'builtin';
|
||||
const BUILTIN_ASSET_THRESHOLD = new BigNumber(Infinity);
|
||||
|
||||
type WithdrawThresholdsStore = {
|
||||
thresholds: Record<string, { value: BigNumber; ts: number }>;
|
||||
setThreshold: (contractAddress: string, threshold: BigNumber) => void;
|
||||
};
|
||||
|
||||
const useWithdrawThresholdsStore = create<WithdrawThresholdsStore>()((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'
|
||||
? asset.source.contractAddress
|
||||
: BUILTIN_ASSET_ADDRESS;
|
||||
|
||||
export const useWithdrawThresholds = (assets: Asset[] | undefined) => {
|
||||
const getThreshold = useGetWithdrawThreshold();
|
||||
const logger = localLoggerFactory({ application: 'web3' });
|
||||
const thresholds = useWithdrawThresholdsStore((state) => state.thresholds);
|
||||
const setThreshold = useWithdrawThresholdsStore(
|
||||
(state) => state.setThreshold
|
||||
);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -9,6 +9,11 @@ import { initializeUrlConnector } from './url-connector';
|
||||
import { WALLETCONNECT_PROJECT_ID } from './constants';
|
||||
import { useWeb3ConnectStore } from './web3-connect-store';
|
||||
import { theme } from '@vegaprotocol/tailwindcss-config';
|
||||
import { ethers } from 'ethers';
|
||||
|
||||
export const createDefaultProvider = (providerUrl: string, chainId: number) => {
|
||||
return new ethers.providers.JsonRpcProvider(providerUrl, chainId);
|
||||
};
|
||||
|
||||
export const initializeCoinbaseConnector = (providerUrl: string) =>
|
||||
initializeConnector<CoinbaseWallet>(
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import type { Web3ReactHooks } from '@web3-react/core';
|
||||
import { Web3ReactProvider } from '@web3-react/core';
|
||||
import type { Connector } from '@web3-react/types';
|
||||
import { useMemo } from 'react';
|
||||
import { Web3ReactProvider } from '@web3-react/core';
|
||||
import type { Web3ReactHooks } from '@web3-react/core';
|
||||
import type { Connector } from '@web3-react/types';
|
||||
import type { DefaultWeb3ProviderContextShape } from './default-web3-provider';
|
||||
import { DefaultWeb3ProviderContext } from './default-web3-provider';
|
||||
|
||||
interface Web3ProviderProps {
|
||||
children: JSX.Element | JSX.Element[];
|
||||
connectors: [Connector, Web3ReactHooks][];
|
||||
defaultProvider?: DefaultWeb3ProviderContextShape['provider'];
|
||||
}
|
||||
|
||||
export const Web3Provider = ({ children, connectors }: Web3ProviderProps) => {
|
||||
export const Web3Provider = ({
|
||||
children,
|
||||
connectors,
|
||||
defaultProvider,
|
||||
}: Web3ProviderProps) => {
|
||||
/**
|
||||
* The connectors prop passed to Web3ReactProvider must be referentially static.
|
||||
* https://github.com/Uniswap/web3-react/blob/31742897f9fddb38e00e36c2516029d3df9a9c54/packages/core/src/provider.tsx#L66
|
||||
@@ -20,8 +27,10 @@ export const Web3Provider = ({ children, connectors }: Web3ProviderProps) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Web3ReactProvider key={key} connectors={connectors}>
|
||||
{children}
|
||||
</Web3ReactProvider>
|
||||
<DefaultWeb3ProviderContext.Provider value={{ provider: defaultProvider }}>
|
||||
<Web3ReactProvider key={key} connectors={connectors}>
|
||||
{children}
|
||||
</Web3ReactProvider>
|
||||
</DefaultWeb3ProviderContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,3 +11,4 @@ export * from './lib/withdrawal-dialog';
|
||||
export * from './lib/withdrawal-feedback';
|
||||
export * from './lib/withdrawals-provider';
|
||||
export * from './lib/withdrawals-table';
|
||||
export * from './lib/use-ready-to-complete-withdrawals-toast';
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import {
|
||||
useVegaWallet,
|
||||
useWithdrawalApprovalQuery,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { Toast } 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 { t } from '@vegaprotocol/i18n';
|
||||
import { formatNumber, toBigNum } from '@vegaprotocol/utils';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
useEthWithdrawApprovalsStore,
|
||||
useWithdrawDelay,
|
||||
useWithdrawThresholds,
|
||||
} from '@vegaprotocol/web3';
|
||||
import { withdrawalProvider } from './withdrawals-provider';
|
||||
import type { WithdrawalFieldsFragment } from './__generated__/Withdrawal';
|
||||
|
||||
const TOAST_ID = `ready-to-withdraw`;
|
||||
type UseReadyToWithdrawalToastsOptions = {
|
||||
withdrawalsLink: string;
|
||||
};
|
||||
|
||||
export const useReadyToWithdrawalToasts = ({
|
||||
withdrawalsLink,
|
||||
}: UseReadyToWithdrawalToastsOptions) => {
|
||||
const [setToast, hasToast, updateToast] = useToasts((store) => [
|
||||
store.setToast,
|
||||
store.hasToast,
|
||||
store.update,
|
||||
]);
|
||||
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const { data } = useDataProvider({
|
||||
dataProvider: withdrawalProvider,
|
||||
variables: { partyId: pubKey || '' },
|
||||
skip: !pubKey || isReadOnly,
|
||||
});
|
||||
const delay = useWithdrawDelay(); // seconds
|
||||
const incompleteWithdrawals = useMemo(
|
||||
() => data?.filter((w) => !w.txHash),
|
||||
[data]
|
||||
);
|
||||
|
||||
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 onClose = useCallback(() => {
|
||||
updateToast(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 ? (
|
||||
<SingleReadyToWithdrawToastContent withdrawal={readyToComplete[0]} />
|
||||
) : (
|
||||
<MultipleReadyToWithdrawToastContent
|
||||
count={readyToComplete.length}
|
||||
withdrawalsLink={withdrawalsLink}
|
||||
/>
|
||||
),
|
||||
onClose,
|
||||
};
|
||||
// set only once, unless removed
|
||||
if (!hasToast(TOAST_ID)) setToast(toast);
|
||||
}, [hasToast, onClose, readyToComplete, setToast, withdrawalsLink]);
|
||||
};
|
||||
|
||||
const MultipleReadyToWithdrawToastContent = ({
|
||||
count,
|
||||
withdrawalsLink,
|
||||
}: {
|
||||
count: number;
|
||||
withdrawalsLink?: string;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<>
|
||||
<ToastHeading>{t('Withdrawals ready')}</ToastHeading>
|
||||
<p>
|
||||
{t(
|
||||
'Complete these %s withdrawals to release your funds',
|
||||
count.toString()
|
||||
)}
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
<Button
|
||||
data-testid="toast-view-withdrawals"
|
||||
size="xs"
|
||||
onClick={() =>
|
||||
withdrawalsLink ? navigate(withdrawalsLink) : undefined
|
||||
}
|
||||
>
|
||||
{t('View withdrawals')}
|
||||
</Button>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SingleReadyToWithdrawToastContent = ({
|
||||
withdrawal,
|
||||
}: {
|
||||
withdrawal: WithdrawalFieldsFragment;
|
||||
}) => {
|
||||
const { createEthWithdrawalApproval } = useEthWithdrawApprovalsStore(
|
||||
(state) => ({
|
||||
createEthWithdrawalApproval: state.create,
|
||||
})
|
||||
);
|
||||
|
||||
const { data: approval } = useWithdrawalApprovalQuery({
|
||||
variables: {
|
||||
withdrawalId: withdrawal.id,
|
||||
},
|
||||
});
|
||||
const completeButton = (
|
||||
<p className="mt-1">
|
||||
<Button
|
||||
data-testid="toast-complete-withdrawal"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
createEthWithdrawalApproval(
|
||||
withdrawal,
|
||||
approval?.erc20WithdrawalApproval
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t('Complete withdrawal')}
|
||||
</Button>
|
||||
</p>
|
||||
);
|
||||
const amount = formatNumber(
|
||||
toBigNum(withdrawal.amount, withdrawal.asset.decimals),
|
||||
withdrawal.asset.decimals
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ToastHeading>{t('Withdrawal ready')}</ToastHeading>
|
||||
<p>{t('Complete the withdrawal to release your funds')}</p>
|
||||
<Panel>
|
||||
<strong>
|
||||
{t('Withdraw')} {amount} {withdrawal.asset.symbol}
|
||||
</strong>
|
||||
</Panel>
|
||||
{completeButton}
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user