feat(trading): asset balances inside asset selector (deposit, withdraw form) (#3590)

This commit is contained in:
Art 2023-05-03 21:35:05 +02:00 committed by GitHub
parent ce22da1c9a
commit a9267de653
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 223 additions and 13 deletions

View File

@ -1,13 +1,50 @@
import { Option } from '@vegaprotocol/ui-toolkit';
import type { AssetFieldsFragment } from './__generated__/Asset';
import classNames from 'classnames';
import { t } from '@vegaprotocol/i18n';
import type { ReactNode } from 'react';
export const AssetOption = ({ asset }: { asset: AssetFieldsFragment }) => {
type AssetOptionProps = {
asset: AssetFieldsFragment;
balance?: ReactNode;
};
export const Balance = ({
balance,
symbol,
}: {
balance?: string;
symbol: string;
}) =>
balance ? (
<div className="mt-1 font-alpha">
{balance} {symbol}
</div>
) : (
<div className="text-vega-orange-500">{t('Fetching balance…')}</div>
);
export const AssetOption = ({ asset, balance }: AssetOptionProps) => {
return (
<Option key={asset.id} value={asset.id}>
<div className="flex flex-col items-start">
<span>{asset.name}</span>
<div className="text-[10px] font-mono w-full text-left break-all">
<span className="text-gray-500">{asset.id} -</span> {asset.symbol}
<div className="flex flex-row align-baseline gap-2">
<span>{asset.name}</span>{' '}
<span
className={classNames(
'bg-vega-light-200 dark:bg-vega-dark-200',
'text-black dark:text-white text-xs',
'py-[2px] px-[4px] rounded'
)}
>
{asset.symbol}
</span>
</div>
{balance}
<div className="text-[12px] font-mono w-full text-left break-all">
<span className="text-vega-light-300 dark:text-vega-dark-300">
{asset.id}
</span>
</div>
</div>
</Option>

View File

@ -1,8 +1,9 @@
export * from './__generated__/Asset';
export * from './__generated__/Assets';
export * from './asset-data-provider';
export * from './assets-data-provider';
export * from './asset-details-dialog';
export * from './asset-details-table';
export * from './asset-option';
export * from './assets-data-provider';
export * from './constants';
export * from './use-balances-store';

View File

@ -0,0 +1,69 @@
import type BigNumber from 'bignumber.js';
import type { AssetFieldsFragment } from './__generated__/Asset';
import { create } from 'zustand';
import * as Sentry from '@sentry/react';
import { immer } from 'zustand/middleware/immer';
type AssetWithBalance = {
asset: AssetFieldsFragment;
balanceOnEth?: BigNumber;
balanceOnVega?: BigNumber;
updatedAt: number;
};
type SetBalanceArgs = Omit<AssetWithBalance, 'updatedAt'> & {
ethBalanceFetcher?: () => Promise<BigNumber | undefined>;
};
type BalancesStore = {
balances: (AssetWithBalance & { fetchFromEth?: () => void })[];
getBalance: (assetId: string) => AssetWithBalance | undefined;
setBalance: (args: SetBalanceArgs) => void;
refetch: (assetId: string) => void;
};
export const useBalancesStore = create(
immer<BalancesStore>((set, get) => ({
balances: [],
getBalance: (assetId) =>
get().balances.find(({ asset: a }) => a.id === assetId),
setBalance: ({ asset, balanceOnEth, balanceOnVega, ethBalanceFetcher }) => {
set((state) => {
const found = state.balances.find(({ asset: a }) => a.id === asset.id);
const fetchFromEth = ethBalanceFetcher
? () => {
if (!ethBalanceFetcher) return;
ethBalanceFetcher()
.then((balance) => {
if (balance) {
get().setBalance({ asset, balanceOnEth: balance });
}
})
.catch((err) => {
Sentry.captureException(err);
});
}
: undefined;
if (found) {
if (balanceOnEth) found.balanceOnEth = balanceOnEth;
if (balanceOnVega) found.balanceOnVega = balanceOnVega;
if (fetchFromEth) found.fetchFromEth = fetchFromEth;
found.updatedAt = Date.now();
} else {
state.balances.push({
asset,
balanceOnEth,
balanceOnVega,
fetchFromEth,
updatedAt: Date.now(),
});
}
});
},
refetch: (assetId) => {
const found = get().balances.find((a) => a.asset.id === assetId);
found?.fetchFromEth?.();
},
}))
);

View File

@ -0,0 +1,42 @@
import { useCallback, useEffect } from 'react';
import { useGetBalanceOfERC20Token } from './use-get-balance-of-erc20-token';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { useBalancesStore } from '@vegaprotocol/assets';
import { Balance } from '@vegaprotocol/assets';
import { isAssetTypeERC20 } from '@vegaprotocol/utils';
import { useTokenContract } from '@vegaprotocol/web3';
const REFETCH_DELAY = 5000;
export const AssetBalance = ({ asset }: { asset: AssetFieldsFragment }) => {
const [setBalance, getBalance] = useBalancesStore((state) => [
state.setBalance,
state.getBalance,
]);
const tokenContract = useTokenContract(
isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined
);
const ethBalanceFetcher = useGetBalanceOfERC20Token(tokenContract, asset);
const fetchFromEth = useCallback(async () => {
const balance = await ethBalanceFetcher();
if (balance) {
setBalance({ asset, balanceOnEth: balance, ethBalanceFetcher });
}
}, [asset, ethBalanceFetcher, setBalance]);
useEffect(() => {
const balance = getBalance(asset.id);
if (!balance || Date.now() - balance.updatedAt > REFETCH_DELAY) {
fetchFromEth();
}
}, [asset.id, fetchFromEth, getBalance]);
return (
<Balance
balance={getBalance(asset.id)?.balanceOnEth?.toString()}
symbol={asset.symbol}
/>
);
};

View File

@ -41,6 +41,7 @@ import type { DepositBalances } from './use-deposit-balances';
import { FaucetNotification } from './faucet-notification';
import { ApproveNotification } from './approve-notification';
import { usePersistentDeposit } from './use-persistent-deposit';
import { AssetBalance } from './asset-balance';
interface FormFields {
asset: string;
@ -253,9 +254,15 @@ export const DepositForm = ({
value={selectedAsset?.id}
hasError={Boolean(errors.asset?.message)}
>
{assets.filter(isAssetTypeERC20).map((a) => (
<AssetOption asset={a} key={a.id} />
))}
{assets
.filter((asset) => isAssetTypeERC20(asset))
.map((asset) => (
<AssetOption
asset={asset}
key={asset.id}
balance={<AssetBalance asset={asset} />}
/>
))}
</RichSelect>
)}
/>

View File

@ -1,15 +1,16 @@
export * from './__generated__/Deposit';
export * from './asset-balance';
export * from './deposit-container';
export * from './deposit-dialog';
export * from './deposit-form';
export * from './deposit-limits';
export * from './deposit-manager';
export * from './deposits-provider';
export * from './deposits-table';
export * from './use-deposit-balances';
export * from './deposits-provider';
export * from './use-get-allowance';
export * from './use-get-balance-of-erc20-token';
export * from './use-get-deposit-maximum';
export * from './use-get-deposited-amount';
export * from './use-submit-approval';
export * from './use-submit-faucet';
export * from './deposit-dialog';

View File

@ -5,6 +5,7 @@ import {
} from '@vegaprotocol/web3';
import { isAssetTypeERC20 } from '@vegaprotocol/utils';
import type { Asset } from '@vegaprotocol/assets';
import { useBalancesStore } from '@vegaprotocol/assets';
import { useEffect, useState } from 'react';
export const useSubmitFaucet = (
@ -25,8 +26,9 @@ export const useSubmitFaucet = (
useEffect(() => {
if (tx?.status === EthTxStatus.Confirmed) {
getBalances();
if (asset) useBalancesStore.getState().refetch(asset.id);
}
}, [tx?.status, getBalances]);
}, [tx?.status, getBalances, asset]);
return {
id,

View File

@ -7,7 +7,7 @@ interface Asset {
};
}
interface ERC20Asset {
export interface ERC20Asset {
__typename?: 'Asset';
source: {
__typename: 'ERC20';

View File

@ -0,0 +1,30 @@
import { useAccountBalance } from '@vegaprotocol/accounts';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { useBalancesStore } from '@vegaprotocol/assets';
import { Balance } from '@vegaprotocol/assets';
import { addDecimal } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
import { useEffect } from 'react';
export const AssetBalance = ({ asset }: { asset: AssetFieldsFragment }) => {
const [setBalance, getBalance] = useBalancesStore((state) => [
state.setBalance,
state.getBalance,
]);
const { accountBalance, accountDecimals } = useAccountBalance(asset?.id);
useEffect(() => {
const balance =
accountBalance && accountDecimals
? new BigNumber(addDecimal(accountBalance, accountDecimals))
: undefined;
setBalance({ asset, balanceOnVega: balance });
}, [accountBalance, accountDecimals, asset, setBalance]);
return (
<Balance
balance={getBalance(asset.id)?.balanceOnVega?.toString()}
symbol={asset.symbol}
/>
);
};

View File

@ -1,6 +1,7 @@
import { render, screen, act, waitFor } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import type { Account } from '@vegaprotocol/accounts';
import { useAccountBalance } from '@vegaprotocol/accounts';
import { WithdrawFormContainer } from './withdraw-form-container';
import * as Types from '@vegaprotocol/types';
import { useWeb3React } from '@web3-react/core';
@ -12,6 +13,7 @@ jest.mock('@vegaprotocol/react-helpers', () => ({
}),
}));
jest.mock('@web3-react/core');
jest.mock('@vegaprotocol/accounts');
describe('WithdrawFormContainer', () => {
const props = {
@ -91,6 +93,10 @@ describe('WithdrawFormContainer', () => {
beforeEach(() => {
(useWeb3React as jest.Mock).mockReturnValue({ account: MOCK_ETH_ADDRESS });
(useAccountBalance as jest.Mock).mockReturnValue({
accountBalance: 0,
accountDecimals: null,
});
});
afterEach(() => {
jest.resetAllMocks();

View File

@ -8,6 +8,11 @@ import type { Asset } from '@vegaprotocol/assets';
jest.mock('@web3-react/core');
jest.mock('@vegaprotocol/accounts', () => ({
...jest.requireActual('@vegaprotocol/accounts'),
useAccountBalance: jest.fn(() => ({ accountBalance: 0, accountDecimals: 0 })),
}));
const MOCK_ETH_ADDRESS = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
let assets: Asset[];

View File

@ -33,6 +33,7 @@ import {
useWeb3ConnectStore,
useWeb3Disconnect,
} from '@vegaprotocol/web3';
import { AssetBalance } from './asset-balance';
interface FormFields {
asset: string;
@ -154,7 +155,11 @@ export const WithdrawForm = ({
hasError={Boolean(errors.asset?.message)}
>
{assets.filter(isAssetTypeERC20).map((a) => (
<AssetOption key={a.id} asset={a} />
<AssetOption
key={a.id}
asset={a}
balance={<AssetBalance asset={a} />}
/>
))}
</RichSelect>
);

View File

@ -25,6 +25,11 @@ jest.mock('./use-withdraw-asset', () => ({
useWithdrawAsset: () => withdrawAsset,
}));
jest.mock('@vegaprotocol/accounts', () => ({
...jest.requireActual('@vegaprotocol/accounts'),
useAccountBalance: jest.fn(() => ({ accountBalance: 0, accountDecimals: 0 })),
}));
jest.mock('@vegaprotocol/web3', () => ({
...jest.requireActual('@vegaprotocol/web3'),
useGetWithdrawThreshold: () => {