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:
parent
4ca22c4e98
commit
48ce7978ee
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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>;
|
||||||
|
}
|
||||||
|
@ -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';
|
||||||
|
65
libs/withdraws/src/lib/pending-withdrawals-table.spec.tsx
Normal file
65
libs/withdraws/src/lib/pending-withdrawals-table.spec.tsx
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
201
libs/withdraws/src/lib/pending-withdrawals-table.tsx
Normal file
201
libs/withdraws/src/lib/pending-withdrawals-table.tsx
Normal 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;
|
||||||
|
};
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -6,49 +6,28 @@ 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
|
||||||
rowData={withdrawals}
|
|
||||||
overlayNoRowsTemplate={t('No withdrawals')}
|
overlayNoRowsTemplate={t('No withdrawals')}
|
||||||
defaultColDef={{ flex: 1, resizable: true }}
|
defaultColDef={{ flex: 1, resizable: true }}
|
||||||
style={{ width: '100%', height: '100%' }}
|
style={{ width: '100%' }}
|
||||||
components={{ StatusCell, RecipientCell }}
|
components={{ RecipientCell, StatusCell }}
|
||||||
suppressCellFocus={true}
|
suppressCellFocus={true}
|
||||||
|
domLayout="autoHeight"
|
||||||
|
rowHeight={30}
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<AgGridColumn headerName="Asset" field="asset.symbol" />
|
<AgGridColumn headerName="Asset" field="asset.symbol" />
|
||||||
<AgGridColumn
|
<AgGridColumn
|
||||||
@ -77,19 +56,26 @@ export const WithdrawalsTable = ({ withdrawals }: WithdrawalsTableProps) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<AgGridColumn
|
<AgGridColumn
|
||||||
headerName={t('Created at')}
|
headerName={t('Completed')}
|
||||||
field="createdTimestamp"
|
field="withdrawnTimestamp"
|
||||||
valueFormatter={({
|
valueFormatter={({
|
||||||
value,
|
data,
|
||||||
}: VegaValueFormatterParams<
|
}: VegaValueFormatterParams<
|
||||||
WithdrawalFields,
|
WithdrawalFields,
|
||||||
'createdTimestamp'
|
'withdrawnTimestamp'
|
||||||
>) => {
|
>) => {
|
||||||
return getDateTimeFormat().format(new Date(value));
|
const ts = data.withdrawnTimestamp;
|
||||||
|
if (!ts) return '-';
|
||||||
|
return getDateTimeFormat().format(new Date(ts));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<AgGridColumn
|
<AgGridColumn
|
||||||
headerName={t('TX hash')}
|
headerName={t('Status')}
|
||||||
|
field="status"
|
||||||
|
cellRenderer="StatusCell"
|
||||||
|
/>
|
||||||
|
<AgGridColumn
|
||||||
|
headerName={t('Transaction')}
|
||||||
field="txHash"
|
field="txHash"
|
||||||
cellRenderer={({
|
cellRenderer={({
|
||||||
value,
|
value,
|
||||||
@ -107,80 +93,21 @@ export const WithdrawalsTable = ({ withdrawals }: WithdrawalsTableProps) => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<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>
|
</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 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;
|
|
||||||
};
|
|
||||||
|
Loading…
Reference in New Issue
Block a user