feat: updated withdrawal tab (849) (#1579)

* feat: updated withdrawal tab (849)

* added pending table to the token app, added pending completed filtering to the data provider

* amended and added unit tests

* amended time formats

* added units

* fixes errors

* addressed comments
This commit is contained in:
Art 2022-10-06 17:40:34 +02:00 committed by GitHub
parent 4ca22c4e98
commit 48ce7978ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 450 additions and 256 deletions

View File

@ -5,6 +5,7 @@ import { Heading } from '../../components/heading';
import { SplashLoader } from '../../components/splash-loader'; import { SplashLoader } from '../../components/splash-loader';
import { VegaWalletContainer } from '../../components/vega-wallet-container'; import { VegaWalletContainer } from '../../components/vega-wallet-container';
import { import {
PendingWithdrawalsTable,
useWithdrawals, useWithdrawals,
WithdrawalDialogs, WithdrawalDialogs,
WithdrawalsTable, WithdrawalsTable,
@ -30,7 +31,7 @@ const Withdrawals = ({ name }: RouteChildProps) => {
const WithdrawPendingContainer = () => { const WithdrawPendingContainer = () => {
const [withdrawDialog, setWithdrawDialog] = useState(false); const [withdrawDialog, setWithdrawDialog] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const { withdrawals, loading, error } = useWithdrawals(); const { pending, completed, loading, error } = useWithdrawals();
if (error) { if (error) {
return ( return (
@ -58,7 +59,11 @@ const WithdrawPendingContainer = () => {
<p>{t('withdrawalsText')}</p> <p>{t('withdrawalsText')}</p>
<p className="mb-8">{t('withdrawalsPreparedWarningText')}</p> <p className="mb-8">{t('withdrawalsPreparedWarningText')}</p>
<div className="w-full h-[500px]"> <div className="w-full h-[500px]">
<WithdrawalsTable withdrawals={withdrawals} /> {pending && pending.length > 0 && (
<PendingWithdrawalsTable rowData={pending} />
)}
<h4 className="pt-3 pb-1">{t('Withdrawal history')}</h4>
<WithdrawalsTable rowData={completed} />
</div> </div>
<WithdrawalDialogs <WithdrawalDialogs
withdrawDialog={withdrawDialog} withdrawDialog={withdrawDialog}

View File

@ -1,5 +1,6 @@
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
import { import {
PendingWithdrawalsTable,
useWithdrawals, useWithdrawals,
WithdrawalDialogs, WithdrawalDialogs,
WithdrawalsTable, WithdrawalsTable,
@ -10,7 +11,7 @@ import { VegaWalletContainer } from '../../components/vega-wallet-container';
import { Web3Container } from '@vegaprotocol/web3'; import { Web3Container } from '@vegaprotocol/web3';
export const WithdrawalsContainer = () => { export const WithdrawalsContainer = () => {
const { withdrawals, loading, error } = useWithdrawals(); const { pending, completed, loading, error } = useWithdrawals();
const [withdrawDialog, setWithdrawDialog] = useState(false); const [withdrawDialog, setWithdrawDialog] = useState(false);
return ( return (
@ -25,17 +26,27 @@ export const WithdrawalsContainer = () => {
onClick={() => setWithdrawDialog(true)} onClick={() => setWithdrawDialog(true)}
data-testid="withdraw-dialog-button" data-testid="withdraw-dialog-button"
> >
{t('Withdraw')} {t('Make withdrawal')}
</Button> </Button>
</header> </header>
<div> <div className="h-full px-4">
<AsyncRenderer <AsyncRenderer
data={withdrawals} data={{ pending, completed }}
loading={loading} loading={loading}
error={error} error={error}
render={(data) => { render={({ pending, completed }) => (
return <WithdrawalsTable withdrawals={data} />; <>
}} {pending && pending.length > 0 && (
<>
<h4 className="pt-3 pb-1">{t('Pending withdrawals')}</h4>
<PendingWithdrawalsTable rowData={pending} />
</>
)}
<h4 className="pt-3 pb-1">{t('Withdrawal history')}</h4>
<WithdrawalsTable rowData={completed} />
</>
)}
/> />
</div> </div>
</div> </div>

View File

@ -4,6 +4,9 @@ import type {
ValueFormatterParams, ValueFormatterParams,
} from 'ag-grid-community'; } from 'ag-grid-community';
import type { IDatasource, IGetRowsParams } from 'ag-grid-community';
import type { AgGridReactProps } from 'ag-grid-react';
export * from './ag-grid-lazy'; export * from './ag-grid-lazy';
export * from './ag-grid-dynamic'; export * from './ag-grid-dynamic';
@ -27,3 +30,16 @@ export type VegaICellRendererParams<
TRow, TRow,
TField extends Field = string TField extends Field = string
> = RowHelper<ICellRendererParams, TRow, TField>; > = RowHelper<ICellRendererParams, TRow, TField>;
export interface GetRowsParams<T> extends IGetRowsParams {
successCallback(rowsThisBlock: T[], lastRow?: number): void;
}
export interface Datasource<T> extends IDatasource {
getRows(params: GetRowsParams<T>): void;
}
export interface TypedDataAgGrid<T> extends AgGridReactProps {
rowData?: T[] | null;
datasource?: Datasource<T>;
}

View File

@ -3,6 +3,7 @@ export * from './lib/withdraw-form';
export * from './lib/withdraw-form-container'; export * from './lib/withdraw-form-container';
export * from './lib/withdraw-manager'; export * from './lib/withdraw-manager';
export * from './lib/withdrawals-table'; export * from './lib/withdrawals-table';
export * from './lib/pending-withdrawals-table';
export * from './lib/withdrawal-feedback'; export * from './lib/withdrawal-feedback';
export * from './lib/use-complete-withdraw'; export * from './lib/use-complete-withdraw';
export * from './lib/use-create-withdraw'; export * from './lib/use-create-withdraw';

View File

@ -0,0 +1,65 @@
import { MockedProvider } from '@apollo/client/testing';
import { act, fireEvent, render, screen } from '@testing-library/react';
import { generateWithdrawal } from './test-helpers';
import { CompleteCell } from './pending-withdrawals-table';
import { PendingWithdrawalsTable } from './pending-withdrawals-table';
import { getTimeFormat } from '@vegaprotocol/react-helpers';
import type { TypedDataAgGrid } from '@vegaprotocol/ui-toolkit';
import type { WithdrawalFields } from './__generated__/WithdrawalFields';
jest.mock('@web3-react/core', () => ({
useWeb3React: () => ({ provider: undefined }),
}));
const generateTable = (props: TypedDataAgGrid<WithdrawalFields>) => (
<MockedProvider>
<PendingWithdrawalsTable {...props} />
</MockedProvider>
);
describe('PendingWithdrawalsTable', () => {
it('displays correct columns', async () => {
const withdrawal = generateWithdrawal();
await act(async () => {
render(generateTable({ rowData: [withdrawal] }));
});
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(5);
expect(headers.map((h) => h.textContent?.trim())).toEqual([
'Asset',
'Amount',
'Recipient',
'Created',
'',
]);
});
it('displays given withdrawals', async () => {
const withdrawal = generateWithdrawal();
await act(async () => {
render(generateTable({ rowData: [withdrawal] }));
});
const cells = screen.getAllByRole('gridcell');
const expectedValues = [
'asset-symbol',
'1.00',
'123456…123456',
getTimeFormat().format(new Date(withdrawal.createdTimestamp)),
'Complete withdrawal',
];
cells.forEach((cell, i) => {
expect(cell).toHaveTextContent(expectedValues[i]);
});
});
});
describe('CompleteCell', () => {
const mockComplete = jest.fn();
const data = generateWithdrawal();
it('opens the dialog', () => {
render(<CompleteCell complete={mockComplete} data={data} />);
fireEvent.click(
screen.getByText('Complete withdrawal', { selector: 'button' })
);
expect(mockComplete).toBeCalled();
});
});

View File

@ -0,0 +1,201 @@
import { AgGridColumn } from 'ag-grid-react';
import {
getDateTimeFormat,
t,
truncateByChars,
addDecimalsFormatNumber,
} from '@vegaprotocol/react-helpers';
import type {
TypedDataAgGrid,
VegaICellRendererParams,
VegaValueFormatterParams,
} from '@vegaprotocol/ui-toolkit';
import { Button } from '@vegaprotocol/ui-toolkit';
import {
Dialog,
Link,
AgGridDynamic as AgGrid,
Intent,
Loader,
Icon,
} from '@vegaprotocol/ui-toolkit';
import { useEnvironment } from '@vegaprotocol/environment';
import { useCompleteWithdraw } from './use-complete-withdraw';
import type { WithdrawalFields } from './__generated__/WithdrawalFields';
import type { VerifyState } from './use-verify-withdrawal';
import { ApprovalStatus, useVerifyWithdrawal } from './use-verify-withdrawal';
export const PendingWithdrawalsTable = (
props: TypedDataAgGrid<WithdrawalFields>
) => {
const { ETHERSCAN_URL } = useEnvironment();
const {
submit,
reset: resetTx,
Dialog: EthereumTransactionDialog,
} = useCompleteWithdraw();
const {
verify,
state: verifyState,
reset: resetVerification,
} = useVerifyWithdrawal();
return (
<>
<AgGrid
overlayNoRowsTemplate={t('No withdrawals')}
defaultColDef={{ flex: 1, resizable: true }}
style={{ width: '100%' }}
components={{ CompleteCell }}
suppressCellFocus={true}
domLayout="autoHeight"
rowHeight={30}
{...props}
>
<AgGridColumn headerName="Asset" field="asset.symbol" />
<AgGridColumn
headerName={t('Amount')}
field="amount"
valueFormatter={({
value,
data,
}: VegaValueFormatterParams<WithdrawalFields, 'amount'>) => {
return addDecimalsFormatNumber(value, data.asset.decimals);
}}
/>
<AgGridColumn
headerName={t('Recipient')}
field="details.receiverAddress"
cellRenderer={({
ethUrl,
value,
valueFormatted,
}: VegaICellRendererParams<
WithdrawalFields,
'details.receiverAddress'
> & {
ethUrl: string;
}) => (
<Link
title={t('View on Etherscan (opens in a new tab)')}
href={`${ethUrl}/address/${value}`}
data-testid="etherscan-link"
target="_blank"
>
{valueFormatted}
</Link>
)}
cellRendererParams={{ ethUrl: ETHERSCAN_URL }}
valueFormatter={({
value,
}: VegaValueFormatterParams<
WithdrawalFields,
'details.receiverAddress'
>) => {
if (!value) return '-';
return truncateByChars(value);
}}
/>
<AgGridColumn
headerName={t('Created')}
field="createdTimestamp"
valueFormatter={({
value,
}: VegaValueFormatterParams<
WithdrawalFields,
'createdTimestamp'
>) => {
return getDateTimeFormat().format(new Date(value));
}}
/>
<AgGridColumn
headerName=""
field="status"
flex={2}
cellRendererParams={{
complete: async (withdrawal: WithdrawalFields) => {
const verified = await verify(withdrawal);
if (!verified) {
return;
}
submit(withdrawal.id);
},
}}
cellRenderer="CompleteCell"
/>
</AgGrid>
<Dialog
title={t('Withdrawal verification')}
onChange={(isOpen) => {
if (!isOpen) {
resetTx();
resetVerification();
}
}}
open={verifyState.dialogOpen}
size="small"
{...getVerifyDialogProps(verifyState.status)}
>
<VerificationStatus state={verifyState} />
</Dialog>
<EthereumTransactionDialog />
</>
);
};
export type CompleteCellProps = {
data: WithdrawalFields;
complete: (withdrawal: WithdrawalFields) => void;
};
export const CompleteCell = ({ data, complete }: CompleteCellProps) => (
<Button size="xs" onClick={() => complete(data)}>
{t('Complete withdrawal')}
</Button>
);
const getVerifyDialogProps = (status: ApprovalStatus) => {
if (status === ApprovalStatus.Error) {
return {
intent: Intent.Danger,
icon: <Icon name="warning-sign" />,
};
}
if (status === ApprovalStatus.Pending) {
return { intent: Intent.None, icon: <Loader size="small" /> };
}
if (status === ApprovalStatus.Delayed) {
return { intent: Intent.Warning, icon: <Icon name="time" /> };
}
return { intent: Intent.None };
};
const VerificationStatus = ({ state }: { state: VerifyState }) => {
if (state.status === ApprovalStatus.Error) {
return <p>{t('Something went wrong')}</p>;
}
if (state.status === ApprovalStatus.Pending) {
return <p>{t('Verifying...')}</p>;
}
if (state.status === ApprovalStatus.Delayed && state.completeTimestamp) {
const formattedTime = getDateTimeFormat().format(
new Date(state.completeTimestamp)
);
return (
<>
<p className="mb-2">
{t("The amount you're withdrawing has triggered a time delay")}
</p>
<p>{t(`Cannot be completed until ${formattedTime}`)}</p>
</>
);
}
return null;
};

View File

@ -111,11 +111,33 @@ export const useWithdrawals = () => {
); );
}, [data]); }, [data]);
/**
* withdrawals that have to be completed by a user.
*/
const pending = useMemo(() => {
return withdrawals.filter((w) => !w.txHash);
}, [withdrawals]);
/**
* withdrawals that are completed or being completed
*/
const completed = useMemo(() => {
return withdrawals
.filter((w) => w.txHash)
.sort((a, b) =>
(b.withdrawnTimestamp || b.createdTimestamp).localeCompare(
a.withdrawnTimestamp || a.createdTimestamp
)
);
}, [withdrawals]);
return { return {
data, data,
loading, loading,
error, error,
withdrawals, withdrawals,
pending,
completed,
}; };
}; };

View File

@ -1,24 +1,18 @@
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { act, fireEvent, render, screen } from '@testing-library/react'; import { act, render, screen } from '@testing-library/react';
import { import { getTimeFormat } from '@vegaprotocol/react-helpers';
addDecimalsFormatNumber, import { WithdrawalStatus } from '@vegaprotocol/types';
getDateTimeFormat, import type { TypedDataAgGrid } from '@vegaprotocol/ui-toolkit';
} from '@vegaprotocol/react-helpers';
import { WithdrawalStatus, WithdrawalStatusMapping } from '@vegaprotocol/types';
import { generateWithdrawal } from './test-helpers'; import { generateWithdrawal } from './test-helpers';
import type {
StatusCellProps,
WithdrawalsTableProps,
} from './withdrawals-table';
import { StatusCell } from './withdrawals-table'; import { StatusCell } from './withdrawals-table';
import { WithdrawalsTable } from './withdrawals-table'; import { WithdrawalsTable } from './withdrawals-table';
import type { Withdrawals_party_withdrawalsConnection_edges_node } from './__generated__/Withdrawals'; import type { WithdrawalFields } from './__generated__/WithdrawalFields';
jest.mock('@web3-react/core', () => ({ jest.mock('@web3-react/core', () => ({
useWeb3React: () => ({ provider: undefined }), useWeb3React: () => ({ provider: undefined }),
})); }));
const generateJsx = (props: WithdrawalsTableProps) => ( const generateJsx = (props: TypedDataAgGrid<WithdrawalFields>) => (
<MockedProvider> <MockedProvider>
<WithdrawalsTable {...props} /> <WithdrawalsTable {...props} />
</MockedProvider> </MockedProvider>
@ -28,7 +22,7 @@ describe('renders the correct columns', () => {
it('incomplete withdrawal', async () => { it('incomplete withdrawal', async () => {
const withdrawal = generateWithdrawal(); const withdrawal = generateWithdrawal();
await act(async () => { await act(async () => {
render(generateJsx({ withdrawals: [withdrawal] })); render(generateJsx({ rowData: [withdrawal] }));
}); });
const headers = screen.getAllByRole('columnheader'); const headers = screen.getAllByRole('columnheader');
@ -37,19 +31,19 @@ describe('renders the correct columns', () => {
'Asset', 'Asset',
'Amount', 'Amount',
'Recipient', 'Recipient',
'Created at', 'Completed',
'TX hash',
'Status', 'Status',
'Transaction',
]); ]);
const cells = screen.getAllByRole('gridcell'); const cells = screen.getAllByRole('gridcell');
const expectedValues = [ const expectedValues = [
'asset-symbol', 'asset-symbol',
addDecimalsFormatNumber(withdrawal.amount, withdrawal.asset.decimals), '1.00',
'123456\u2026123456', '123456…123456',
getDateTimeFormat().format(new Date(withdrawal.createdTimestamp)), '-',
'Pending',
'-', '-',
WithdrawalStatusMapping[withdrawal.status],
]; ];
cells.forEach((cell, i) => { cells.forEach((cell, i) => {
expect(cell).toHaveTextContent(expectedValues[i]); expect(cell).toHaveTextContent(expectedValues[i]);
@ -59,21 +53,22 @@ describe('renders the correct columns', () => {
it('completed withdrawal', async () => { it('completed withdrawal', async () => {
const withdrawal = generateWithdrawal({ const withdrawal = generateWithdrawal({
txHash: '0x1234567891011121314', txHash: '0x1234567891011121314',
withdrawnTimestamp: '2022-04-21T00:00:00',
status: WithdrawalStatus.STATUS_FINALIZED, status: WithdrawalStatus.STATUS_FINALIZED,
}); });
await act(async () => { await act(async () => {
render(generateJsx({ withdrawals: [withdrawal] })); render(generateJsx({ rowData: [withdrawal] }));
}); });
const cells = screen.getAllByRole('gridcell'); const cells = screen.getAllByRole('gridcell');
const expectedValues = [ const expectedValues = [
'asset-symbol', 'asset-symbol',
addDecimalsFormatNumber(withdrawal.amount, withdrawal.asset.decimals), '1.00',
'123456…123456', '123456…123456',
getDateTimeFormat().format(new Date(withdrawal.createdTimestamp)), getTimeFormat().format(new Date(withdrawal.withdrawnTimestamp as string)),
'Completed',
'0x1234…121314', '0x1234…121314',
WithdrawalStatusMapping[withdrawal.status],
]; ];
cells.forEach((cell, i) => { cells.forEach((cell, i) => {
expect(cell).toHaveTextContent(expectedValues[i]); expect(cell).toHaveTextContent(expectedValues[i]);
@ -82,51 +77,47 @@ describe('renders the correct columns', () => {
}); });
describe('StatusCell', () => { describe('StatusCell', () => {
let props: StatusCellProps; let props: { data: WithdrawalFields };
let withdrawal: Withdrawals_party_withdrawalsConnection_edges_node; let withdrawal: WithdrawalFields;
let mockComplete: jest.Mock;
beforeEach(() => { beforeEach(() => {
withdrawal = generateWithdrawal(); withdrawal = generateWithdrawal();
mockComplete = jest.fn();
// @ts-ignore dont need full ICellRendererParams
props = { props = {
value: withdrawal.status,
data: withdrawal, data: withdrawal,
complete: mockComplete,
}; };
}); });
it('Open', () => { it('Open', () => {
props.value = WithdrawalStatus.STATUS_FINALIZED;
props.data.pendingOnForeignChain = false; props.data.pendingOnForeignChain = false;
props.data.txHash = null; props.data.txHash = null;
render(<StatusCell {...props} />); render(<StatusCell {...props} />);
expect(screen.getByText('Open')).toBeInTheDocument(); expect(screen.getByText('Pending')).toBeInTheDocument();
fireEvent.click(screen.getByText('Complete', { selector: 'button' }));
expect(mockComplete).toHaveBeenCalled();
}); });
it('Pending', () => { it('Pending', () => {
props.value = WithdrawalStatus.STATUS_FINALIZED;
props.data.pendingOnForeignChain = true; props.data.pendingOnForeignChain = true;
props.data.txHash = '0x123'; props.data.txHash = '0x123';
render(<StatusCell {...props} />); render(<StatusCell {...props} />);
expect(screen.getByText('Pending')).toBeInTheDocument(); expect(screen.getByText('Pending')).toBeInTheDocument();
expect(screen.getByText('View on Etherscan')).toHaveAttribute(
'href',
expect.stringContaining(props.data.txHash)
);
}); });
it('Finalized', () => { it('Completed', () => {
props.value = WithdrawalStatus.STATUS_FINALIZED;
props.data.pendingOnForeignChain = false; props.data.pendingOnForeignChain = false;
props.data.txHash = '0x123'; props.data.txHash = '0x123';
props.data.status = WithdrawalStatus.STATUS_FINALIZED;
render(<StatusCell {...props} />); render(<StatusCell {...props} />);
expect(screen.getByText('Finalized')).toBeInTheDocument(); expect(screen.getByText('Completed')).toBeInTheDocument();
});
it('Rejected', () => {
props.data.pendingOnForeignChain = false;
props.data.txHash = '0x123';
props.data.status = WithdrawalStatus.STATUS_REJECTED;
render(<StatusCell {...props} />);
expect(screen.getByText('Rejected')).toBeInTheDocument();
}); });
}); });

View File

@ -6,181 +6,108 @@ import {
addDecimalsFormatNumber, addDecimalsFormatNumber,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import type { import type {
TypedDataAgGrid,
VegaICellRendererParams, VegaICellRendererParams,
VegaValueFormatterParams, VegaValueFormatterParams,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { import { Link, AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
Dialog,
Link,
AgGridDynamic as AgGrid,
Intent,
Loader,
Icon,
} from '@vegaprotocol/ui-toolkit';
import { useEnvironment } from '@vegaprotocol/environment'; import { useEnvironment } from '@vegaprotocol/environment';
import { useCompleteWithdraw } from './use-complete-withdraw';
import type { WithdrawalFields } from './__generated__/WithdrawalFields'; import type { WithdrawalFields } from './__generated__/WithdrawalFields';
import type { VerifyState } from './use-verify-withdrawal'; import { WithdrawalStatus } from '@vegaprotocol/types';
import { ApprovalStatus, useVerifyWithdrawal } from './use-verify-withdrawal';
export interface WithdrawalsTableProps { export const WithdrawalsTable = (props: TypedDataAgGrid<WithdrawalFields>) => {
withdrawals: WithdrawalFields[];
}
export const WithdrawalsTable = ({ withdrawals }: WithdrawalsTableProps) => {
const { ETHERSCAN_URL } = useEnvironment(); const { ETHERSCAN_URL } = useEnvironment();
const {
submit,
reset: resetTx,
Dialog: EthereumTransactionDialog,
} = useCompleteWithdraw();
const {
verify,
state: verifyState,
reset: resetVerification,
} = useVerifyWithdrawal();
return ( return (
<> <AgGrid
<AgGrid overlayNoRowsTemplate={t('No withdrawals')}
rowData={withdrawals} defaultColDef={{ flex: 1, resizable: true }}
overlayNoRowsTemplate={t('No withdrawals')} style={{ width: '100%' }}
defaultColDef={{ flex: 1, resizable: true }} components={{ RecipientCell, StatusCell }}
style={{ width: '100%', height: '100%' }} suppressCellFocus={true}
components={{ StatusCell, RecipientCell }} domLayout="autoHeight"
suppressCellFocus={true} rowHeight={30}
> {...props}
<AgGridColumn headerName="Asset" field="asset.symbol" /> >
<AgGridColumn <AgGridColumn headerName="Asset" field="asset.symbol" />
headerName={t('Amount')} <AgGridColumn
field="amount" headerName={t('Amount')}
valueFormatter={({ field="amount"
value, valueFormatter={({
data, value,
}: VegaValueFormatterParams<WithdrawalFields, 'amount'>) => { data,
return addDecimalsFormatNumber(value, data.asset.decimals); }: VegaValueFormatterParams<WithdrawalFields, 'amount'>) => {
}} return addDecimalsFormatNumber(value, data.asset.decimals);
/>
<AgGridColumn
headerName={t('Recipient')}
field="details.receiverAddress"
cellRenderer="RecipientCell"
cellRendererParams={{ ethUrl: ETHERSCAN_URL }}
valueFormatter={({
value,
}: VegaValueFormatterParams<
WithdrawalFields,
'details.receiverAddress'
>) => {
if (!value) return '-';
return truncateByChars(value);
}}
/>
<AgGridColumn
headerName={t('Created at')}
field="createdTimestamp"
valueFormatter={({
value,
}: VegaValueFormatterParams<
WithdrawalFields,
'createdTimestamp'
>) => {
return getDateTimeFormat().format(new Date(value));
}}
/>
<AgGridColumn
headerName={t('TX hash')}
field="txHash"
cellRenderer={({
value,
}: VegaValueFormatterParams<WithdrawalFields, 'txHash'>) => {
if (!value) return '-';
return (
<Link
title={t('View transaction on Etherscan')}
href={`${ETHERSCAN_URL}/tx/${value}`}
data-testid="etherscan-link"
target="_blank"
>
{truncateByChars(value)}
</Link>
);
}}
/>
<AgGridColumn
headerName={t('Status')}
field="status"
cellRenderer="StatusCell"
cellRendererParams={{
complete: async (withdrawal: WithdrawalFields) => {
const verified = await verify(withdrawal);
if (!verified) {
return;
}
submit(withdrawal.id);
},
ethUrl: ETHERSCAN_URL,
}}
/>
</AgGrid>
<Dialog
title={t('Withdrawal verification')}
onChange={(isOpen) => {
if (!isOpen) {
resetTx();
resetVerification();
}
}} }}
open={verifyState.dialogOpen} />
size="small" <AgGridColumn
{...getVerifyDialogProps(verifyState.status)} headerName={t('Recipient')}
> field="details.receiverAddress"
<VerificationStatus state={verifyState} /> cellRenderer="RecipientCell"
</Dialog> cellRendererParams={{ ethUrl: ETHERSCAN_URL }}
<EthereumTransactionDialog /> valueFormatter={({
</> value,
}: VegaValueFormatterParams<
WithdrawalFields,
'details.receiverAddress'
>) => {
if (!value) return '-';
return truncateByChars(value);
}}
/>
<AgGridColumn
headerName={t('Completed')}
field="withdrawnTimestamp"
valueFormatter={({
data,
}: VegaValueFormatterParams<
WithdrawalFields,
'withdrawnTimestamp'
>) => {
const ts = data.withdrawnTimestamp;
if (!ts) return '-';
return getDateTimeFormat().format(new Date(ts));
}}
/>
<AgGridColumn
headerName={t('Status')}
field="status"
cellRenderer="StatusCell"
/>
<AgGridColumn
headerName={t('Transaction')}
field="txHash"
cellRenderer={({
value,
}: VegaValueFormatterParams<WithdrawalFields, 'txHash'>) => {
if (!value) return '-';
return (
<Link
title={t('View transaction on Etherscan')}
href={`${ETHERSCAN_URL}/tx/${value}`}
data-testid="etherscan-link"
target="_blank"
>
{truncateByChars(value)}
</Link>
);
}}
/>
</AgGrid>
); );
}; };
export interface StatusCellProps export const StatusCell = ({ data }: { data: WithdrawalFields }) => {
extends VegaICellRendererParams<WithdrawalFields, 'status'> { if (data.pendingOnForeignChain || !data.txHash) {
ethUrl: string; return <span>{t('Pending')}</span>;
complete: (withdrawal: WithdrawalFields) => void;
}
export const StatusCell = ({ ethUrl, data, complete }: StatusCellProps) => {
if (data.pendingOnForeignChain) {
return (
<div className="flex justify-between gap-8">
{t('Pending')}
{data.txHash && (
<Link
title={t('View transaction on Etherscan')}
href={`${ethUrl}/tx/${data.txHash}`}
data-testid="etherscan-link"
target="_blank"
>
{t('View on Etherscan')}
</Link>
)}
</div>
);
} }
if (data.status === WithdrawalStatus.STATUS_FINALIZED) {
if (!data.txHash) { return <span>{t('Completed')}</span>;
return (
<div className="flex justify-between gap-8">
{t('Open')}
<button className="underline" onClick={() => complete(data)}>
{t('Complete')}
</button>
</div>
);
} }
if (data.status === WithdrawalStatus.STATUS_REJECTED) {
return <span>{t('Finalized')}</span>; return <span>{t('Rejected')}</span>;
}
return <span>{t('Failed')}</span>;
}; };
export interface RecipientCellProps export interface RecipientCellProps
@ -204,48 +131,3 @@ const RecipientCell = ({
</Link> </Link>
); );
}; };
const getVerifyDialogProps = (status: ApprovalStatus) => {
if (status === ApprovalStatus.Error) {
return {
intent: Intent.Danger,
icon: <Icon name="warning-sign" />,
};
}
if (status === ApprovalStatus.Pending) {
return { intent: Intent.None, icon: <Loader size="small" /> };
}
if (status === ApprovalStatus.Delayed) {
return { intent: Intent.Warning, icon: <Icon name="time" /> };
}
return { intent: Intent.None };
};
const VerificationStatus = ({ state }: { state: VerifyState }) => {
if (state.status === ApprovalStatus.Error) {
return <p>{t('Something went wrong')}</p>;
}
if (state.status === ApprovalStatus.Pending) {
return <p>{t('Verifying...')}</p>;
}
if (state.status === ApprovalStatus.Delayed && state.completeTimestamp) {
const formattedTime = getDateTimeFormat().format(
new Date(state.completeTimestamp)
);
return (
<>
<p className="mb-2">
{t("The amount you're withdrawing has triggered a time delay")}
</p>
<p>{t(`Cannot be completed until ${formattedTime}`)}</p>
</>
);
}
return null;
};