feat(trading): asset balances inside asset selector (deposit, withdraw form) (#3590)
This commit is contained in:
parent
ce22da1c9a
commit
a9267de653
@ -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>
|
||||
|
@ -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';
|
||||
|
69
libs/assets/src/lib/use-balances-store.ts
Normal file
69
libs/assets/src/lib/use-balances-store.ts
Normal 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?.();
|
||||
},
|
||||
}))
|
||||
);
|
42
libs/deposits/src/lib/asset-balance.tsx
Normal file
42
libs/deposits/src/lib/asset-balance.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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,8 +254,14 @@ 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>
|
||||
)}
|
||||
|
@ -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';
|
||||
|
@ -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,
|
||||
|
@ -7,7 +7,7 @@ interface Asset {
|
||||
};
|
||||
}
|
||||
|
||||
interface ERC20Asset {
|
||||
export interface ERC20Asset {
|
||||
__typename?: 'Asset';
|
||||
source: {
|
||||
__typename: 'ERC20';
|
||||
|
30
libs/withdraws/src/lib/asset-balance.tsx
Normal file
30
libs/withdraws/src/lib/asset-balance.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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();
|
||||
|
@ -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[];
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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: () => {
|
||||
|
Loading…
Reference in New Issue
Block a user