feat(trading,deposits): improve ethereum connection and approve step (#2926)

This commit is contained in:
Matthew Russell 2023-02-22 16:27:17 -08:00 committed by GitHub
parent 58d8f7857e
commit 63698fbb80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 485 additions and 228 deletions

View File

@ -72,7 +72,11 @@ 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('deposit-approve-submit').click(); cy.getByTestId('approve-warning').should(
'contain.text',
`Deposits of ${btcSymbol} not approved`
);
cy.getByTestId('deposit-submit').click();
cy.getByTestId('dialog-title').should('contain.text', 'Approve complete'); cy.getByTestId('dialog-title').should('contain.text', 'Approve complete');
cy.get('[data-testid="Return to deposit"]').click(); cy.get('[data-testid="Return to deposit"]').click();
cy.get(amountField).clear().type('10'); cy.get(amountField).clear().type('10');
@ -407,7 +411,7 @@ describe('capsule', { tags: '@slow' }, () => {
cy.getByTestId('deposit-button').click(); cy.getByTestId('deposit-button').click();
connectEthereumWallet('Unknown'); connectEthereumWallet('Unknown');
cy.get(assetSelectField, txTimeout).select(vegaName, { force: true }); cy.get(assetSelectField, txTimeout).select(vegaName, { force: true });
cy.getByTestId('deposit-approve-submit').click(); cy.getByTestId('deposit-submit').click();
cy.getByTestId('dialog-title').should('contain.text', 'Approve complete'); cy.getByTestId('dialog-title').should('contain.text', 'Approve complete');
cy.get('[data-testid="Return to deposit"]').click(); cy.get('[data-testid="Return to deposit"]').click();
cy.get(amountField).clear().type('10000'); cy.get(amountField).clear().type('10000');

View File

@ -1,3 +1,5 @@
import { removeDecimal } from '@vegaprotocol/cypress';
import { ethers } from 'ethers';
import { connectEthereumWallet } from '../support/ethereum-wallet'; import { connectEthereumWallet } from '../support/ethereum-wallet';
import { selectAsset } from '../support/helpers'; import { selectAsset } from '../support/helpers';
@ -9,7 +11,7 @@ const formFieldError = 'input-error-text';
const ASSET_EURO = 1; const ASSET_EURO = 1;
describe('deposit form validation', { tags: '@smoke' }, () => { describe('deposit form validation', { tags: '@smoke' }, () => {
before(() => { function openDepositForm() {
cy.mockWeb3Provider(); cy.mockWeb3Provider();
cy.mockSubscription(); cy.mockSubscription();
cy.mockTradingPage(); cy.mockTradingPage();
@ -20,10 +22,14 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
cy.getByTestId('deposit-button').click(); cy.getByTestId('deposit-button').click();
cy.wait('@Assets'); cy.wait('@Assets');
connectEthereumWallet('MetaMask'); connectEthereumWallet('MetaMask');
cy.getByTestId('deposit-submit').click(); }
before(() => {
openDepositForm();
}); });
it('handles empty fields', () => { it('handles empty fields', () => {
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); cy.getByTestId(formFieldError).should('have.length', 2);
}); });
@ -44,6 +50,13 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
}); });
it('invalid amount', () => { it('invalid amount', () => {
mockWeb3DepositCalls({
allowance: '1000',
depositLifetimeLimit: '1000',
balance: '800',
deposited: '0',
dps: 5,
});
// Deposit amount smaller than minimum viable for selected asset // Deposit amount smaller than minimum viable for selected asset
// Select an amount so that we have a known decimal places value to work with // Select an amount so that we have a known decimal places value to work with
selectAsset(ASSET_EURO); selectAsset(ASSET_EURO);
@ -56,12 +69,16 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
}); });
it('insufficient funds', () => { it('insufficient funds', () => {
// 1001-DEPO-005 mockWeb3DepositCalls({
// Deposit amount is valid, but less than approved. This will always be the case because our allowance: '1000',
// CI wallet wont have approved any assets depositLifetimeLimit: '1000',
balance: '800',
deposited: '0',
dps: 5,
});
cy.get(amountField) cy.get(amountField)
.clear() .clear()
.type('100') .type('850')
.next(`[data-testid="${formFieldError}"]`) .next(`[data-testid="${formFieldError}"]`)
.should('have.text', 'Insufficient amount in Ethereum wallet'); .should('have.text', 'Insufficient amount in Ethereum wallet');
}); });
@ -88,3 +105,90 @@ describe('deposit actions', { tags: '@smoke' }, () => {
cy.getByTestId('deposit-submit').should('be.visible'); cy.getByTestId('deposit-submit').should('be.visible');
}); });
}); });
function mockWeb3DepositCalls({
allowance,
depositLifetimeLimit,
balance,
deposited,
dps,
}: {
allowance: string;
depositLifetimeLimit: string;
balance: string;
deposited: string;
dps: number;
}) {
const assetContractAddress = '0x0158031158bb4df2ad02eaa31e8963e84ea978a4';
const collateralBridgeAddress = '0x7fe27d970bc8afc3b11cc8d9737bfb66b1efd799';
const toResult = (value: string, dps: number) => {
const rawValue = removeDecimal(value, dps);
return ethers.utils.hexZeroPad(
ethers.utils.hexlify(parseInt(rawValue)),
32
);
};
cy.intercept('POST', 'http://localhost:8545', (req) => {
// Mock chainId call
if (req.body.method === 'eth_chainId') {
req.alias = 'eth_chainId';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: '0xaa36a7', // 11155111 for sepolia chain id
});
}
// Mock deposited amount
if (req.body.method === 'eth_getStorageAt') {
req.alias = 'eth_getStorageAt';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(deposited, dps),
});
}
if (req.body.method === 'eth_call') {
// Mock approved amount for asset on collateral bridge
if (
req.body.params[0].to === assetContractAddress &&
req.body.params[0].data ===
'0xdd62ed3e000000000000000000000000ee7d375bcb50c26d52e1a4a472d8822a2a22d94f0000000000000000000000007fe27d970bc8afc3b11cc8d9737bfb66b1efd799'
) {
req.alias = 'eth_call_allowance';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(allowance, dps),
});
}
// Mock balance of asset in Ethereum wallet
else if (
req.body.params[0].to === assetContractAddress &&
req.body.params[0].data ===
'0x70a08231000000000000000000000000ee7d375bcb50c26d52e1a4a472d8822a2a22d94f'
) {
req.alias = 'eth_call_balanceOf';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(balance, dps),
});
}
// Mock deposit lifetime limit
else if (
req.body.params[0].to === collateralBridgeAddress &&
req.body.params[0].data ===
'0x354a897a0000000000000000000000000158031158bb4df2ad02eaa31e8963e84ea978a4'
) {
req.alias = 'eth_call_get_deposit_maximum'; // deposit lifetime limit
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(depositLifetimeLimit, dps),
});
}
}
});
}

