Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/Refactor tx handlers #222

Merged
merged 10 commits into from
Jun 2, 2023
230 changes: 44 additions & 186 deletions apps/common/utils/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {captureException} from '@sentry/nextjs';
import {getEthZapperContract} from '@vaults/utils';
import VAULT_MIGRATOR_ABI from '@vaults/utils/abi/vaultMigrator.abi';
import {erc20ABI, prepareWriteContract, readContract, waitForTransaction, writeContract} from '@wagmi/core';
import {toast} from '@yearn-finance/web-lib/components/yToast';
import PARTNER_VAULT_ABI from '@yearn-finance/web-lib/utils/abi/partner.vault.abi';
import VAULT_ABI from '@yearn-finance/web-lib/utils/abi/vault.abi';
import ZAP_ETH_TO_YVETH_ABI from '@yearn-finance/web-lib/utils/abi/zapEthToYvEth.abi';
Expand All @@ -11,7 +10,7 @@ import {toAddress} from '@yearn-finance/web-lib/utils/address';
import {MAX_UINT_256} from '@yearn-finance/web-lib/utils/constants';
import {defaultTxStatus} from '@yearn-finance/web-lib/utils/web3/transaction';
import {assert} from '@common/utils/assert';
import {assertAddress, toWagmiProvider} from '@common/utils/toWagmiProvider';
import {assertAddress, handleTx, toWagmiProvider} from '@common/utils/toWagmiProvider';

