feat(deposits): deposit flow (#3062)

This commit is contained in:
Matthew Russell 2023-03-07 00:17:02 -08:00 committed by GitHub
parent a8eef1cb53
commit adca4600c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 677 additions and 347 deletions

View File

@ -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(

View File

@ -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', () => {

View File

@ -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();

View File

@ -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(

View File

@ -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"

View File

@ -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"

View File

@ -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}

View File

@ -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"

View 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;
};

View File

@ -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>

View File

@ -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>
); );
}; };

View File

@ -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', () => {

View File

@ -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>
)}
</>
);
}; };

View File

@ -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} />,
}, },

View File

@ -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} />
</>
); );
}; };

View 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;
};

View File

@ -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 };
}; };

View File

@ -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);
}
}, },
}; };
}; };

View File

@ -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);
}, },
}; };
}; };

View File

@ -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,

View File

@ -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;
} }