View File

@ -17,19 +17,50 @@ describe('connect hosted wallet', { tags: '@smoke' }, () => {
}); });
it('can connect', () => { it('can connect', () => {
// Mock authentication
cy.intercept('POST', 'https://wallet.testnet.vega.xyz/api/v1/auth/token', {
body: {
token: 'test-token',
},
});
// Mock getting keys from wallet
cy.intercept('GET', 'https://wallet.testnet.vega.xyz/api/v1/keys', {
body: {
keys: [
{
algorithm: {
name: 'algo',
version: 1,
},
index: 0,
meta: [],
pub: 'HOSTED_PUBKEY',
tainted: false,
},
],
},
});
cy.getByTestId(connectVegaBtn).click(); cy.getByTestId(connectVegaBtn).click();
mockConnectWallet();
cy.contains('Connect Vega wallet'); cy.contains('Connect Vega wallet');
cy.contains('Hosted Fairground wallet'); cy.contains('Hosted Fairground wallet');
cy.getByTestId('connectors-list') cy.getByTestId('connectors-list')
.find('[data-testid="connector-jsonRpc"]') .find('[data-testid="connector-hosted"]')
.click(); .click();
cy.wait('@walletReq'); cy.getByTestId(form).find('#wallet').click().type('user');
cy.getByTestId(form).find('#passphrase').click().type('pass');
cy.getByTestId('rest-connector-form').find('button[type=submit]').click();
cy.getByTestId(manageVegaBtn).should('exist'); cy.getByTestId(manageVegaBtn).should('exist');
}); });
it('doesnt connect with invalid credentials', () => { it('doesnt connect with invalid credentials', () => {
// Mock incorrect username/password
cy.intercept('POST', 'https://wallet.testnet.vega.xyz/api/v1/auth/token', {
body: {
error: 'No wallet',
},
statusCode: 403, // 403 forbidden invalid crednetials
});
cy.getByTestId(connectVegaBtn).click(); cy.getByTestId(connectVegaBtn).click();
cy.getByTestId('connectors-list') cy.getByTestId('connectors-list')
.find('[data-testid="connector-hosted"]') .find('[data-testid="connector-hosted"]')
@ -37,10 +68,10 @@ describe('connect hosted wallet', { tags: '@smoke' }, () => {
cy.getByTestId(form).find('#wallet').click().type('invalid name'); cy.getByTestId(form).find('#wallet').click().type('invalid name');
cy.getByTestId(form).find('#passphrase').click().type('invalid password'); cy.getByTestId(form).find('#passphrase').click().type('invalid password');
cy.getByTestId('rest-connector-form').find('button[type=submit]').click(); cy.getByTestId('rest-connector-form').find('button[type=submit]').click();
cy.getByTestId('form-error').should('have.text', 'No wallet detected'); cy.getByTestId('form-error').should('have.text', 'Invalid credentials');
}); });
it('doesnt connect with invalid fields', () => { it('doesnt connect with empty fields', () => {
cy.getByTestId(connectVegaBtn).click(); cy.getByTestId(connectVegaBtn).click();
cy.getByTestId('connectors-list') cy.getByTestId('connectors-list')
.find('[data-testid="connector-hosted"]') .find('[data-testid="connector-hosted"]')

View File

@ -318,6 +318,7 @@ const SummaryMessage = memo(
} }
if (!pubKey) { if (!pubKey) {
return ( return (
<div className="mb-4">
<Notification <Notification
testId={'deal-ticket-connect-wallet'} testId={'deal-ticket-connect-wallet'}
intent={Intent.Warning} intent={Intent.Warning}
@ -337,14 +338,17 @@ const SummaryMessage = memo(
size: 'md', size: 'md',
}} }}
/> />
</div>
); );
} }
if (errorMessage === SummaryValidationType.NoCollateral) { if (errorMessage === SummaryValidationType.NoCollateral) {
return ( return (
<div className="mb-4">
<ZeroBalanceError <ZeroBalanceError
asset={market.tradableInstrument.instrument.product.settlementAsset} asset={market.tradableInstrument.instrument.product.settlementAsset}
onClickCollateral={onClickCollateral} onClickCollateral={onClickCollateral}
/> />
</div>
); );
} }
@ -363,7 +367,11 @@ const SummaryMessage = memo(
// If there is no blocking error but user doesn't have enough // If there is no blocking error but user doesn't have enough
// balance render the margin warning, but still allow submission // balance render the margin warning, but still allow submission
if (balanceError) { if (balanceError) {
return <MarginWarning balance={balance} margin={margin} asset={asset} />; return (
<div className="mb-4">
<MarginWarning balance={balance} margin={margin} asset={asset} />;
</div>
);
} }
// Show auction mode warning // Show auction mode warning
@ -375,6 +383,7 @@ const SummaryMessage = memo(
].includes(marketData.marketTradingMode) ].includes(marketData.marketTradingMode)
) { ) {
return ( return (
<div className="mb-4">
<Notification <Notification
intent={Intent.Warning} intent={Intent.Warning}
testId={'dealticket-warning-auction'} testId={'dealticket-warning-auction'}
@ -382,6 +391,7 @@ const SummaryMessage = memo(
'Any orders placed now will not trade until the auction ends' 'Any orders placed now will not trade until the auction ends'
)} )}
/> />
</div>
); );
} }