import type {BaseError, ContractFunctionExecutionError, Hex} from 'viem';
import type {Connector} from 'wagmi';
Expand Down Expand Up @@ -74,60 +73,27 @@ type TApproveERC20 = TWriteTransaction & {
amount: bigint;
};
export async function approveERC20(props: TApproveERC20): Promise<TTxResponse> {
assertAddress(props.contractAddress, 'contractAddress');
assertAddress(props.spenderAddress, 'spenderAddress');

props.statusHandler?.({...defaultTxStatus, pending: true});
try {
const wagmiProvider = await toWagmiProvider(props.connector);
try {
const config = await prepareWriteContract({
...wagmiProvider,
try {
return await handleTx(props, {
address: props.contractAddress,
abi: erc20ABI,
functionName: 'approve',
args: [props.spenderAddress, props.amount]
});
} catch (error) {
const err = error as ContractFunctionExecutionError;
if (err.name === 'ContractFunctionExecutionError') {
return await handleTx(props, {
address: props.contractAddress,
abi: erc20ABI,
abi: ALTERNATE_ERC20_APPROVE_ABI,
functionName: 'approve',
args: [props.spenderAddress, props.amount]
});
const {hash} = await writeContract(config.request);
const receipt = await waitForTransaction({chainId: wagmiProvider.chainId, hash});
if (receipt.status === 'success') {
props.statusHandler?.({...defaultTxStatus, success: true});
} else if (receipt.status === 'reverted') {
props.statusHandler?.({...defaultTxStatus, error: true});
}
return ({isSuccessful: receipt.status === 'success', receipt});
} catch (error) {
const err = error as ContractFunctionExecutionError;
if (err.name === 'ContractFunctionExecutionError') {
const alternateConfig = await prepareWriteContract({
...wagmiProvider,
address: props.contractAddress,
abi: ALTERNATE_ERC20_APPROVE_ABI,
functionName: 'approve',
args: [props.spenderAddress, props.amount]
});
const {hash} = await writeContract(alternateConfig.request);
const receipt = await waitForTransaction({chainId: wagmiProvider.chainId, hash});
if (receipt.status === 'success') {
props.statusHandler?.({...defaultTxStatus, success: true});
} else if (receipt.status === 'reverted') {
props.statusHandler?.({...defaultTxStatus, error: true});
}
return ({isSuccessful: receipt.status === 'success', receipt});
}
return ({isSuccessful: false, error: err || ''});
}
} catch (error) {
console.error(error);
const errorAsBaseError = error as BaseError;
captureException(errorAsBaseError);
props.statusHandler?.({...defaultTxStatus, error: true});
return ({isSuccessful: false, error: errorAsBaseError || ''});
} finally {
setTimeout((): void => {
props.statusHandler?.({...defaultTxStatus});
}, 3000);
}
return ({isSuccessful: false, error: new Error('Unknown error')});
}

/* 🔵 - Yearn Finance **********************************************************
Expand All @@ -141,39 +107,14 @@ type TDeposit = TWriteTransaction & {
amount: bigint;
};
export async function deposit(props: TDeposit): Promise<TTxResponse> {
assertAddress(props.contractAddress, 'contractAddress');
assert(props.amount > 0n, 'Amount is 0');

props.statusHandler?.({...defaultTxStatus, pending: true});
const wagmiProvider = await toWagmiProvider(props.connector);

try {
const config = await prepareWriteContract({
...wagmiProvider,
address: props.contractAddress,
abi: VAULT_ABI,
functionName: 'deposit',
args: [props.amount]
});
const {hash} = await writeContract(config.request);
const receipt = await waitForTransaction({chainId: wagmiProvider.chainId, hash});
if (receipt.status === 'success') {
props.statusHandler?.({...defaultTxStatus, success: true});
} else if (receipt.status === 'reverted') {
props.statusHandler?.({...defaultTxStatus, error: true});
}
return ({isSuccessful: receipt.status === 'success', receipt});
} catch (error) {
console.error(error);
const errorAsBaseError = error as BaseError;
captureException(errorAsBaseError);
props.statusHandler?.({...defaultTxStatus, error: true});
return ({isSuccessful: false, error: errorAsBaseError || ''});
} finally {
setTimeout((): void => {
props.statusHandler?.({...defaultTxStatus});
}, 3000);
}
return await handleTx(props, {
address: props.contractAddress,
abi: VAULT_ABI,
functionName: 'deposit',
args: [props.amount]
});
}

/* 🔵 - Yearn Finance **********************************************************
Expand Down Expand Up @@ -252,44 +193,19 @@ type TDepositViaPartner = TWriteTransaction & {
amount: bigint;
};
export async function depositViaPartner(props: TDepositViaPartner): Promise<TTxResponse> {
assertAddress(props.contractAddress, 'contractAddress');
assertAddress(props.vaultAddress, 'vaultAddress');
assert(props.amount > 0n, 'Amount is 0');

props.statusHandler?.({...defaultTxStatus, pending: true});
const wagmiProvider = await toWagmiProvider(props.connector);

try {
const config = await prepareWriteContract({
...wagmiProvider,
address: props.contractAddress,
abi: PARTNER_VAULT_ABI,
functionName: 'deposit',
args: [
props.vaultAddress,
props.partnerAddress || toAddress(process.env.PARTNER_ID_ADDRESS),
props.amount
]
});
const {hash} = await writeContract(config.request);
const receipt = await waitForTransaction({chainId: wagmiProvider.chainId, hash});
if (receipt.status === 'success') {
props.statusHandler?.({...defaultTxStatus, success: true});
} else if (receipt.status === 'reverted') {
props.statusHandler?.({...defaultTxStatus, error: true});
}
return ({isSuccessful: receipt.status === 'success', receipt});
} catch (error) {
console.error(error);
const errorAsBaseError = error as BaseError;
captureException(errorAsBaseError);
props.statusHandler?.({...defaultTxStatus, error: true});
return ({isSuccessful: false, error: errorAsBaseError || ''});
} finally {
setTimeout((): void => {
props.statusHandler?.({...defaultTxStatus});
}, 3000);
}
return await handleTx(props, {
address: props.contractAddress,
abi: PARTNER_VAULT_ABI,
functionName: 'deposit',
args: [
props.vaultAddress,
props.partnerAddress || toAddress(process.env.PARTNER_ID_ADDRESS),
props.amount
]
});
}

/* 🔵 - Yearn Finance **********************************************************
Expand Down Expand Up @@ -362,41 +278,18 @@ export async function withdrawETH(props: TWithdrawEth): Promise<TTxResponse> {
** @param amount - The amount of ETH to withdraw.
******************************************************************************/
type TWithdrawShares = TWriteTransaction & {
amount: bigint;
amount: bigint | undefined;
};
export async function withdrawShares(props: TWithdrawShares): Promise<TTxResponse> {
assertAddress(props.contractAddress, 'contractAddress');
assert(typeof props.amount === 'bigint', 'Amount is not a bigint');
assert(props.amount > 0n, 'Amount is 0');

props.statusHandler?.({...defaultTxStatus, pending: true});
const wagmiProvider = await toWagmiProvider(props.connector);
try {
const config = await prepareWriteContract({
...wagmiProvider,
address: props.contractAddress,
abi: VAULT_ABI,
functionName: 'withdraw',
args: [props.amount]
});
const {hash} = await writeContract(config.request);
const receipt = await waitForTransaction({chainId: wagmiProvider.chainId, hash});
if (receipt.status === 'success') {
props.statusHandler?.({...defaultTxStatus, success: true});
} else if (receipt.status === 'reverted') {
props.statusHandler?.({...defaultTxStatus, error: true});
}
return ({isSuccessful: receipt.status === 'success', receipt});
} catch (error) {
console.error(error);
const errorAsBaseError = error as BaseError;
captureException(errorAsBaseError);
props.statusHandler?.({...defaultTxStatus, error: true});
return ({isSuccessful: false, error: errorAsBaseError || ''});
} finally {
setTimeout((): void => {
props.statusHandler?.({...defaultTxStatus});
}, 3000);
}
return await handleTx(props, {
address: props.contractAddress,
abi: VAULT_ABI,
functionName: 'withdraw',
args: [props.amount]
});
}

/* 🔵 - Yearn Finance **********************************************************
Expand All @@ -412,48 +305,13 @@ type TMigrateShares = TWriteTransaction & {
toVault: TAddress | undefined;
};
export async function migrateShares(props: TMigrateShares): Promise<TTxResponse> {
assertAddress(props.contractAddress, 'contractAddress');
assertAddress(props.fromVault, 'fromVault');
assertAddress(props.toVault, 'toVault');

const wagmiProvider = await toWagmiProvider(props.connector);
try {
const config = await prepareWriteContract({
...wagmiProvider,
address: props.contractAddress,
abi: VAULT_MIGRATOR_ABI,
functionName: 'migrateAll',
args: [props.fromVault, props.toVault]
});

const gas = config?.request?.gas || 0n;
const estimateGas = new Intl.NumberFormat([navigator.language || 'fr-FR', 'en-US']).format(gas);
const safeGas = new Intl.NumberFormat([navigator.language || 'fr-FR', 'en-US']).format(gas * 13n / 10n);
toast({
type: 'info',
content: `Gas estimate for migration is ${estimateGas}. We'll use ${safeGas} to give some margin and reduce the risk of transaction failure.`,
duration: 10000
});
console.info(`Gas estimate for migration is ${estimateGas}. We'll use ${safeGas} to give some margin and reduce the risk of transaction failure.`);
config.request.gas = gas * 13n / 10n;

const {hash} = await writeContract(config.request);
const receipt = await waitForTransaction({chainId: wagmiProvider.chainId, hash});
if (receipt.status === 'success') {
props.statusHandler?.({...defaultTxStatus, success: true});
} else if (receipt.status === 'reverted') {
props.statusHandler?.({...defaultTxStatus, error: true});
}
return ({isSuccessful: receipt.status === 'success', receipt});
} catch (error) {
console.error(error);
const errorAsBaseError = error as BaseError;
captureException(errorAsBaseError);
props.statusHandler?.({...defaultTxStatus, error: true});
return ({isSuccessful: false, error: errorAsBaseError || ''});
} finally {
setTimeout((): void => {
props.statusHandler?.({...defaultTxStatus});
}, 3000);
}
return await handleTx(props, {
address: props.contractAddress,
abi: VAULT_MIGRATOR_ABI,
functionName: 'migrateAll',
args: [props.fromVault, props.toVault]
});
}
62 changes: 60 additions & 2 deletions apps/common/utils/toWagmiProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import {captureException} from '@sentry/nextjs';
import {prepareWriteContract, waitForTransaction, writeContract} from '@wagmi/core';
import {toAddress} from '@yearn-finance/web-lib/utils/address';
import {ETH_TOKEN_ADDRESS, ZERO_ADDRESS} from '@yearn-finance/web-lib/utils/constants';
import {isTAddress} from '@yearn-finance/web-lib/utils/isTAddress';
import {defaultTxStatus} from '@yearn-finance/web-lib/utils/web3/transaction';
import {assert} from '@common/utils/assert';

import type {Abi, BaseError, SimulateContractParameters} from 'viem';
import type {Connector} from 'wagmi';
import type {TAddress} from '@yearn-finance/web-lib/types';
import type {defaultTxStatus} from '@yearn-finance/web-lib/utils/web3/transaction';
import type {GetWalletClientResult} from '@wagmi/core';
import type {TTxResponse} from '@yearn-finance/web-lib/utils/web3/transaction';
import type {GetWalletClientResult, WalletClient} from '@wagmi/core';

export type TWagmiProviderContract = {
walletClient: GetWalletClientResult,
Expand Down Expand Up @@ -38,3 +42,57 @@ export function assertAddress(addr: string | TAddress | undefined, name?: string
assert(toAddress(addr) !== ZERO_ADDRESS, `${name || 'Address'} is 0x0`);
assert(toAddress(addr) !== ETH_TOKEN_ADDRESS, `${name || 'Address'} is 0xE`);
}

type TPrepareWriteContractConfig<
TAbi extends Abi | readonly unknown[] = Abi,
TFunctionName extends string = string,
TChainId extends number = number,
TWalletClient extends WalletClient = WalletClient
> = Omit<SimulateContractParameters<TAbi, TFunctionName>, 'chain' | 'address'> & {
chainId?: TChainId | number
walletClient?: TWalletClient | null
address: TAddress | undefined
}
export async function handleTx<
TAbi extends Abi | readonly unknown[],
TFunctionName extends string,
TChainId extends number,
TWalletClient extends WalletClient = WalletClient
>(
args: TWriteTransaction,
props: TPrepareWriteContractConfig<TAbi, TFunctionName, TChainId, TWalletClient>
): Promise<TTxResponse> {
args.statusHandler?.({...defaultTxStatus, pending: true});
const wagmiProvider = await toWagmiProvider(args.connector);

//Some extra assertions
Majorfi marked this conversation as resolved.
Show resolved Hide resolved
Majorfi marked this conversation as resolved.
Show resolved Hide resolved
assertAddress(props.address, 'contractAddress');
assertAddress(wagmiProvider.address, 'userAddress');
Majorfi marked this conversation as resolved.
Show resolved Hide resolved
try {
const config = await prepareWriteContract({
...wagmiProvider,
...props as TPrepareWriteContractConfig,
address: props.address
});
const {hash} = await writeContract(config.request);
const receipt = await waitForTransaction({chainId: wagmiProvider.chainId, hash});
if (receipt.status === 'success') {
args.statusHandler?.({...defaultTxStatus, success: true});
} else if (receipt.status === 'reverted') {
args.statusHandler?.({...defaultTxStatus, error: true});
}
return ({isSuccessful: receipt.status === 'success', receipt});
} catch (error) {
console.error(error);
const errorAsBaseError = error as BaseError;
if (process.env.NODE_ENV === 'production') {
captureException(errorAsBaseError);
}
args.statusHandler?.({...defaultTxStatus, error: true});
return ({isSuccessful: false, error: errorAsBaseError || ''});
} finally {
setTimeout((): void => {
args.statusHandler?.({...defaultTxStatus});
}, 3000);
}
}
Loading