From e47298761a667d31c070647c742f70e11d1def05 Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Mon, 25 Jul 2022 09:48:19 +0100 Subject: [PATCH] Feat/Use callStatic to improve error messaging (#831) * feat: make use max only use account balance, add custom max messages * fix: withdraw threshold limit display * feat: add callstatic to ethereum transaction hook * feat: improve types for useTransaction hook * chore: fix types and remove ts-ignore * chore: convert all smart contract wrapper methods to match metaclass methods * fix: this context for calling tx * chore: fix comment and any type * chore: typo Co-authored-by: Edd Co-authored-by: Edd --- apps/token/src/app-loader.tsx | 4 +- .../balance-manager/balance-manager.tsx | 4 +- .../contexts/contracts/contracts-provider.tsx | 2 +- .../hooks/use-get-user-tranche-balances.ts | 4 +- .../hooks/use-refresh-associated-balances.ts | 4 +- apps/token/src/hooks/use-refresh-balances.ts | 8 +- .../src/routes/redemption/tranche/index.tsx | 2 +- .../src/routes/staking/associate/hooks.ts | 2 +- .../src/routes/staking/disassociate/hooks.ts | 4 +- libs/deposits/src/lib/deposit-form.tsx | 6 +- libs/deposits/src/lib/deposit-limits.tsx | 5 +- libs/deposits/src/lib/deposit-manager.tsx | 15 ++- .../src/lib/use-get-deposit-limits.ts | 2 +- libs/deposits/src/lib/use-submit-approval.ts | 24 ++--- libs/deposits/src/lib/use-submit-deposit.ts | 36 +++---- libs/deposits/src/lib/use-submit-faucet.ts | 14 +-- .../src/lib/order-hooks/use-order-edit.tsx | 3 + .../src/contracts/collateral-bridge-new.ts | 23 ++-- .../src/contracts/collateral-bridge.ts | 25 ++--- .../src/contracts/staking-bridge.ts | 10 +- .../src/contracts/token-vesting.ts | 18 ++-- libs/smart-contracts/src/contracts/token.ts | 3 + libs/web3/src/lib/ethereum-error.ts | 6 +- .../transaction-dialog.spec.tsx | 10 +- .../transaction-dialog/transaction-dialog.tsx | 26 ++++- .../src/lib/use-ethereum-transaction.spec.tsx | 102 +++++++++--------- libs/web3/src/lib/use-ethereum-transaction.ts | 65 +++++++---- .../src/lib/use-complete-withdraw.spec.tsx | 14 ++- .../src/lib/use-complete-withdraw.ts | 72 +++++-------- .../src/lib/use-get-withdraw-limits.tsx | 2 +- libs/withdraws/src/lib/use-withdraw.spec.tsx | 15 ++- libs/withdraws/src/lib/use-withdraw.ts | 67 ++++++------ libs/withdraws/src/lib/withdraw-dialog.tsx | 7 +- .../src/lib/withdraw-manager.spec.tsx | 2 +- 34 files changed, 319 insertions(+), 287 deletions(-) diff --git a/apps/token/src/app-loader.tsx b/apps/token/src/app-loader.tsx index 3ea4ceea8..9eaf5483e 100644 --- a/apps/token/src/app-loader.tsx +++ b/apps/token/src/app-loader.tsx @@ -40,8 +40,8 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => { decimals, ] = await Promise.all([ token.totalSupply(), - staking.totalStaked(), - vesting.totalStaked(), + staking.total_staked(), + vesting.total_staked(), token.decimals(), ]); diff --git a/apps/token/src/components/balance-manager/balance-manager.tsx b/apps/token/src/components/balance-manager/balance-manager.tsx index 69ebe1964..f12e4cd23 100644 --- a/apps/token/src/components/balance-manager/balance-manager.tsx +++ b/apps/token/src/components/balance-manager/balance-manager.tsx @@ -41,9 +41,9 @@ export const BalanceManager = ({ children }: BalanceManagerProps) => { if (!account || !config) return; try { const [b, w, stats, a] = await Promise.all([ - contracts.vesting.userTotalAllTranches(account), + contracts.vesting.user_total_all_tranches(account), contracts.token.balanceOf(account), - contracts.vesting.userStats(account), + contracts.vesting.user_stats(account), contracts.token.allowance( account, config.staking_bridge_contract.address diff --git a/apps/token/src/contexts/contracts/contracts-provider.tsx b/apps/token/src/contexts/contracts/contracts-provider.tsx index c3896dc3c..5fb26a47f 100644 --- a/apps/token/src/contexts/contracts/contracts-provider.tsx +++ b/apps/token/src/contexts/contracts/contracts-provider.tsx @@ -54,7 +54,7 @@ export const ContractsProvider = ({ children }: { children: JSX.Element }) => { config.staking_bridge_contract.address, signer || provider ); - const vegaAddress = await staking.stakingToken(); + const vegaAddress = await staking.staking_token(); setContracts({ token: new Token(vegaAddress, signer || provider), diff --git a/apps/token/src/hooks/use-get-user-tranche-balances.ts b/apps/token/src/hooks/use-get-user-tranche-balances.ts index 5e58bcb1d..0ee441f6f 100644 --- a/apps/token/src/hooks/use-get-user-tranche-balances.ts +++ b/apps/token/src/hooks/use-get-user-tranche-balances.ts @@ -37,8 +37,8 @@ export const useGetUserTrancheBalances = ( const trancheIds = [0, ...userTranches.map((t) => t.tranche_id)]; const promises = trancheIds.map(async (tId) => { const [t, v] = await Promise.all([ - vesting.getTrancheBalance(address, tId), - vesting.getVestedForTranche(address, tId), + vesting.get_tranche_balance(address, tId), + vesting.get_vested_for_tranche(address, tId), ]); const total = toBigNum(t, decimals); diff --git a/apps/token/src/hooks/use-refresh-associated-balances.ts b/apps/token/src/hooks/use-refresh-associated-balances.ts index f87ae011e..40dcf8d37 100644 --- a/apps/token/src/hooks/use-refresh-associated-balances.ts +++ b/apps/token/src/hooks/use-refresh-associated-balances.ts @@ -18,8 +18,8 @@ export function useRefreshAssociatedBalances() { async (ethAddress: string, vegaKey: string) => { const [walletAssociatedBalance, vestingAssociatedBalance] = await Promise.all([ - staking.stakeBalance(ethAddress, vegaKey), - vesting.stakeBalance(ethAddress, vegaKey), + staking.stake_balance(ethAddress, vegaKey), + vesting.stake_balance(ethAddress, vegaKey), ]); appDispatch({ diff --git a/apps/token/src/hooks/use-refresh-balances.ts b/apps/token/src/hooks/use-refresh-balances.ts index fe24de33a..bdfa7fd40 100644 --- a/apps/token/src/hooks/use-refresh-balances.ts +++ b/apps/token/src/hooks/use-refresh-balances.ts @@ -24,13 +24,13 @@ export const useRefreshBalances = (address: string) => { try { const [b, w, stats, a, walletStakeBalance, vestingStakeBalance] = await Promise.all([ - vesting.userTotalAllTranches(address), + vesting.user_total_all_tranches(address), token.balanceOf(address), - vesting.userStats(address), + vesting.user_stats(address), token.allowance(address, config.staking_bridge_contract.address), // Refresh connected vega key balances as well if we are connected to a vega key - keypair?.pub ? staking.stakeBalance(address, keypair.pub) : null, - keypair?.pub ? vesting.stakeBalance(address, keypair.pub) : null, + keypair?.pub ? staking.stake_balance(address, keypair.pub) : null, + keypair?.pub ? vesting.stake_balance(address, keypair.pub) : null, ]); const balance = toBigNum(b, decimals); diff --git a/apps/token/src/routes/redemption/tranche/index.tsx b/apps/token/src/routes/redemption/tranche/index.tsx index a3f6d17b7..9f48a56b3 100644 --- a/apps/token/src/routes/redemption/tranche/index.tsx +++ b/apps/token/src/routes/redemption/tranche/index.tsx @@ -41,7 +41,7 @@ export const RedeemFromTranche = () => { state: txState, perform, dispatch: txDispatch, - } = useTransaction(() => vesting.withdrawFromTranche(numberId)); + } = useTransaction(() => vesting.withdraw_from_tranche(numberId)); const { token } = useContracts(); const redeemedAmount = React.useMemo(() => { diff --git a/apps/token/src/routes/staking/associate/hooks.ts b/apps/token/src/routes/staking/associate/hooks.ts index e819167ef..29dec6586 100644 --- a/apps/token/src/routes/staking/associate/hooks.ts +++ b/apps/token/src/routes/staking/associate/hooks.ts @@ -29,7 +29,7 @@ export const useAddStake = ( appState: { decimals }, } = useAppState(); const contractAdd = useTransaction( - () => vesting.stakeTokens(removeDecimal(amount, decimals), vegaKey), + () => vesting.stake_tokens(removeDecimal(amount, decimals), vegaKey), confirmations ); const walletAdd = useTransaction( diff --git a/apps/token/src/routes/staking/disassociate/hooks.ts b/apps/token/src/routes/staking/disassociate/hooks.ts index 574651000..59f3f1c15 100644 --- a/apps/token/src/routes/staking/disassociate/hooks.ts +++ b/apps/token/src/routes/staking/disassociate/hooks.ts @@ -21,10 +21,10 @@ export const useRemoveStake = ( // which if staked > wallet balance means you cannot unstaked // even worse if you stake everything then you can't unstake anything! const contractRemove = useTransaction(() => - vesting.removeStake(removeDecimal(amount, appState.decimals), vegaKey) + vesting.remove_stake(removeDecimal(amount, appState.decimals), vegaKey) ); const walletRemove = useTransaction(() => - staking.removeStake(removeDecimal(amount, appState.decimals), vegaKey) + staking.remove_stake(removeDecimal(amount, appState.decimals), vegaKey) ); const refreshBalances = useRefreshBalances(address); const getAssociationBreakdown = useGetAssociationBreakdown( diff --git a/libs/deposits/src/lib/deposit-form.tsx b/libs/deposits/src/lib/deposit-form.tsx index f93694d3b..fb2fbf9b9 100644 --- a/libs/deposits/src/lib/deposit-form.tsx +++ b/libs/deposits/src/lib/deposit-form.tsx @@ -38,13 +38,13 @@ export interface DepositFormProps { selectedAsset?: Asset; onSelectAsset: (assetId: string) => void; balance: BigNumber | undefined; - submitApprove: () => Promise; + submitApprove: () => void; submitDeposit: (args: { assetSource: string; amount: string; vegaPublicKey: string; - }) => Promise; - requestFaucet: () => Promise; + }) => void; + requestFaucet: () => void; limits: { max: BigNumber; deposited: BigNumber; diff --git a/libs/deposits/src/lib/deposit-limits.tsx b/libs/deposits/src/lib/deposit-limits.tsx index b27ab8ced..8599a2d14 100644 --- a/libs/deposits/src/lib/deposit-limits.tsx +++ b/libs/deposits/src/lib/deposit-limits.tsx @@ -23,7 +23,10 @@ export const DepositLimits = ({ limits, balance }: DepositLimitsProps) => { if (limits.deposited.isEqualTo(0)) { remaining = maxLimit; } else { - remaining = limits.max.minus(limits.deposited).toString(); + const amountRemaining = limits.max.minus(limits.deposited); + remaining = amountRemaining.isGreaterThan(1_000_000) + ? t('1m+') + : amountRemaining.toString(); } return ( diff --git a/libs/deposits/src/lib/deposit-manager.tsx b/libs/deposits/src/lib/deposit-manager.tsx index 8a51882e0..88e1daa7f 100644 --- a/libs/deposits/src/lib/deposit-manager.tsx +++ b/libs/deposits/src/lib/deposit-manager.tsx @@ -13,6 +13,7 @@ import { useEthereumConfig, } from '@vegaprotocol/web3'; import { useTokenContract } from '@vegaprotocol/web3'; +import { removeDecimal } from '@vegaprotocol/react-helpers'; interface ERC20AssetSource { __typename: 'ERC20'; @@ -77,7 +78,7 @@ export const DepositManager = ({ ); // Set up approve transaction - const approve = useSubmitApproval(tokenContract, asset?.decimals); + const approve = useSubmitApproval(tokenContract); // Set up deposit transaction const { confirmationEvent, ...deposit } = useSubmitDeposit(); @@ -109,9 +110,15 @@ export const DepositManager = ({ selectedAsset={asset} onSelectAsset={(id) => setAssetId(id)} assets={sortBy(assets, 'name')} - submitApprove={approve.perform} - submitDeposit={deposit.perform} - requestFaucet={faucet.perform} + 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); + }} + requestFaucet={() => faucet.perform()} limits={limits} allowance={allowance} isFaucetable={isFaucetable} diff --git a/libs/deposits/src/lib/use-get-deposit-limits.ts b/libs/deposits/src/lib/use-get-deposit-limits.ts index 7ffeb1c7d..9173ea2af 100644 --- a/libs/deposits/src/lib/use-get-deposit-limits.ts +++ b/libs/deposits/src/lib/use-get-deposit-limits.ts @@ -20,7 +20,7 @@ export const useGetDepositLimits = (asset?: Asset) => { return; } - return contract.getDepositMaximum(asset.source.contractAddress); + return contract.get_deposit_maximum(asset.source.contractAddress); }, [asset, contract]); useEffect(() => { diff --git a/libs/deposits/src/lib/use-submit-approval.ts b/libs/deposits/src/lib/use-submit-approval.ts index 3db4e847e..3ee9f9c5f 100644 --- a/libs/deposits/src/lib/use-submit-approval.ts +++ b/libs/deposits/src/lib/use-submit-approval.ts @@ -1,22 +1,10 @@ -import { removeDecimal } from '@vegaprotocol/react-helpers'; import type { Token } from '@vegaprotocol/smart-contracts'; -import { useEthereumConfig, useEthereumTransaction } from '@vegaprotocol/web3'; - -export const useSubmitApproval = ( - contract: Token | null, - decimals: number | undefined -) => { - const { config } = useEthereumConfig(); - - const transaction = useEthereumTransaction(() => { - if (!contract || !config || decimals === undefined) { - return null; - } - - const amount = removeDecimal('1000000', decimals); - - return contract.approve(config.collateral_bridge_contract.address, amount); - }); +import { useEthereumTransaction } from '@vegaprotocol/web3'; +export const useSubmitApproval = (contract: Token | null) => { + const transaction = useEthereumTransaction( + contract, + 'approve' + ); return transaction; }; diff --git a/libs/deposits/src/lib/use-submit-deposit.ts b/libs/deposits/src/lib/use-submit-deposit.ts index 599a2622c..5457bee38 100644 --- a/libs/deposits/src/lib/use-submit-deposit.ts +++ b/libs/deposits/src/lib/use-submit-deposit.ts @@ -12,6 +12,11 @@ import { useEthereumConfig, useEthereumTransaction, } from '@vegaprotocol/web3'; +import type { + CollateralBridge, + CollateralBridgeNew, +} from '@vegaprotocol/smart-contracts'; +import { prepend0x } from '@vegaprotocol/smart-contracts'; const DEPOSIT_EVENT_SUB = gql` subscription DepositEvent($partyId: ID!) { @@ -35,26 +40,10 @@ export const useSubmitDeposit = () => { // 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(null); - - const { transaction, perform } = useEthereumTransaction<{ - assetSource: string; - amount: string; - vegaPublicKey: string; - }>((args) => { - if (!contract) { - return null; - } - // New deposit started clear old confirmation event and start - // tracking deposits for the new public key - setConfirmationEvent(null); - setPartyId(args.vegaPublicKey); - - return contract.depositAsset( - args.assetSource, - args.amount, - args.vegaPublicKey - ); - }, config?.confirmations); + const { transaction, perform } = useEthereumTransaction< + CollateralBridgeNew | CollateralBridge, + 'deposit_asset' + >(contract, 'deposit_asset', config?.confirmations); useSubscription(DEPOSIT_EVENT_SUB, { variables: { partyId: partyId ? remove0x(partyId) : '' }, @@ -90,7 +79,12 @@ export const useSubmitDeposit = () => { return { ...transaction, - perform, + perform: (...args: Parameters) => { + setConfirmationEvent(null); + setPartyId(args[2]); + const publicKey = prepend0x(args[2]); + perform(args[0], args[1], publicKey); + }, confirmationEvent, }; }; diff --git a/libs/deposits/src/lib/use-submit-faucet.ts b/libs/deposits/src/lib/use-submit-faucet.ts index d0bf94ced..a0eb4ab7a 100644 --- a/libs/deposits/src/lib/use-submit-faucet.ts +++ b/libs/deposits/src/lib/use-submit-faucet.ts @@ -1,14 +1,10 @@ -import { Token } from '@vegaprotocol/smart-contracts'; -import type { TokenFaucetable } from '@vegaprotocol/smart-contracts'; +import type { Token, TokenFaucetable } from '@vegaprotocol/smart-contracts'; import { useEthereumTransaction } from '@vegaprotocol/web3'; export const useSubmitFaucet = (contract: Token | TokenFaucetable | null) => { - const transaction = useEthereumTransaction(() => { - if (!contract || contract instanceof Token) { - return null; - } - return contract.faucet(); - }); - + const transaction = useEthereumTransaction( + contract, + 'faucet' + ); return transaction; }; diff --git a/libs/orders/src/lib/order-hooks/use-order-edit.tsx b/libs/orders/src/lib/order-hooks/use-order-edit.tsx index 4c81abf44..3fde8a2a8 100644 --- a/libs/orders/src/lib/order-hooks/use-order-edit.tsx +++ b/libs/orders/src/lib/order-hooks/use-order-edit.tsx @@ -50,11 +50,14 @@ export const useOrderEdit = () => { orderAmendment: { orderId: order.id, marketId: order.market.id, + // @ts-ignore fix me please! price: { value: removeDecimal(order.price, order.market?.decimalPlaces), }, timeInForce: VegaWalletOrderTimeInForce[order.timeInForce], + // @ts-ignore fix me please! sizeDelta: 0, + // @ts-ignore fix me please! expiresAt: order.expiresAt ? { value: diff --git a/libs/smart-contracts/src/contracts/collateral-bridge-new.ts b/libs/smart-contracts/src/contracts/collateral-bridge-new.ts index a9e725f21..963eefb9c 100644 --- a/libs/smart-contracts/src/contracts/collateral-bridge-new.ts +++ b/libs/smart-contracts/src/contracts/collateral-bridge-new.ts @@ -1,7 +1,6 @@ import type { BigNumber } from 'ethers'; import { ethers } from 'ethers'; import abi from '../abis/erc20_bridge_new_abi.json'; -import { prepend0x } from '../utils'; export class CollateralBridgeNew { public contract: ethers.Contract; @@ -14,32 +13,28 @@ export class CollateralBridgeNew { this.contract = new ethers.Contract(address, abi, signerOrProvider); } - depositAsset(assetSource: string, amount: string, vegaPublicKey: string) { - return this.contract.deposit_asset( - assetSource, - amount, - prepend0x(vegaPublicKey) - ); + deposit_asset(assetSource: string, amount: string, vegaPublicKey: string) { + return this.contract.deposit_asset(assetSource, amount, vegaPublicKey); } - getAssetSource(vegaAssetId: string) { + get_asset_source(vegaAssetId: string) { return this.contract.get_asset_source(vegaAssetId); } - getDepositMaximum(assetSource: string): Promise { + get_deposit_maximum(assetSource: string): Promise { return this.contract.get_asset_deposit_lifetime_limit(assetSource); } - getMultisigControlAddres() { + get_multisig_control_address() { return this.contract.get_multisig_control_address(); } - getVegaAssetId(address: string) { + get_vega_asset_id(address: string) { return this.contract.get_vega_asset_id(address); } - isAssetListed(address: string) { + is_asset_listed(address: string) { return this.contract.is_asset_listed(address); } - getWithdrawThreshold(assetSource: string) { + get_withdraw_threshold(assetSource: string) { return this.contract.get_withdraw_threshold(assetSource); } - withdrawAsset( + withdraw_asset( assetSource: string, amount: string, target: string, diff --git a/libs/smart-contracts/src/contracts/collateral-bridge.ts b/libs/smart-contracts/src/contracts/collateral-bridge.ts index 71c0b6b1e..29d3705c4 100644 --- a/libs/smart-contracts/src/contracts/collateral-bridge.ts +++ b/libs/smart-contracts/src/contracts/collateral-bridge.ts @@ -1,7 +1,6 @@ import type { BigNumber } from 'ethers'; import { ethers } from 'ethers'; import abi from '../abis/erc20_bridge_abi.json'; -import { prepend0x } from '../utils'; export class CollateralBridge { public contract: ethers.Contract; @@ -16,35 +15,31 @@ export class CollateralBridge { this.address = address; } - depositAsset(assetSource: string, amount: string, vegaPublicKey: string) { - return this.contract.deposit_asset( - assetSource, - amount, - prepend0x(vegaPublicKey) - ); + deposit_asset(assetSource: string, amount: string, vegaPublicKey: string) { + return this.contract.deposit_asset(assetSource, amount, vegaPublicKey); } - getAssetSource(vegaAssetId: string) { + get_asset_source(vegaAssetId: string) { return this.contract.get_asset_source(vegaAssetId); } - getDepositMaximum(assetSource: string): Promise { + get_deposit_maximum(assetSource: string): Promise { return this.contract.get_deposit_maximum(assetSource); } - getDepositMinimum(assetSource: string): Promise { + get_deposit_minimum(assetSource: string): Promise { return this.contract.get_deposit_minimum(assetSource); } - getMultisigControlAddres() { + get_multisig_control_address() { return this.contract.get_multisig_control_address(); } - getVegaAssetId(address: string) { + get_vega_asset_id(address: string) { return this.contract.get_vega_asset_id(address); } - isAssetListed(address: string) { + is_asset_listed(address: string) { return this.contract.is_asset_listed(address); } - getWithdrawThreshold(assetSource: string) { + get_withdraw_threshold(assetSource: string) { return this.contract.get_withdraw_threshold(assetSource); } - withdrawAsset( + withdraw_asset( assetSource: string, amount: string, target: string, diff --git a/libs/smart-contracts/src/contracts/staking-bridge.ts b/libs/smart-contracts/src/contracts/staking-bridge.ts index 19c0fdbff..86da676f8 100644 --- a/libs/smart-contracts/src/contracts/staking-bridge.ts +++ b/libs/smart-contracts/src/contracts/staking-bridge.ts @@ -17,23 +17,23 @@ export class StakingBridge { stake(amount: string, vegaPublicKey: string) { return this.contract.stake(amount, prepend0x(vegaPublicKey)); } - removeStake(amount: string, vegaPublicKey: string) { + remove_stake(amount: string, vegaPublicKey: string) { return this.contract.remove_stake(amount, prepend0x(vegaPublicKey)); } - transferStake(amount: string, newAddress: string, vegaPublicKey: string) { + transfer_stake(amount: string, newAddress: string, vegaPublicKey: string) { return this.contract.transfer_stake( amount, newAddress, prepend0x(vegaPublicKey) ); } - stakingToken() { + staking_token() { return this.contract.staking_token(); } - stakeBalance(target: string, vegaPublicKey: string) { + stake_balance(target: string, vegaPublicKey: string) { return this.contract.stake_balance(target, prepend0x(vegaPublicKey)); } - totalStaked() { + total_staked() { return this.contract.total_staked(); } } diff --git a/libs/smart-contracts/src/contracts/token-vesting.ts b/libs/smart-contracts/src/contracts/token-vesting.ts index ce3bcd5eb..0ec6fdb9d 100644 --- a/libs/smart-contracts/src/contracts/token-vesting.ts +++ b/libs/smart-contracts/src/contracts/token-vesting.ts @@ -14,31 +14,31 @@ export class TokenVesting { this.address = address; } - stakeTokens(amount: string, vegaPublicKey: string) { + stake_tokens(amount: string, vegaPublicKey: string) { return this.contract.stake_tokens(amount, prepend0x(vegaPublicKey)); } - removeStake(amount: string, vegaPublicKey: string) { + remove_stake(amount: string, vegaPublicKey: string) { return this.contract.remove_stake(amount, prepend0x(vegaPublicKey)); } - stakeBalance(address: string, vegaPublicKey: string) { + stake_balance(address: string, vegaPublicKey: string) { return this.contract.stake_balance(address, prepend0x(vegaPublicKey)); } - totalStaked() { + total_staked() { return this.contract.total_staked(); } - userStats(address: string) { + user_stats(address: string) { return this.contract.user_stats(address); } - getTrancheBalance(address: string, trancheId: number) { + get_tranche_balance(address: string, trancheId: number) { return this.contract.get_tranche_balance(address, trancheId); } - getVestedForTranche(address: string, trancheId: number) { + get_vested_for_tranche(address: string, trancheId: number) { return this.contract.get_vested_for_tranche(address, trancheId); } - userTotalAllTranches(address: string) { + user_total_all_tranches(address: string) { return this.contract.user_total_all_tranches(address); } - withdrawFromTranche(trancheId: number) { + withdraw_from_tranche(trancheId: number) { return this.contract.withdraw_from_tranche(trancheId); } } diff --git a/libs/smart-contracts/src/contracts/token.ts b/libs/smart-contracts/src/contracts/token.ts index 7f0751913..48ecb7839 100644 --- a/libs/smart-contracts/src/contracts/token.ts +++ b/libs/smart-contracts/src/contracts/token.ts @@ -29,4 +29,7 @@ export class Token { decimals(): Promise { return this.contract.decimals(); } + faucet() { + /* No op */ + } } diff --git a/libs/web3/src/lib/ethereum-error.ts b/libs/web3/src/lib/ethereum-error.ts index 7adca374f..b8c653bb2 100644 --- a/libs/web3/src/lib/ethereum-error.ts +++ b/libs/web3/src/lib/ethereum-error.ts @@ -1,10 +1,12 @@ export class EthereumError extends Error { code: number; + reason: string; - constructor(message: string, code: number) { + constructor(message: string, code: number, reason: string) { super(message); - this.code = code; this.name = 'EthereumError'; + this.code = code; + this.reason = reason; } } diff --git a/libs/web3/src/lib/transaction-dialog/transaction-dialog.spec.tsx b/libs/web3/src/lib/transaction-dialog/transaction-dialog.spec.tsx index 4da74982f..183218bdb 100644 --- a/libs/web3/src/lib/transaction-dialog/transaction-dialog.spec.tsx +++ b/libs/web3/src/lib/transaction-dialog/transaction-dialog.spec.tsx @@ -40,7 +40,7 @@ it('Opens when tx starts and closes if the user rejects the tx', () => { rerender( generateJsx({ status: EthTxStatus.Error, - error: new EthereumError('User rejected', 4001), + error: new EthereumError('User rejected', 4001, 'reason'), }) ); @@ -82,11 +82,15 @@ it('Dialog states', () => { expect(screen.getByText('Ethereum transaction complete')).toBeInTheDocument(); const errorMsg = 'Something went wrong'; + const reason = 'Transaction failed'; rerender( - generateJsx({ status: EthTxStatus.Error, error: new Error(errorMsg) }) + generateJsx({ + status: EthTxStatus.Error, + error: new EthereumError(errorMsg, 1, reason), + }) ); expect(screen.getByText(`${props.name} failed`)).toBeInTheDocument(); - expect(screen.getByText(errorMsg)).toBeInTheDocument(); + expect(screen.getByText(`Error: ${reason}`)).toBeInTheDocument(); }); it('Success state waits for confirmation event if provided', () => { diff --git a/libs/web3/src/lib/transaction-dialog/transaction-dialog.tsx b/libs/web3/src/lib/transaction-dialog/transaction-dialog.tsx index 83c397d75..b4a0da625 100644 --- a/libs/web3/src/lib/transaction-dialog/transaction-dialog.tsx +++ b/libs/web3/src/lib/transaction-dialog/transaction-dialog.tsx @@ -1,7 +1,8 @@ import { useEffect, useRef, useState } from 'react'; import { t } from '@vegaprotocol/react-helpers'; import { Dialog, Icon, Intent, Loader } from '@vegaprotocol/ui-toolkit'; -import { isExpectedEthereumError } from '../ethereum-error'; +import { isEthereumError, isExpectedEthereumError } from '../ethereum-error'; +import type { TxError } from '../use-ethereum-transaction'; import { EthTxStatus } from '../use-ethereum-transaction'; import { ConfirmRow, TxRow, ConfirmationEventRow } from './dialog-rows'; import { DialogWrapper } from './dialog-wrapper'; @@ -9,7 +10,7 @@ import { DialogWrapper } from './dialog-wrapper'; export interface TransactionDialogProps { name: string; status: EthTxStatus; - error: Error | null; + error: TxError | null; confirmations: number; txHash: string | null; requiredConfirmations?: number; @@ -32,9 +33,26 @@ export const TransactionDialog = ({ const renderContent = () => { if (status === EthTxStatus.Error) { + const classNames = 'break-all text-black dark:text-white'; + if (isEthereumError(error)) { + return ( +

+ {t('Error')}: {error.reason} +

+ ); + } + + if (error instanceof Error) { + return ( +

+ {t('Error')}: {error.message} +

+ ); + } + return ( -

- {error && error.message} +

+ {t('Error')}: {t('Unknown error')}

); } diff --git a/libs/web3/src/lib/use-ethereum-transaction.spec.tsx b/libs/web3/src/lib/use-ethereum-transaction.spec.tsx index a15dc8cf4..1172caf4f 100644 --- a/libs/web3/src/lib/use-ethereum-transaction.spec.tsx +++ b/libs/web3/src/lib/use-ethereum-transaction.spec.tsx @@ -1,11 +1,7 @@ -import { MockedProvider } from '@apollo/client/testing'; -import { waitFor } from '@testing-library/react'; import { renderHook, act } from '@testing-library/react-hooks/dom'; import { EthTxStatus } from './use-ethereum-transaction'; -import type { ReactNode } from 'react'; import { useEthereumTransaction } from './use-ethereum-transaction'; import type { ethers } from 'ethers'; -import { EthereumError } from './ethereum-error'; beforeAll(() => { jest.useFakeTimers(); @@ -17,53 +13,52 @@ afterAll(() => { class MockContract { static txHash = 'tx-hash'; - confirmations = 0; - depositAsset(args: { - assetSource: string; - amount: string; - vegaPublicKey: string; - }): Promise { + contract = { + callStatic: { + deposit_asset() { + return new Promise((resolve) => { + setTimeout(() => { + resolve(true); + }, 10); + }); + }, + }, + } as unknown as ethers.Contract; + + deposit_asset(assetSource: string, amount: string, vegaPublicKey: string) { return Promise.resolve({ hash: MockContract.txHash, wait: () => { - this.confirmations++; + confirmations++; return new Promise((resolve) => { - setTimeout( - () => - resolve({ - from: 'foo', - confirmations: this.confirmations, - } as ethers.ContractReceipt), - 100 - ); + setTimeout(() => { + resolve({ + from: 'foo', + confirmations, + } as ethers.ContractReceipt); + }, 100); }); }, } as ethers.ContractTransaction); } } +let confirmations = 0; const mockContract = new MockContract(); const requiredConfirmations = 3; -function setup(perform: () => void) { - const wrapper = ({ children }: { children: ReactNode }) => ( - {children} - ); - return renderHook( - // @ts-ignore force MockContract - () => useEthereumTransaction(perform, requiredConfirmations), - { wrapper } +function setup(methodName: 'deposit_asset' = 'deposit_asset') { + return renderHook(() => + useEthereumTransaction( + mockContract, + methodName, + requiredConfirmations + ) ); } it('Ethereum transaction flow', async () => { - const { result } = setup(() => { - return mockContract.depositAsset({ - assetSource: 'asset-source', - amount: '100', - vegaPublicKey: 'vega-key', - }); - }); + const { result } = setup(); expect(result.current).toEqual({ transaction: { @@ -77,27 +72,30 @@ it('Ethereum transaction flow', async () => { reset: expect.any(Function), }); - act(() => { - result.current.perform(); - }); + result.current.perform('asset-source', '100', 'vega-key'); - expect(result.current.transaction.status).toEqual(EthTxStatus.Requested); + expect(result.current.transaction.status).toEqual(EthTxStatus.Default); // still default as we await result of static call expect(result.current.transaction.confirmations).toBe(0); - await waitFor(() => { - expect(result.current.transaction.status).toEqual(EthTxStatus.Pending); - expect(result.current.transaction.txHash).toEqual(MockContract.txHash); + await act(async () => { + jest.advanceTimersByTime(10); }); + expect(result.current.transaction.status).toEqual(EthTxStatus.Pending); + expect(result.current.transaction.txHash).toEqual(MockContract.txHash); + expect(result.current.transaction.confirmations).toBe(0); + await act(async () => { jest.advanceTimersByTime(100); }); + expect(result.current.transaction.confirmations).toBe(1); expect(result.current.transaction.status).toEqual(EthTxStatus.Pending); await act(async () => { jest.advanceTimersByTime(100); }); + expect(result.current.transaction.confirmations).toBe(2); expect(result.current.transaction.status).toEqual(EthTxStatus.Pending); @@ -114,18 +112,14 @@ it('Ethereum transaction flow', async () => { }); }); -it('Error handling', async () => { - const { result } = setup(() => { - throw new EthereumError(errorMsg, 500); +describe('error handling', () => { + it('ensures correct method is used', async () => { + const { result } = setup('non-existing-method' as 'deposit_asset'); + + act(() => { + result.current.perform('asset-rouce', '100', 'vega-key'); + }); + + expect(result.current.transaction.status).toEqual(EthTxStatus.Error); }); - - const errorMsg = 'test-error'; - - act(() => { - result.current.perform(); - }); - - expect(result.current.transaction.status).toEqual(EthTxStatus.Error); - expect(result.current.transaction.error instanceof EthereumError).toBe(true); - expect(result.current.transaction.error?.message).toBe(errorMsg); }); diff --git a/libs/web3/src/lib/use-ethereum-transaction.ts b/libs/web3/src/lib/use-ethereum-transaction.ts index 47f30b267..9e02f66de 100644 --- a/libs/web3/src/lib/use-ethereum-transaction.ts +++ b/libs/web3/src/lib/use-ethereum-transaction.ts @@ -1,6 +1,7 @@ import type { ethers } from 'ethers'; import { useCallback, useState } from 'react'; -import { EthereumError, isEthereumError } from './ethereum-error'; +import type { EthereumError } from './ethereum-error'; +import { isEthereumError } from './ethereum-error'; export enum EthTxStatus { Default = 'Default', @@ -28,10 +29,16 @@ export const initialState = { confirmations: 0, }; -export const useEthereumTransaction = ( - performTransaction: ( - args: TArgs - ) => Promise | null, +type DefaultContract = { + contract: ethers.Contract; +}; + +export const useEthereumTransaction = < + TContract extends DefaultContract, + TMethod extends string +>( + contract: TContract | null, + methodName: keyof TContract, requiredConfirmations = 1 ) => { const [transaction, _setTransaction] = useState(initialState); @@ -44,7 +51,27 @@ export const useEthereumTransaction = ( }, []); const perform = useCallback( - async (args: TArgs) => { + // @ts-ignore TS errors here as TMethod doesn't satisfy the constraints on TContract + // its a tricky one to fix but does enforce the correct types when calling perform + async (...args: Parameters) => { + try { + if ( + !contract || + typeof contract[methodName] !== 'function' || + typeof contract.contract.callStatic[methodName as string] !== + 'function' + ) { + throw new Error('method not found on contract'); + } + await contract.contract.callStatic[methodName as string](...args); + } catch (err) { + setTransaction({ + status: EthTxStatus.Error, + error: err as EthereumError, + }); + return; + } + setTransaction({ status: EthTxStatus.Requested, error: null, @@ -52,14 +79,13 @@ export const useEthereumTransaction = ( }); try { - const res = performTransaction(args); + const method = contract[methodName]; - if (res === null) { - setTransaction({ status: EthTxStatus.Default }); - return; + if (!method || typeof method !== 'function') { + throw new Error('method not found on contract'); } - const tx = await res; + const tx = await method.call(contract, ...args); let receipt: ethers.ContractReceipt | null = null; @@ -67,22 +93,21 @@ export const useEthereumTransaction = ( for (let i = 1; i <= requiredConfirmations; i++) { receipt = await tx.wait(i); - setTransaction({ confirmations: receipt.confirmations }); + setTransaction({ + confirmations: receipt + ? receipt.confirmations + : requiredConfirmations, + }); } if (!receipt) { - throw new Error('No receipt after confirmations are met'); + throw new Error('no receipt after confirmations are met'); } setTransaction({ status: EthTxStatus.Complete, receipt }); } catch (err) { - if (err instanceof Error) { + if (err instanceof Error || isEthereumError(err)) { setTransaction({ status: EthTxStatus.Error, error: err }); - } else if (isEthereumError(err)) { - setTransaction({ - status: EthTxStatus.Error, - error: new EthereumError(err.message, err.code), - }); } else { setTransaction({ status: EthTxStatus.Error, @@ -91,7 +116,7 @@ export const useEthereumTransaction = ( } } }, - [performTransaction, requiredConfirmations, setTransaction] + [contract, methodName, requiredConfirmations, setTransaction] ); const reset = useCallback(() => { diff --git a/libs/withdraws/src/lib/use-complete-withdraw.spec.tsx b/libs/withdraws/src/lib/use-complete-withdraw.spec.tsx index d0d8d040d..9b719637b 100644 --- a/libs/withdraws/src/lib/use-complete-withdraw.spec.tsx +++ b/libs/withdraws/src/lib/use-complete-withdraw.spec.tsx @@ -11,7 +11,10 @@ import * as sentry from '@sentry/react'; import type { Erc20ApprovalNew_erc20WithdrawalApproval } from './__generated__/Erc20ApprovalNew'; jest.mock('@vegaprotocol/web3', () => ({ - useBridgeContract: jest.fn(), + useBridgeContract: jest.fn().mockReturnValue({ + withdraw_asset: jest.fn(), + isNewContract: true, + }), useEthereumTransaction: jest.fn(), })); @@ -56,7 +59,14 @@ it('Should perform the Ethereum transaction with the fetched approval', async () result.current.submit(withdrawalId); }); await waitFor(() => { - expect(mockPerform).toHaveBeenCalledWith(erc20WithdrawalApproval); + expect(mockPerform).toHaveBeenCalledWith( + erc20WithdrawalApproval.assetSource, + erc20WithdrawalApproval.amount, + erc20WithdrawalApproval.targetAddress, + erc20WithdrawalApproval.creation, + erc20WithdrawalApproval.nonce, + erc20WithdrawalApproval.signatures + ); expect(result.current.withdrawalId).toBe(withdrawalId); }); }); diff --git a/libs/withdraws/src/lib/use-complete-withdraw.ts b/libs/withdraws/src/lib/use-complete-withdraw.ts index dc4751dd0..286562b0e 100644 --- a/libs/withdraws/src/lib/use-complete-withdraw.ts +++ b/libs/withdraws/src/lib/use-complete-withdraw.ts @@ -21,58 +21,22 @@ export const PENDING_WITHDRAWAL_FRAGMMENT = gql` } `; -export interface NewWithdrawTransactionArgs { - assetSource: string; - amount: string; - nonce: string; - signatures: string; - targetAddress: string; - creation: string; -} - -export interface WithdrawTransactionArgs { - assetSource: string; - amount: string; - nonce: string; - signatures: string; - targetAddress: string; -} - export const useCompleteWithdraw = (isNewContract: boolean) => { const { query, cache } = useApolloClient(); const contract = useBridgeContract(isNewContract); const [id, setId] = useState(''); const { transaction, perform } = useEthereumTransaction< - WithdrawTransactionArgs | NewWithdrawTransactionArgs - >((args) => { - if (!contract) { - return null; - } - if (contract.isNewContract) { - const withdrawalData = args as NewWithdrawTransactionArgs; - return (contract as CollateralBridgeNew).withdrawAsset( - withdrawalData.assetSource, - withdrawalData.amount, - withdrawalData.targetAddress, - withdrawalData.creation, - withdrawalData.nonce, - withdrawalData.signatures - ); - } else { - return (contract as CollateralBridge).withdrawAsset( - args.assetSource, - args.amount, - args.targetAddress, - args.nonce, - args.signatures - ); - } - }); + CollateralBridgeNew | CollateralBridge, + 'withdraw_asset' + >(contract, 'withdraw_asset'); const submit = useCallback( async (withdrawalId: string) => { setId(withdrawalId); try { + if (!contract) { + return; + } const res = await query< Erc20Approval | Erc20ApprovalNew, Erc20ApprovalVariables @@ -83,16 +47,34 @@ export const useCompleteWithdraw = (isNewContract: boolean) => { variables: { withdrawalId }, }); - if (!res.data.erc20WithdrawalApproval) { + const approval = res.data.erc20WithdrawalApproval; + if (!approval) { throw new Error('Could not retrieve withdrawal approval'); } - perform(res.data.erc20WithdrawalApproval); + if (contract.isNewContract && 'creation' in approval) { + perform( + approval.assetSource, + approval.amount, + approval.targetAddress, + approval.creation, + approval.nonce, + approval.signatures + ); + } else { + perform( + approval.assetSource, + approval.amount, + approval.targetAddress, + approval.nonce, + approval.signatures + ); + } } catch (err) { captureException(err); } }, - [query, isNewContract, perform] + [contract, query, isNewContract, perform] ); useEffect(() => { diff --git a/libs/withdraws/src/lib/use-get-withdraw-limits.tsx b/libs/withdraws/src/lib/use-get-withdraw-limits.tsx index 19b582019..12d6eec65 100644 --- a/libs/withdraws/src/lib/use-get-withdraw-limits.tsx +++ b/libs/withdraws/src/lib/use-get-withdraw-limits.tsx @@ -11,7 +11,7 @@ export const useGetWithdrawLimits = (asset?: Asset) => { return; } - return contract.getWithdrawThreshold(asset.source.contractAddress); + return contract.get_withdraw_threshold(asset.source.contractAddress); }, [asset, contract]); const { diff --git a/libs/withdraws/src/lib/use-withdraw.spec.tsx b/libs/withdraws/src/lib/use-withdraw.spec.tsx index 226b078d7..26be98d14 100644 --- a/libs/withdraws/src/lib/use-withdraw.spec.tsx +++ b/libs/withdraws/src/lib/use-withdraw.spec.tsx @@ -10,7 +10,10 @@ import { useWithdraw } from './use-withdraw'; import type { Erc20ApprovalNew } from './__generated__/Erc20ApprovalNew'; jest.mock('@vegaprotocol/web3', () => ({ - useBridgeContract: jest.fn(), + useBridgeContract: jest.fn().mockReturnValue({ + withdraw_asset: jest.fn(), + isNewContract: true, + }), useEthereumTransaction: jest.fn(), })); @@ -141,9 +144,15 @@ it('Creates withdrawal and immediately submits Ethereum transaction', async () = // @ts-ignore MockedRespones types inteferring mockERC20Approval.result.data.erc20WithdrawalApproval ); + // @ts-ignore MockedRespones types inteferring + const withdrawal = mockERC20Approval.result.data.erc20WithdrawalApproval; expect(mockPerform).toHaveBeenCalledWith( - // @ts-ignore MockedRespones types inteferring - mockERC20Approval.result.data.erc20WithdrawalApproval + withdrawal.assetSource, + withdrawal.amount, + withdrawal.targetAddress, + withdrawal.creation, + withdrawal.nonce, + withdrawal.signatures ); }); diff --git a/libs/withdraws/src/lib/use-withdraw.ts b/libs/withdraws/src/lib/use-withdraw.ts index 43644c64e..25a411a7d 100644 --- a/libs/withdraws/src/lib/use-withdraw.ts +++ b/libs/withdraws/src/lib/use-withdraw.ts @@ -4,20 +4,19 @@ import { useBridgeContract, useEthereumTransaction } from '@vegaprotocol/web3'; import { useVegaTransaction, useVegaWallet } from '@vegaprotocol/wallet'; import { useCallback, useEffect, useState } from 'react'; import { ERC20_APPROVAL_QUERY, ERC20_APPROVAL_QUERY_NEW } from './queries'; -import type { - NewWithdrawTransactionArgs, - WithdrawTransactionArgs, -} from './use-complete-withdraw'; import type { Erc20Approval, Erc20ApprovalVariables, Erc20Approval_erc20WithdrawalApproval, } from './__generated__/Erc20Approval'; +import type { + Erc20ApprovalNew, + Erc20ApprovalNew_erc20WithdrawalApproval, +} from './__generated__/Erc20ApprovalNew'; import type { CollateralBridge, CollateralBridgeNew, } from '@vegaprotocol/smart-contracts'; -import type { Erc20ApprovalNew } from './__generated__/Erc20ApprovalNew'; export interface WithdrawalFields { amount: string; @@ -27,8 +26,11 @@ export interface WithdrawalFields { export const useWithdraw = (cancelled: boolean, isNewContract: boolean) => { const [withdrawalId, setWithdrawalId] = useState(null); - const [approval, setApproval] = - useState(null); + const [approval, setApproval] = useState< + | Erc20Approval_erc20WithdrawalApproval + | Erc20ApprovalNew_erc20WithdrawalApproval + | null + >(null); const contract = useBridgeContract(isNewContract); const { keypair } = useVegaWallet(); @@ -42,30 +44,10 @@ export const useWithdraw = (cancelled: boolean, isNewContract: boolean) => { transaction: ethTx, perform, reset: resetEthTx, - } = useEthereumTransaction((args) => { - if (!contract) { - return null; - } - if (contract.isNewContract) { - const withdrawalArguments = args as NewWithdrawTransactionArgs; - return (contract as CollateralBridgeNew).withdrawAsset( - withdrawalArguments.assetSource, - withdrawalArguments.amount, - withdrawalArguments.targetAddress, - withdrawalArguments.creation, - withdrawalArguments.nonce, - withdrawalArguments.signatures - ); - } else { - return (contract as CollateralBridge).withdrawAsset( - args.assetSource, - args.amount, - args.targetAddress, - args.nonce, - args.signatures - ); - } - }); + } = useEthereumTransaction< + CollateralBridgeNew | CollateralBridge, + 'withdraw_asset' + >(contract, 'withdraw_asset'); const { data, stopPolling } = useQuery< Erc20Approval | Erc20ApprovalNew, @@ -114,11 +96,28 @@ export const useWithdraw = (cancelled: boolean, isNewContract: boolean) => { }, [data, stopPolling]); useEffect(() => { - if (approval && !cancelled) { - perform(approval); + if (approval && contract && !cancelled) { + if (contract.isNewContract && 'creation' in approval) { + perform( + approval.assetSource, + approval.amount, + approval.targetAddress, + approval.creation, + approval.nonce, + approval.signatures + ); + } else { + perform( + approval.assetSource, + approval.amount, + approval.targetAddress, + approval.nonce, + approval.signatures + ); + } } // eslint-disable-next-line - }, [approval]); + }, [approval, contract]); const reset = useCallback(() => { resetVegaTx(); diff --git a/libs/withdraws/src/lib/withdraw-dialog.tsx b/libs/withdraws/src/lib/withdraw-dialog.tsx index 4f3f5b16a..a8484df4a 100644 --- a/libs/withdraws/src/lib/withdraw-dialog.tsx +++ b/libs/withdraws/src/lib/withdraw-dialog.tsx @@ -4,6 +4,7 @@ import type { VegaTxState } from '@vegaprotocol/wallet'; import { VegaTxStatus } from '@vegaprotocol/wallet'; import type { ReactNode } from 'react'; import type { EthTxState } from '@vegaprotocol/web3'; +import { isEthereumError } from '@vegaprotocol/web3'; import { EthTxStatus } from '@vegaprotocol/web3'; import { t } from '@vegaprotocol/react-helpers'; import type { Erc20Approval_erc20WithdrawalApproval } from './__generated__/Erc20Approval'; @@ -132,7 +133,11 @@ const getProps = ( intent: Intent.Danger, children: ( - {ethTx.error ? ethTx.error.message : t('Something went wrong')} + {isEthereumError(ethTx.error) + ? `Error: ${ethTx.error.reason}` + : ethTx.error instanceof Error + ? t(`Error: ${ethTx.error.message}`) + : t('Something went wrong')} ), }, diff --git a/libs/withdraws/src/lib/withdraw-manager.spec.tsx b/libs/withdraws/src/lib/withdraw-manager.spec.tsx index 83b5caa8e..3985b86c0 100644 --- a/libs/withdraws/src/lib/withdraw-manager.spec.tsx +++ b/libs/withdraws/src/lib/withdraw-manager.spec.tsx @@ -76,7 +76,7 @@ it('Expected Ethereum error closes the dialog', async () => { ethTx: { ...useWithdrawValue.ethTx, status: EthTxStatus.Error, - error: new EthereumError('User rejected transaction', 4001), + error: new EthereumError('User rejected transaction', 4001, 'reason'), }, }); rerender(generateJsx(props));