refacotr: deposit manager (#867)

* refactor: deposit manager with a zustand store and refetching balances after contracts complete

* refactor: remove assetId query string functionality

* chore: remove unused import

* chore: add a comment with a link to code explanation

* refactor: capture errors from deposit value get functions

* refactor: add error handling for async perform funcs

* feat: add assets to react helpers for types and erc20 check
This commit is contained in:
Matthew Russell 2022-07-28 13:23:59 +01:00 committed by GitHub
parent 3498b9d54b
commit 11be7aaa8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 444 additions and 331 deletions

View File

@ -18,6 +18,7 @@ import type {
} from './__generated__/Delegations'; } from './__generated__/Delegations';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { useContracts } from '../../contexts/contracts/contracts-context'; import { useContracts } from '../../contexts/contracts/contracts-context';
import { isAssetTypeERC20 } from '@vegaprotocol/react-helpers';
const DELEGATIONS_QUERY = gql` const DELEGATIONS_QUERY = gql`
query Delegations($partyId: ID!) { query Delegations($partyId: ID!) {
@ -117,7 +118,7 @@ export const usePollForDelegations = () => {
.filter((a) => a.type === AccountType.General) .filter((a) => a.type === AccountType.General)
.map((a) => { .map((a) => {
const isVega = const isVega =
a.asset.source.__typename === 'ERC20' && isAssetTypeERC20(a.asset) &&
a.asset.source.contractAddress === vegaToken.address; a.asset.source.contractAddress === vegaToken.address;
return { return {
@ -131,8 +132,7 @@ export const usePollForDelegations = () => {
), ),
image: isVega ? vegaBlack : noIcon, image: isVega ? vegaBlack : noIcon,
border: isVega, border: isVega,
address: address: isAssetTypeERC20(a.asset)
a.asset.source.__typename === 'ERC20'
? a.asset.source.contractAddress ? a.asset.source.contractAddress
: undefined, : undefined,
}; };

View File

@ -16,14 +16,10 @@ const DEPOSIT_PAGE_QUERY = gql`
} }
`; `;
interface DepositContainerProps {
assetId?: string;
}
/** /**
* Fetches data required for the Deposit page * Fetches data required for the Deposit page
*/ */
export const DepositContainer = ({ assetId }: DepositContainerProps) => { export const DepositContainer = () => {
const { VEGA_ENV } = useEnvironment(); const { VEGA_ENV } = useEnvironment();
return ( return (
@ -41,7 +37,6 @@ export const DepositContainer = ({ assetId }: DepositContainerProps) => {
return ( return (
<DepositManager <DepositManager
assets={data.assets} assets={data.assets}
initialAssetId={assetId}
isFaucetable={VEGA_ENV !== 'MAINNET'} isFaucetable={VEGA_ENV !== 'MAINNET'}
/> />
); );

View File

@ -1,29 +1,12 @@
import { useRouter } from 'next/router';
import { useMemo } from 'react';
import { Web3Container } from '../../../components/web3-container'; import { Web3Container } from '../../../components/web3-container';
import { DepositContainer } from './deposit-container'; import { DepositContainer } from './deposit-container';
const Deposit = () => { const Deposit = () => {
const { query } = useRouter();
// AssetId can be specified in the query string to allow link to deposit a particular asset
const assetId = useMemo(() => {
if (query.assetId && Array.isArray(query.assetId)) {
return undefined;
}
if (Array.isArray(query.assetId)) {
return undefined;
}
return query.assetId;
}, [query]);
return ( return (
<Web3Container> <Web3Container>
<div className="max-w-[420px] p-24 mx-auto"> <div className="max-w-[420px] p-24 mx-auto">
<h1 className="text-h3 mb-12">Deposit</h1> <h1 className="text-h3 mb-12">Deposit</h1>
<DepositContainer assetId={assetId} /> <DepositContainer />
</div> </div>
</Web3Container> </Web3Container>
); );

View File

@ -37,10 +37,8 @@ beforeEach(() => {
submitApprove: jest.fn(), submitApprove: jest.fn(),
submitDeposit: jest.fn(), submitDeposit: jest.fn(),
requestFaucet: jest.fn(), requestFaucet: jest.fn(),
limits: {
max: new BigNumber(20), max: new BigNumber(20),
deposited: new BigNumber(10), deposited: new BigNumber(10),
},
allowance: new BigNumber(30), allowance: new BigNumber(30),
isFaucetable: true, isFaucetable: true,
}; };
@ -134,7 +132,8 @@ describe('Deposit form', () => {
<DepositForm <DepositForm
{...props} {...props}
balance={new BigNumber(100)} balance={new BigNumber(100)}
limits={{ max: new BigNumber(100), deposited: new BigNumber(10) }} max={new BigNumber(100)}
deposited={new BigNumber(10)}
/> />
); );
@ -213,18 +212,16 @@ describe('Deposit form', () => {
const mockUseWeb3React = useWeb3React as jest.Mock; const mockUseWeb3React = useWeb3React as jest.Mock;
mockUseWeb3React.mockReturnValue({ account }); mockUseWeb3React.mockReturnValue({ account });
const limits = {
max: new BigNumber(20),
deposited: new BigNumber(10),
};
const balance = new BigNumber(50); const balance = new BigNumber(50);
const max = new BigNumber(20);
const deposited = new BigNumber(10);
render( render(
<DepositForm <DepositForm
{...props} {...props}
allowance={new BigNumber(100)} allowance={new BigNumber(100)}
balance={balance} balance={balance}
limits={limits} max={max}
deposited={deposited}
selectedAsset={asset} selectedAsset={asset}
/> />
); );
@ -237,13 +234,13 @@ describe('Deposit form', () => {
expect( expect(
screen.getByText('Maximum total deposit amount', { selector: 'th' }) screen.getByText('Maximum total deposit amount', { selector: 'th' })
.nextElementSibling .nextElementSibling
).toHaveTextContent(limits.max.toString()); ).toHaveTextContent(max.toString());
expect( expect(
screen.getByText('Deposited', { selector: 'th' }).nextElementSibling screen.getByText('Deposited', { selector: 'th' }).nextElementSibling
).toHaveTextContent(limits.deposited.toString()); ).toHaveTextContent(deposited.toString());
expect( expect(
screen.getByText('Remaining', { selector: 'th' }).nextElementSibling screen.getByText('Remaining', { selector: 'th' }).nextElementSibling
).toHaveTextContent(limits.max.minus(limits.deposited).toString()); ).toHaveTextContent(max.minus(deposited).toString());
fireEvent.change(screen.getByLabelText('Amount'), { fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '8' }, target: { value: '8' },
@ -257,7 +254,7 @@ describe('Deposit form', () => {
expect(props.submitDeposit).toHaveBeenCalledWith({ expect(props.submitDeposit).toHaveBeenCalledWith({
// @ts-ignore contract address definitely defined // @ts-ignore contract address definitely defined
assetSource: asset.source.contractAddress, assetSource: asset.source.contractAddress,
amount: '800', amount: '8',
vegaPublicKey: vegaKey, vegaPublicKey: vegaKey,
}); });
}); });

View File

@ -1,5 +1,5 @@
import type { Asset } from '@vegaprotocol/react-helpers';
import { import {
removeDecimal,
ethereumAddress, ethereumAddress,
t, t,
required, required,
@ -7,6 +7,7 @@ import {
minSafe, minSafe,
maxSafe, maxSafe,
addDecimal, addDecimal,
isAssetTypeERC20,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import { import {
Button, Button,
@ -21,10 +22,9 @@ import { useWeb3React } from '@web3-react/core';
import { Web3WalletInput } from '@vegaprotocol/web3'; import { Web3WalletInput } from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useMemo, useEffect } from 'react'; import { useMemo } from 'react';
import { useForm, useWatch } from 'react-hook-form'; import { Controller, useForm, useWatch } from 'react-hook-form';
import { DepositLimits } from './deposit-limits'; import { DepositLimits } from './deposit-limits';
import type { Asset } from './deposit-manager';
interface FormFields { interface FormFields {
asset: string; asset: string;
@ -45,10 +45,8 @@ export interface DepositFormProps {
vegaPublicKey: string; vegaPublicKey: string;
}) => void; }) => void;
requestFaucet: () => void; requestFaucet: () => void;
limits: { max: BigNumber | undefined;
max: BigNumber; deposited: BigNumber | undefined;
deposited: BigNumber;
} | null;
allowance: BigNumber | undefined; allowance: BigNumber | undefined;
isFaucetable?: boolean; isFaucetable?: boolean;
} }
@ -58,10 +56,11 @@ export const DepositForm = ({
selectedAsset, selectedAsset,
onSelectAsset, onSelectAsset,
balance, balance,
max,
deposited,
submitApprove, submitApprove,
submitDeposit, submitDeposit,
requestFaucet, requestFaucet,
limits,
allowance, allowance,
isFaucetable, isFaucetable,
}: DepositFormProps) => { }: DepositFormProps) => {
@ -89,15 +88,14 @@ export const DepositForm = ({
submitDeposit({ submitDeposit({
assetSource: selectedAsset.source.contractAddress, assetSource: selectedAsset.source.contractAddress,
amount: removeDecimal(fields.amount, selectedAsset.decimals), amount: fields.amount,
vegaPublicKey: fields.to, vegaPublicKey: fields.to,
}); });
}; };
const assetId = useWatch({ name: 'asset', control });
const amount = useWatch({ name: 'amount', control }); const amount = useWatch({ name: 'amount', control });
const max = 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);
@ -106,8 +104,8 @@ export const DepositForm = ({
let maxLimit = new BigNumber(Infinity); let maxLimit = new BigNumber(Infinity);
// A max limit of zero indicates that there is no limit // A max limit of zero indicates that there is no limit
if (limits && limits.max.isGreaterThan(0)) { if (max && deposited && max.isGreaterThan(0)) {
maxLimit = limits.max.minus(limits.deposited); maxLimit = max.minus(deposited);
} }
return { return {
@ -116,7 +114,7 @@ export const DepositForm = ({
limit: maxLimit, limit: maxLimit,
amount: BigNumber.minimum(maxLimit, maxApproved, maxAvailable), amount: BigNumber.minimum(maxLimit, maxApproved, maxAvailable),
}; };
}, [limits, allowance, balance]); }, [max, deposited, allowance, balance]);
const min = useMemo(() => { const min = useMemo(() => {
// Min viable amount given asset decimals EG for WEI 0.000000000000000001 // Min viable amount given asset decimals EG for WEI 0.000000000000000001
@ -127,10 +125,6 @@ export const DepositForm = ({
return minViableAmount; return minViableAmount;
}, [selectedAsset]); }, [selectedAsset]);
useEffect(() => {
onSelectAsset(assetId);
}, [assetId, onSelectAsset]);
return ( return (
<form <form
onSubmit={handleSubmit(onDeposit)} onSubmit={handleSubmit(onDeposit)}
@ -154,16 +148,28 @@ export const DepositForm = ({
)} )}
</FormGroup> </FormGroup>
<FormGroup label={t('Asset')} labelFor="asset" className="relative"> <FormGroup label={t('Asset')} labelFor="asset" className="relative">
<Select {...register('asset', { validate: { required } })} id="asset"> <Controller
control={control}
name="asset"
rules={{ validate: { required } }}
render={({ field }) => (
<Select
id="asset"
{...field}
onChange={(e) => {
field.onChange(e);
onSelectAsset(e.target.value);
}}
>
<option value="">{t('Please select')}</option> <option value="">{t('Please select')}</option>
{assets {assets.filter(isAssetTypeERC20).map((a) => (
.filter((a) => a.source.__typename === 'ERC20')
.map((a) => (
<option key={a.id} value={a.id}> <option key={a.id} value={a.id}>
{a.name} {a.name}
</option> </option>
))} ))}
</Select> </Select>
)}
/>
{errors.asset?.message && ( {errors.asset?.message && (
<InputError intent="danger" className="mt-4" forInput="asset"> <InputError intent="danger" className="mt-4" forInput="asset">
{errors.asset.message} {errors.asset.message}
@ -196,9 +202,9 @@ export const DepositForm = ({
</UseButton> </UseButton>
)} )}
</FormGroup> </FormGroup>
{selectedAsset && limits && ( {selectedAsset && max && deposited && (
<div className="mb-20"> <div className="mb-20">
<DepositLimits limits={limits} balance={balance} /> <DepositLimits max={max} deposited={deposited} balance={balance} />
</div> </div>
)} )}
<FormGroup label={t('Amount')} labelFor="amount" className="relative"> <FormGroup label={t('Amount')} labelFor="amount" className="relative">
@ -212,14 +218,14 @@ export const DepositForm = ({
minSafe: (value) => minSafe(new BigNumber(min))(value), minSafe: (value) => minSafe(new BigNumber(min))(value),
maxSafe: (v) => { maxSafe: (v) => {
const value = new BigNumber(v); const value = new BigNumber(v);
if (value.isGreaterThan(max.available)) { if (value.isGreaterThan(maxAmount.available)) {
return t('Insufficient amount in Ethereum wallet'); return t('Insufficient amount in Ethereum wallet');
} else if (value.isGreaterThan(max.limit)) { } else if (value.isGreaterThan(maxAmount.limit)) {
return t('Amount is above temporary deposit limit'); return t('Amount is above temporary deposit limit');
} else if (value.isGreaterThan(max.approved)) { } else if (value.isGreaterThan(maxAmount.approved)) {
return t('Amount is above approved amount'); return t('Amount is above approved amount');
} }
return maxSafe(max.amount)(v); return maxSafe(maxAmount.amount)(v);
}, },
}, },
})} })}

View File

@ -2,28 +2,30 @@ import { t } from '@vegaprotocol/react-helpers';
import type BigNumber from 'bignumber.js'; import type BigNumber from 'bignumber.js';
interface DepositLimitsProps { interface DepositLimitsProps {
limits: {
max: BigNumber; max: BigNumber;
deposited: BigNumber; deposited: BigNumber;
};
balance?: BigNumber; balance?: BigNumber;
} }
export const DepositLimits = ({ limits, balance }: DepositLimitsProps) => { export const DepositLimits = ({
max,
deposited,
balance,
}: DepositLimitsProps) => {
let maxLimit = ''; let maxLimit = '';
if (limits.max.isEqualTo(Infinity)) { if (max.isEqualTo(Infinity)) {
maxLimit = t('No limit'); maxLimit = t('No limit');
} else if (limits.max.isGreaterThan(1_000_000)) { } else if (max.isGreaterThan(1_000_000)) {
maxLimit = t('1m+'); maxLimit = t('1m+');
} else { } else {
maxLimit = limits.max.toString(); maxLimit = max.toString();
} }
let remaining = ''; let remaining = '';
if (limits.deposited.isEqualTo(0)) { if (deposited.isEqualTo(0)) {
remaining = maxLimit; remaining = maxLimit;
} else { } else {
const amountRemaining = limits.max.minus(limits.deposited); const amountRemaining = max.minus(deposited);
remaining = amountRemaining.isGreaterThan(1_000_000) remaining = amountRemaining.isGreaterThan(1_000_000)
? t('1m+') ? t('1m+')
: amountRemaining.toString(); : amountRemaining.toString();
@ -44,7 +46,7 @@ export const DepositLimits = ({ limits, balance }: DepositLimitsProps) => {
</tr> </tr>
<tr> <tr>
<th className="text-left font-normal">{t('Deposited')}</th> <th className="text-left font-normal">{t('Deposited')}</th>
<td className="text-right">{limits.deposited.toString()}</td> <td className="text-right">{deposited.toString()}</td>
</tr> </tr>
<tr> <tr>
<th className="text-left font-normal">{t('Remaining')}</th> <th className="text-left font-normal">{t('Remaining')}</th>

View File

@ -1,121 +1,56 @@
import { useEffect, useMemo, useState } from 'react';
import { DepositForm } from './deposit-form'; import { DepositForm } from './deposit-form';
import { useGetBalanceOfERC20Token } from './use-get-balance-of-erc20-token';
import { useSubmitDeposit } from './use-submit-deposit'; import { useSubmitDeposit } from './use-submit-deposit';
import sortBy from 'lodash/sortBy'; import sortBy from 'lodash/sortBy';
import { useSubmitApproval } from './use-submit-approval'; import { useSubmitApproval } from './use-submit-approval';
import { useGetDepositLimits } from './use-get-deposit-limits';
import { useGetAllowance } from './use-get-allowance';
import { useSubmitFaucet } from './use-submit-faucet'; import { useSubmitFaucet } from './use-submit-faucet';
import { EthTxStatus, useEthereumConfig } from '@vegaprotocol/web3'; import { useDepositStore } from './deposit-store';
import { useTokenContract } from '@vegaprotocol/web3'; import { useCallback } from 'react';
import { removeDecimal } from '@vegaprotocol/react-helpers'; import { useDepositBalances } from './use-deposit-balances';
import type { Asset } from '@vegaprotocol/react-helpers';
interface ERC20AssetSource {
__typename: 'ERC20';
contractAddress: string;
}
interface BuiltinAssetSource {
__typename: 'BuiltinAsset';
}
type AssetSource = ERC20AssetSource | BuiltinAssetSource;
export interface Asset {
__typename: 'Asset';
id: string;
symbol: string;
name: string;
decimals: number;
source: AssetSource;
}
interface DepositManagerProps { interface DepositManagerProps {
assets: Asset[]; assets: Asset[];
initialAssetId?: string; isFaucetable: boolean;
isFaucetable?: boolean;
} }
export const DepositManager = ({ export const DepositManager = ({
assets, assets,
initialAssetId,
isFaucetable, isFaucetable,
}: DepositManagerProps) => { }: DepositManagerProps) => {
const [assetId, setAssetId] = useState<string | undefined>(initialAssetId); const { asset, balance, allowance, deposited, max, update } =
useDepositStore();
// Find the asset object from the select box useDepositBalances(isFaucetable);
const asset = useMemo(() => {
const asset = assets?.find((a) => a.id === assetId);
return asset;
}, [assets, assetId]);
const { config } = useEthereumConfig();
const tokenContract = useTokenContract(
asset?.source.__typename === 'ERC20'
? asset.source.contractAddress
: undefined,
isFaucetable
);
// Get users balance of the erc20 token selected
const { balance, refetch: refetchBalance } = useGetBalanceOfERC20Token(
tokenContract,
asset?.decimals
);
// Get temporary deposit limits
const limits = useGetDepositLimits(asset);
// Get allowance (approved spending limit of brdige contract) for the selected asset
const { allowance, refetch: refetchAllowance } = useGetAllowance(
tokenContract,
asset?.decimals
);
// Set up approve transaction // Set up approve transaction
const approve = useSubmitApproval(tokenContract); const approve = useSubmitApproval();
// Set up deposit transaction // Set up deposit transaction
const deposit = useSubmitDeposit(); const deposit = useSubmitDeposit();
// Set up faucet transaction // Set up faucet transaction
const faucet = useSubmitFaucet(tokenContract); const faucet = useSubmitFaucet();
// Update balance after confirmation event has been received const handleSelectAsset = useCallback(
useEffect(() => { (id) => {
if ( const asset = assets.find((a) => a.id === id);
faucet.transaction.status === EthTxStatus.Confirmed || if (!asset) return;
deposit.transaction.status === EthTxStatus.Confirmed update({ asset });
) { },
refetchBalance(); [assets, update]
} );
}, [deposit.transaction.status, faucet.transaction.status, refetchBalance]);
// After an approval transaction refetch allowance
useEffect(() => {
if (approve.transaction.status === EthTxStatus.Confirmed) {
refetchAllowance();
}
}, [approve.transaction.status, refetchAllowance]);
return ( return (
<> <>
<DepositForm <DepositForm
balance={balance} balance={balance}
selectedAsset={asset} selectedAsset={asset}
onSelectAsset={(id) => setAssetId(id)} onSelectAsset={handleSelectAsset}
assets={sortBy(assets, 'name')} assets={sortBy(assets, 'name')}
submitApprove={() => { submitApprove={() => approve.perform()}
if (!asset || !config) return; submitDeposit={(args) => deposit.perform(args)}
const amount = removeDecimal('1000000', asset.decimals);
approve.perform(config.collateral_bridge_contract.address, amount);
}}
submitDeposit={(args) => {
deposit.perform(args.assetSource, args.amount, args.vegaPublicKey);
}}
requestFaucet={() => faucet.perform()} requestFaucet={() => faucet.perform()}
limits={limits} deposited={deposited}
max={max}
allowance={allowance} allowance={allowance}
isFaucetable={isFaucetable} isFaucetable={isFaucetable}
/> />

View File

@ -0,0 +1,24 @@
import type { Asset } from '@vegaprotocol/react-helpers';
import BigNumber from 'bignumber.js';
import type { SetState } from 'zustand';
import create from 'zustand';
interface DepositStore {
balance: BigNumber;
allowance: BigNumber;
asset: Asset | undefined;
deposited: BigNumber;
max: BigNumber;
update: (state: Partial<DepositStore>) => void;
}
export const useDepositStore = create((set: SetState<DepositStore>) => ({
balance: new BigNumber(0),
allowance: new BigNumber(0),
deposited: new BigNumber(0),
max: new BigNumber(0),
asset: undefined,
update: (updatedState) => {
set(updatedState);
},
}));

View File

@ -0,0 +1,59 @@
import { useBridgeContract, useTokenContract } from '@vegaprotocol/web3';
import { useEffect } from 'react';
import * as Sentry from '@sentry/react';
import { useDepositStore } from './deposit-store';
import { useGetAllowance } from './use-get-allowance';
import { useGetBalanceOfERC20Token } from './use-get-balance-of-erc20-token';
import { useGetDepositMaximum } from './use-get-deposit-maximum';
import { useGetDepositedAmount } from './use-get-deposited-amount';
import { isAssetTypeERC20 } from '@vegaprotocol/react-helpers';
/**
* Hook which fetches all the balances required for despoiting
* whenever the asset changes in the form
*/
export const useDepositBalances = (isFaucetable: boolean) => {
const { asset, update } = useDepositStore();
const tokenContract = useTokenContract(
isAssetTypeERC20(asset) ? asset : undefined,
isFaucetable
);
const bridgeContract = useBridgeContract(true);
const getAllowance = useGetAllowance(tokenContract, asset);
const getBalance = useGetBalanceOfERC20Token(tokenContract, asset);
const getDepositMaximum = useGetDepositMaximum(bridgeContract, asset);
const getDepositedAmount = useGetDepositedAmount(asset);
useEffect(() => {
const getBalances = async () => {
try {
const [max, deposited, balance, allowance] = await Promise.all([
getDepositMaximum(),
getDepositedAmount(),
getBalance(),
getAllowance(),
]);
update({
max,
deposited,
balance,
allowance,
});
} catch (err) {
Sentry.captureException(err);
}
};
if (asset) {
getBalances();
}
}, [
asset,
update,
getDepositMaximum,
getDepositedAmount,
getAllowance,
getBalance,
]);
};

View File

@ -1,30 +1,35 @@
import type { Token } from '@vegaprotocol/smart-contracts'; import type { Token } from '@vegaprotocol/smart-contracts';
import * as Sentry from '@sentry/react';
import { useWeb3React } from '@web3-react/core'; import { useWeb3React } from '@web3-react/core';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useEthereumConfig, useEthereumReadContract } from '@vegaprotocol/web3'; import { useEthereumConfig } from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import type { Asset } from '@vegaprotocol/react-helpers';
import { addDecimal } from '@vegaprotocol/react-helpers'; import { addDecimal } from '@vegaprotocol/react-helpers';
export const useGetAllowance = (contract: Token | null, decimals?: number) => { export const useGetAllowance = (
contract: Token | null,
asset: Asset | undefined
) => {
const { account } = useWeb3React(); const { account } = useWeb3React();
const { config } = useEthereumConfig(); const { config } = useEthereumConfig();
const getAllowance = useCallback(() => { const getAllowance = useCallback(async () => {
if (!contract || !account || !config) { if (!contract || !account || !config || !asset) {
return; return;
} }
return contract.allowance( try {
const res = await contract.allowance(
account, account,
config.collateral_bridge_contract.address config.collateral_bridge_contract.address
); );
}, [contract, account, config]);
const { state, refetch } = useEthereumReadContract(getAllowance); return new BigNumber(addDecimal(res.toString(), asset.decimals));
} catch (err) {
Sentry.captureException(err);
return;
}
}, [contract, account, config, asset]);
const allowance = return getAllowance;
state.data && decimals
? new BigNumber(addDecimal(state.data.toString(), decimals))
: undefined;
return { allowance, refetch };
}; };

View File

@ -1,30 +1,29 @@
import { useEthereumReadContract } from '@vegaprotocol/web3';
import type { Token } from '@vegaprotocol/smart-contracts'; import type { Token } from '@vegaprotocol/smart-contracts';
import * as Sentry from '@sentry/react';
import { useWeb3React } from '@web3-react/core'; import { useWeb3React } from '@web3-react/core';
import { useCallback } from 'react'; import { useCallback } from 'react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import type { Asset } from '@vegaprotocol/react-helpers';
import { addDecimal } from '@vegaprotocol/react-helpers'; import { addDecimal } from '@vegaprotocol/react-helpers';
export const useGetBalanceOfERC20Token = ( export const useGetBalanceOfERC20Token = (
contract: Token | null, contract: Token | null,
decimals: number | undefined asset: Asset | undefined
) => { ) => {
const { account } = useWeb3React(); const { account } = useWeb3React();
const getBalance = useCallback(async () => {
const getBalance = useCallback(() => { if (!contract || !asset || !account) {
if (!contract || !account) {
return; return;
} }
return contract.balanceOf(account); try {
}, [contract, account]); const res = await contract.balanceOf(account);
return new BigNumber(addDecimal(res.toString(), asset.decimals));
} catch (err) {
Sentry.captureException(err);
return;
}
}, [contract, asset, account]);
const { state, refetch } = useEthereumReadContract(getBalance); return getBalance;
const balance =
state.data && decimals
? new BigNumber(addDecimal(state.data?.toString(), decimals))
: undefined;
return { balance, refetch };
}; };

View File

@ -1,69 +0,0 @@
import { useCallback, useEffect, useState } from 'react';
import { ethers } from 'ethers';
import type { Asset } from './deposit-manager';
import {
useBridgeContract,
useEthereumConfig,
useEthereumReadContract,
} from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js';
import { addDecimal } from '@vegaprotocol/react-helpers';
import { useWeb3React } from '@web3-react/core';
export const useGetDepositLimits = (asset?: Asset) => {
const { account, provider } = useWeb3React();
const { config } = useEthereumConfig();
const contract = useBridgeContract(true);
const [userTotal, setUserTotal] = useState<BigNumber | null>(null);
const getLimits = useCallback(async () => {
if (!contract || !asset || asset.source.__typename !== 'ERC20') {
return;
}
return contract.get_deposit_maximum(asset.source.contractAddress);
}, [asset, contract]);
useEffect(() => {
if (
!provider ||
!config ||
!account ||
!asset ||
asset.source.__typename !== 'ERC20'
) {
return;
}
const abicoder = new ethers.utils.AbiCoder();
const innerHash = ethers.utils.keccak256(
abicoder.encode(['address', 'uint256'], [account, 4])
);
const storageLocation = ethers.utils.keccak256(
abicoder.encode(
['address', 'bytes32'],
[asset.source.contractAddress, innerHash]
)
);
(async () => {
const res = await provider.getStorageAt(
config.collateral_bridge_contract.address,
storageLocation
);
const value = new BigNumber(res, 16).toString();
setUserTotal(new BigNumber(addDecimal(value, asset.decimals)));
})();
}, [provider, config, account, asset]);
const {
state: { data },
} = useEthereumReadContract(getLimits);
if (!data || !userTotal || !asset) return null;
const max = new BigNumber(addDecimal(data.toString(), asset.decimals));
return {
max: max.isEqualTo(0) ? new BigNumber(Infinity) : max,
deposited: userTotal,
};
};

View File

@ -0,0 +1,32 @@
import { useCallback } from 'react';
import * as Sentry from '@sentry/react';
import BigNumber from 'bignumber.js';
import type { Asset } from '@vegaprotocol/react-helpers';
import { addDecimal } from '@vegaprotocol/react-helpers';
import type {
CollateralBridge,
CollateralBridgeNew,
} from '@vegaprotocol/smart-contracts';
export const useGetDepositMaximum = (
contract: CollateralBridge | CollateralBridgeNew | null,
asset: Asset | undefined
) => {
const getDepositMaximum = useCallback(async () => {
if (!contract || !asset || asset.source.__typename !== 'ERC20') {
return;
}
try {
const res = await contract.get_deposit_maximum(
asset.source.contractAddress
);
const max = new BigNumber(addDecimal(res.toString(), asset.decimals));
return max.isEqualTo(0) ? new BigNumber(Infinity) : max;
} catch (err) {
Sentry.captureException(err);
return;
}
}, [contract, asset]);
return getDepositMaximum;
};

View File

@ -0,0 +1,50 @@
import { useCallback } from 'react';
import * as Sentry from '@sentry/react';
import { ethers } from 'ethers';
import { useEthereumConfig } from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js';
import type { Asset } from '@vegaprotocol/react-helpers';
import { addDecimal } from '@vegaprotocol/react-helpers';
import { useWeb3React } from '@web3-react/core';
export const useGetDepositedAmount = (asset: Asset | undefined) => {
const { account, provider } = useWeb3React();
const { config } = useEthereumConfig();
// For an explaination of how this code works see here: https://gist.github.com/emilbayes/44a36f59b06b1f3edb9cf914041544ed
const getDepositedAmount = useCallback(async () => {
if (
!provider ||
!config ||
!account ||
!asset ||
asset.source.__typename !== 'ERC20'
) {
return;
}
try {
const abicoder = new ethers.utils.AbiCoder();
const innerHash = ethers.utils.keccak256(
abicoder.encode(['address', 'uint256'], [account, 4])
);
const storageLocation = ethers.utils.keccak256(
abicoder.encode(
['address', 'bytes32'],
[asset.source.contractAddress, innerHash]
)
);
const res = await provider.getStorageAt(
config.collateral_bridge_contract.address,
storageLocation
);
const value = new BigNumber(res, 16).toString();
return new BigNumber(addDecimal(value, asset.decimals));
} catch (err) {
Sentry.captureException(err);
return;
}
}, [provider, asset, config, account]);
return getDepositedAmount;
};

View File

@ -1,10 +1,41 @@
import { isAssetTypeERC20, removeDecimal } from '@vegaprotocol/react-helpers';
import * as Sentry from '@sentry/react';
import type { Token } from '@vegaprotocol/smart-contracts'; import type { Token } from '@vegaprotocol/smart-contracts';
import { useEthereumTransaction } from '@vegaprotocol/web3'; import {
useEthereumConfig,
useEthereumTransaction,
useTokenContract,
} from '@vegaprotocol/web3';
import { useDepositStore } from './deposit-store';
import { useGetAllowance } from './use-get-allowance';
export const useSubmitApproval = (contract: Token | null) => { export const useSubmitApproval = () => {
const { config } = useEthereumConfig();
const { asset, update } = useDepositStore();
const contract = useTokenContract(
isAssetTypeERC20(asset) ? asset : undefined,
true
);
const getAllowance = useGetAllowance(contract, asset);
const transaction = useEthereumTransaction<Token, 'approve'>( const transaction = useEthereumTransaction<Token, 'approve'>(
contract, contract,
'approve' 'approve'
); );
return transaction; return {
...transaction,
perform: async () => {
if (!asset || !config) return;
try {
const amount = removeDecimal('1000000', asset.decimals);
await transaction.perform(
config.collateral_bridge_contract.address,
amount
);
const allowance = await getAllowance();
update({ allowance });
} catch (err) {
Sentry.captureException(err);
}
},
};
}; };

View File

@ -1,21 +1,29 @@
import { gql, useSubscription } from '@apollo/client'; import { gql, useSubscription } from '@apollo/client';
import * as Sentry from '@sentry/react';
import type { import type {
DepositEvent, DepositEvent,
DepositEventVariables, DepositEventVariables,
} from './__generated__/DepositEvent'; } from './__generated__/DepositEvent';
import { DepositStatus } from '@vegaprotocol/types'; import { DepositStatus } from '@vegaprotocol/types';
import { useState } from 'react'; import { useState } from 'react';
import { remove0x } from '@vegaprotocol/react-helpers'; import {
isAssetTypeERC20,
remove0x,
removeDecimal,
} from '@vegaprotocol/react-helpers';
import { import {
useBridgeContract, useBridgeContract,
useEthereumConfig, useEthereumConfig,
useEthereumTransaction, useEthereumTransaction,
useTokenContract,
} from '@vegaprotocol/web3'; } from '@vegaprotocol/web3';
import type { import type {
CollateralBridge, CollateralBridge,
CollateralBridgeNew, CollateralBridgeNew,
} from '@vegaprotocol/smart-contracts'; } from '@vegaprotocol/smart-contracts';
import { prepend0x } from '@vegaprotocol/smart-contracts'; import { prepend0x } from '@vegaprotocol/smart-contracts';
import { useDepositStore } from './deposit-store';
import { useGetBalanceOfERC20Token } from './use-get-balance-of-erc20-token';
const DEPOSIT_EVENT_SUB = gql` const DEPOSIT_EVENT_SUB = gql`
subscription DepositEvent($partyId: ID!) { subscription DepositEvent($partyId: ID!) {
@ -32,17 +40,24 @@ const DEPOSIT_EVENT_SUB = gql`
`; `;
export const useSubmitDeposit = () => { export const useSubmitDeposit = () => {
const { asset, update } = useDepositStore();
const { config } = useEthereumConfig(); const { config } = useEthereumConfig();
const contract = useBridgeContract(true); const bridgeContract = useBridgeContract(true);
const tokenContract = useTokenContract(
isAssetTypeERC20(asset) ? asset : undefined,
true
);
// Store public key from contract arguments for use in the subscription, // Store public key from contract arguments for use in the subscription,
// NOTE: it may be different from the users connected key // NOTE: it may be different from the users connected key
const [partyId, setPartyId] = useState<string | null>(null); const [partyId, setPartyId] = useState<string | null>(null);
const getBalance = useGetBalanceOfERC20Token(tokenContract, asset);
const transaction = useEthereumTransaction< const transaction = useEthereumTransaction<
CollateralBridgeNew | CollateralBridge, CollateralBridgeNew | CollateralBridge,
'deposit_asset' 'deposit_asset'
>(contract, 'deposit_asset', config?.confirmations, true); >(bridgeContract, 'deposit_asset', config?.confirmations, true);
useSubscription<DepositEvent, DepositEventVariables>(DEPOSIT_EVENT_SUB, { useSubscription<DepositEvent, DepositEventVariables>(DEPOSIT_EVENT_SUB, {
variables: { partyId: partyId ? remove0x(partyId) : '' }, variables: { partyId: partyId ? remove0x(partyId) : '' },
@ -78,10 +93,22 @@ export const useSubmitDeposit = () => {
return { return {
...transaction, ...transaction,
perform: (...args: Parameters<typeof transaction.perform>) => { perform: async (args: {
setPartyId(args[2]); assetSource: string;
const publicKey = prepend0x(args[2]); amount: string;
transaction.perform(args[0], args[1], publicKey); vegaPublicKey: string;
}) => {
if (!asset) return;
try {
setPartyId(args.vegaPublicKey);
const publicKey = prepend0x(args.vegaPublicKey);
const amount = removeDecimal(args.amount, asset.decimals);
await transaction.perform(args.assetSource, amount, publicKey);
const balance = await getBalance();
update({ balance });
} catch (err) {
Sentry.captureException(err);
}
}, },
}; };
}; };

View File

@ -1,10 +1,31 @@
import type { Token, TokenFaucetable } from '@vegaprotocol/smart-contracts'; import type { TokenFaucetable } from '@vegaprotocol/smart-contracts';
import { useEthereumTransaction } from '@vegaprotocol/web3'; import * as Sentry from '@sentry/react';
import { useEthereumTransaction, useTokenContract } from '@vegaprotocol/web3';
import { useDepositStore } from './deposit-store';
import { useGetBalanceOfERC20Token } from './use-get-balance-of-erc20-token';
import { isAssetTypeERC20 } from '@vegaprotocol/react-helpers';
export const useSubmitFaucet = (contract: Token | TokenFaucetable | null) => { export const useSubmitFaucet = () => {
const { asset, update } = useDepositStore();
const contract = useTokenContract(
isAssetTypeERC20(asset) ? asset : undefined,
true
);
const getBalance = useGetBalanceOfERC20Token(contract, asset);
const transaction = useEthereumTransaction<TokenFaucetable, 'faucet'>( const transaction = useEthereumTransaction<TokenFaucetable, 'faucet'>(
contract, contract,
'faucet' 'faucet'
); );
return transaction; return {
...transaction,
perform: async () => {
try {
await transaction.perform();
const balance = await getBalance();
update({ balance });
} catch (err) {
Sentry.captureException(err);
}
},
};
}; };

View File

@ -1,4 +1,5 @@
export * from './hooks'; export * from './hooks';
export * from './lib/assets';
export * from './lib/context'; export * from './lib/context';
export * from './lib/determine-id'; export * from './lib/determine-id';
export * from './lib/format'; export * from './lib/format';

View File

@ -0,0 +1,30 @@
export interface ERC20AssetSource {
__typename: 'ERC20';
contractAddress: string;
}
export interface BuiltinAssetSource {
__typename: 'BuiltinAsset';
}
export interface Asset {
__typename: 'Asset';
id: string;
symbol: string;
name: string;
decimals: number;
source: ERC20AssetSource | BuiltinAssetSource;
}
export type ERC20Asset = Omit<Asset, 'source'> & {
source: ERC20AssetSource;
};
export type BuiltinAsset = Omit<Asset, 'source'> & {
source: BuiltinAssetSource;
};
export const isAssetTypeERC20 = (asset?: Asset): asset is ERC20Asset => {
if (!asset?.source) return false;
return asset.source.__typename === 'ERC20';
};

View File

@ -51,9 +51,11 @@ export const useEthereumReadContract = <T>(
const response = await result; const response = await result;
if (cancelRequest.current) return; if (cancelRequest.current) return;
dispatch({ type: ActionType.FETCHED, payload: response }); dispatch({ type: ActionType.FETCHED, payload: response });
return response;
} catch (error) { } catch (error) {
if (cancelRequest.current) return; if (cancelRequest.current) return;
dispatch({ type: ActionType.ERROR, error: error as Error }); dispatch({ type: ActionType.ERROR, error: error as Error });
return;
} }
}, [contractFunc]); }, [contractFunc]);

View File

@ -130,6 +130,7 @@ export const useEthereumTransaction = <
error: new Error('Something went wrong'), error: new Error('Something went wrong'),
}); });
} }
return;
} }
}, },
[ [

View File

@ -1,26 +1,25 @@
import type { ERC20Asset } from '@vegaprotocol/react-helpers';
import { Token, TokenFaucetable } from '@vegaprotocol/smart-contracts'; import { Token, TokenFaucetable } from '@vegaprotocol/smart-contracts';
import { useWeb3React } from '@web3-react/core'; import { useWeb3React } from '@web3-react/core';
import { useMemo } from 'react'; import { useMemo } from 'react';
export const useTokenContract = ( export const useTokenContract = (asset?: ERC20Asset, faucetable = false) => {
contractAddress?: string,
faucetable = false
) => {
const { provider } = useWeb3React(); const { provider } = useWeb3React();
const contract = useMemo(() => { const contract = useMemo(() => {
if (!provider || !contractAddress) { if (!provider || !asset) {
return null; return null;
} }
const signer = provider.getSigner(); const signer = provider.getSigner();
const address = asset.source.contractAddress;
if (faucetable) { if (faucetable) {
return new TokenFaucetable(contractAddress, signer || provider); return new TokenFaucetable(address, signer || provider);
} else { } else {
return new Token(contractAddress, signer || provider); return new Token(address, signer || provider);
} }
}, [provider, contractAddress, faucetable]); }, [provider, asset, faucetable]);
return contract; return contract;
}; };

View File

@ -1,11 +1,13 @@
import type { Asset } from '@vegaprotocol/react-helpers';
import { AccountType, WithdrawalStatus } from '@vegaprotocol/types'; import { AccountType, WithdrawalStatus } from '@vegaprotocol/types';
import merge from 'lodash/merge'; import merge from 'lodash/merge';
import type { PartialDeep } from 'type-fest'; import type { PartialDeep } from 'type-fest';
import type { Asset, Account } from './types'; import type { Account } from './types';
import type { Withdrawals_party_withdrawals } from './__generated__/Withdrawals'; import type { Withdrawals_party_withdrawals } from './__generated__/Withdrawals';
export const generateAsset = (override?: PartialDeep<Asset>) => { export const generateAsset = (override?: PartialDeep<Asset>) => {
const defaultAsset: Asset = { const defaultAsset: Asset = {
__typename: 'Asset',
id: 'asset-id', id: 'asset-id',
symbol: 'asset-symbol', symbol: 'asset-symbol',
name: 'asset-name', name: 'asset-name',

View File

@ -1,24 +1,5 @@
import type { AccountType } from '@vegaprotocol/types'; import type { AccountType } from '@vegaprotocol/types';
interface ERC20AssetSource {
__typename: 'ERC20';
contractAddress: string;
}
interface BuiltinAssetSource {
__typename: 'BuiltinAsset';
}
type AssetSource = ERC20AssetSource | BuiltinAssetSource;
export interface Asset {
id: string;
symbol: string;
name: string;
decimals: number;
source: AssetSource;
}
export interface Account { export interface Account {
type: AccountType; type: AccountType;
balance: string; balance: string;

View File

@ -1,7 +1,7 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import type { Asset } from './types';
import { useBridgeContract, useEthereumReadContract } from '@vegaprotocol/web3'; import { useBridgeContract, useEthereumReadContract } from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import type { Asset } from '@vegaprotocol/react-helpers';
import { addDecimal } from '@vegaprotocol/react-helpers'; import { addDecimal } from '@vegaprotocol/react-helpers';
export const useGetWithdrawLimits = (asset?: Asset) => { export const useGetWithdrawLimits = (asset?: Asset) => {

View File

@ -3,8 +3,8 @@ import BigNumber from 'bignumber.js';
import { useWeb3React } from '@web3-react/core'; import { useWeb3React } from '@web3-react/core';
import { WithdrawForm } from './withdraw-form'; import { WithdrawForm } from './withdraw-form';
import { generateAsset } from './test-helpers'; import { generateAsset } from './test-helpers';
import type { Asset } from './types';
import type { WithdrawFormProps } from './withdraw-form'; import type { WithdrawFormProps } from './withdraw-form';
import type { Asset } from '@vegaprotocol/react-helpers';
jest.mock('@web3-react/core'); jest.mock('@web3-react/core');

View File

@ -1,3 +1,4 @@
import type { Asset } from '@vegaprotocol/react-helpers';
import { import {
ethereumAddress, ethereumAddress,
minSafe, minSafe,
@ -5,6 +6,7 @@ import {
removeDecimal, removeDecimal,
required, required,
maxSafe, maxSafe,
isAssetTypeERC20,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import { import {
Button, Button,
@ -19,7 +21,6 @@ import BigNumber from 'bignumber.js';
import type { ButtonHTMLAttributes, ReactNode } from 'react'; import type { ButtonHTMLAttributes, ReactNode } from 'react';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import type { WithdrawalFields } from './use-withdraw'; import type { WithdrawalFields } from './use-withdraw';
import type { Asset } from './types';
import { WithdrawLimits } from './withdraw-limits'; import { WithdrawLimits } from './withdraw-limits';
interface FormFields { interface FormFields {
@ -99,9 +100,7 @@ export const WithdrawForm = ({
id="asset" id="asset"
> >
<option value="">{t('Please select')}</option> <option value="">{t('Please select')}</option>
{assets {assets.filter(isAssetTypeERC20).map((a) => (
.filter((a) => a.source.__typename === 'ERC20')
.map((a) => (
<option key={a.id} value={a.id}> <option key={a.id} value={a.id}>
{a.name} {a.name}
</option> </option>

View File

@ -5,10 +5,11 @@ import type { WithdrawalFields } from './use-withdraw';
import { useWithdraw } from './use-withdraw'; import { useWithdraw } from './use-withdraw';
import { WithdrawDialog } from './withdraw-dialog'; import { WithdrawDialog } from './withdraw-dialog';
import { isExpectedEthereumError, EthTxStatus } from '@vegaprotocol/web3'; import { isExpectedEthereumError, EthTxStatus } from '@vegaprotocol/web3';
import type { Asset } from '@vegaprotocol/react-helpers';
import { addDecimal } from '@vegaprotocol/react-helpers'; import { addDecimal } from '@vegaprotocol/react-helpers';
import { AccountType } from '@vegaprotocol/types'; import { AccountType } from '@vegaprotocol/types';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import type { Account, Asset } from './types'; import type { Account } from './types';
import { useGetWithdrawLimits } from './use-get-withdraw-limits'; import { useGetWithdrawLimits } from './use-get-withdraw-limits';
export interface WithdrawManagerProps { export interface WithdrawManagerProps {