refactor: handle Ethereum dialog state from hook (#851)
* refactor: pass dialog down from hook * feat: convert dialog to be returned as component * chore: fix linting
This commit is contained in:
parent
62b6cd7580
commit
b7f08def47
@ -16,7 +16,6 @@ import { addDecimal } from '../../lib/decimals';
|
|||||||
import { truncateMiddle } from '../../lib/truncate-middle';
|
import { truncateMiddle } from '../../lib/truncate-middle';
|
||||||
import type { Withdrawals_party_withdrawals } from '@vegaprotocol/withdraws';
|
import type { Withdrawals_party_withdrawals } from '@vegaprotocol/withdraws';
|
||||||
import { useCompleteWithdraw, useWithdrawals } from '@vegaprotocol/withdraws';
|
import { useCompleteWithdraw, useWithdrawals } from '@vegaprotocol/withdraws';
|
||||||
import { TransactionDialog } from '@vegaprotocol/web3';
|
|
||||||
import { WithdrawalStatus } from '../../__generated__/globalTypes';
|
import { WithdrawalStatus } from '../../__generated__/globalTypes';
|
||||||
import { Flags } from '../../config';
|
import { Flags } from '../../config';
|
||||||
|
|
||||||
@ -35,9 +34,7 @@ const Withdrawals = () => {
|
|||||||
|
|
||||||
const WithdrawPendingContainer = () => {
|
const WithdrawPendingContainer = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { transaction, submit } = useCompleteWithdraw(
|
const { submit, Dialog } = useCompleteWithdraw(Flags.USE_NEW_BRIDGE_CONTRACT);
|
||||||
Flags.USE_NEW_BRIDGE_CONTRACT
|
|
||||||
);
|
|
||||||
const { data, loading, error } = useWithdrawals();
|
const { data, loading, error } = useWithdrawals();
|
||||||
|
|
||||||
const withdrawals = React.useMemo(() => {
|
const withdrawals = React.useMemo(() => {
|
||||||
@ -83,7 +80,7 @@ const WithdrawPendingContainer = () => {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<TransactionDialog name="withdraw" {...transaction} />
|
<Dialog />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -7,11 +7,7 @@ import { useSubmitApproval } from './use-submit-approval';
|
|||||||
import { useGetDepositLimits } from './use-get-deposit-limits';
|
import { useGetDepositLimits } from './use-get-deposit-limits';
|
||||||
import { useGetAllowance } from './use-get-allowance';
|
import { useGetAllowance } from './use-get-allowance';
|
||||||
import { useSubmitFaucet } from './use-submit-faucet';
|
import { useSubmitFaucet } from './use-submit-faucet';
|
||||||
import {
|
import { EthTxStatus, useEthereumConfig } from '@vegaprotocol/web3';
|
||||||
EthTxStatus,
|
|
||||||
TransactionDialog,
|
|
||||||
useEthereumConfig,
|
|
||||||
} from '@vegaprotocol/web3';
|
|
||||||
import { useTokenContract } from '@vegaprotocol/web3';
|
import { useTokenContract } from '@vegaprotocol/web3';
|
||||||
import { removeDecimal } from '@vegaprotocol/react-helpers';
|
import { removeDecimal } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
@ -81,7 +77,7 @@ export const DepositManager = ({
|
|||||||
const approve = useSubmitApproval(tokenContract);
|
const approve = useSubmitApproval(tokenContract);
|
||||||
|
|
||||||
// Set up deposit transaction
|
// Set up deposit transaction
|
||||||
const { confirmationEvent, ...deposit } = useSubmitDeposit();
|
const deposit = useSubmitDeposit();
|
||||||
|
|
||||||
// Set up faucet transaction
|
// Set up faucet transaction
|
||||||
const faucet = useSubmitFaucet(tokenContract);
|
const faucet = useSubmitFaucet(tokenContract);
|
||||||
@ -89,16 +85,16 @@ export const DepositManager = ({
|
|||||||
// Update balance after confirmation event has been received
|
// Update balance after confirmation event has been received
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
faucet.transaction.status === EthTxStatus.Complete ||
|
faucet.transaction.status === EthTxStatus.Confirmed ||
|
||||||
confirmationEvent !== null
|
deposit.transaction.status === EthTxStatus.Confirmed
|
||||||
) {
|
) {
|
||||||
refetchBalance();
|
refetchBalance();
|
||||||
}
|
}
|
||||||
}, [confirmationEvent, refetchBalance, faucet.transaction.status]);
|
}, [deposit.transaction.status, faucet.transaction.status, refetchBalance]);
|
||||||
|
|
||||||
// After an approval transaction refetch allowance
|
// After an approval transaction refetch allowance
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (approve.transaction.status === EthTxStatus.Complete) {
|
if (approve.transaction.status === EthTxStatus.Confirmed) {
|
||||||
refetchAllowance();
|
refetchAllowance();
|
||||||
}
|
}
|
||||||
}, [approve.transaction.status, refetchAllowance]);
|
}, [approve.transaction.status, refetchAllowance]);
|
||||||
@ -123,15 +119,9 @@ export const DepositManager = ({
|
|||||||
allowance={allowance}
|
allowance={allowance}
|
||||||
isFaucetable={isFaucetable}
|
isFaucetable={isFaucetable}
|
||||||
/>
|
/>
|
||||||
<TransactionDialog {...approve.transaction} name="approve" />
|
<approve.Dialog />
|
||||||
<TransactionDialog {...faucet.transaction} name="faucet" />
|
<faucet.Dialog />
|
||||||
<TransactionDialog
|
<deposit.Dialog />
|
||||||
{...deposit}
|
|
||||||
name="deposit"
|
|
||||||
confirmed={Boolean(confirmationEvent)}
|
|
||||||
// Must wait for additional confirmations for Vega to pick up the Ethereum transaction
|
|
||||||
requiredConfirmations={config?.confirmations}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,6 @@ import { gql, useSubscription } from '@apollo/client';
|
|||||||
import type {
|
import type {
|
||||||
DepositEvent,
|
DepositEvent,
|
||||||
DepositEventVariables,
|
DepositEventVariables,
|
||||||
DepositEvent_busEvents_event_Deposit,
|
|
||||||
} from './__generated__/DepositEvent';
|
} from './__generated__/DepositEvent';
|
||||||
import { DepositStatus } from '@vegaprotocol/types';
|
import { DepositStatus } from '@vegaprotocol/types';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@ -35,15 +34,15 @@ const DEPOSIT_EVENT_SUB = gql`
|
|||||||
export const useSubmitDeposit = () => {
|
export const useSubmitDeposit = () => {
|
||||||
const { config } = useEthereumConfig();
|
const { config } = useEthereumConfig();
|
||||||
const contract = useBridgeContract(true);
|
const contract = useBridgeContract(true);
|
||||||
const [confirmationEvent, setConfirmationEvent] =
|
|
||||||
useState<DepositEvent_busEvents_event_Deposit | null>(null);
|
|
||||||
// Store public key from contract arguments for use in the subscription,
|
// Store public key from contract arguments for use in the subscription,
|
||||||
// NOTE: it may be different from the users connected key
|
// NOTE: it may be different from the users connected key
|
||||||
const [partyId, setPartyId] = useState<string | null>(null);
|
const [partyId, setPartyId] = useState<string | null>(null);
|
||||||
const { transaction, perform } = useEthereumTransaction<
|
|
||||||
|
const transaction = useEthereumTransaction<
|
||||||
CollateralBridgeNew | CollateralBridge,
|
CollateralBridgeNew | CollateralBridge,
|
||||||
'deposit_asset'
|
'deposit_asset'
|
||||||
>(contract, 'deposit_asset', config?.confirmations);
|
>(contract, 'deposit_asset', config?.confirmations, true);
|
||||||
|
|
||||||
useSubscription<DepositEvent, DepositEventVariables>(DEPOSIT_EVENT_SUB, {
|
useSubscription<DepositEvent, DepositEventVariables>(DEPOSIT_EVENT_SUB, {
|
||||||
variables: { partyId: partyId ? remove0x(partyId) : '' },
|
variables: { partyId: partyId ? remove0x(partyId) : '' },
|
||||||
@ -59,7 +58,7 @@ export const useSubmitDeposit = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
e.event.txHash === transaction.txHash &&
|
e.event.txHash === transaction.transaction.txHash &&
|
||||||
// Note there is a bug in data node where the subscription is not emitted when the status
|
// Note there is a bug in data node where the subscription is not emitted when the status
|
||||||
// changes from 'Open' to 'Finalized' as a result the deposit UI will hang in a pending state right now
|
// changes from 'Open' to 'Finalized' as a result the deposit UI will hang in a pending state right now
|
||||||
// https://github.com/vegaprotocol/data-node/issues/460
|
// https://github.com/vegaprotocol/data-node/issues/460
|
||||||
@ -72,19 +71,17 @@ export const useSubmitDeposit = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (matchingDeposit && matchingDeposit.event.__typename === 'Deposit') {
|
if (matchingDeposit && matchingDeposit.event.__typename === 'Deposit') {
|
||||||
setConfirmationEvent(matchingDeposit.event);
|
transaction.setConfirmed();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...transaction,
|
...transaction,
|
||||||
perform: (...args: Parameters<typeof perform>) => {
|
perform: (...args: Parameters<typeof transaction.perform>) => {
|
||||||
setConfirmationEvent(null);
|
|
||||||
setPartyId(args[2]);
|
setPartyId(args[2]);
|
||||||
const publicKey = prepend0x(args[2]);
|
const publicKey = prepend0x(args[2]);
|
||||||
perform(args[0], args[1], publicKey);
|
transaction.perform(args[0], args[1], publicKey);
|
||||||
},
|
},
|
||||||
confirmationEvent,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
@ -79,18 +79,14 @@ export const TxRow = ({
|
|||||||
|
|
||||||
interface ConfirmationEventRowProps {
|
interface ConfirmationEventRowProps {
|
||||||
status: EthTxStatus;
|
status: EthTxStatus;
|
||||||
confirmed: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConfirmationEventRow = ({
|
export const ConfirmationEventRow = ({ status }: ConfirmationEventRowProps) => {
|
||||||
status,
|
if (status !== EthTxStatus.Complete && status !== EthTxStatus.Confirmed) {
|
||||||
confirmed,
|
|
||||||
}: ConfirmationEventRowProps) => {
|
|
||||||
if (status !== EthTxStatus.Complete) {
|
|
||||||
return <p>{t('Vega confirmation')}</p>;
|
return <p>{t('Vega confirmation')}</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!confirmed) {
|
if (status === EthTxStatus.Complete) {
|
||||||
return (
|
return (
|
||||||
<p className="text-black dark:text-white">
|
<p className="text-black dark:text-white">
|
||||||
{t('Vega is confirming your transaction...')}
|
{t('Vega is confirming your transaction...')}
|
||||||
|
@ -15,9 +15,7 @@ export const DialogWrapper = ({
|
|||||||
<div className="flex gap-12 max-w-full text-ui">
|
<div className="flex gap-12 max-w-full text-ui">
|
||||||
<div className="pt-8 fill-current">{icon}</div>
|
<div className="pt-8 fill-current">{icon}</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h1 className="text-h4 text-black dark:text-white capitalize mb-12">
|
<h1 className="text-h4 text-black dark:text-white mb-12">{title}</h1>
|
||||||
{title}
|
|
||||||
</h1>
|
|
||||||
<div className="text-black-40 dark:text-white-40">{children}</div>
|
<div className="text-black-40 dark:text-white-40">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import merge from 'lodash/merge';
|
||||||
|
import type { PartialDeep } from 'type-fest';
|
||||||
import { EthereumError } from '../ethereum-error';
|
import { EthereumError } from '../ethereum-error';
|
||||||
import { EthTxStatus } from '../use-ethereum-transaction';
|
import { EthTxStatus } from '../use-ethereum-transaction';
|
||||||
import type { TransactionDialogProps } from './transaction-dialog';
|
import type { TransactionDialogProps } from './transaction-dialog';
|
||||||
@ -15,15 +17,21 @@ let props: TransactionDialogProps;
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
props = {
|
props = {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
|
onChange: jest.fn(),
|
||||||
|
transaction: {
|
||||||
status: EthTxStatus.Default,
|
status: EthTxStatus.Default,
|
||||||
txHash: null,
|
txHash: null,
|
||||||
error: null,
|
error: null,
|
||||||
confirmations: 1,
|
confirmations: 1,
|
||||||
|
receipt: null,
|
||||||
|
dialogOpen: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const generateJsx = (moreProps?: Partial<TransactionDialogProps>) => {
|
const generateJsx = (moreProps?: PartialDeep<TransactionDialogProps>) => {
|
||||||
return <TransactionDialog {...props} {...moreProps} />;
|
const mergedProps = merge(props, moreProps);
|
||||||
|
return <TransactionDialog {...mergedProps} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
it('Opens when tx starts and closes if the user rejects the tx', () => {
|
it('Opens when tx starts and closes if the user rejects the tx', () => {
|
||||||
@ -32,15 +40,17 @@ it('Opens when tx starts and closes if the user rejects the tx', () => {
|
|||||||
// Dialog closed by default
|
// Dialog closed by default
|
||||||
expect(container).toBeEmptyDOMElement();
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
|
||||||
rerender(generateJsx({ status: EthTxStatus.Pending }));
|
rerender(generateJsx({ transaction: { status: EthTxStatus.Pending } }));
|
||||||
|
|
||||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||||
|
|
||||||
// User rejecting the tx closes the dialog
|
// User rejecting the tx closes the dialog
|
||||||
rerender(
|
rerender(
|
||||||
generateJsx({
|
generateJsx({
|
||||||
|
transaction: {
|
||||||
status: EthTxStatus.Error,
|
status: EthTxStatus.Error,
|
||||||
error: new EthereumError('User rejected', 4001, 'reason'),
|
error: new EthereumError('User rejected', 4001, 'reason'),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -49,26 +59,34 @@ it('Opens when tx starts and closes if the user rejects the tx', () => {
|
|||||||
|
|
||||||
it('Doesn\t repoen if user dismissed the dialog', () => {
|
it('Doesn\t repoen if user dismissed the dialog', () => {
|
||||||
const { container, rerender } = render(
|
const { container, rerender } = render(
|
||||||
generateJsx({ status: EthTxStatus.Pending })
|
generateJsx({ transaction: { status: EthTxStatus.Pending } })
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId('dialog-close'));
|
fireEvent.click(screen.getByTestId('dialog-close'));
|
||||||
|
|
||||||
expect(container).toBeEmptyDOMElement();
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
|
||||||
rerender(generateJsx({ status: EthTxStatus.Complete }));
|
rerender(generateJsx({ transaction: { status: EthTxStatus.Complete } }));
|
||||||
|
|
||||||
// Should still be closed even though tx updated
|
// Should still be closed even though tx updated
|
||||||
expect(container).toBeEmptyDOMElement();
|
expect(container).toBeEmptyDOMElement();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Dialog states', () => {
|
it('Dialog states', () => {
|
||||||
const { rerender } = render(generateJsx({ status: EthTxStatus.Requested }));
|
// Requested
|
||||||
|
const { rerender } = render(
|
||||||
|
generateJsx({ transaction: { status: EthTxStatus.Requested } })
|
||||||
|
);
|
||||||
expect(screen.getByText('Confirm transaction')).toBeInTheDocument();
|
expect(screen.getByText('Confirm transaction')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Confirm transaction in wallet')).toBeInTheDocument();
|
expect(screen.getByText('Confirm transaction in wallet')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Await Ethereum transaction')).toBeInTheDocument();
|
expect(screen.getByText('Await Ethereum transaction')).toBeInTheDocument();
|
||||||
|
|
||||||
rerender(generateJsx({ status: EthTxStatus.Pending, confirmations: 0 }));
|
// Pending
|
||||||
|
rerender(
|
||||||
|
generateJsx({
|
||||||
|
transaction: { status: EthTxStatus.Pending, confirmations: 0 },
|
||||||
|
})
|
||||||
|
);
|
||||||
expect(screen.getByText(`${props.name} pending`)).toBeInTheDocument();
|
expect(screen.getByText(`${props.name} pending`)).toBeInTheDocument();
|
||||||
expect(screen.getByText('Confirmed in wallet')).toBeInTheDocument();
|
expect(screen.getByText('Confirmed in wallet')).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
@ -76,38 +94,40 @@ it('Dialog states', () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('link')).toBeInTheDocument();
|
expect(screen.getByTestId('link')).toBeInTheDocument();
|
||||||
|
|
||||||
rerender(generateJsx({ status: EthTxStatus.Complete, confirmations: 1 }));
|
// Ethereum complete
|
||||||
expect(screen.getByText(`${props.name} complete`)).toBeInTheDocument();
|
rerender(
|
||||||
|
generateJsx({
|
||||||
|
transaction: { status: EthTxStatus.Complete, confirmations: 1 },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(screen.getByText(`${props.name} pending`)).toBeInTheDocument();
|
||||||
expect(screen.getByText('Confirmed in wallet')).toBeInTheDocument();
|
expect(screen.getByText('Confirmed in wallet')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Ethereum transaction complete')).toBeInTheDocument();
|
expect(screen.getByText('Ethereum transaction complete')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Ethereum confirmed (via api)
|
||||||
|
rerender(
|
||||||
|
generateJsx({
|
||||||
|
transaction: {
|
||||||
|
status: EthTxStatus.Confirmed,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(screen.getByText(`${props.name} complete`)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Confirmed in wallet')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Transaction confirmed')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Error
|
||||||
const errorMsg = 'Something went wrong';
|
const errorMsg = 'Something went wrong';
|
||||||
const reason = 'Transaction failed';
|
const reason = 'Transaction failed';
|
||||||
rerender(
|
rerender(
|
||||||
generateJsx({
|
generateJsx({
|
||||||
|
transaction: {
|
||||||
status: EthTxStatus.Error,
|
status: EthTxStatus.Error,
|
||||||
error: new EthereumError(errorMsg, 1, reason),
|
error: new EthereumError(errorMsg, 1, reason),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(screen.getByText(`${props.name} failed`)).toBeInTheDocument();
|
expect(screen.getByText(`${props.name} failed`)).toBeInTheDocument();
|
||||||
expect(screen.getByText(`Error: ${reason}`)).toBeInTheDocument();
|
expect(screen.getByText(`Error: ${reason}`)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Success state waits for confirmation event if provided', () => {
|
|
||||||
const { rerender } = render(
|
|
||||||
generateJsx({ status: EthTxStatus.Complete, confirmed: false })
|
|
||||||
);
|
|
||||||
expect(screen.getByText(`${props.name} pending`)).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Confirmed in wallet')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Ethereum transaction complete')).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getByText('Vega is confirming your transaction...')
|
|
||||||
).toBeInTheDocument();
|
|
||||||
|
|
||||||
// @ts-ignore enforce truthy on confirmation event
|
|
||||||
rerender(generateJsx({ confirmed: true, status: EthTxStatus.Complete }));
|
|
||||||
expect(
|
|
||||||
screen.queryByText('Vega is confirming your transaction...')
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Transaction confirmed')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
@ -1,35 +1,27 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import { Dialog, Icon, Intent, Loader } from '@vegaprotocol/ui-toolkit';
|
import { Dialog, Icon, Intent, Loader } from '@vegaprotocol/ui-toolkit';
|
||||||
import { isEthereumError, isExpectedEthereumError } from '../ethereum-error';
|
import { isEthereumError } from '../ethereum-error';
|
||||||
import type { TxError } from '../use-ethereum-transaction';
|
import type { EthTxState } from '../use-ethereum-transaction';
|
||||||
import { EthTxStatus } from '../use-ethereum-transaction';
|
import { EthTxStatus } from '../use-ethereum-transaction';
|
||||||
import { ConfirmRow, TxRow, ConfirmationEventRow } from './dialog-rows';
|
import { ConfirmRow, TxRow, ConfirmationEventRow } from './dialog-rows';
|
||||||
import { DialogWrapper } from './dialog-wrapper';
|
import { DialogWrapper } from './dialog-wrapper';
|
||||||
|
|
||||||
export interface TransactionDialogProps {
|
export interface TransactionDialogProps {
|
||||||
name: string;
|
name: string;
|
||||||
status: EthTxStatus;
|
onChange: (isOpen: boolean) => void;
|
||||||
error: TxError | null;
|
transaction: EthTxState;
|
||||||
confirmations: number;
|
|
||||||
txHash: string | null;
|
|
||||||
requiredConfirmations?: number;
|
|
||||||
// Undefined means this dialog isn't expecting an additional event for a complete state, a boolean
|
// Undefined means this dialog isn't expecting an additional event for a complete state, a boolean
|
||||||
// value means it is but hasn't been received yet
|
// value means it is but hasn't been received yet
|
||||||
confirmed?: boolean;
|
requiredConfirmations?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TransactionDialog = ({
|
export const TransactionDialog = ({
|
||||||
|
onChange,
|
||||||
name,
|
name,
|
||||||
status,
|
transaction,
|
||||||
error,
|
|
||||||
confirmations,
|
|
||||||
txHash,
|
|
||||||
requiredConfirmations = 1,
|
requiredConfirmations = 1,
|
||||||
confirmed,
|
|
||||||
}: TransactionDialogProps) => {
|
}: TransactionDialogProps) => {
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const { status, error, confirmations, txHash } = transaction;
|
||||||
const dialogDismissed = useRef(false);
|
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (status === EthTxStatus.Error) {
|
if (status === EthTxStatus.Error) {
|
||||||
@ -67,15 +59,18 @@ export const TransactionDialog = ({
|
|||||||
requiredConfirmations={requiredConfirmations}
|
requiredConfirmations={requiredConfirmations}
|
||||||
highlightComplete={false}
|
highlightComplete={false}
|
||||||
/>
|
/>
|
||||||
{confirmed !== undefined && (
|
<ConfirmationEventRow status={status} />
|
||||||
<ConfirmationEventRow status={status} confirmed={confirmed} />
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getWrapperProps = () => {
|
const getWrapperProps = () => {
|
||||||
const propsMap = {
|
const propsMap = {
|
||||||
|
[EthTxStatus.Default]: {
|
||||||
|
title: '',
|
||||||
|
icon: null,
|
||||||
|
intent: undefined,
|
||||||
|
},
|
||||||
[EthTxStatus.Error]: {
|
[EthTxStatus.Error]: {
|
||||||
title: t(`${name} failed`),
|
title: t(`${name} failed`),
|
||||||
icon: <Icon name="warning-sign" size={20} />,
|
icon: <Icon name="warning-sign" size={20} />,
|
||||||
@ -92,56 +87,24 @@ export const TransactionDialog = ({
|
|||||||
intent: Intent.None,
|
intent: Intent.None,
|
||||||
},
|
},
|
||||||
[EthTxStatus.Complete]: {
|
[EthTxStatus.Complete]: {
|
||||||
|
title: t(`${name} pending`),
|
||||||
|
icon: <Loader size="small" />,
|
||||||
|
intent: Intent.None,
|
||||||
|
},
|
||||||
|
[EthTxStatus.Confirmed]: {
|
||||||
title: t(`${name} complete`),
|
title: t(`${name} complete`),
|
||||||
icon: <Icon name="tick" />,
|
icon: <Icon name="tick" />,
|
||||||
intent: Intent.Success,
|
intent: Intent.Success,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Dialog not showing
|
|
||||||
if (status === EthTxStatus.Default) {
|
|
||||||
return { intent: undefined, title: '', icon: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirmation event bool is required so
|
|
||||||
if (confirmed !== undefined) {
|
|
||||||
// Vega has confirmed Tx
|
|
||||||
if (confirmed === true) {
|
|
||||||
return propsMap[EthTxStatus.Complete];
|
|
||||||
}
|
|
||||||
// Tx is complete but still awaiting for Vega to confirm
|
|
||||||
else if (status === EthTxStatus.Complete) {
|
|
||||||
return propsMap[EthTxStatus.Pending];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return propsMap[status];
|
return propsMap[status];
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Close dialog if error is due to user rejecting the tx
|
|
||||||
if (status === EthTxStatus.Error && isExpectedEthereumError(error)) {
|
|
||||||
setDialogOpen(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status !== EthTxStatus.Default && !dialogDismissed.current) {
|
|
||||||
setDialogOpen(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}, [status, error]);
|
|
||||||
|
|
||||||
const { intent, ...wrapperProps } = getWrapperProps();
|
const { intent, ...wrapperProps } = getWrapperProps();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog open={transaction.dialogOpen} onChange={onChange} intent={intent}>
|
||||||
open={dialogOpen}
|
|
||||||
onChange={(isOpen) => {
|
|
||||||
setDialogOpen(isOpen);
|
|
||||||
dialogDismissed.current = true;
|
|
||||||
}}
|
|
||||||
intent={intent}
|
|
||||||
>
|
|
||||||
<DialogWrapper {...wrapperProps}>{renderContent()}</DialogWrapper>
|
<DialogWrapper {...wrapperProps}>{renderContent()}</DialogWrapper>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
@ -67,14 +67,19 @@ it('Ethereum transaction flow', async () => {
|
|||||||
error: null,
|
error: null,
|
||||||
confirmations: 0,
|
confirmations: 0,
|
||||||
receipt: null,
|
receipt: null,
|
||||||
|
dialogOpen: false,
|
||||||
},
|
},
|
||||||
|
Dialog: expect.any(Function),
|
||||||
|
setConfirmed: expect.any(Function),
|
||||||
perform: expect.any(Function),
|
perform: expect.any(Function),
|
||||||
reset: expect.any(Function),
|
reset: expect.any(Function),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
result.current.perform('asset-source', '100', 'vega-key');
|
result.current.perform('asset-source', '100', 'vega-key');
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.current.transaction.status).toEqual(EthTxStatus.Default); // still default as we await result of static call
|
expect(result.current.transaction.status).toEqual(EthTxStatus.Requested); // still default as we await result of static call
|
||||||
expect(result.current.transaction.confirmations).toBe(0);
|
expect(result.current.transaction.confirmations).toBe(0);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@ -105,7 +110,7 @@ it('Ethereum transaction flow', async () => {
|
|||||||
expect(result.current.transaction.confirmations).toBe(3);
|
expect(result.current.transaction.confirmations).toBe(3);
|
||||||
|
|
||||||
// Now complete as required confirmations have been surpassed
|
// Now complete as required confirmations have been surpassed
|
||||||
expect(result.current.transaction.status).toEqual(EthTxStatus.Complete);
|
expect(result.current.transaction.status).toEqual(EthTxStatus.Confirmed);
|
||||||
expect(result.current.transaction.receipt).toEqual({
|
expect(result.current.transaction.receipt).toEqual({
|
||||||
from: 'foo',
|
from: 'foo',
|
||||||
confirmations: result.current.transaction.confirmations,
|
confirmations: result.current.transaction.confirmations,
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
|
import { formatLabel } from '@vegaprotocol/react-helpers';
|
||||||
import type { ethers } from 'ethers';
|
import type { ethers } from 'ethers';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import type { EthereumError } from './ethereum-error';
|
import type { EthereumError } from './ethereum-error';
|
||||||
|
import { isExpectedEthereumError } from './ethereum-error';
|
||||||
import { isEthereumError } from './ethereum-error';
|
import { isEthereumError } from './ethereum-error';
|
||||||
|
import { TransactionDialog } from './transaction-dialog';
|
||||||
|
|
||||||
export enum EthTxStatus {
|
export enum EthTxStatus {
|
||||||
Default = 'Default',
|
Default = 'Default',
|
||||||
Requested = 'Requested',
|
Requested = 'Requested',
|
||||||
Pending = 'Pending',
|
Pending = 'Pending',
|
||||||
Complete = 'Complete',
|
Complete = 'Complete',
|
||||||
|
Confirmed = 'Confirmed',
|
||||||
Error = 'Error',
|
Error = 'Error',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,6 +23,7 @@ export interface EthTxState {
|
|||||||
txHash: string | null;
|
txHash: string | null;
|
||||||
receipt: ethers.ContractReceipt | null;
|
receipt: ethers.ContractReceipt | null;
|
||||||
confirmations: number;
|
confirmations: number;
|
||||||
|
dialogOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initialState = {
|
export const initialState = {
|
||||||
@ -27,6 +32,7 @@ export const initialState = {
|
|||||||
txHash: null,
|
txHash: null,
|
||||||
receipt: null,
|
receipt: null,
|
||||||
confirmations: 0,
|
confirmations: 0,
|
||||||
|
dialogOpen: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
type DefaultContract = {
|
type DefaultContract = {
|
||||||
@ -39,7 +45,8 @@ export const useEthereumTransaction = <
|
|||||||
>(
|
>(
|
||||||
contract: TContract | null,
|
contract: TContract | null,
|
||||||
methodName: keyof TContract,
|
methodName: keyof TContract,
|
||||||
requiredConfirmations = 1
|
requiredConfirmations = 1,
|
||||||
|
requiresConfirmation = false
|
||||||
) => {
|
) => {
|
||||||
const [transaction, _setTransaction] = useState<EthTxState>(initialState);
|
const [transaction, _setTransaction] = useState<EthTxState>(initialState);
|
||||||
|
|
||||||
@ -54,6 +61,13 @@ export const useEthereumTransaction = <
|
|||||||
// @ts-ignore TS errors here as TMethod doesn't satisfy the constraints on TContract
|
// @ts-ignore TS errors here as TMethod doesn't satisfy the constraints on TContract
|
||||||
// its a tricky one to fix but does enforce the correct types when calling perform
|
// its a tricky one to fix but does enforce the correct types when calling perform
|
||||||
async (...args: Parameters<TContract[TMethod]>) => {
|
async (...args: Parameters<TContract[TMethod]>) => {
|
||||||
|
setTransaction({
|
||||||
|
status: EthTxStatus.Requested,
|
||||||
|
error: null,
|
||||||
|
confirmations: 0,
|
||||||
|
dialogOpen: true,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
!contract ||
|
!contract ||
|
||||||
@ -72,12 +86,6 @@ export const useEthereumTransaction = <
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTransaction({
|
|
||||||
status: EthTxStatus.Requested,
|
|
||||||
error: null,
|
|
||||||
confirmations: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const method = contract[methodName];
|
const method = contract[methodName];
|
||||||
|
|
||||||
@ -104,10 +112,18 @@ export const useEthereumTransaction = <
|
|||||||
throw new Error('no receipt after confirmations are met');
|
throw new Error('no receipt after confirmations are met');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (requiresConfirmation) {
|
||||||
setTransaction({ status: EthTxStatus.Complete, receipt });
|
setTransaction({ status: EthTxStatus.Complete, receipt });
|
||||||
|
} else {
|
||||||
|
setTransaction({ status: EthTxStatus.Confirmed, receipt });
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error || isEthereumError(err)) {
|
if (err instanceof Error || isEthereumError(err)) {
|
||||||
|
if (isExpectedEthereumError(err)) {
|
||||||
|
setTransaction({ dialogOpen: false });
|
||||||
|
} else {
|
||||||
setTransaction({ status: EthTxStatus.Error, error: err });
|
setTransaction({ status: EthTxStatus.Error, error: err });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setTransaction({
|
setTransaction({
|
||||||
status: EthTxStatus.Error,
|
status: EthTxStatus.Error,
|
||||||
@ -116,12 +132,35 @@ export const useEthereumTransaction = <
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[contract, methodName, requiredConfirmations, setTransaction]
|
[
|
||||||
|
contract,
|
||||||
|
methodName,
|
||||||
|
requiredConfirmations,
|
||||||
|
requiresConfirmation,
|
||||||
|
setTransaction,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
setTransaction(initialState);
|
setTransaction(initialState);
|
||||||
}, [setTransaction]);
|
}, [setTransaction]);
|
||||||
|
|
||||||
return { perform, transaction, reset };
|
const setConfirmed = useCallback(() => {
|
||||||
|
setTransaction({ status: EthTxStatus.Confirmed });
|
||||||
|
}, [setTransaction]);
|
||||||
|
|
||||||
|
const Dialog = useMemo(() => {
|
||||||
|
return () => (
|
||||||
|
<TransactionDialog
|
||||||
|
name={formatLabel(methodName as string)}
|
||||||
|
onChange={() => {
|
||||||
|
reset();
|
||||||
|
}}
|
||||||
|
transaction={transaction}
|
||||||
|
requiredConfirmations={requiredConfirmations}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [methodName, transaction, requiredConfirmations, reset]);
|
||||||
|
|
||||||
|
return { perform, transaction, reset, setConfirmed, Dialog };
|
||||||
};
|
};
|
@ -25,7 +25,7 @@ export const useCompleteWithdraw = (isNewContract: boolean) => {
|
|||||||
const { query, cache } = useApolloClient();
|
const { query, cache } = useApolloClient();
|
||||||
const contract = useBridgeContract(isNewContract);
|
const contract = useBridgeContract(isNewContract);
|
||||||
const [id, setId] = useState('');
|
const [id, setId] = useState('');
|
||||||
const { transaction, perform } = useEthereumTransaction<
|
const { transaction, perform, Dialog } = useEthereumTransaction<
|
||||||
CollateralBridgeNew | CollateralBridge,
|
CollateralBridgeNew | CollateralBridge,
|
||||||
'withdraw_asset'
|
'withdraw_asset'
|
||||||
>(contract, 'withdraw_asset');
|
>(contract, 'withdraw_asset');
|
||||||
@ -91,5 +91,5 @@ export const useCompleteWithdraw = (isNewContract: boolean) => {
|
|||||||
}
|
}
|
||||||
}, [cache, transaction.txHash, id]);
|
}, [cache, transaction.txHash, id]);
|
||||||
|
|
||||||
return { transaction, submit, withdrawalId: id };
|
return { transaction, Dialog, submit, withdrawalId: id };
|
||||||
};
|
};
|
||||||
|
@ -120,6 +120,25 @@ const getProps = (
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const completeProps = {
|
||||||
|
title: t('Withdrawal complete'),
|
||||||
|
icon: <Icon name="tick" />,
|
||||||
|
intent: Intent.Success,
|
||||||
|
children: (
|
||||||
|
<Step>
|
||||||
|
<span>{t('Ethereum transaction complete')}</span>
|
||||||
|
<Link
|
||||||
|
href={`${ethUrl}/tx/${ethTx.txHash}`}
|
||||||
|
title={t('View transaction on Etherscan')}
|
||||||
|
className="text-vega-pink dark:text-vega-yellow"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{t('View on Etherscan')}
|
||||||
|
</Link>
|
||||||
|
</Step>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
const ethTxPropsMap: Record<EthTxStatus, DialogProps> = {
|
const ethTxPropsMap: Record<EthTxStatus, DialogProps> = {
|
||||||
[EthTxStatus.Default]: {
|
[EthTxStatus.Default]: {
|
||||||
title: '',
|
title: '',
|
||||||
@ -169,24 +188,8 @@ const getProps = (
|
|||||||
</Step>
|
</Step>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
[EthTxStatus.Complete]: {
|
[EthTxStatus.Complete]: completeProps,
|
||||||
title: t('Withdrawal complete'),
|
[EthTxStatus.Confirmed]: completeProps,
|
||||||
icon: <Icon name="tick" />,
|
|
||||||
intent: Intent.Success,
|
|
||||||
children: (
|
|
||||||
<Step>
|
|
||||||
<span>{t('Ethereum transaction complete')}</span>
|
|
||||||
<Link
|
|
||||||
href={`${ethUrl}/tx/${ethTx.txHash}`}
|
|
||||||
title={t('View transaction on Etherscan')}
|
|
||||||
className="text-vega-pink dark:text-vega-yellow"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{t('View on Etherscan')}
|
|
||||||
</Link>
|
|
||||||
</Step>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return approval ? ethTxPropsMap[ethTx.status] : vegaTxPropsMap[vegaTx.status];
|
return approval ? ethTxPropsMap[ethTx.status] : vegaTxPropsMap[vegaTx.status];
|
||||||
|
@ -12,7 +12,6 @@ import {
|
|||||||
import { WithdrawalStatus } from '@vegaprotocol/types';
|
import { WithdrawalStatus } from '@vegaprotocol/types';
|
||||||
import { Link, AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
import { Link, AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||||
import { useEnvironment } from '@vegaprotocol/environment';
|
import { useEnvironment } from '@vegaprotocol/environment';
|
||||||
import { TransactionDialog } from '@vegaprotocol/web3';
|
|
||||||
import { useCompleteWithdraw } from './use-complete-withdraw';
|
import { useCompleteWithdraw } from './use-complete-withdraw';
|
||||||
import type { Withdrawals_party_withdrawals } from './__generated__/Withdrawals';
|
import type { Withdrawals_party_withdrawals } from './__generated__/Withdrawals';
|
||||||
|
|
||||||
@ -22,7 +21,7 @@ export interface WithdrawalsTableProps {
|
|||||||
|
|
||||||
export const WithdrawalsTable = ({ withdrawals }: WithdrawalsTableProps) => {
|
export const WithdrawalsTable = ({ withdrawals }: WithdrawalsTableProps) => {
|
||||||
const { ETHERSCAN_URL } = useEnvironment();
|
const { ETHERSCAN_URL } = useEnvironment();
|
||||||
const { transaction, submit } = useCompleteWithdraw(true);
|
const { submit, Dialog } = useCompleteWithdraw(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -65,7 +64,7 @@ export const WithdrawalsTable = ({ withdrawals }: WithdrawalsTableProps) => {
|
|||||||
cellRendererParams={{ complete: submit, ethUrl: ETHERSCAN_URL }}
|
cellRendererParams={{ complete: submit, ethUrl: ETHERSCAN_URL }}
|
||||||
/>
|
/>
|
||||||
</AgGrid>
|
</AgGrid>
|
||||||
<TransactionDialog name="withdraw" {...transaction} />
|
<Dialog />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user