View File

@ -4,10 +4,12 @@ import type { DepositFormProps } from './deposit-form';
import { DepositForm } from './deposit-form'; import { DepositForm } from './deposit-form';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
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';
jest.mock('@vegaprotocol/wallet'); jest.mock('@vegaprotocol/wallet');
jest.mock('@vegaprotocol/web3');
jest.mock('@web3-react/core'); jest.mock('@web3-react/core');
const mockConnector = { deactivate: jest.fn() }; const mockConnector = { deactivate: jest.fn() };
@ -37,6 +39,8 @@ function generateAsset(): AssetFieldsFragment {
let asset: AssetFieldsFragment; let asset: AssetFieldsFragment;
let props: DepositFormProps; let props: DepositFormProps;
const MOCK_ETH_ADDRESS = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853'; const MOCK_ETH_ADDRESS = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
const MOCK_VEGA_KEY =
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
beforeEach(() => { beforeEach(() => {
asset = generateAsset(); asset = generateAsset();
@ -89,14 +93,17 @@ describe('Deposit form', () => {
}); });
}); });
it('fails when submitted with invalid ethereum address', async () => { it('fails when Ethereum wallet not connected', async () => {
(useWeb3React as jest.Mock).mockReturnValue({ account: '123' }); (useWeb3React as jest.Mock).mockReturnValue({
isActive: false,
account: '',
});
render(<DepositForm {...props} />); render(<DepositForm {...props} />);
fireEvent.submit(screen.getByTestId('deposit-form')); fireEvent.submit(screen.getByTestId('deposit-form'));
expect( expect(
await screen.findByText('Invalid Ethereum address') await screen.findByText('Connect Ethereum wallet')
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@ -138,7 +145,7 @@ describe('Deposit form', () => {
fireEvent.submit(screen.getByTestId('deposit-form')); fireEvent.submit(screen.getByTestId('deposit-form'));
expect( expect(
await screen.findByText('Insufficient amount in Ethereum wallet') await screen.findByText('Amount is above deposit limit')
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@ -159,7 +166,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();
}); });
@ -193,9 +200,9 @@ describe('Deposit form', () => {
}); });
}); });
it('handles deposit approvals', () => { it('handles deposit approvals', async () => {
const mockUseVegaWallet = useVegaWallet as jest.Mock; const mockUseVegaWallet = useVegaWallet as jest.Mock;
mockUseVegaWallet.mockReturnValue({ pubKey: null }); mockUseVegaWallet.mockReturnValue({ pubKey: MOCK_VEGA_KEY });
const mockUseWeb3React = useWeb3React as jest.Mock; const mockUseWeb3React = useWeb3React as jest.Mock;
mockUseWeb3React.mockReturnValue({ mockUseWeb3React.mockReturnValue({
@ -212,14 +219,19 @@ describe('Deposit form', () => {
/> />
); );
fireEvent.click( expect(screen.queryByLabelText('Amount')).not.toBeInTheDocument();
screen.getByText(`Approve ${asset.symbol}`, { expect(screen.getByTestId('approve-warning')).toHaveTextContent(
selector: '[type="button"]', `Deposits of ${asset.symbol} not approved`
})
); );
fireEvent.click(
screen.getByRole('button', { name: `Approve ${asset.symbol}` })
);
await waitFor(() => {
expect(props.submitApprove).toHaveBeenCalled(); expect(props.submitApprove).toHaveBeenCalled();
}); });
});
it('handles submitting a deposit', async () => { it('handles submitting a deposit', async () => {
const pubKey = const pubKey =
@ -284,4 +296,55 @@ describe('Deposit form', () => {
render(<DepositForm {...props} />); render(<DepositForm {...props} />);
expect(await screen.queryAllByTestId('view-asset-details')).toHaveLength(0); expect(await screen.queryAllByTestId('view-asset-details')).toHaveLength(0);
}); });
it('renders a connect button if Ethereum wallet is not connected', () => {
(useWeb3React as jest.Mock).mockReturnValue({
isActive: false,
account: '',
});
render(<DepositForm {...props} />);
expect(screen.getByRole('button', { name: 'Connect' })).toBeInTheDocument();
expect(
screen.queryByLabelText('From (Ethereum address)')
).not.toBeInTheDocument();
});
it('renders a disabled input if Ethereum wallet is connected', () => {
(useWeb3React as jest.Mock).mockReturnValue({
isActive: true,
account: MOCK_ETH_ADDRESS,
});
render(<DepositForm {...props} />);
expect(
screen.queryByRole('button', { name: 'Connect' })
).not.toBeInTheDocument();
const fromInput = screen.getByLabelText('From (Ethereum address)');
expect(fromInput).toHaveValue(MOCK_ETH_ADDRESS);
expect(fromInput).toBeDisabled();
expect(fromInput).toHaveAttribute('readonly');
});
it('prevents submission if you are on the wrong chain', () => {
(useWeb3React as jest.Mock).mockReturnValue({
isActive: true,
account: MOCK_ETH_ADDRESS,
chainId: 1,
});
(useWeb3ConnectStore as unknown as jest.Mock).mockImplementation(
// eslint-disable-next-line
(selector: (result: ReturnType<typeof useWeb3ConnectStore>) => any) => {
return selector({
desiredChainId: 11155111,
open: jest.fn(),
foo: 'asdf',
});
}
);
render(<DepositForm {...props} />);
expect(screen.getByTestId('chain-error')).toHaveTextContent(
/this app only works on/i
);
});
}); });

View File

@ -1,8 +1,8 @@
import type { Asset } from '@vegaprotocol/assets'; import type { Asset } from '@vegaprotocol/assets';
import { AssetOption } from '@vegaprotocol/assets'; import { AssetOption } from '@vegaprotocol/assets';
import { import {
ethereumAddress,
t, t,
ethereumAddress,
required, required,
vegaPublicKey, vegaPublicKey,
minSafe, minSafe,
@ -14,17 +14,19 @@ import {
import { import {
Button, Button,
FormGroup, FormGroup,
Icon,
Input, Input,
InputError, InputError,
RichSelect, RichSelect,
Notification,
Intent,
} 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, ReactNode } from 'react'; import type { ButtonHTMLAttributes } from 'react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { Controller, useForm, useWatch } from 'react-hook-form'; import type { FieldError } 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';
import { import {
@ -72,7 +74,8 @@ export const DepositForm = ({
isFaucetable, isFaucetable,
}: DepositFormProps) => { }: DepositFormProps) => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore(); const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
const { account } = useWeb3React(); const openDialog = useWeb3ConnectStore((store) => store.open);
const { isActive, account } = useWeb3React();
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
const { const {
register, register,
@ -83,26 +86,27 @@ export const DepositForm = ({
formState: { errors }, formState: { errors },
} = useForm<FormFields>({ } = useForm<FormFields>({
defaultValues: { defaultValues: {
from: account,
to: pubKey ? pubKey : undefined, to: pubKey ? pubKey : undefined,
asset: selectedAsset?.id || '', asset: selectedAsset?.id || '',
}, },
}); });
const onDeposit = 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) {
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 amount = useWatch({ name: 'amount', control });
const maxAmount = useMemo(() => { const maxAmount = useMemo(() => {
const maxApproved = allowance ? allowance : new BigNumber(0); const maxApproved = allowance ? allowance : new BigNumber(0);
const maxAvailable = balance ? balance : new BigNumber(0); const maxAvailable = balance ? balance : new BigNumber(0);
@ -133,9 +137,12 @@ export const DepositForm = ({
return minViableAmount; return minViableAmount;
}, [selectedAsset]); }, [selectedAsset]);
const approved = allowance && allowance.isGreaterThan(0) ? true : false;
const formState = getFormState(selectedAsset, isActive, approved);
return ( return (
<form <form
onSubmit={handleSubmit(onDeposit)} onSubmit={handleSubmit(onSubmit)}
noValidate={true} noValidate={true}
data-testid="deposit-form" data-testid="deposit-form"
> >
@ -143,8 +150,28 @@ export const DepositForm = ({
label={t('From (Ethereum address)')} label={t('From (Ethereum address)')}
labelFor="ethereum-address" labelFor="ethereum-address"
> >
<Controller
name="from"
control={control}
rules={{
validate: {
required: (value) => {
if (!value) return t('Connect Ethereum wallet');
return true;
},
ethereumAddress,
},
}}
defaultValue={account}
render={() => {
if (isActive && account) {
return (
<>
<Input <Input
id="ethereum-address" id="ethereum-address"
value={account}
readOnly={true}
disabled={true}
{...register('from', { {...register('from', {
validate: { validate: {
required, required,
@ -152,10 +179,25 @@ export const DepositForm = ({
}, },
})} })}
/> />
<EthereumButton <DisconnectEthereumButton
clearAddress={() => { onDisconnect={() => {
setValue('from', ''); setValue('from', ''); // clear from value so required ethereum connection validation works
clearErrors('from'); }}
/>
</>
);
}
return (
<Button
onClick={openDialog}
variant="primary"
fill={true}
type="button"
data-testid="connect-eth-wallet-btn"
>
{t('Connect')}
</Button>
);
}} }}
/> />
{errors.from?.message && ( {errors.from?.message && (
@ -241,9 +283,11 @@ export const DepositForm = ({
deposited={deposited} deposited={deposited}
balance={balance} balance={balance}
asset={selectedAsset} asset={selectedAsset}
allowance={allowance}
/> />
</div> </div>
)} )}
{formState === 'deposit' && (
<FormGroup label={t('Amount')} labelFor="amount"> <FormGroup label={t('Amount')} labelFor="amount">
<Input <Input
type="number" type="number"
@ -253,24 +297,35 @@ export const DepositForm = ({
validate: { validate: {
required, required,
minSafe: (value) => minSafe(new BigNumber(min))(value), minSafe: (value) => minSafe(new BigNumber(min))(value),
maxSafe: (v) => { approved: (v) => {
const value = new BigNumber(v);
if (value.isGreaterThan(maxAmount.approved)) {
return t('Amount is above approved amount');
}
return true;
},
limit: (v) => {
const value = new BigNumber(v);
if (value.isGreaterThan(maxAmount.limit)) {
return t('Amount is above deposit limit');
}
return true;
},
balance: (v) => {
const value = new BigNumber(v); const value = new BigNumber(v);
if (value.isGreaterThan(maxAmount.available)) { if (value.isGreaterThan(maxAmount.available)) {
return t('Insufficient amount in Ethereum wallet'); return t('Insufficient amount in Ethereum wallet');
} else if (value.isGreaterThan(maxAmount.limit)) {
return t('Amount is above temporary deposit limit');
} else if (value.isGreaterThan(maxAmount.approved)) {
return t('Amount is above approved amount');
} }
return true;
},
maxSafe: (v) => {
return maxSafe(maxAmount.amount)(v); return maxSafe(maxAmount.amount)(v);
}, },
}, },
})} })}
/> />
{errors.amount?.message && ( {errors.amount?.message && (
<InputError intent="danger" forInput="amount"> <AmountError error={errors.amount} submitApprove={submitApprove} />
{errors.amount.message}
</InputError>
)} )}
{selectedAsset && balance && ( {selectedAsset && balance && (
<UseButton <UseButton
@ -283,106 +338,81 @@ export const DepositForm = ({
</UseButton> </UseButton>
)} )}
</FormGroup> </FormGroup>
<FormButton )}
selectedAsset={selectedAsset} <FormButton selectedAsset={selectedAsset} formState={formState} />
amount={new BigNumber(amount || 0)}
allowance={allowance}
onApproveClick={submitApprove}
/>
</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; selectedAsset?: Asset;
amount: BigNumber; formState: ReturnType<typeof getFormState>;
allowance: BigNumber | undefined;
onApproveClick: () => void;
} }
const FormButton = ({ const FormButton = ({ selectedAsset, formState }: FormButtonProps) => {
selectedAsset,
amount,
allowance,
onApproveClick,
}: FormButtonProps) => {
const { open, desiredChainId } = useWeb3ConnectStore((store) => ({
open: store.open,
desiredChainId: store.desiredChainId,
}));
const { isActive, chainId } = useWeb3React(); const { isActive, chainId } = useWeb3React();
const approved = const desiredChainId = useWeb3ConnectStore((store) => store.desiredChainId);
allowance && allowance.isGreaterThan(0) && amount.isLessThan(allowance); const submitText =
let button = null; formState === 'approve'
let message: ReactNode = ''; ? t(`Approve ${selectedAsset ? selectedAsset.symbol : ''}`)
: t('Deposit');
if (!isActive) { const invalidChain = isActive && chainId !== desiredChainId;
button = (
<Button onClick={open} data-testid="connect-eth-wallet-btn">
{t('Connect Ethereum wallet')}
</Button>
);
} else if (chainId !== desiredChainId) {
const chainName = getChainName(desiredChainId);
message = t(`This app only works on ${chainName}.`);
button = (
<Button
type="submit"
data-testid="deposit-submit"
variant="primary"
fill={true}
disabled={true}
>
{t('Deposit')}
</Button>
);
} else if (!selectedAsset) {
button = (
<Button
type="submit"
data-testid="deposit-submit"
variant="primary"
fill={true}
>
{t('Deposit')}
</Button>
);
} else if (approved) {
message = (
<>
<Icon name="tick" className="mr-2" />
<span>{t('Approved')}</span>
</>
);
button = (
<Button
type="submit"
data-testid="deposit-submit"
variant="primary"
fill={true}
>
{t('Deposit')}
</Button>
);
} else {
message = t(`Deposits of ${selectedAsset.symbol} not approved`);
button = (
<Button
onClick={onApproveClick}
data-testid="deposit-approve-submit"
variant="primary"
fill={true}
>
{t(`Approve ${selectedAsset.symbol}`)}
</Button>
);
}
return ( return (
<div className="flex flex-col gap-4"> <>
{message && <p className="text-center">{message}</p>} {formState === 'approve' && (
{button} <div className="mb-2">
<Notification
intent={Intent.Warning}
testId="approve-warning"
message={t(`Deposits of ${selectedAsset?.symbol} not approved`)}
/>
</div> </div>
)}
{invalidChain && (
<div className="mb-2">
<Notification
intent={Intent.Danger}
testId="chain-error"
message={t(
`This app only works on ${getChainName(desiredChainId)}.`
)}
/>
</div>
)}
<Button
type="submit"
data-testid="deposit-submit"
variant={isActive ? 'primary' : 'default'}
fill={true}
disabled={invalidChain}
>
{submitText}
</Button>
</>
); );
}; };
@ -398,21 +428,20 @@ const UseButton = (props: UseButtonProps) => {
); );
}; };
const EthereumButton = ({ clearAddress }: { clearAddress: () => void }) => { const DisconnectEthereumButton = ({
const openDialog = useWeb3ConnectStore((state) => state.open); onDisconnect,
const { isActive, connector } = useWeb3React(); }: {
onDisconnect: () => void;
}) => {
const { connector } = useWeb3React();
const [, , removeEagerConnector] = useLocalStorage(ETHEREUM_EAGER_CONNECT); const [, , removeEagerConnector] = useLocalStorage(ETHEREUM_EAGER_CONNECT);
if (!isActive) {
return <UseButton onClick={openDialog}>{t('Connect')}</UseButton>;
}
return ( return (
<UseButton <UseButton
onClick={() => { onClick={() => {
connector.deactivate(); connector.deactivate();
clearAddress();
removeEagerConnector(); removeEagerConnector();
onDisconnect();
}} }}
data-testid="disconnect-ethereum-wallet" data-testid="disconnect-ethereum-wallet"
> >
@ -420,3 +449,14 @@ const EthereumButton = ({ clearAddress }: { clearAddress: () => void }) => {
</UseButton> </UseButton>
); );
}; };
const getFormState = (
selectedAsset: Asset | undefined,
isActive: boolean,
approved: boolean
) => {
if (!selectedAsset) return 'deposit';
if (!isActive) return 'deposit';
if (approved) return 'deposit';
return 'approve';
};

View File

@ -11,6 +11,7 @@ interface DepositLimitsProps {
deposited: BigNumber; deposited: BigNumber;
asset: Asset; asset: Asset;
balance?: BigNumber; balance?: BigNumber;
allowance?: BigNumber;
} }
export const DepositLimits = ({ export const DepositLimits = ({
@ -18,6 +19,7 @@ export const DepositLimits = ({
deposited, deposited,
asset, asset,
balance, balance,
allowance,
}: DepositLimitsProps) => { }: DepositLimitsProps) => {
const limits = [ const limits = [
{ {
@ -44,6 +46,12 @@ export const DepositLimits = ({
rawValue: max.minus(deposited), rawValue: max.minus(deposited),
value: compactNumber(max.minus(deposited), asset.decimals), value: compactNumber(max.minus(deposited), asset.decimals),
}, },
{
key: 'ALLOWANCE',
label: t('Approved'),
rawValue: allowance,
value: allowance ? compactNumber(allowance, asset.decimals) : '-',
},
]; ];
return ( return (

View File

@ -27,12 +27,13 @@ export const AssetProposalNotification = ({
</> </>
); );
return ( return (
<div className="mb-2">
<Notification <Notification
intent={Intent.Warning} intent={Intent.Warning}
message={message} message={message}
testId="asset-proposal-notification" testId="asset-proposal-notification"
className="mb-2"
/> />
</div>
); );
} }

View File

@ -34,7 +34,6 @@ export const MarketProposalNotification = ({
intent={Intent.Warning} intent={Intent.Warning}
message={message} message={message}
testId="market-proposal-notification" testId="market-proposal-notification"
className="px-2 py-1"
/> />
</div> </div>
); );

View File

@ -19,7 +19,6 @@ type NotificationProps = {
size?: ButtonSize; size?: ButtonSize;
}; };
testId?: string; testId?: string;
className?: string;
}; };
const getIcon = (intent: Intent): IconName => { const getIcon = (intent: Intent): IconName => {
@ -39,7 +38,6 @@ export const Notification = ({
title, title,
testId, testId,
buttonProps, buttonProps,
className,
}: NotificationProps) => { }: NotificationProps) => {
return ( return (
<div <div
@ -61,8 +59,7 @@ export const Notification = ({
intent === Intent.Warning, intent === Intent.Warning,
'bg-vega-pink-300 dark:bg-vega-pink-650': intent === Intent.Danger, 'bg-vega-pink-300 dark:bg-vega-pink-650': intent === Intent.Danger,
}, },
'border rounded p-2 flex items-start gap-2.5 my-4', 'border rounded p-2 flex items-start gap-2.5'
className
)} )}
> >
<div <div