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

View File

@ -16,14 +16,10 @@ const DEPOSIT_PAGE_QUERY = gql`
}
`;
interface DepositContainerProps {
assetId?: string;
}
/**
* Fetches data required for the Deposit page
*/
export const DepositContainer = ({ assetId }: DepositContainerProps) => {
export const DepositContainer = () => {
const { VEGA_ENV } = useEnvironment();
return (
@ -41,7 +37,6 @@ export const DepositContainer = ({ assetId }: DepositContainerProps) => {
return (
<DepositManager
assets={data.assets}
initialAssetId={assetId}
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 { DepositContainer } from './deposit-container';
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 (
<Web3Container>
<div className="max-w-[420px] p-24 mx-auto">
<h1 className="text-h3 mb-12">Deposit</h1>
<DepositContainer assetId={assetId} />
<DepositContainer />
</div>
</Web3Container>
);

View File

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

View File

@ -1,5 +1,5 @@
import type { Asset } from '@vegaprotocol/react-helpers';
import {
removeDecimal,
ethereumAddress,
t,
required,
@ -7,6 +7,7 @@ import {
minSafe,
maxSafe,
addDecimal,
isAssetTypeERC20,
} from '@vegaprotocol/react-helpers';
import {
Button,
@ -21,10 +22,9 @@ import { useWeb3React } from '@web3-react/core';
import { Web3WalletInput } from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js';
import type { ReactNode } from 'react';
import { useMemo, useEffect } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { useMemo } from 'react';
import { Controller, useForm, useWatch } from 'react-hook-form';
import { DepositLimits } from './deposit-limits';
import type { Asset } from './deposit-manager';
interface FormFields {
asset: string;
@ -45,10 +45,8 @@ export interface DepositFormProps {
vegaPublicKey: string;
}) => void;
requestFaucet: () => void;
limits: {
max: BigNumber;
deposited: BigNumber;
} | null;
max: BigNumber | undefined;
deposited: BigNumber | undefined;
allowance: BigNumber | undefined;
isFaucetable?: boolean;
}
@ -58,10 +56,11 @@ export const DepositForm = ({
selectedAsset,
onSelectAsset,
balance,
max,
deposited,
submitApprove,
submitDeposit,
requestFaucet,
limits,
allowance,
isFaucetable,
}: DepositFormProps) => {
@ -89,15 +88,14 @@ export const DepositForm = ({
submitDeposit({
assetSource: selectedAsset.source.contractAddress,
amount: removeDecimal(fields.amount, selectedAsset.decimals),
amount: fields.amount,
vegaPublicKey: fields.to,
});
};
const assetId = useWatch({ name: 'asset', control });
const amount = useWatch({ name: 'amount', control });
const max = useMemo(() => {
const maxAmount = useMemo(() => {
const maxApproved = allowance ? allowance : new BigNumber(0);
const maxAvailable = balance ? balance : new BigNumber(0);
@ -106,8 +104,8 @@ export const DepositForm = ({
let maxLimit = new BigNumber(Infinity);
// A max limit of zero indicates that there is no limit
if (limits && limits.max.isGreaterThan(0)) {
maxLimit = limits.max.minus(limits.deposited);
if (max && deposited && max.isGreaterThan(0)) {
maxLimit = max.minus(deposited);
}
return {
@ -116,7 +114,7 @@ export const DepositForm = ({
limit: maxLimit,
amount: BigNumber.minimum(maxLimit, maxApproved, maxAvailable),
};
}, [limits, allowance, balance]);
}, [max, deposited, allowance, balance]);
const min = useMemo(() => {
// Min viable amount given asset decimals EG for WEI 0.000000000000000001
@ -127,10 +125,6 @@ export const DepositForm = ({
return minViableAmount;
}, [selectedAsset]);
useEffect(() => {
onSelectAsset(assetId);
}, [assetId, onSelectAsset]);
return (
<form
onSubmit={handleSubmit(onDeposit)}
@ -154,16 +148,28 @@ export const DepositForm = ({
)}
</FormGroup>
<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>
{assets
.filter((a) => a.source.__typename === 'ERC20')
.map((a) => (
{assets.filter(isAssetTypeERC20).map((a) => (
<option key={a.id} value={a.id}>
{a.name}
</option>
))}
</Select>
)}
/>
{errors.asset?.message && (
<InputError intent="danger" className="mt-4" forInput="asset">
{errors.asset.message}
@ -196,9 +202,9 @@ export const DepositForm = ({
</UseButton>
)}
</FormGroup>
{selectedAsset && limits && (
{selectedAsset && max && deposited && (
<div className="mb-20">
<DepositLimits limits={limits} balance={balance} />
<DepositLimits max={max} deposited={deposited} balance={balance} />
</div>
)}
<FormGroup label={t('Amount')} labelFor="amount" className="relative">
@ -212,14 +218,14 @@ export const DepositForm = ({
minSafe: (value) => minSafe(new BigNumber(min))(value),
maxSafe: (v) => {
const value = new BigNumber(v);
if (value.isGreaterThan(max.available)) {
if (value.isGreaterThan(maxAmount.available)) {
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');
} else if (value.isGreaterThan(max.approved)) {
} else if (value.isGreaterThan(maxAmount.approved)) {
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';
interface DepositLimitsProps {
limits: {
max: BigNumber;
deposited: BigNumber;
};
balance?: BigNumber;
}
export const DepositLimits = ({ limits, balance }: DepositLimitsProps) => {
export const DepositLimits = ({
max,
deposited,
balance,
}: DepositLimitsProps) => {
let maxLimit = '';
if (limits.max.isEqualTo(Infinity)) {
if (max.isEqualTo(Infinity)) {
maxLimit = t('No limit');
} else if (limits.max.isGreaterThan(1_000_000)) {
} else if (max.isGreaterThan(1_000_000)) {
maxLimit = t('1m+');
} else {
maxLimit = limits.max.toString();
maxLimit = max.toString();
}
let remaining = '';
if (limits.deposited.isEqualTo(0)) {
if (deposited.isEqualTo(0)) {
remaining = maxLimit;
} else {
const amountRemaining = limits.max.minus(limits.deposited);
const amountRemaining = max.minus(deposited);
remaining = amountRemaining.isGreaterThan(1_000_000)
? t('1m+')
: amountRemaining.toString();
@ -44,7 +46,7 @@ export const DepositLimits = ({ limits, balance }: DepositLimitsProps) => {
</tr>
<tr>
<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>
<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 { useGetBalanceOfERC20Token } from './use-get-balance-of-erc20-token';
import { useSubmitDeposit } from './use-submit-deposit';
import sortBy from 'lodash/sortBy';
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 { EthTxStatus, useEthereumConfig } from '@vegaprotocol/web3';
import { useTokenContract } from '@vegaprotocol/web3';
import { removeDecimal } 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;
}
import { useDepositStore } from './deposit-store';
import { useCallback } from 'react';
import { useDepositBalances } from './use-deposit-balances';
import type { Asset } from '@vegaprotocol/react-helpers';
interface DepositManagerProps {
assets: Asset[];
initialAssetId?: string;
isFaucetable?: boolean;
isFaucetable: boolean;
}
export const DepositManager = ({
assets,
initialAssetId,
isFaucetable,
}: DepositManagerProps) => {
const [assetId, setAssetId] = useState<string | undefined>(initialAssetId);
// Find the asset object from the select box
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
);
const { asset, balance, allowance, deposited, max, update } =
useDepositStore();
useDepositBalances(isFaucetable);
// Set up approve transaction
const approve = useSubmitApproval(tokenContract);
const approve = useSubmitApproval();
// Set up deposit transaction
const deposit = useSubmitDeposit();
// Set up faucet transaction
const faucet = useSubmitFaucet(tokenContract);
const faucet = useSubmitFaucet();
// Update balance after confirmation event has been received
useEffect(() => {
if (
faucet.transaction.status === EthTxStatus.Confirmed ||
deposit.transaction.status === EthTxStatus.Confirmed
) {
refetchBalance();
}
}, [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]);
const handleSelectAsset = useCallback(
(id) => {
const asset = assets.find((a) => a.id === id);
if (!asset) return;
update({ asset });
},
[assets, update]
);
return (
<>
<DepositForm
balance={balance}
selectedAsset={asset}
onSelectAsset={(id) => setAssetId(id)}
onSelectAsset={handleSelectAsset}
assets={sortBy(assets, 'name')}
submitApprove={() => {
if (!asset || !config) return;
const amount = removeDecimal('1000000', asset.decimals);
approve.perform(config.collateral_bridge_contract.address, amount);
}}
submitDeposit={(args) => {
deposit.perform(args.assetSource, args.amount, args.vegaPublicKey);
}}
submitApprove={() => approve.perform()}
submitDeposit={(args) => deposit.perform(args)}
requestFaucet={() => faucet.perform()}
limits={limits}
deposited={deposited}
max={max}
allowance={allowance}
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 * as Sentry from '@sentry/react';
import { useWeb3React } from '@web3-react/core';
import { useCallback } from 'react';
import { useEthereumConfig, useEthereumReadContract } from '@vegaprotocol/web3';
import { useEthereumConfig } from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js';
import type { Asset } 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 { config } = useEthereumConfig();
const getAllowance = useCallback(() => {
if (!contract || !account || !config) {
const getAllowance = useCallback(async () => {
if (!contract || !account || !config || !asset) {
return;
}
return contract.allowance(
try {
const res = await contract.allowance(
account,
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 =
state.data && decimals
? new BigNumber(addDecimal(state.data.toString(), decimals))
: undefined;
return { allowance, refetch };
return getAllowance;
};

View File

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

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 { 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'>(
contract,
'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 * as Sentry from '@sentry/react';
import type {
DepositEvent,
DepositEventVariables,
} from './__generated__/DepositEvent';
import { DepositStatus } from '@vegaprotocol/types';
import { useState } from 'react';
import { remove0x } from '@vegaprotocol/react-helpers';
import {
isAssetTypeERC20,
remove0x,
removeDecimal,
} from '@vegaprotocol/react-helpers';
import {
useBridgeContract,
useEthereumConfig,
useEthereumTransaction,
useTokenContract,
} from '@vegaprotocol/web3';
import type {
CollateralBridge,
CollateralBridgeNew,
} 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`
subscription DepositEvent($partyId: ID!) {
@ -32,17 +40,24 @@ const DEPOSIT_EVENT_SUB = gql`
`;
export const useSubmitDeposit = () => {
const { asset, update } = useDepositStore();
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,
// NOTE: it may be different from the users connected key
const [partyId, setPartyId] = useState<string | null>(null);
const getBalance = useGetBalanceOfERC20Token(tokenContract, asset);
const transaction = useEthereumTransaction<
CollateralBridgeNew | CollateralBridge,
'deposit_asset'
>(contract, 'deposit_asset', config?.confirmations, true);
>(bridgeContract, 'deposit_asset', config?.confirmations, true);
useSubscription<DepositEvent, DepositEventVariables>(DEPOSIT_EVENT_SUB, {
variables: { partyId: partyId ? remove0x(partyId) : '' },
@ -78,10 +93,22 @@ export const useSubmitDeposit = () => {
return {
...transaction,
perform: (...args: Parameters<typeof transaction.perform>) => {
setPartyId(args[2]);
const publicKey = prepend0x(args[2]);
transaction.perform(args[0], args[1], publicKey);
perform: async (args: {
assetSource: string;
amount: string;
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 { useEthereumTransaction } from '@vegaprotocol/web3';
import type { TokenFaucetable } from '@vegaprotocol/smart-contracts';
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'>(
contract,
'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 './lib/assets';
export * from './lib/context';
export * from './lib/determine-id';
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;
if (cancelRequest.current) return;
dispatch({ type: ActionType.FETCHED, payload: response });
return response;
} catch (error) {
if (cancelRequest.current) return;
dispatch({ type: ActionType.ERROR, error: error as Error });
return;
}
}, [contractFunc]);

View File

@ -130,6 +130,7 @@ export const useEthereumTransaction = <
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 { useWeb3React } from '@web3-react/core';
import { useMemo } from 'react';
export const useTokenContract = (
contractAddress?: string,
faucetable = false
) => {
export const useTokenContract = (asset?: ERC20Asset, faucetable = false) => {
const { provider } = useWeb3React();
const contract = useMemo(() => {
if (!provider || !contractAddress) {
if (!provider || !asset) {
return null;
}
const signer = provider.getSigner();
const address = asset.source.contractAddress;
if (faucetable) {
return new TokenFaucetable(contractAddress, signer || provider);
return new TokenFaucetable(address, signer || provider);
} else {
return new Token(contractAddress, signer || provider);
return new Token(address, signer || provider);
}
}, [provider, contractAddress, faucetable]);
}, [provider, asset, faucetable]);
return contract;
};

View File

@ -1,11 +1,13 @@
import type { Asset } from '@vegaprotocol/react-helpers';
import { AccountType, WithdrawalStatus } from '@vegaprotocol/types';
import merge from 'lodash/merge';
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';
export const generateAsset = (override?: PartialDeep<Asset>) => {
const defaultAsset: Asset = {
__typename: 'Asset',
id: 'asset-id',
symbol: 'asset-symbol',
name: 'asset-name',

View File

@ -1,24 +1,5 @@
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 {
type: AccountType;
balance: string;

View File

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

View File

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

View File

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

View File

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