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 { Option } from '@vegaprotocol/ui-toolkit';
|
||||||
import type { AssetFieldsFragment } from './__generated__/Asset';
|
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 (
|
return (
|
||||||
<Option key={asset.id} value={asset.id}>
|
<Option key={asset.id} value={asset.id}>
|
||||||
<div className="flex flex-col items-start">
|
<div className="flex flex-col items-start">
|
||||||
<span>{asset.name}</span>
|
<div className="flex flex-row align-baseline gap-2">
|
||||||
<div className="text-[10px] font-mono w-full text-left break-all">
|
<span>{asset.name}</span>{' '}
|
||||||
<span className="text-gray-500">{asset.id} -</span> {asset.symbol}
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</Option>
|
</Option>
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
export * from './__generated__/Asset';
|
export * from './__generated__/Asset';
|
||||||
export * from './__generated__/Assets';
|
export * from './__generated__/Assets';
|
||||||
export * from './asset-data-provider';
|
export * from './asset-data-provider';
|
||||||
export * from './assets-data-provider';
|
|
||||||
export * from './asset-details-dialog';
|
export * from './asset-details-dialog';
|
||||||
export * from './asset-details-table';
|
export * from './asset-details-table';
|
||||||
export * from './asset-option';
|
export * from './asset-option';
|
||||||
|
export * from './assets-data-provider';
|
||||||
export * from './constants';
|
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 { FaucetNotification } from './faucet-notification';
|
||||||
import { ApproveNotification } from './approve-notification';
|
import { ApproveNotification } from './approve-notification';
|
||||||
import { usePersistentDeposit } from './use-persistent-deposit';
|
import { usePersistentDeposit } from './use-persistent-deposit';
|
||||||
|
import { AssetBalance } from './asset-balance';
|
||||||
|
|
||||||
interface FormFields {
|
interface FormFields {
|
||||||
asset: string;
|
asset: string;
|
||||||
@ -253,9 +254,15 @@ export const DepositForm = ({
|
|||||||
value={selectedAsset?.id}
|
value={selectedAsset?.id}
|
||||||
hasError={Boolean(errors.asset?.message)}
|
hasError={Boolean(errors.asset?.message)}
|
||||||
>
|
>
|
||||||
{assets.filter(isAssetTypeERC20).map((a) => (
|
{assets
|
||||||
<AssetOption asset={a} key={a.id} />
|
.filter((asset) => isAssetTypeERC20(asset))
|
||||||
))}
|
.map((asset) => (
|
||||||
|
<AssetOption
|
||||||
|
asset={asset}
|
||||||
|
key={asset.id}
|
||||||
|
balance={<AssetBalance asset={asset} />}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</RichSelect>
|
</RichSelect>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
export * from './__generated__/Deposit';
|
export * from './__generated__/Deposit';
|
||||||
|
export * from './asset-balance';
|
||||||
export * from './deposit-container';
|
export * from './deposit-container';
|
||||||
|
export * from './deposit-dialog';
|
||||||
export * from './deposit-form';
|
export * from './deposit-form';
|
||||||
export * from './deposit-limits';
|
export * from './deposit-limits';
|
||||||
export * from './deposit-manager';
|
export * from './deposit-manager';
|
||||||
|
export * from './deposits-provider';
|
||||||
export * from './deposits-table';
|
export * from './deposits-table';
|
||||||
export * from './use-deposit-balances';
|
export * from './use-deposit-balances';
|
||||||
export * from './deposits-provider';
|
|
||||||
export * from './use-get-allowance';
|
export * from './use-get-allowance';
|
||||||
export * from './use-get-balance-of-erc20-token';
|
export * from './use-get-balance-of-erc20-token';
|
||||||
export * from './use-get-deposit-maximum';
|
export * from './use-get-deposit-maximum';
|
||||||
export * from './use-get-deposited-amount';
|
export * from './use-get-deposited-amount';
|
||||||
export * from './use-submit-approval';
|
export * from './use-submit-approval';
|
||||||
export * from './use-submit-faucet';
|
export * from './use-submit-faucet';
|
||||||
export * from './deposit-dialog';
|
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
} from '@vegaprotocol/web3';
|
} 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 { useBalancesStore } from '@vegaprotocol/assets';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export const useSubmitFaucet = (
|
export const useSubmitFaucet = (
|
||||||
@ -25,8 +26,9 @@ export const useSubmitFaucet = (
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tx?.status === EthTxStatus.Confirmed) {
|
if (tx?.status === EthTxStatus.Confirmed) {
|
||||||
getBalances();
|
getBalances();
|
||||||
|
if (asset) useBalancesStore.getState().refetch(asset.id);
|
||||||
}
|
}
|
||||||
}, [tx?.status, getBalances]);
|
}, [tx?.status, getBalances, asset]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
|
@ -7,7 +7,7 @@ interface Asset {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ERC20Asset {
|
export interface ERC20Asset {
|
||||||
__typename?: 'Asset';
|
__typename?: 'Asset';
|
||||||
source: {
|
source: {
|
||||||
__typename: 'ERC20';
|
__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 { render, screen, act, waitFor } from '@testing-library/react';
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
import type { Account } from '@vegaprotocol/accounts';
|
import type { Account } from '@vegaprotocol/accounts';
|
||||||
|
import { useAccountBalance } from '@vegaprotocol/accounts';
|
||||||
import { WithdrawFormContainer } from './withdraw-form-container';
|
import { WithdrawFormContainer } from './withdraw-form-container';
|
||||||
import * as Types from '@vegaprotocol/types';
|
import * as Types from '@vegaprotocol/types';
|
||||||
import { useWeb3React } from '@web3-react/core';
|
import { useWeb3React } from '@web3-react/core';
|
||||||
@ -12,6 +13,7 @@ jest.mock('@vegaprotocol/react-helpers', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
jest.mock('@web3-react/core');
|
jest.mock('@web3-react/core');
|
||||||
|
jest.mock('@vegaprotocol/accounts');
|
||||||
|
|
||||||
describe('WithdrawFormContainer', () => {
|
describe('WithdrawFormContainer', () => {
|
||||||
const props = {
|
const props = {
|
||||||
@ -91,6 +93,10 @@ describe('WithdrawFormContainer', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(useWeb3React as jest.Mock).mockReturnValue({ account: MOCK_ETH_ADDRESS });
|
(useWeb3React as jest.Mock).mockReturnValue({ account: MOCK_ETH_ADDRESS });
|
||||||
|
(useAccountBalance as jest.Mock).mockReturnValue({
|
||||||
|
accountBalance: 0,
|
||||||
|
accountDecimals: null,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
|
@ -8,6 +8,11 @@ import type { Asset } from '@vegaprotocol/assets';
|
|||||||
|
|
||||||
jest.mock('@web3-react/core');
|
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';
|
const MOCK_ETH_ADDRESS = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
|
||||||
|
|
||||||
let assets: Asset[];
|
let assets: Asset[];
|
||||||
|
@ -33,6 +33,7 @@ import {
|
|||||||
useWeb3ConnectStore,
|
useWeb3ConnectStore,
|
||||||
useWeb3Disconnect,
|
useWeb3Disconnect,
|
||||||
} from '@vegaprotocol/web3';
|
} from '@vegaprotocol/web3';
|
||||||
|
import { AssetBalance } from './asset-balance';
|
||||||
|
|
||||||
interface FormFields {
|
interface FormFields {
|
||||||
asset: string;
|
asset: string;
|
||||||
@ -154,7 +155,11 @@ export const WithdrawForm = ({
|
|||||||
hasError={Boolean(errors.asset?.message)}
|
hasError={Boolean(errors.asset?.message)}
|
||||||
>
|
>
|
||||||
{assets.filter(isAssetTypeERC20).map((a) => (
|
{assets.filter(isAssetTypeERC20).map((a) => (
|
||||||
<AssetOption key={a.id} asset={a} />
|
<AssetOption
|
||||||
|
key={a.id}
|
||||||
|
asset={a}
|
||||||
|
balance={<AssetBalance asset={a} />}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</RichSelect>
|
</RichSelect>
|
||||||
);
|
);
|
||||||
|
@ -25,6 +25,11 @@ jest.mock('./use-withdraw-asset', () => ({
|
|||||||
useWithdrawAsset: () => withdrawAsset,
|
useWithdrawAsset: () => withdrawAsset,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('@vegaprotocol/accounts', () => ({
|
||||||
|
...jest.requireActual('@vegaprotocol/accounts'),
|
||||||
|
useAccountBalance: jest.fn(() => ({ accountBalance: 0, accountDecimals: 0 })),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('@vegaprotocol/web3', () => ({
|
jest.mock('@vegaprotocol/web3', () => ({
|
||||||
...jest.requireActual('@vegaprotocol/web3'),
|
...jest.requireActual('@vegaprotocol/web3'),
|
||||||
useGetWithdrawThreshold: () => {
|
useGetWithdrawThreshold: () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user