feat(deposits): deposit flow (#3062)
This commit is contained in:
parent
a8eef1cb53
commit
adca4600c2
@ -41,6 +41,7 @@ const completeWithdrawalBtn = 'complete-withdrawal';
|
|||||||
const submitTransferBtn = '[type="submit"]';
|
const submitTransferBtn = '[type="submit"]';
|
||||||
const transferForm = 'transfer-form';
|
const transferForm = 'transfer-form';
|
||||||
const depositSubmit = 'deposit-submit';
|
const depositSubmit = 'deposit-submit';
|
||||||
|
const approveSubmit = 'approve-submit';
|
||||||
|
|
||||||
// Because the tests are run on a live network to optimize time, the tests are interdependent and must be run in the given order.
|
// Because the tests are run on a live network to optimize time, the tests are interdependent and must be run in the given order.
|
||||||
describe('capsule - without MultiSign', { tags: '@slow' }, () => {
|
describe('capsule - without MultiSign', { tags: '@slow' }, () => {
|
||||||
@ -73,13 +74,13 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
|
|||||||
cy.getByTestId('deposit-button').click();
|
cy.getByTestId('deposit-button').click();
|
||||||
connectEthereumWallet('Unknown');
|
connectEthereumWallet('Unknown');
|
||||||
cy.get(assetSelectField, txTimeout).select(btcName, { force: true });
|
cy.get(assetSelectField, txTimeout).select(btcName, { force: true });
|
||||||
cy.getByTestId('approve-warning').should(
|
cy.getByTestId('approve-default').should(
|
||||||
'contain.text',
|
'contain.text',
|
||||||
`Deposits of ${btcSymbol} not approved`
|
`Before you can make a deposit of your chosen asset, ${btcSymbol}, you need to approve its use in your Ethereum wallet`
|
||||||
);
|
);
|
||||||
cy.getByTestId(depositSubmit).click();
|
cy.getByTestId(approveSubmit).click();
|
||||||
cy.getByTestId('dialog-title').should('contain.text', 'Approve complete');
|
cy.getByTestId('approve-pending').should('exist');
|
||||||
cy.get('[data-testid="Return to deposit"]').click();
|
cy.getByTestId('approve-confirmed').should('exist');
|
||||||
cy.get(amountField).clear().type('10');
|
cy.get(amountField).clear().type('10');
|
||||||
cy.getByTestId(depositSubmit).click();
|
cy.getByTestId(depositSubmit).click();
|
||||||
cy.getByTestId(toastContent, txTimeout).should(
|
cy.getByTestId(toastContent, txTimeout).should(
|
||||||
|
@ -31,7 +31,13 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
|
|||||||
it('handles empty fields', () => {
|
it('handles empty fields', () => {
|
||||||
cy.getByTestId('deposit-submit').click();
|
cy.getByTestId('deposit-submit').click();
|
||||||
cy.getByTestId(formFieldError).should('contain.text', 'Required');
|
cy.getByTestId(formFieldError).should('contain.text', 'Required');
|
||||||
cy.getByTestId(formFieldError).should('have.length', 2);
|
// once Ethereum wallet is connected and key selected the only field that will
|
||||||
|
// error is the asset select
|
||||||
|
cy.getByTestId(formFieldError).should('have.length', 1);
|
||||||
|
cy.get('[data-testid="input-error-text"][aria-describedby="asset"]').should(
|
||||||
|
'have.length',
|
||||||
|
1
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('unable to select assets not enabled', () => {
|
it('unable to select assets not enabled', () => {
|
||||||
@ -41,12 +47,13 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
|
|||||||
cy.get(assetSelectField + ' option:contains(Asset 4)').should('not.exist');
|
cy.get(assetSelectField + ' option:contains(Asset 4)').should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('invalid public key', () => {
|
it('invalid public key when entering address manually', () => {
|
||||||
cy.get(toAddressField)
|
cy.getByTestId('enter-pubkey-manually').click();
|
||||||
.clear()
|
cy.get(toAddressField).clear().type('INVALID_DEPOSIT_TO_ADDRESS');
|
||||||
.type('INVALID_DEPOSIT_TO_ADDRESS')
|
cy.get(`[data-testid="${formFieldError}"][aria-describedby="to"]`).should(
|
||||||
.next(`[data-testid="${formFieldError}"]`)
|
'have.text',
|
||||||
.should('have.text', 'Invalid Vega key');
|
'Invalid Vega key'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('invalid amount', () => {
|
it('invalid amount', () => {
|
||||||
|
@ -151,7 +151,7 @@ describe('ethereum wallet', { tags: '@smoke' }, () => {
|
|||||||
cy.getByTestId('Deposits').click();
|
cy.getByTestId('Deposits').click();
|
||||||
cy.getByTestId('deposit-button').click();
|
cy.getByTestId('deposit-button').click();
|
||||||
connectEthereumWallet('MetaMask');
|
connectEthereumWallet('MetaMask');
|
||||||
cy.get('#ethereum-address').should('have.value', ethWalletAddress);
|
cy.getByTestId('ethereum-address').should('have.text', ethWalletAddress);
|
||||||
cy.getByTestId('disconnect-ethereum-wallet')
|
cy.getByTestId('disconnect-ethereum-wallet')
|
||||||
.should('have.text', 'Disconnect')
|
.should('have.text', 'Disconnect')
|
||||||
.click();
|
.click();
|
||||||
|
@ -177,22 +177,14 @@ export const useEthereumTransactionToasts = () => {
|
|||||||
store.remove,
|
store.remove,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [dismissTx, deleteTx] = useEthTransactionStore((state) => [
|
const dismissTx = useEthTransactionStore((state) => state.dismiss);
|
||||||
state.dismiss,
|
|
||||||
state.delete,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const onClose = useCallback(
|
const onClose = useCallback(
|
||||||
(tx: EthStoredTxState) => () => {
|
(tx: EthStoredTxState) => () => {
|
||||||
const safeToDelete = isFinal(tx);
|
dismissTx(tx.id);
|
||||||
if (safeToDelete) {
|
|
||||||
deleteTx(tx.id);
|
|
||||||
} else {
|
|
||||||
dismissTx(tx.id);
|
|
||||||
}
|
|
||||||
removeToast(`eth-${tx.id}`);
|
removeToast(`eth-${tx.id}`);
|
||||||
},
|
},
|
||||||
[deleteTx, dismissTx, removeToast]
|
[dismissTx, removeToast]
|
||||||
);
|
);
|
||||||
|
|
||||||
const fromEthTransaction = useCallback(
|
const fromEthTransaction = useCallback(
|
||||||
|
@ -22,7 +22,11 @@ export const ExpirySelector = ({
|
|||||||
const dateFormatted = formatForInput(date);
|
const dateFormatted = formatForInput(date);
|
||||||
const minDate = formatForInput(date);
|
const minDate = formatForInput(date);
|
||||||
return (
|
return (
|
||||||
<FormGroup label={t('Expiry time/date')} labelFor="expiration">
|
<FormGroup
|
||||||
|
label={t('Expiry time/date')}
|
||||||
|
labelFor="expiration"
|
||||||
|
compact={true}
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
data-testid="date-picker-field"
|
data-testid="date-picker-field"
|
||||||
id="expiration"
|
id="expiration"
|
||||||
|
@ -26,7 +26,11 @@ export const SideSelector = ({ value, onSelect }: SideSelectorProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup label={t('Direction')} labelFor="order-side-toggle">
|
<FormGroup
|
||||||
|
label={t('Direction')}
|
||||||
|
labelFor="order-side-toggle"
|
||||||
|
compact={true}
|
||||||
|
>
|
||||||
<Toggle
|
<Toggle
|
||||||
id="order-side-toggle"
|
id="order-side-toggle"
|
||||||
name="order-side"
|
name="order-side"
|
||||||
|
@ -119,7 +119,11 @@ export const TimeInForceSelector = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup label={t('Time in force')} labelFor="select-time-in-force">
|
<FormGroup
|
||||||
|
label={t('Time in force')}
|
||||||
|
labelFor="select-time-in-force"
|
||||||
|
compact={true}
|
||||||
|
>
|
||||||
<Select
|
<Select
|
||||||
id="select-time-in-force"
|
id="select-time-in-force"
|
||||||
value={value}
|
value={value}
|
||||||
|
@ -74,7 +74,7 @@ export const TypeSelector = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup label={t('Order type')} labelFor="order-type">
|
<FormGroup label={t('Order type')} labelFor="order-type" compact={true}>
|
||||||
<Toggle
|
<Toggle
|
||||||
id="order-type"
|
id="order-type"
|
||||||
name="order-type"
|
name="order-type"
|
||||||
|
215
libs/deposits/src/lib/approve-notification.tsx
Normal file
215
libs/deposits/src/lib/approve-notification.tsx
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
import type { Asset } from '@vegaprotocol/assets';
|
||||||
|
import { useEnvironment } from '@vegaprotocol/environment';
|
||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { ExternalLink, Intent, Notification } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { formatNumber } from '@vegaprotocol/utils';
|
||||||
|
import type { EthStoredTxState } from '@vegaprotocol/web3';
|
||||||
|
import { EthTxStatus, useEthTransactionStore } from '@vegaprotocol/web3';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import type { DepositBalances } from './use-deposit-balances';
|
||||||
|
|
||||||
|
interface ApproveNotificationProps {
|
||||||
|
isActive: boolean;
|
||||||
|
selectedAsset?: Asset;
|
||||||
|
onApprove: () => void;
|
||||||
|
approved: boolean;
|
||||||
|
balances: DepositBalances | null;
|
||||||
|
amount: string;
|
||||||
|
approveTxId: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ApproveNotification = ({
|
||||||
|
isActive,
|
||||||
|
selectedAsset,
|
||||||
|
onApprove,
|
||||||
|
amount,
|
||||||
|
balances,
|
||||||
|
approved,
|
||||||
|
approveTxId,
|
||||||
|
}: ApproveNotificationProps) => {
|
||||||
|
const tx = useEthTransactionStore((state) => {
|
||||||
|
return state.transactions.find((t) => t?.id === approveTxId);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isActive) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedAsset) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!balances) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const approvePrompt = (
|
||||||
|
<div className="mb-4">
|
||||||
|
<Notification
|
||||||
|
intent={Intent.Warning}
|
||||||
|
testId="approve-default"
|
||||||
|
message={t(
|
||||||
|
`Before you can make a deposit of your chosen asset, ${selectedAsset?.symbol}, you need to approve its use in your Ethereum wallet`
|
||||||
|
)}
|
||||||
|
buttonProps={{
|
||||||
|
size: 'sm',
|
||||||
|
text: `Approve ${selectedAsset?.symbol}`,
|
||||||
|
action: onApprove,
|
||||||
|
dataTestId: 'approve-submit',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
const reApprovePrompt = (
|
||||||
|
<div className="mb-4">
|
||||||
|
<Notification
|
||||||
|
intent={Intent.Warning}
|
||||||
|
testId="reapprove-default"
|
||||||
|
message={t(
|
||||||
|
`Approve again to deposit more than ${formatNumber(
|
||||||
|
balances.allowance.toString()
|
||||||
|
)}`
|
||||||
|
)}
|
||||||
|
buttonProps={{
|
||||||
|
size: 'sm',
|
||||||
|
text: `Approve ${selectedAsset?.symbol}`,
|
||||||
|
action: onApprove,
|
||||||
|
dataTestId: 'reapprove-submit',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
const approvalFeedback = (
|
||||||
|
<ApprovalTxFeedback
|
||||||
|
tx={tx}
|
||||||
|
selectedAsset={selectedAsset}
|
||||||
|
allowance={balances.allowance}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// always show requested and pending states
|
||||||
|
if (
|
||||||
|
tx &&
|
||||||
|
[EthTxStatus.Requested, EthTxStatus.Pending, EthTxStatus.Complete].includes(
|
||||||
|
tx.status
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return approvalFeedback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!approved) {
|
||||||
|
return approvePrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new BigNumber(amount).isGreaterThan(balances.allowance)) {
|
||||||
|
return reApprovePrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
tx &&
|
||||||
|
tx.status === EthTxStatus.Error &&
|
||||||
|
// @ts-ignore tx.error not typed correctly
|
||||||
|
tx.error.code === 'ACTION_REJECTED'
|
||||||
|
) {
|
||||||
|
return approvePrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return approvalFeedback;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ApprovalTxFeedback = ({
|
||||||
|
tx,
|
||||||
|
selectedAsset,
|
||||||
|
allowance,
|
||||||
|
}: {
|
||||||
|
tx: EthStoredTxState | undefined;
|
||||||
|
selectedAsset: Asset;
|
||||||
|
allowance?: BigNumber;
|
||||||
|
}) => {
|
||||||
|
const { ETHERSCAN_URL } = useEnvironment();
|
||||||
|
|
||||||
|
if (!tx) return null;
|
||||||
|
|
||||||
|
const txLink = tx.txHash && (
|
||||||
|
<ExternalLink href={`${ETHERSCAN_URL}/tx/${tx.txHash}`}>
|
||||||
|
{t('View on Etherscan')}
|
||||||
|
</ExternalLink>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tx.status === EthTxStatus.Error) {
|
||||||
|
return (
|
||||||
|
<div className="mb-4">
|
||||||
|
<Notification
|
||||||
|
intent={Intent.Danger}
|
||||||
|
testId="approve-error"
|
||||||
|
message={
|
||||||
|
<p>
|
||||||
|
{t('Approval failed')} {txLink}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tx.status === EthTxStatus.Requested) {
|
||||||
|
return (
|
||||||
|
<div className="mb-4">
|
||||||
|
<Notification
|
||||||
|
intent={Intent.Warning}
|
||||||
|
testId="approve-requested"
|
||||||
|
message={t(
|
||||||
|
`Go to your Ethereum wallet and approve the transaction to enable the use of ${selectedAsset?.symbol}`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tx.status === EthTxStatus.Pending) {
|
||||||
|
return (
|
||||||
|
<div className="mb-4">
|
||||||
|
<Notification
|
||||||
|
intent={Intent.Primary}
|
||||||
|
testId="approve-pending"
|
||||||
|
message={
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
{t(
|
||||||
|
`Your ${selectedAsset?.symbol} is being confirmed by the Ethereum network. When this is complete, you can continue your deposit`
|
||||||
|
)}{' '}
|
||||||
|
</p>
|
||||||
|
{txLink && <p>{txLink}</p>}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tx.status === EthTxStatus.Confirmed) {
|
||||||
|
return (
|
||||||
|
<div className="mb-4">
|
||||||
|
<Notification
|
||||||
|
intent={Intent.Success}
|
||||||
|
testId="approve-confirmed"
|
||||||
|
message={
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
{t(
|
||||||
|
`You can now make deposits in ${
|
||||||
|
selectedAsset?.symbol
|
||||||
|
}, up to a maximum of ${formatNumber(
|
||||||
|
allowance?.toString() || 0
|
||||||
|
)}`
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
{txLink && <p>{txLink}</p>}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
@ -4,18 +4,11 @@ import { DepositManager } from './deposit-manager';
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||||
import { enabledAssetsProvider } from '@vegaprotocol/assets';
|
import { enabledAssetsProvider } from '@vegaprotocol/assets';
|
||||||
import type { DepositDialogStylePropsSetter } from './deposit-dialog';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches data required for the Deposit page
|
* Fetches data required for the Deposit page
|
||||||
*/
|
*/
|
||||||
export const DepositContainer = ({
|
export const DepositContainer = ({ assetId }: { assetId?: string }) => {
|
||||||
assetId,
|
|
||||||
setDialogStyleProps,
|
|
||||||
}: {
|
|
||||||
assetId?: string;
|
|
||||||
setDialogStyleProps?: DepositDialogStylePropsSetter;
|
|
||||||
}) => {
|
|
||||||
const { VEGA_ENV } = useEnvironment();
|
const { VEGA_ENV } = useEnvironment();
|
||||||
const { data, loading, error } = useDataProvider({
|
const { data, loading, error } = useDataProvider({
|
||||||
dataProvider: enabledAssetsProvider,
|
dataProvider: enabledAssetsProvider,
|
||||||
@ -28,7 +21,6 @@ export const DepositContainer = ({
|
|||||||
assetId={assetId}
|
assetId={assetId}
|
||||||
assets={data}
|
assets={data}
|
||||||
isFaucetable={VEGA_ENV !== Networks.MAINNET}
|
isFaucetable={VEGA_ENV !== Networks.MAINNET}
|
||||||
setDialogStyleProps={setDialogStyleProps}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Splash>
|
<Splash>
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import type { Intent } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { Dialog } from '@vegaprotocol/ui-toolkit';
|
import { Dialog } from '@vegaprotocol/ui-toolkit';
|
||||||
import { useCallback, useState } from 'react';
|
|
||||||
import { DepositContainer } from './deposit-container';
|
import { DepositContainer } from './deposit-container';
|
||||||
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
|
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
|
||||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||||
@ -24,22 +22,6 @@ export const useDepositDialog = create<State & Actions>((set) => ({
|
|||||||
close: () => set(() => ({ assetId: undefined, isOpen: false })),
|
close: () => set(() => ({ assetId: undefined, isOpen: false })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export type DepositDialogStyleProps = {
|
|
||||||
title: string;
|
|
||||||
icon?: JSX.Element;
|
|
||||||
intent?: Intent;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DepositDialogStylePropsSetter = (
|
|
||||||
props?: DepositDialogStyleProps
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
const DEFAULT_STYLE: DepositDialogStyleProps = {
|
|
||||||
title: t('Deposit'),
|
|
||||||
intent: undefined,
|
|
||||||
icon: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DepositDialog = () => {
|
export const DepositDialog = () => {
|
||||||
const { assetId, isOpen, open, close } = useDepositDialog();
|
const { assetId, isOpen, open, close } = useDepositDialog();
|
||||||
const assetDetailsDialogOpen = useAssetDetailsDialogStore(
|
const assetDetailsDialogOpen = useAssetDetailsDialogStore(
|
||||||
@ -48,25 +30,13 @@ export const DepositDialog = () => {
|
|||||||
const connectWalletDialogIsOpen = useWeb3ConnectStore(
|
const connectWalletDialogIsOpen = useWeb3ConnectStore(
|
||||||
(state) => state.isOpen
|
(state) => state.isOpen
|
||||||
);
|
);
|
||||||
const [dialogStyleProps, _setDialogStyleProps] = useState(DEFAULT_STYLE);
|
|
||||||
const setDialogStyleProps: DepositDialogStylePropsSetter =
|
|
||||||
useCallback<DepositDialogStylePropsSetter>(
|
|
||||||
(props) =>
|
|
||||||
props
|
|
||||||
? _setDialogStyleProps(props)
|
|
||||||
: _setDialogStyleProps(DEFAULT_STYLE),
|
|
||||||
[_setDialogStyleProps]
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={isOpen && !(connectWalletDialogIsOpen || assetDetailsDialogOpen)}
|
open={isOpen && !(connectWalletDialogIsOpen || assetDetailsDialogOpen)}
|
||||||
onChange={(isOpen) => (isOpen ? open() : close())}
|
onChange={(isOpen) => (isOpen ? open() : close())}
|
||||||
{...dialogStyleProps}
|
title={t('Deposit')}
|
||||||
>
|
>
|
||||||
<DepositContainer
|
<DepositContainer assetId={assetId} />
|
||||||
assetId={assetId}
|
|
||||||
setDialogStyleProps={setDialogStyleProps}
|
|
||||||
/>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -7,6 +7,7 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
|
|||||||
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
|
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
|
||||||
import { useWeb3React } from '@web3-react/core';
|
import { useWeb3React } from '@web3-react/core';
|
||||||
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||||
|
import type { DepositBalances } from './use-deposit-balances';
|
||||||
|
|
||||||
jest.mock('@vegaprotocol/wallet');
|
jest.mock('@vegaprotocol/wallet');
|
||||||
jest.mock('@vegaprotocol/web3');
|
jest.mock('@vegaprotocol/web3');
|
||||||
@ -38,27 +39,34 @@ function generateAsset(): AssetFieldsFragment {
|
|||||||
|
|
||||||
let asset: AssetFieldsFragment;
|
let asset: AssetFieldsFragment;
|
||||||
let props: DepositFormProps;
|
let props: DepositFormProps;
|
||||||
|
let balances: DepositBalances;
|
||||||
const MOCK_ETH_ADDRESS = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
|
const MOCK_ETH_ADDRESS = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
|
||||||
const MOCK_VEGA_KEY =
|
const MOCK_VEGA_KEY =
|
||||||
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
|
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
asset = generateAsset();
|
asset = generateAsset();
|
||||||
|
balances = {
|
||||||
|
balance: new BigNumber(5),
|
||||||
|
max: new BigNumber(20),
|
||||||
|
allowance: new BigNumber(30),
|
||||||
|
deposited: new BigNumber(10),
|
||||||
|
};
|
||||||
props = {
|
props = {
|
||||||
assets: [asset],
|
assets: [asset],
|
||||||
selectedAsset: undefined,
|
selectedAsset: undefined,
|
||||||
onSelectAsset: jest.fn(),
|
onSelectAsset: jest.fn(),
|
||||||
balance: new BigNumber(5),
|
balances,
|
||||||
submitApprove: jest.fn(),
|
submitApprove: jest.fn(),
|
||||||
submitDeposit: jest.fn(),
|
submitDeposit: jest.fn(),
|
||||||
requestFaucet: jest.fn(),
|
submitFaucet: jest.fn(),
|
||||||
max: new BigNumber(20),
|
onDisconnect: jest.fn(),
|
||||||
deposited: new BigNumber(10),
|
approveTxId: null,
|
||||||
allowance: new BigNumber(30),
|
faucetTxId: null,
|
||||||
isFaucetable: true,
|
isFaucetable: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
(useVegaWallet as jest.Mock).mockReturnValue({ pubKey: null });
|
(useVegaWallet as jest.Mock).mockReturnValue({ pubKey: null, pubKeys: [] });
|
||||||
(useWeb3React as jest.Mock).mockReturnValue({
|
(useWeb3React as jest.Mock).mockReturnValue({
|
||||||
isActive: true,
|
isActive: true,
|
||||||
account: MOCK_ETH_ADDRESS,
|
account: MOCK_ETH_ADDRESS,
|
||||||
@ -71,7 +79,8 @@ describe('Deposit form', () => {
|
|||||||
render(<DepositForm {...props} />);
|
render(<DepositForm {...props} />);
|
||||||
|
|
||||||
// Assert default values (including) from/to provided by useVegaWallet and useWeb3React
|
// Assert default values (including) from/to provided by useVegaWallet and useWeb3React
|
||||||
expect(screen.getByLabelText('From (Ethereum address)')).toHaveValue(
|
expect(screen.getByText('From (Ethereum address)')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('ethereum-address')).toHaveTextContent(
|
||||||
MOCK_ETH_ADDRESS
|
MOCK_ETH_ADDRESS
|
||||||
);
|
);
|
||||||
expect(screen.getByLabelText('Asset')).toHaveValue('');
|
expect(screen.getByLabelText('Asset')).toHaveValue('');
|
||||||
@ -145,7 +154,7 @@ describe('Deposit form', () => {
|
|||||||
fireEvent.submit(screen.getByTestId('deposit-form'));
|
fireEvent.submit(screen.getByTestId('deposit-form'));
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await screen.findByText('Amount is above deposit limit')
|
await screen.findByText('Amount is above lifetime deposit limit')
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -153,9 +162,12 @@ describe('Deposit form', () => {
|
|||||||
render(
|
render(
|
||||||
<DepositForm
|
<DepositForm
|
||||||
{...props}
|
{...props}
|
||||||
balance={new BigNumber(100)}
|
balances={{
|
||||||
max={new BigNumber(100)}
|
...balances,
|
||||||
deposited={new BigNumber(10)}
|
balance: BigNumber(100),
|
||||||
|
max: new BigNumber(100),
|
||||||
|
deposited: new BigNumber(10),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -166,7 +178,7 @@ describe('Deposit form', () => {
|
|||||||
fireEvent.submit(screen.getByTestId('deposit-form'));
|
fireEvent.submit(screen.getByTestId('deposit-form'));
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await screen.findByText('Amount is above approved amount.')
|
await screen.findByText('Amount is above approved amount')
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -214,14 +226,17 @@ describe('Deposit form', () => {
|
|||||||
render(
|
render(
|
||||||
<DepositForm
|
<DepositForm
|
||||||
{...props}
|
{...props}
|
||||||
allowance={new BigNumber(0)}
|
balances={{
|
||||||
|
...balances,
|
||||||
|
allowance: new BigNumber(0),
|
||||||
|
}}
|
||||||
selectedAsset={asset}
|
selectedAsset={asset}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.queryByLabelText('Amount')).not.toBeInTheDocument();
|
expect(screen.queryByLabelText('Amount')).not.toBeInTheDocument();
|
||||||
expect(screen.getByTestId('approve-warning')).toHaveTextContent(
|
expect(screen.getByTestId('approve-default')).toHaveTextContent(
|
||||||
`Deposits of ${asset.symbol} not approved`
|
`Before you can make a deposit of your chosen asset, ${asset.symbol}, you need to approve its use in your Ethereum wallet`
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
@ -253,10 +268,12 @@ describe('Deposit form', () => {
|
|||||||
render(
|
render(
|
||||||
<DepositForm
|
<DepositForm
|
||||||
{...props}
|
{...props}
|
||||||
allowance={new BigNumber(100)}
|
balances={{
|
||||||
balance={balance}
|
allowance: new BigNumber(100),
|
||||||
max={max}
|
balance,
|
||||||
deposited={deposited}
|
max,
|
||||||
|
deposited,
|
||||||
|
}}
|
||||||
selectedAsset={asset}
|
selectedAsset={asset}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -320,10 +337,10 @@ describe('Deposit form', () => {
|
|||||||
expect(
|
expect(
|
||||||
screen.queryByRole('button', { name: 'Connect' })
|
screen.queryByRole('button', { name: 'Connect' })
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
const fromInput = screen.getByLabelText('From (Ethereum address)');
|
expect(screen.getByText('From (Ethereum address)')).toBeInTheDocument();
|
||||||
expect(fromInput).toHaveValue(MOCK_ETH_ADDRESS);
|
expect(screen.getByTestId('ethereum-address')).toHaveTextContent(
|
||||||
expect(fromInput).toBeDisabled();
|
MOCK_ETH_ADDRESS
|
||||||
expect(fromInput).toHaveAttribute('readonly');
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('prevents submission if you are on the wrong chain', () => {
|
it('prevents submission if you are on the wrong chain', () => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { Asset } from '@vegaprotocol/assets';
|
import type { Asset, AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||||
import { AssetOption } from '@vegaprotocol/assets';
|
import { AssetOption } from '@vegaprotocol/assets';
|
||||||
import {
|
import {
|
||||||
ethereumAddress,
|
ethereumAddress,
|
||||||
@ -19,13 +19,16 @@ import {
|
|||||||
RichSelect,
|
RichSelect,
|
||||||
Notification,
|
Notification,
|
||||||
Intent,
|
Intent,
|
||||||
|
ButtonLink,
|
||||||
|
Select,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
import { useWeb3React } from '@web3-react/core';
|
import { useWeb3React } from '@web3-react/core';
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
import type { ButtonHTMLAttributes } from 'react';
|
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||||
|
import { useState } from 'react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import type { FieldError } from 'react-hook-form';
|
import { useWatch } from 'react-hook-form';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { DepositLimits } from './deposit-limits';
|
import { DepositLimits } from './deposit-limits';
|
||||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||||
@ -34,6 +37,9 @@ import {
|
|||||||
useWeb3ConnectStore,
|
useWeb3ConnectStore,
|
||||||
getChainName,
|
getChainName,
|
||||||
} from '@vegaprotocol/web3';
|
} from '@vegaprotocol/web3';
|
||||||
|
import type { DepositBalances } from './use-deposit-balances';
|
||||||
|
import { FaucetNotification } from './faucet-notification';
|
||||||
|
import { ApproveNotification } from './approve-notification';
|
||||||
|
|
||||||
interface FormFields {
|
interface FormFields {
|
||||||
asset: string;
|
asset: string;
|
||||||
@ -45,38 +51,38 @@ interface FormFields {
|
|||||||
export interface DepositFormProps {
|
export interface DepositFormProps {
|
||||||
assets: Asset[];
|
assets: Asset[];
|
||||||
selectedAsset?: Asset;
|
selectedAsset?: Asset;
|
||||||
|
balances: DepositBalances | null;
|
||||||
onSelectAsset: (assetId: string) => void;
|
onSelectAsset: (assetId: string) => void;
|
||||||
balance: BigNumber | undefined;
|
onDisconnect: () => void;
|
||||||
submitApprove: () => void;
|
submitApprove: () => void;
|
||||||
|
approveTxId: number | null;
|
||||||
|
submitFaucet: () => void;
|
||||||
|
faucetTxId: number | null;
|
||||||
submitDeposit: (args: {
|
submitDeposit: (args: {
|
||||||
assetSource: string;
|
assetSource: string;
|
||||||
amount: string;
|
amount: string;
|
||||||
vegaPublicKey: string;
|
vegaPublicKey: string;
|
||||||
}) => void;
|
}) => void;
|
||||||
requestFaucet: () => void;
|
|
||||||
max: BigNumber | undefined;
|
|
||||||
deposited: BigNumber | undefined;
|
|
||||||
allowance: BigNumber | undefined;
|
|
||||||
isFaucetable?: boolean;
|
isFaucetable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DepositForm = ({
|
export const DepositForm = ({
|
||||||
assets,
|
assets,
|
||||||
selectedAsset,
|
selectedAsset,
|
||||||
|
balances,
|
||||||
onSelectAsset,
|
onSelectAsset,
|
||||||
balance,
|
onDisconnect,
|
||||||
max,
|
|
||||||
deposited,
|
|
||||||
submitApprove,
|
submitApprove,
|
||||||
submitDeposit,
|
submitDeposit,
|
||||||
requestFaucet,
|
submitFaucet,
|
||||||
allowance,
|
faucetTxId,
|
||||||
|
approveTxId,
|
||||||
isFaucetable,
|
isFaucetable,
|
||||||
}: DepositFormProps) => {
|
}: DepositFormProps) => {
|
||||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||||
const openDialog = useWeb3ConnectStore((store) => store.open);
|
const openDialog = useWeb3ConnectStore((store) => store.open);
|
||||||
const { isActive, account } = useWeb3React();
|
const { isActive, account } = useWeb3React();
|
||||||
const { pubKey } = useVegaWallet();
|
const { pubKey, pubKeys: _pubKeys } = useVegaWallet();
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@ -91,43 +97,21 @@ export const DepositForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const amount = useWatch({ name: 'amount', control });
|
||||||
|
|
||||||
const onSubmit = async (fields: FormFields) => {
|
const onSubmit = async (fields: FormFields) => {
|
||||||
if (!selectedAsset || selectedAsset.source.__typename !== 'ERC20') {
|
if (!selectedAsset || selectedAsset.source.__typename !== 'ERC20') {
|
||||||
throw new Error('Invalid asset');
|
throw new Error('Invalid asset');
|
||||||
}
|
}
|
||||||
|
if (!approved) throw new Error('Deposits not approved');
|
||||||
|
|
||||||
if (approved) {
|
submitDeposit({
|
||||||
submitDeposit({
|
assetSource: selectedAsset.source.contractAddress,
|
||||||
assetSource: selectedAsset.source.contractAddress,
|
amount: fields.amount,
|
||||||
amount: fields.amount,
|
vegaPublicKey: fields.to,
|
||||||
vegaPublicKey: fields.to,
|
});
|
||||||
});
|
|
||||||
} else {
|
|
||||||
submitApprove();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const maxAmount = useMemo(() => {
|
|
||||||
const maxApproved = allowance ? allowance : new BigNumber(0);
|
|
||||||
const maxAvailable = balance ? balance : new BigNumber(0);
|
|
||||||
|
|
||||||
// limits.max is a lifetime deposit limit, so the actual max value for form
|
|
||||||
// input is the max minus whats already been deposited
|
|
||||||
let maxLimit = new BigNumber(Infinity);
|
|
||||||
|
|
||||||
// A max limit of zero indicates that there is no limit
|
|
||||||
if (max && deposited && max.isGreaterThan(0)) {
|
|
||||||
maxLimit = max.minus(deposited);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
approved: maxApproved,
|
|
||||||
available: maxAvailable,
|
|
||||||
limit: maxLimit,
|
|
||||||
amount: BigNumber.minimum(maxLimit, maxApproved, maxAvailable),
|
|
||||||
};
|
|
||||||
}, [max, deposited, allowance, balance]);
|
|
||||||
|
|
||||||
const min = useMemo(() => {
|
const min = useMemo(() => {
|
||||||
// Min viable amount given asset decimals EG for WEI 0.000000000000000001
|
// Min viable amount given asset decimals EG for WEI 0.000000000000000001
|
||||||
const minViableAmount = selectedAsset
|
const minViableAmount = selectedAsset
|
||||||
@ -137,8 +121,15 @@ export const DepositForm = ({
|
|||||||
return minViableAmount;
|
return minViableAmount;
|
||||||
}, [selectedAsset]);
|
}, [selectedAsset]);
|
||||||
|
|
||||||
const approved = allowance && allowance.isGreaterThan(0) ? true : false;
|
const pubKeys = useMemo(() => {
|
||||||
const formState = getFormState(selectedAsset, isActive, approved);
|
return _pubKeys ? _pubKeys.map((pk) => pk.publicKey) : [];
|
||||||
|
}, [_pubKeys]);
|
||||||
|
|
||||||
|
const approved = balances
|
||||||
|
? balances.allowance.isGreaterThan(0)
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
: false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
@ -166,32 +157,23 @@ export const DepositForm = ({
|
|||||||
render={() => {
|
render={() => {
|
||||||
if (isActive && account) {
|
if (isActive && account) {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="text-sm" aria-describedby="ethereum-address">
|
||||||
<Input
|
<p className="mb-1" data-testid="ethereum-address">
|
||||||
id="ethereum-address"
|
{account}
|
||||||
value={account}
|
</p>
|
||||||
readOnly={true}
|
|
||||||
disabled={true}
|
|
||||||
{...register('from', {
|
|
||||||
validate: {
|
|
||||||
required,
|
|
||||||
ethereumAddress,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<DisconnectEthereumButton
|
<DisconnectEthereumButton
|
||||||
onDisconnect={() => {
|
onDisconnect={() => {
|
||||||
setValue('from', ''); // clear from value so required ethereum connection validation works
|
setValue('from', ''); // clear from value so required ethereum connection validation works
|
||||||
|
onDisconnect();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
onClick={openDialog}
|
onClick={openDialog}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
fill={true}
|
|
||||||
type="button"
|
type="button"
|
||||||
data-testid="connect-eth-wallet-btn"
|
data-testid="connect-eth-wallet-btn"
|
||||||
>
|
>
|
||||||
@ -238,7 +220,7 @@ export const DepositForm = ({
|
|||||||
</InputError>
|
</InputError>
|
||||||
)}
|
)}
|
||||||
{isFaucetable && selectedAsset && (
|
{isFaucetable && selectedAsset && (
|
||||||
<UseButton onClick={requestFaucet}>
|
<UseButton onClick={submitFaucet}>
|
||||||
{t(`Get ${selectedAsset.symbol}`)}
|
{t(`Get ${selectedAsset.symbol}`)}
|
||||||
</UseButton>
|
</UseButton>
|
||||||
)}
|
)}
|
||||||
@ -255,39 +237,55 @@ export const DepositForm = ({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
<FaucetNotification
|
||||||
|
isActive={isActive}
|
||||||
|
selectedAsset={selectedAsset}
|
||||||
|
faucetTxId={faucetTxId}
|
||||||
|
/>
|
||||||
<FormGroup label={t('To (Vega key)')} labelFor="to">
|
<FormGroup label={t('To (Vega key)')} labelFor="to">
|
||||||
<Input
|
<AddressField
|
||||||
{...register('to', { validate: { required, vegaPublicKey } })}
|
pubKeys={pubKeys}
|
||||||
id="to"
|
onChange={() => setValue('to', '')}
|
||||||
|
select={
|
||||||
|
<Select {...register('to')} id="to" defaultValue="">
|
||||||
|
<option value="" disabled={true}>
|
||||||
|
{t('Please select')}
|
||||||
|
</option>
|
||||||
|
{pubKeys?.length &&
|
||||||
|
pubKeys.map((pk) => (
|
||||||
|
<option key={pk} value={pk}>
|
||||||
|
{pk}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
}
|
||||||
|
input={
|
||||||
|
<Input
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||||
|
autoFocus={true} // focus input immediately after is shown
|
||||||
|
id="to"
|
||||||
|
type="text"
|
||||||
|
{...register('to', {
|
||||||
|
validate: {
|
||||||
|
required,
|
||||||
|
vegaPublicKey,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{errors.to?.message && (
|
{errors.to?.message && (
|
||||||
<InputError intent="danger" forInput="to">
|
<InputError intent="danger" forInput="to">
|
||||||
{errors.to.message}
|
{errors.to.message}
|
||||||
</InputError>
|
</InputError>
|
||||||
)}
|
)}
|
||||||
{pubKey && (
|
|
||||||
<UseButton
|
|
||||||
onClick={() => {
|
|
||||||
setValue('to', pubKey);
|
|
||||||
clearErrors('to');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('Use connected')}
|
|
||||||
</UseButton>
|
|
||||||
)}
|
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
{selectedAsset && max && deposited && (
|
{selectedAsset && balances && (
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<DepositLimits
|
<DepositLimits {...balances} asset={selectedAsset} />
|
||||||
max={max}
|
|
||||||
deposited={deposited}
|
|
||||||
balance={balance}
|
|
||||||
asset={selectedAsset}
|
|
||||||
allowance={allowance}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{formState === 'deposit' && (
|
{approved && (
|
||||||
<FormGroup label={t('Amount')} labelFor="amount">
|
<FormGroup label={t('Amount')} labelFor="amount">
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
@ -299,38 +297,52 @@ export const DepositForm = ({
|
|||||||
minSafe: (value) => minSafe(new BigNumber(min))(value),
|
minSafe: (value) => minSafe(new BigNumber(min))(value),
|
||||||
approved: (v) => {
|
approved: (v) => {
|
||||||
const value = new BigNumber(v);
|
const value = new BigNumber(v);
|
||||||
if (value.isGreaterThan(maxAmount.approved)) {
|
if (value.isGreaterThan(balances?.allowance || 0)) {
|
||||||
return t('Amount is above approved amount');
|
return t('Amount is above approved amount');
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
limit: (v) => {
|
limit: (v) => {
|
||||||
const value = new BigNumber(v);
|
const value = new BigNumber(v);
|
||||||
if (value.isGreaterThan(maxAmount.limit)) {
|
if (!balances) {
|
||||||
return t('Amount is above deposit limit');
|
return t('Could not verify balances of account'); // this should never happen
|
||||||
|
}
|
||||||
|
|
||||||
|
let lifetimeLimit = new BigNumber(Infinity);
|
||||||
|
if (balances.max.isGreaterThan(0)) {
|
||||||
|
lifetimeLimit = balances.max.minus(balances.deposited);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.isGreaterThan(lifetimeLimit)) {
|
||||||
|
return t('Amount is above lifetime deposit limit');
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
balance: (v) => {
|
balance: (v) => {
|
||||||
const value = new BigNumber(v);
|
const value = new BigNumber(v);
|
||||||
if (value.isGreaterThan(maxAmount.available)) {
|
if (value.isGreaterThan(balances?.balance || 0)) {
|
||||||
return t('Insufficient amount in Ethereum wallet');
|
return t('Insufficient amount in Ethereum wallet');
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
maxSafe: (v) => {
|
maxSafe: (v) => {
|
||||||
return maxSafe(maxAmount.amount)(v);
|
return maxSafe(balances?.balance || new BigNumber(0))(v);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
{errors.amount?.message && (
|
{errors.amount?.message && (
|
||||||
<AmountError error={errors.amount} submitApprove={submitApprove} />
|
<InputError intent="danger" forInput="amount">
|
||||||
|
{errors.amount.message}
|
||||||
|
</InputError>
|
||||||
)}
|
)}
|
||||||
{selectedAsset && balance && (
|
{selectedAsset && balances && (
|
||||||
<UseButton
|
<UseButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setValue('amount', balance.toFixed(selectedAsset.decimals));
|
setValue(
|
||||||
|
'amount',
|
||||||
|
balances.balance.toFixed(selectedAsset.decimals)
|
||||||
|
);
|
||||||
clearErrors('amount');
|
clearErrors('amount');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -339,59 +351,31 @@ export const DepositForm = ({
|
|||||||
)}
|
)}
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
)}
|
)}
|
||||||
<FormButton selectedAsset={selectedAsset} formState={formState} />
|
<ApproveNotification
|
||||||
|
isActive={isActive}
|
||||||
|
approveTxId={approveTxId}
|
||||||
|
selectedAsset={selectedAsset}
|
||||||
|
onApprove={submitApprove}
|
||||||
|
balances={balances}
|
||||||
|
approved={approved}
|
||||||
|
amount={amount}
|
||||||
|
/>
|
||||||
|
<FormButton approved={approved} selectedAsset={selectedAsset} />
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AmountError = ({
|
|
||||||
error,
|
|
||||||
submitApprove,
|
|
||||||
}: {
|
|
||||||
error: FieldError;
|
|
||||||
submitApprove: () => void;
|
|
||||||
}) => {
|
|
||||||
if (error.type === 'approved') {
|
|
||||||
return (
|
|
||||||
<InputError intent="danger" forInput="amount">
|
|
||||||
{error.message}.
|
|
||||||
<button onClick={submitApprove} className="underline ml-2">
|
|
||||||
{t('Update approve amount')}
|
|
||||||
</button>
|
|
||||||
</InputError>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<InputError intent="danger" forInput="amount">
|
|
||||||
{error.message}
|
|
||||||
</InputError>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface FormButtonProps {
|
interface FormButtonProps {
|
||||||
selectedAsset?: Asset;
|
approved: boolean;
|
||||||
formState: ReturnType<typeof getFormState>;
|
selectedAsset: AssetFieldsFragment | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormButton = ({ selectedAsset, formState }: FormButtonProps) => {
|
const FormButton = ({ approved, selectedAsset }: FormButtonProps) => {
|
||||||
const { isActive, chainId } = useWeb3React();
|
const { isActive, chainId } = useWeb3React();
|
||||||
const desiredChainId = useWeb3ConnectStore((store) => store.desiredChainId);
|
const desiredChainId = useWeb3ConnectStore((store) => store.desiredChainId);
|
||||||
const submitText =
|
|
||||||
formState === 'approve'
|
|
||||||
? t(`Approve ${selectedAsset ? selectedAsset.symbol : ''}`)
|
|
||||||
: t('Deposit');
|
|
||||||
const invalidChain = isActive && chainId !== desiredChainId;
|
const invalidChain = isActive && chainId !== desiredChainId;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{formState === 'approve' && (
|
|
||||||
<div className="mb-2">
|
|
||||||
<Notification
|
|
||||||
intent={Intent.Warning}
|
|
||||||
testId="approve-warning"
|
|
||||||
message={t(`Deposits of ${selectedAsset?.symbol} not approved`)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{invalidChain && (
|
{invalidChain && (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<Notification
|
<Notification
|
||||||
@ -408,9 +392,9 @@ const FormButton = ({ selectedAsset, formState }: FormButtonProps) => {
|
|||||||
data-testid="deposit-submit"
|
data-testid="deposit-submit"
|
||||||
variant={isActive ? 'primary' : 'default'}
|
variant={isActive ? 'primary' : 'default'}
|
||||||
fill={true}
|
fill={true}
|
||||||
disabled={invalidChain}
|
disabled={invalidChain || (selectedAsset && !approved)}
|
||||||
>
|
>
|
||||||
{submitText}
|
{t('Deposit')}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -437,7 +421,7 @@ const DisconnectEthereumButton = ({
|
|||||||
const [, , removeEagerConnector] = useLocalStorage(ETHEREUM_EAGER_CONNECT);
|
const [, , removeEagerConnector] = useLocalStorage(ETHEREUM_EAGER_CONNECT);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UseButton
|
<ButtonLink
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
connector.deactivate();
|
connector.deactivate();
|
||||||
removeEagerConnector();
|
removeEagerConnector();
|
||||||
@ -446,17 +430,46 @@ const DisconnectEthereumButton = ({
|
|||||||
data-testid="disconnect-ethereum-wallet"
|
data-testid="disconnect-ethereum-wallet"
|
||||||
>
|
>
|
||||||
{t('Disconnect')}
|
{t('Disconnect')}
|
||||||
</UseButton>
|
</ButtonLink>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFormState = (
|
interface AddressInputProps {
|
||||||
selectedAsset: Asset | undefined,
|
pubKeys: string[] | null;
|
||||||
isActive: boolean,
|
select: ReactNode;
|
||||||
approved: boolean
|
input: ReactNode;
|
||||||
) => {
|
onChange: () => void;
|
||||||
if (!selectedAsset) return 'deposit';
|
}
|
||||||
if (!isActive) return 'deposit';
|
|
||||||
if (approved) return 'deposit';
|
export const AddressField = ({
|
||||||
return 'approve';
|
pubKeys,
|
||||||
|
select,
|
||||||
|
input,
|
||||||
|
onChange,
|
||||||
|
}: AddressInputProps) => {
|
||||||
|
const [isInput, setIsInput] = useState(() => {
|
||||||
|
if (pubKeys && pubKeys.length <= 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isInput ? input : select}
|
||||||
|
{pubKeys && pubKeys.length > 1 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setIsInput((curr) => !curr);
|
||||||
|
onChange();
|
||||||
|
}}
|
||||||
|
className="ml-auto text-sm absolute top-0 right-0 underline"
|
||||||
|
data-testid="enter-pubkey-manually"
|
||||||
|
>
|
||||||
|
{isInput ? t('Select from wallet') : t('Enter manually')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -35,7 +35,7 @@ export const DepositLimits = ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'MAX_LIMIT',
|
key: 'MAX_LIMIT',
|
||||||
label: t('Maximum total deposit amount'),
|
label: t('Lifetime deposit allowance'),
|
||||||
rawValue: max,
|
rawValue: max,
|
||||||
value: <CompactNumber number={max} decimals={asset.decimals} />,
|
value: <CompactNumber number={max} decimals={asset.decimals} />,
|
||||||
},
|
},
|
||||||
|
@ -5,36 +5,26 @@ import { prepend0x } from '@vegaprotocol/smart-contracts';
|
|||||||
import sortBy from 'lodash/sortBy';
|
import sortBy from 'lodash/sortBy';
|
||||||
import { useSubmitApproval } from './use-submit-approval';
|
import { useSubmitApproval } from './use-submit-approval';
|
||||||
import { useSubmitFaucet } from './use-submit-faucet';
|
import { useSubmitFaucet } from './use-submit-faucet';
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useDepositBalances } from './use-deposit-balances';
|
import { useDepositBalances } from './use-deposit-balances';
|
||||||
import { useDepositDialog } from './deposit-dialog';
|
import { useDepositDialog } from './deposit-dialog';
|
||||||
import type { Asset } from '@vegaprotocol/assets';
|
import type { Asset } from '@vegaprotocol/assets';
|
||||||
import type { DepositDialogStylePropsSetter } from './deposit-dialog';
|
|
||||||
import pick from 'lodash/pick';
|
|
||||||
import type { EthTransaction } from '@vegaprotocol/web3';
|
|
||||||
import {
|
import {
|
||||||
EthTxStatus,
|
|
||||||
useEthTransactionStore,
|
useEthTransactionStore,
|
||||||
useBridgeContract,
|
useBridgeContract,
|
||||||
useEthereumConfig,
|
useEthereumConfig,
|
||||||
} from '@vegaprotocol/web3';
|
} from '@vegaprotocol/web3';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
|
|
||||||
interface DepositManagerProps {
|
interface DepositManagerProps {
|
||||||
assetId?: string;
|
assetId?: string;
|
||||||
assets: Asset[];
|
assets: Asset[];
|
||||||
isFaucetable: boolean;
|
isFaucetable: boolean;
|
||||||
setDialogStyleProps?: DepositDialogStylePropsSetter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getProps = (txContent?: EthTransaction['TxContent']) =>
|
|
||||||
txContent ? pick(txContent, ['title', 'icon', 'intent']) : undefined;
|
|
||||||
|
|
||||||
export const DepositManager = ({
|
export const DepositManager = ({
|
||||||
assetId: initialAssetId,
|
assetId: initialAssetId,
|
||||||
assets,
|
assets,
|
||||||
isFaucetable,
|
isFaucetable,
|
||||||
setDialogStyleProps,
|
|
||||||
}: DepositManagerProps) => {
|
}: DepositManagerProps) => {
|
||||||
const createEthTransaction = useEthTransactionStore((state) => state.create);
|
const createEthTransaction = useEthTransactionStore((state) => state.create);
|
||||||
const { config } = useEthereumConfig();
|
const { config } = useEthereumConfig();
|
||||||
@ -43,26 +33,16 @@ export const DepositManager = ({
|
|||||||
const bridgeContract = useBridgeContract();
|
const bridgeContract = useBridgeContract();
|
||||||
const closeDepositDialog = useDepositDialog((state) => state.close);
|
const closeDepositDialog = useDepositDialog((state) => state.close);
|
||||||
|
|
||||||
const { balance, allowance, deposited, max, refresh } = useDepositBalances(
|
const { getBalances, reset, balances } = useDepositBalances(
|
||||||
asset,
|
asset,
|
||||||
isFaucetable
|
isFaucetable
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set up approve transaction
|
// Set up approve transaction
|
||||||
const approve = useSubmitApproval(asset);
|
const approve = useSubmitApproval(asset, getBalances);
|
||||||
|
|
||||||
// Set up faucet transaction
|
// Set up faucet transaction
|
||||||
const faucet = useSubmitFaucet(asset);
|
const faucet = useSubmitFaucet(asset, getBalances);
|
||||||
|
|
||||||
const transactionInProgress = [approve.TxContent, faucet.TxContent].filter(
|
|
||||||
(t) => t.status !== EthTxStatus.Default
|
|
||||||
)[0];
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setDialogStyleProps?.(getProps(transactionInProgress));
|
|
||||||
}, [setDialogStyleProps, transactionInProgress]);
|
|
||||||
|
|
||||||
const returnLabel = t('Return to deposit');
|
|
||||||
|
|
||||||
const submitDeposit = (
|
const submitDeposit = (
|
||||||
args: Parameters<DepositFormProps['submitDeposit']>['0']
|
args: Parameters<DepositFormProps['submitDeposit']>['0']
|
||||||
@ -86,31 +66,24 @@ export const DepositManager = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DepositForm
|
||||||
{!transactionInProgress && (
|
selectedAsset={asset}
|
||||||
<DepositForm
|
onDisconnect={reset}
|
||||||
balance={balance}
|
onSelectAsset={(id) => {
|
||||||
selectedAsset={asset}
|
setAssetId(id);
|
||||||
onSelectAsset={setAssetId}
|
// When we change asset, also clear the tracked faucet/approve transactions so
|
||||||
assets={sortBy(assets, 'name')}
|
// we dont render stale UI
|
||||||
submitApprove={async () => {
|
approve.reset();
|
||||||
await approve.perform();
|
faucet.reset();
|
||||||
refresh();
|
}}
|
||||||
}}
|
assets={sortBy(assets, 'name')}
|
||||||
submitDeposit={submitDeposit}
|
submitApprove={approve.perform}
|
||||||
requestFaucet={async () => {
|
submitDeposit={submitDeposit}
|
||||||
await faucet.perform();
|
submitFaucet={faucet.perform}
|
||||||
refresh();
|
faucetTxId={faucet.id}
|
||||||
}}
|
approveTxId={approve.id}
|
||||||
deposited={deposited}
|
balances={balances}
|
||||||
max={max}
|
isFaucetable={isFaucetable}
|
||||||
allowance={allowance}
|
/>
|
||||||
isFaucetable={isFaucetable}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<approve.TxContent.Content returnLabel={returnLabel} />
|
|
||||||
<faucet.TxContent.Content returnLabel={returnLabel} />
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
108
libs/deposits/src/lib/faucet-notification.tsx
Normal file
108
libs/deposits/src/lib/faucet-notification.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import type { Asset } from '@vegaprotocol/assets';
|
||||||
|
import { useEnvironment } from '@vegaprotocol/environment';
|
||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { ExternalLink, Intent, Notification } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { EthTxStatus, useEthTransactionStore } from '@vegaprotocol/web3';
|
||||||
|
|
||||||
|
interface FaucetNotificationProps {
|
||||||
|
isActive: boolean;
|
||||||
|
selectedAsset?: Asset;
|
||||||
|
faucetTxId: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a notification for the faucet transaction
|
||||||
|
*/
|
||||||
|
export const FaucetNotification = ({
|
||||||
|
isActive,
|
||||||
|
selectedAsset,
|
||||||
|
faucetTxId,
|
||||||
|
}: FaucetNotificationProps) => {
|
||||||
|
const { ETHERSCAN_URL } = useEnvironment();
|
||||||
|
const tx = useEthTransactionStore((state) => {
|
||||||
|
return state.transactions.find((t) => t?.id === faucetTxId);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isActive) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedAsset) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tx) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tx.status === EthTxStatus.Error) {
|
||||||
|
return (
|
||||||
|
<div className="mb-4">
|
||||||
|
<Notification
|
||||||
|
intent={Intent.Danger}
|
||||||
|
testId="faucet-error"
|
||||||
|
// @ts-ignore tx.error not typed correctly
|
||||||
|
message={t(`Faucet failed: ${tx.error?.reason}`)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tx.status === EthTxStatus.Requested) {
|
||||||
|
return (
|
||||||
|
<div className="mb-4">
|
||||||
|
<Notification
|
||||||
|
intent={Intent.Warning}
|
||||||
|
testId="faucet-requested"
|
||||||
|
message={t(
|
||||||
|
`Go to your Ethereum wallet and approve the faucet transaction for ${selectedAsset?.symbol}`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tx.status === EthTxStatus.Pending) {
|
||||||
|
return (
|
||||||
|
<div className="mb-4">
|
||||||
|
<Notification
|
||||||
|
intent={Intent.Primary}
|
||||||
|
testId="faucet-pending"
|
||||||
|
message={
|
||||||
|
<p>
|
||||||
|
{t('Faucet pending...')}{' '}
|
||||||
|
{tx.txHash && (
|
||||||
|
<ExternalLink href={`${ETHERSCAN_URL}/tx/${tx.txHash}`}>
|
||||||
|
{t('View on Etherscan')}
|
||||||
|
</ExternalLink>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tx.status === EthTxStatus.Confirmed) {
|
||||||
|
return (
|
||||||
|
<div className="mb-4">
|
||||||
|
<Notification
|
||||||
|
intent={Intent.Success}
|
||||||
|
testId="faucet-confirmed"
|
||||||
|
message={
|
||||||
|
<p>
|
||||||
|
{t('Faucet successful')}{' '}
|
||||||
|
{tx.txHash && (
|
||||||
|
<ExternalLink href={`${ETHERSCAN_URL}/tx/${tx.txHash}`}>
|
||||||
|
{t('View on Etherscan')}
|
||||||
|
</ExternalLink>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
@ -7,21 +7,17 @@ import { useGetBalanceOfERC20Token } from './use-get-balance-of-erc20-token';
|
|||||||
import { useGetDepositMaximum } from './use-get-deposit-maximum';
|
import { useGetDepositMaximum } from './use-get-deposit-maximum';
|
||||||
import { useGetDepositedAmount } from './use-get-deposited-amount';
|
import { useGetDepositedAmount } from './use-get-deposited-amount';
|
||||||
import { isAssetTypeERC20 } from '@vegaprotocol/utils';
|
import { isAssetTypeERC20 } from '@vegaprotocol/utils';
|
||||||
import { usePrevious } from '@vegaprotocol/react-helpers';
|
|
||||||
import { useAccountBalance } from '@vegaprotocol/accounts';
|
import { useAccountBalance } from '@vegaprotocol/accounts';
|
||||||
import type { Asset } from '@vegaprotocol/assets';
|
import type { Asset } from '@vegaprotocol/assets';
|
||||||
|
|
||||||
type DepositBalances = {
|
export interface DepositBalances {
|
||||||
balance: BigNumber;
|
balance: BigNumber; // amount in Ethereum wallet
|
||||||
allowance: BigNumber;
|
allowance: BigNumber; // amount approved
|
||||||
deposited: BigNumber;
|
deposited: BigNumber; // total amounted deposited over lifetime
|
||||||
max: BigNumber;
|
max: BigNumber; // life time deposit cap
|
||||||
refresh: () => void;
|
}
|
||||||
};
|
|
||||||
|
|
||||||
type DepositBalancesState = Omit<DepositBalances, 'refresh'>;
|
const initialState: DepositBalances = {
|
||||||
|
|
||||||
const initialState: DepositBalancesState = {
|
|
||||||
balance: new BigNumber(0),
|
balance: new BigNumber(0),
|
||||||
allowance: new BigNumber(0),
|
allowance: new BigNumber(0),
|
||||||
deposited: new BigNumber(0),
|
deposited: new BigNumber(0),
|
||||||
@ -35,7 +31,7 @@ const initialState: DepositBalancesState = {
|
|||||||
export const useDepositBalances = (
|
export const useDepositBalances = (
|
||||||
asset: Asset | undefined,
|
asset: Asset | undefined,
|
||||||
isFaucetable: boolean
|
isFaucetable: boolean
|
||||||
): DepositBalances => {
|
) => {
|
||||||
const tokenContract = useTokenContract(
|
const tokenContract = useTokenContract(
|
||||||
isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined,
|
isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined,
|
||||||
isFaucetable
|
isFaucetable
|
||||||
@ -45,21 +41,14 @@ export const useDepositBalances = (
|
|||||||
const getBalance = useGetBalanceOfERC20Token(tokenContract, asset);
|
const getBalance = useGetBalanceOfERC20Token(tokenContract, asset);
|
||||||
const getDepositMaximum = useGetDepositMaximum(bridgeContract, asset);
|
const getDepositMaximum = useGetDepositMaximum(bridgeContract, asset);
|
||||||
const getDepositedAmount = useGetDepositedAmount(asset);
|
const getDepositedAmount = useGetDepositedAmount(asset);
|
||||||
const prevAsset = usePrevious(asset);
|
const [state, setState] = useState<DepositBalances | null>(null);
|
||||||
const [state, setState] = useState<DepositBalancesState>(initialState);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (asset?.id !== prevAsset?.id) {
|
|
||||||
// reset values to initial state when asset changes
|
|
||||||
setState(initialState);
|
|
||||||
}
|
|
||||||
}, [asset?.id, prevAsset?.id]);
|
|
||||||
|
|
||||||
const { accountBalance } = useAccountBalance(asset?.id);
|
const { accountBalance } = useAccountBalance(asset?.id);
|
||||||
|
|
||||||
const getBalances = useCallback(async () => {
|
const getBalances = useCallback(async () => {
|
||||||
if (!asset) return;
|
if (!asset) return;
|
||||||
try {
|
try {
|
||||||
|
setState(null);
|
||||||
const [max, deposited, balance, allowance] = await Promise.all([
|
const [max, deposited, balance, allowance] = await Promise.all([
|
||||||
getDepositMaximum(),
|
getDepositMaximum(),
|
||||||
getDepositedAmount(),
|
getDepositedAmount(),
|
||||||
@ -75,12 +64,17 @@ export const useDepositBalances = (
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
|
setState(null);
|
||||||
}
|
}
|
||||||
}, [asset, getAllowance, getBalance, getDepositMaximum, getDepositedAmount]);
|
}, [asset, getAllowance, getBalance, getDepositMaximum, getDepositedAmount]);
|
||||||
|
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
setState(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getBalances();
|
getBalances();
|
||||||
}, [asset, getBalances, accountBalance]);
|
}, [asset, getBalances, accountBalance]);
|
||||||
|
|
||||||
return { ...state, refresh: getBalances };
|
return { balances: state, getBalances, reset };
|
||||||
};
|
};
|
||||||
|
@ -1,36 +1,48 @@
|
|||||||
import { isAssetTypeERC20, removeDecimal } from '@vegaprotocol/utils';
|
import { isAssetTypeERC20, removeDecimal } from '@vegaprotocol/utils';
|
||||||
import * as Sentry from '@sentry/react';
|
|
||||||
import type { Token } from '@vegaprotocol/smart-contracts';
|
|
||||||
import {
|
import {
|
||||||
|
EthTxStatus,
|
||||||
useEthereumConfig,
|
useEthereumConfig,
|
||||||
useEthereumTransaction,
|
useEthTransactionStore,
|
||||||
useTokenContract,
|
useTokenContract,
|
||||||
} from '@vegaprotocol/web3';
|
} from '@vegaprotocol/web3';
|
||||||
import type { Asset } from '@vegaprotocol/assets';
|
import type { Asset } from '@vegaprotocol/assets';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export const useSubmitApproval = (asset?: Asset) => {
|
export const useSubmitApproval = (
|
||||||
|
asset: Asset | undefined,
|
||||||
|
getBalances: () => void
|
||||||
|
) => {
|
||||||
|
const [id, setId] = useState<number | null>(null);
|
||||||
|
const createEthTransaction = useEthTransactionStore((state) => state.create);
|
||||||
|
const tx = useEthTransactionStore((state) => {
|
||||||
|
return state.transactions.find((t) => t?.id === id);
|
||||||
|
});
|
||||||
const { config } = useEthereumConfig();
|
const { config } = useEthereumConfig();
|
||||||
const contract = useTokenContract(
|
const contract = useTokenContract(
|
||||||
isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined,
|
isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
const transaction = useEthereumTransaction<Token, 'approve'>(
|
|
||||||
contract,
|
// When tx is confirmed refresh balances
|
||||||
'approve'
|
useEffect(() => {
|
||||||
);
|
if (tx?.status === EthTxStatus.Confirmed) {
|
||||||
|
getBalances();
|
||||||
|
}
|
||||||
|
}, [tx?.status, getBalances]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...transaction,
|
id,
|
||||||
perform: async () => {
|
reset: () => {
|
||||||
|
setId(null);
|
||||||
|
},
|
||||||
|
perform: () => {
|
||||||
if (!asset || !config) return;
|
if (!asset || !config) return;
|
||||||
try {
|
const amount = removeDecimal('1000000', asset.decimals);
|
||||||
const amount = removeDecimal('1000000', asset.decimals);
|
const id = createEthTransaction(contract, 'approve', [
|
||||||
await transaction.perform(
|
config?.collateral_bridge_contract.address,
|
||||||
config.collateral_bridge_contract.address,
|
amount,
|
||||||
amount
|
]);
|
||||||
);
|
setId(id);
|
||||||
} catch (err) {
|
|
||||||
Sentry.captureException(err);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,26 +1,41 @@
|
|||||||
import type { TokenFaucetable } from '@vegaprotocol/smart-contracts';
|
import {
|
||||||
import * as Sentry from '@sentry/react';
|
EthTxStatus,
|
||||||
import { useEthereumTransaction, useTokenContract } from '@vegaprotocol/web3';
|
useEthTransactionStore,
|
||||||
|
useTokenContract,
|
||||||
|
} from '@vegaprotocol/web3';
|
||||||
import { isAssetTypeERC20 } from '@vegaprotocol/utils';
|
import { isAssetTypeERC20 } from '@vegaprotocol/utils';
|
||||||
import type { Asset } from '@vegaprotocol/assets';
|
import type { Asset } from '@vegaprotocol/assets';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export const useSubmitFaucet = (asset?: Asset) => {
|
export const useSubmitFaucet = (
|
||||||
|
asset: Asset | undefined,
|
||||||
|
getBalances: () => void
|
||||||
|
) => {
|
||||||
|
const [id, setId] = useState<number | null>(null);
|
||||||
|
const createEthTransaction = useEthTransactionStore((state) => state.create);
|
||||||
|
const tx = useEthTransactionStore((state) => {
|
||||||
|
return state.transactions.find((t) => t?.id === id);
|
||||||
|
});
|
||||||
const contract = useTokenContract(
|
const contract = useTokenContract(
|
||||||
isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined,
|
isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
const transaction = useEthereumTransaction<TokenFaucetable, 'faucet'>(
|
|
||||||
contract,
|
// When tx is confirmed refresh balances
|
||||||
'faucet'
|
useEffect(() => {
|
||||||
);
|
if (tx?.status === EthTxStatus.Confirmed) {
|
||||||
|
getBalances();
|
||||||
|
}
|
||||||
|
}, [tx?.status, getBalances]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...transaction,
|
id,
|
||||||
perform: async () => {
|
reset: () => {
|
||||||
try {
|
setId(null);
|
||||||
await transaction.perform();
|
},
|
||||||
} catch (err) {
|
perform: () => {
|
||||||
Sentry.captureException(err);
|
const id = createEthTransaction(contract, 'faucet', []);
|
||||||
}
|
setId(id);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -9,6 +9,7 @@ export interface FormGroupProps {
|
|||||||
hideLabel?: boolean;
|
hideLabel?: boolean;
|
||||||
labelDescription?: string;
|
labelDescription?: string;
|
||||||
labelAlign?: 'left' | 'right';
|
labelAlign?: 'left' | 'right';
|
||||||
|
compact?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FormGroup = ({
|
export const FormGroup = ({
|
||||||
@ -19,8 +20,16 @@ export const FormGroup = ({
|
|||||||
labelDescription,
|
labelDescription,
|
||||||
labelAlign = 'left',
|
labelAlign = 'left',
|
||||||
hideLabel = false,
|
hideLabel = false,
|
||||||
|
compact = false,
|
||||||
}: FormGroupProps) => {
|
}: FormGroupProps) => {
|
||||||
const wrapperClasses = classNames('relative mb-2', className);
|
const wrapperClasses = classNames(
|
||||||
|
'relative',
|
||||||
|
{
|
||||||
|
'mb-2': compact,
|
||||||
|
'mb-4': !compact,
|
||||||
|
},
|
||||||
|
className
|
||||||
|
);
|
||||||
const labelClasses = classNames('block mb-2 text-sm', {
|
const labelClasses = classNames('block mb-2 text-sm', {
|
||||||
'text-right': labelAlign === 'right',
|
'text-right': labelAlign === 'right',
|
||||||
'sr-only': hideLabel,
|
'sr-only': hideLabel,
|
||||||
|
@ -26,7 +26,7 @@ export interface EthStoredTxState extends EthTxState {
|
|||||||
methodName: ContractMethod;
|
methodName: ContractMethod;
|
||||||
args: string[];
|
args: string[];
|
||||||
requiredConfirmations: number;
|
requiredConfirmations: number;
|
||||||
requiresConfirmation: boolean;
|
requiresConfirmation: boolean; // whether or not the tx needs external confirmation (IE from a subscription even)
|
||||||
assetId?: string;
|
assetId?: string;
|
||||||
deposit?: DepositBusEventFieldsFragment;
|
deposit?: DepositBusEventFieldsFragment;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user