feat(trading,deposits): improve ethereum connection and approve step (#2926)
This commit is contained in:
parent
58d8f7857e
commit
63698fbb80
@ -72,7 +72,11 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
|
||||
cy.getByTestId('deposit-button').click();
|
||||
connectEthereumWallet('Unknown');
|
||||
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.get('[data-testid="Return to deposit"]').click();
|
||||
cy.get(amountField).clear().type('10');
|
||||
@ -407,7 +411,7 @@ describe('capsule', { tags: '@slow' }, () => {
|
||||
cy.getByTestId('deposit-button').click();
|
||||
connectEthereumWallet('Unknown');
|
||||
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.get('[data-testid="Return to deposit"]').click();
|
||||
cy.get(amountField).clear().type('10000');
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { removeDecimal } from '@vegaprotocol/cypress';
|
||||
import { ethers } from 'ethers';
|
||||
import { connectEthereumWallet } from '../support/ethereum-wallet';
|
||||
import { selectAsset } from '../support/helpers';
|
||||
|
||||
@ -9,7 +11,7 @@ const formFieldError = 'input-error-text';
|
||||
const ASSET_EURO = 1;
|
||||
|
||||
describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
before(() => {
|
||||
function openDepositForm() {
|
||||
cy.mockWeb3Provider();
|
||||
cy.mockSubscription();
|
||||
cy.mockTradingPage();
|
||||
@ -20,10 +22,14 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('deposit-button').click();
|
||||
cy.wait('@Assets');
|
||||
connectEthereumWallet('MetaMask');
|
||||
cy.getByTestId('deposit-submit').click();
|
||||
}
|
||||
|
||||
before(() => {
|
||||
openDepositForm();
|
||||
});
|
||||
|
||||
it('handles empty fields', () => {
|
||||
cy.getByTestId('deposit-submit').click();
|
||||
cy.getByTestId(formFieldError).should('contain.text', 'Required');
|
||||
cy.getByTestId(formFieldError).should('have.length', 2);
|
||||
});
|
||||
@ -44,6 +50,13 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('invalid amount', () => {
|
||||
mockWeb3DepositCalls({
|
||||
allowance: '1000',
|
||||
depositLifetimeLimit: '1000',
|
||||
balance: '800',
|
||||
deposited: '0',
|
||||
dps: 5,
|
||||
});
|
||||
// Deposit amount smaller than minimum viable for selected asset
|
||||
// Select an amount so that we have a known decimal places value to work with
|
||||
selectAsset(ASSET_EURO);
|
||||
@ -56,12 +69,16 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('insufficient funds', () => {
|
||||
// 1001-DEPO-005
|
||||
// Deposit amount is valid, but less than approved. This will always be the case because our
|
||||
// CI wallet wont have approved any assets
|
||||
mockWeb3DepositCalls({
|
||||
allowance: '1000',
|
||||
depositLifetimeLimit: '1000',
|
||||
balance: '800',
|
||||
deposited: '0',
|
||||
dps: 5,
|
||||
});
|
||||
cy.get(amountField)
|
||||
.clear()
|
||||
.type('100')
|
||||
.type('850')
|
||||
.next(`[data-testid="${formFieldError}"]`)
|
||||
.should('have.text', 'Insufficient amount in Ethereum wallet');
|
||||
});
|
||||
@ -88,3 +105,90 @@ describe('deposit actions', { tags: '@smoke' }, () => {
|
||||
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),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -17,19 +17,50 @@ describe('connect hosted wallet', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
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();
|
||||
mockConnectWallet();
|
||||
cy.contains('Connect Vega wallet');
|
||||
cy.contains('Hosted Fairground wallet');
|
||||
|
||||
cy.getByTestId('connectors-list')
|
||||
.find('[data-testid="connector-jsonRpc"]')
|
||||
.find('[data-testid="connector-hosted"]')
|
||||
.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');
|
||||
});
|
||||
|
||||
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('connectors-list')
|
||||
.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('#passphrase').click().type('invalid password');
|
||||
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('connectors-list')
|
||||
.find('[data-testid="connector-hosted"]')
|
||||
|
@ -318,33 +318,37 @@ const SummaryMessage = memo(
|
||||
}
|
||||
if (!pubKey) {
|
||||
return (
|
||||
<Notification
|
||||
testId={'deal-ticket-connect-wallet'}
|
||||
intent={Intent.Warning}
|
||||
message={
|
||||
<p className="text-sm pb-2">
|
||||
You need a{' '}
|
||||
<ExternalLink href="https://vega.xyz/wallet">
|
||||
Vega wallet
|
||||
</ExternalLink>{' '}
|
||||
with {assetSymbol} to start trading in this market.
|
||||
</p>
|
||||
}
|
||||
buttonProps={{
|
||||
text: t('Connect wallet'),
|
||||
action: openVegaWalletDialog,
|
||||
dataTestId: 'order-connect-wallet',
|
||||
size: 'md',
|
||||
}}
|
||||
/>
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
testId={'deal-ticket-connect-wallet'}
|
||||
intent={Intent.Warning}
|
||||
message={
|
||||
<p className="text-sm pb-2">
|
||||
You need a{' '}
|
||||
<ExternalLink href="https://vega.xyz/wallet">
|
||||
Vega wallet
|
||||
</ExternalLink>{' '}
|
||||
with {assetSymbol} to start trading in this market.
|
||||
</p>
|
||||
}
|
||||
buttonProps={{
|
||||
text: t('Connect wallet'),
|
||||
action: openVegaWalletDialog,
|
||||
dataTestId: 'order-connect-wallet',
|
||||
size: 'md',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (errorMessage === SummaryValidationType.NoCollateral) {
|
||||
return (
|
||||
<ZeroBalanceError
|
||||
asset={market.tradableInstrument.instrument.product.settlementAsset}
|
||||
onClickCollateral={onClickCollateral}
|
||||
/>
|
||||
<div className="mb-4">
|
||||
<ZeroBalanceError
|
||||
asset={market.tradableInstrument.instrument.product.settlementAsset}
|
||||
onClickCollateral={onClickCollateral}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -363,7 +367,11 @@ const SummaryMessage = memo(
|
||||
// If there is no blocking error but user doesn't have enough
|
||||
// balance render the margin warning, but still allow submission
|
||||
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
|
||||
@ -375,13 +383,15 @@ const SummaryMessage = memo(
|
||||
].includes(marketData.marketTradingMode)
|
||||
) {
|
||||
return (
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId={'dealticket-warning-auction'}
|
||||
message={t(
|
||||
'Any orders placed now will not trade until the auction ends'
|
||||
)}
|
||||
/>
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId={'dealticket-warning-auction'}
|
||||
message={t(
|
||||
'Any orders placed now will not trade until the auction ends'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -4,10 +4,12 @@ import type { DepositFormProps } from './deposit-form';
|
||||
import { DepositForm } from './deposit-form';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
|
||||
jest.mock('@vegaprotocol/wallet');
|
||||
jest.mock('@vegaprotocol/web3');
|
||||
jest.mock('@web3-react/core');
|
||||
|
||||
const mockConnector = { deactivate: jest.fn() };
|
||||
@ -37,6 +39,8 @@ function generateAsset(): AssetFieldsFragment {
|
||||
let asset: AssetFieldsFragment;
|
||||
let props: DepositFormProps;
|
||||
const MOCK_ETH_ADDRESS = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
|
||||
const MOCK_VEGA_KEY =
|
||||
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
|
||||
|
||||
beforeEach(() => {
|
||||
asset = generateAsset();
|
||||
@ -89,14 +93,17 @@ describe('Deposit form', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('fails when submitted with invalid ethereum address', async () => {
|
||||
(useWeb3React as jest.Mock).mockReturnValue({ account: '123' });
|
||||
it('fails when Ethereum wallet not connected', async () => {
|
||||
(useWeb3React as jest.Mock).mockReturnValue({
|
||||
isActive: false,
|
||||
account: '',
|
||||
});
|
||||
render(<DepositForm {...props} />);
|
||||
|
||||
fireEvent.submit(screen.getByTestId('deposit-form'));
|
||||
|
||||
expect(
|
||||
await screen.findByText('Invalid Ethereum address')
|
||||
await screen.findByText('Connect Ethereum wallet')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@ -138,7 +145,7 @@ describe('Deposit form', () => {
|
||||
fireEvent.submit(screen.getByTestId('deposit-form'));
|
||||
|
||||
expect(
|
||||
await screen.findByText('Insufficient amount in Ethereum wallet')
|
||||
await screen.findByText('Amount is above deposit limit')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@ -159,7 +166,7 @@ describe('Deposit form', () => {
|
||||
fireEvent.submit(screen.getByTestId('deposit-form'));
|
||||
|
||||
expect(
|
||||
await screen.findByText('Amount is above approved amount')
|
||||
await screen.findByText('Amount is above approved amount.')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@ -193,9 +200,9 @@ describe('Deposit form', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handles deposit approvals', () => {
|
||||
it('handles deposit approvals', async () => {
|
||||
const mockUseVegaWallet = useVegaWallet as jest.Mock;
|
||||
mockUseVegaWallet.mockReturnValue({ pubKey: null });
|
||||
mockUseVegaWallet.mockReturnValue({ pubKey: MOCK_VEGA_KEY });
|
||||
|
||||
const mockUseWeb3React = useWeb3React as jest.Mock;
|
||||
mockUseWeb3React.mockReturnValue({
|
||||
@ -212,13 +219,18 @@ describe('Deposit form', () => {
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByText(`Approve ${asset.symbol}`, {
|
||||
selector: '[type="button"]',
|
||||
})
|
||||
expect(screen.queryByLabelText('Amount')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('approve-warning')).toHaveTextContent(
|
||||
`Deposits of ${asset.symbol} not approved`
|
||||
);
|
||||
|
||||
expect(props.submitApprove).toHaveBeenCalled();
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: `Approve ${asset.symbol}` })
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(props.submitApprove).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles submitting a deposit', async () => {
|
||||
@ -284,4 +296,55 @@ describe('Deposit form', () => {
|
||||
render(<DepositForm {...props} />);
|
||||
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
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -1,8 +1,8 @@
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { AssetOption } from '@vegaprotocol/assets';
|
||||
import {
|
||||
ethereumAddress,
|
||||
t,
|
||||
ethereumAddress,
|
||||
required,
|
||||
vegaPublicKey,
|
||||
minSafe,
|
||||
@ -14,17 +14,19 @@ import {
|
||||
import {
|
||||
Button,
|
||||
FormGroup,
|
||||
Icon,
|
||||
Input,
|
||||
InputError,
|
||||
RichSelect,
|
||||
Notification,
|
||||
Intent,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||
import type { ButtonHTMLAttributes } 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 { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import {
|
||||
@ -72,7 +74,8 @@ export const DepositForm = ({
|
||||
isFaucetable,
|
||||
}: DepositFormProps) => {
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
const { account } = useWeb3React();
|
||||
const openDialog = useWeb3ConnectStore((store) => store.open);
|
||||
const { isActive, account } = useWeb3React();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const {
|
||||
register,
|
||||
@ -83,26 +86,27 @@ export const DepositForm = ({
|
||||
formState: { errors },
|
||||
} = useForm<FormFields>({
|
||||
defaultValues: {
|
||||
from: account,
|
||||
to: pubKey ? pubKey : undefined,
|
||||
asset: selectedAsset?.id || '',
|
||||
},
|
||||
});
|
||||
|
||||
const onDeposit = async (fields: FormFields) => {
|
||||
const onSubmit = async (fields: FormFields) => {
|
||||
if (!selectedAsset || selectedAsset.source.__typename !== 'ERC20') {
|
||||
throw new Error('Invalid asset');
|
||||
}
|
||||
|
||||
submitDeposit({
|
||||
assetSource: selectedAsset.source.contractAddress,
|
||||
amount: fields.amount,
|
||||
vegaPublicKey: fields.to,
|
||||
});
|
||||
if (approved) {
|
||||
submitDeposit({
|
||||
assetSource: selectedAsset.source.contractAddress,
|
||||
amount: fields.amount,
|
||||
vegaPublicKey: fields.to,
|
||||
});
|
||||
} else {
|
||||
submitApprove();
|
||||
}
|
||||
};
|
||||
|
||||
const amount = useWatch({ name: 'amount', control });
|
||||
|
||||
const maxAmount = useMemo(() => {
|
||||
const maxApproved = allowance ? allowance : new BigNumber(0);
|
||||
const maxAvailable = balance ? balance : new BigNumber(0);
|
||||
@ -133,9 +137,12 @@ export const DepositForm = ({
|
||||
return minViableAmount;
|
||||
}, [selectedAsset]);
|
||||
|
||||
const approved = allowance && allowance.isGreaterThan(0) ? true : false;
|
||||
const formState = getFormState(selectedAsset, isActive, approved);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onDeposit)}
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
noValidate={true}
|
||||
data-testid="deposit-form"
|
||||
>
|
||||
@ -143,19 +150,54 @@ export const DepositForm = ({
|
||||
label={t('From (Ethereum address)')}
|
||||
labelFor="ethereum-address"
|
||||
>
|
||||
<Input
|
||||
id="ethereum-address"
|
||||
{...register('from', {
|
||||
<Controller
|
||||
name="from"
|
||||
control={control}
|
||||
rules={{
|
||||
validate: {
|
||||
required,
|
||||
required: (value) => {
|
||||
if (!value) return t('Connect Ethereum wallet');
|
||||
return true;
|
||||
},
|
||||
ethereumAddress,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<EthereumButton
|
||||
clearAddress={() => {
|
||||
setValue('from', '');
|
||||
clearErrors('from');
|
||||
}}
|
||||
defaultValue={account}
|
||||
render={() => {
|
||||
if (isActive && account) {
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
id="ethereum-address"
|
||||
value={account}
|
||||
readOnly={true}
|
||||
disabled={true}
|
||||
{...register('from', {
|
||||
validate: {
|
||||
required,
|
||||
ethereumAddress,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<DisconnectEthereumButton
|
||||
onDisconnect={() => {
|
||||
setValue('from', ''); // clear from value so required ethereum connection validation works
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
onClick={openDialog}
|
||||
variant="primary"
|
||||
fill={true}
|
||||
type="button"
|
||||
data-testid="connect-eth-wallet-btn"
|
||||
>
|
||||
{t('Connect')}
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{errors.from?.message && (
|
||||
@ -241,148 +283,136 @@ export const DepositForm = ({
|
||||
deposited={deposited}
|
||||
balance={balance}
|
||||
asset={selectedAsset}
|
||||
allowance={allowance}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<FormGroup label={t('Amount')} labelFor="amount">
|
||||
<Input
|
||||
type="number"
|
||||
autoComplete="off"
|
||||
id="amount"
|
||||
{...register('amount', {
|
||||
validate: {
|
||||
required,
|
||||
minSafe: (value) => minSafe(new BigNumber(min))(value),
|
||||
maxSafe: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(maxAmount.available)) {
|
||||
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 maxSafe(maxAmount.amount)(v);
|
||||
{formState === 'deposit' && (
|
||||
<FormGroup label={t('Amount')} labelFor="amount">
|
||||
<Input
|
||||
type="number"
|
||||
autoComplete="off"
|
||||
id="amount"
|
||||
{...register('amount', {
|
||||
validate: {
|
||||
required,
|
||||
minSafe: (value) => minSafe(new BigNumber(min))(value),
|
||||
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);
|
||||
if (value.isGreaterThan(maxAmount.available)) {
|
||||
return t('Insufficient amount in Ethereum wallet');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
maxSafe: (v) => {
|
||||
return maxSafe(maxAmount.amount)(v);
|
||||
},
|
||||
},
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.amount?.message && (
|
||||
<InputError intent="danger" forInput="amount">
|
||||
{errors.amount.message}
|
||||
</InputError>
|
||||
)}
|
||||
{selectedAsset && balance && (
|
||||
<UseButton
|
||||
onClick={() => {
|
||||
setValue('amount', balance.toFixed(selectedAsset.decimals));
|
||||
clearErrors('amount');
|
||||
}}
|
||||
>
|
||||
{t('Use maximum')}
|
||||
</UseButton>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormButton
|
||||
selectedAsset={selectedAsset}
|
||||
amount={new BigNumber(amount || 0)}
|
||||
allowance={allowance}
|
||||
onApproveClick={submitApprove}
|
||||
/>
|
||||
})}
|
||||
/>
|
||||
{errors.amount?.message && (
|
||||
<AmountError error={errors.amount} submitApprove={submitApprove} />
|
||||
)}
|
||||
{selectedAsset && balance && (
|
||||
<UseButton
|
||||
onClick={() => {
|
||||
setValue('amount', balance.toFixed(selectedAsset.decimals));
|
||||
clearErrors('amount');
|
||||
}}
|
||||
>
|
||||
{t('Use maximum')}
|
||||
</UseButton>
|
||||
)}
|
||||
</FormGroup>
|
||||
)}
|
||||
<FormButton selectedAsset={selectedAsset} formState={formState} />
|
||||
</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 {
|
||||
selectedAsset?: Asset;
|
||||
amount: BigNumber;
|
||||
allowance: BigNumber | undefined;
|
||||
onApproveClick: () => void;
|
||||
formState: ReturnType<typeof getFormState>;
|
||||
}
|
||||
|
||||
const FormButton = ({
|
||||
selectedAsset,
|
||||
amount,
|
||||
allowance,
|
||||
onApproveClick,
|
||||
}: FormButtonProps) => {
|
||||
const { open, desiredChainId } = useWeb3ConnectStore((store) => ({
|
||||
open: store.open,
|
||||
desiredChainId: store.desiredChainId,
|
||||
}));
|
||||
const FormButton = ({ selectedAsset, formState }: FormButtonProps) => {
|
||||
const { isActive, chainId } = useWeb3React();
|
||||
const approved =
|
||||
allowance && allowance.isGreaterThan(0) && amount.isLessThan(allowance);
|
||||
let button = null;
|
||||
let message: ReactNode = '';
|
||||
|
||||
if (!isActive) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
const desiredChainId = useWeb3ConnectStore((store) => store.desiredChainId);
|
||||
const submitText =
|
||||
formState === 'approve'
|
||||
? t(`Approve ${selectedAsset ? selectedAsset.symbol : ''}`)
|
||||
: t('Deposit');
|
||||
const invalidChain = isActive && chainId !== desiredChainId;
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{message && <p className="text-center">{message}</p>}
|
||||
{button}
|
||||
</div>
|
||||
<>
|
||||
{formState === 'approve' && (
|
||||
<div className="mb-2">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId="approve-warning"
|
||||
message={t(`Deposits of ${selectedAsset?.symbol} not approved`)}
|
||||
/>
|
||||
</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 openDialog = useWeb3ConnectStore((state) => state.open);
|
||||
const { isActive, connector } = useWeb3React();
|
||||
const DisconnectEthereumButton = ({
|
||||
onDisconnect,
|
||||
}: {
|
||||
onDisconnect: () => void;
|
||||
}) => {
|
||||
const { connector } = useWeb3React();
|
||||
const [, , removeEagerConnector] = useLocalStorage(ETHEREUM_EAGER_CONNECT);
|
||||
|
||||
if (!isActive) {
|
||||
return <UseButton onClick={openDialog}>{t('Connect')}</UseButton>;
|
||||
}
|
||||
|
||||
return (
|
||||
<UseButton
|
||||
onClick={() => {
|
||||
connector.deactivate();
|
||||
clearAddress();
|
||||
removeEagerConnector();
|
||||
onDisconnect();
|
||||
}}
|
||||
data-testid="disconnect-ethereum-wallet"
|
||||
>
|
||||
@ -420,3 +449,14 @@ const EthereumButton = ({ clearAddress }: { clearAddress: () => void }) => {
|
||||
</UseButton>
|
||||
);
|
||||
};
|
||||
|
||||
const getFormState = (
|
||||
selectedAsset: Asset | undefined,
|
||||
isActive: boolean,
|
||||
approved: boolean
|
||||
) => {
|
||||
if (!selectedAsset) return 'deposit';
|
||||
if (!isActive) return 'deposit';
|
||||
if (approved) return 'deposit';
|
||||
return 'approve';
|
||||
};
|
||||
|
@ -11,6 +11,7 @@ interface DepositLimitsProps {
|
||||
deposited: BigNumber;
|
||||
asset: Asset;
|
||||
balance?: BigNumber;
|
||||
allowance?: BigNumber;
|
||||
}
|
||||
|
||||
export const DepositLimits = ({
|
||||
@ -18,6 +19,7 @@ export const DepositLimits = ({
|
||||
deposited,
|
||||
asset,
|
||||
balance,
|
||||
allowance,
|
||||
}: DepositLimitsProps) => {
|
||||
const limits = [
|
||||
{
|
||||
@ -44,6 +46,12 @@ export const DepositLimits = ({
|
||||
rawValue: max.minus(deposited),
|
||||
value: compactNumber(max.minus(deposited), asset.decimals),
|
||||
},
|
||||
{
|
||||
key: 'ALLOWANCE',
|
||||
label: t('Approved'),
|
||||
rawValue: allowance,
|
||||
value: allowance ? compactNumber(allowance, asset.decimals) : '-',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
@ -27,12 +27,13 @@ export const AssetProposalNotification = ({
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
message={message}
|
||||
testId="asset-proposal-notification"
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="mb-2">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
message={message}
|
||||
testId="asset-proposal-notification"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,6 @@ export const MarketProposalNotification = ({
|
||||
intent={Intent.Warning}
|
||||
message={message}
|
||||
testId="market-proposal-notification"
|
||||
className="px-2 py-1"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -19,7 +19,6 @@ type NotificationProps = {
|
||||
size?: ButtonSize;
|
||||
};
|
||||
testId?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const getIcon = (intent: Intent): IconName => {
|
||||
@ -39,7 +38,6 @@ export const Notification = ({
|
||||
title,
|
||||
testId,
|
||||
buttonProps,
|
||||
className,
|
||||
}: NotificationProps) => {
|
||||
return (
|
||||
<div
|
||||
@ -61,8 +59,7 @@ export const Notification = ({
|
||||
intent === Intent.Warning,
|
||||
'bg-vega-pink-300 dark:bg-vega-pink-650': intent === Intent.Danger,
|
||||
},
|
||||
'border rounded p-2 flex items-start gap-2.5 my-4',
|
||||
className
|
||||
'border rounded p-2 flex items-start gap-2.5'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
Loading…
Reference in New Issue
Block a user