diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index c606a144281..00000000000 --- a/jest.config.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testPathIgnorePatterns: ['/node_modules/', '.d.ts', '.js', '__mocks__', 'mockData'], - clearMocks: true, - roots: [''], - collectCoverage: false, - setupFiles: ['/.jest/setup.js'], - coverageDirectory: 'coverage', - coveragePathIgnorePatterns: ['/node_modules/', 'dist', '__mocks__', 'mockData'], - moduleNameMapper: { - '^@shapeshiftoss\\/([^/]+)': ['@shapeshiftoss/$1/src', '@shapeshiftoss/$1'], - }, - globals: { - 'ts-jest': { - sourceMap: true, - isolatedModules: true, - }, - }, -} diff --git a/package.json b/package.json index b7dd7a1b196..d78ff532191 100644 --- a/package.json +++ b/package.json @@ -272,6 +272,35 @@ "react-dom@^18.2.0": "patch:react-dom@npm%3A18.2.0#./.yarn/patches/react-dom-npm-18.2.0-dd675bca1c.patch" }, "jest": { - "resetMocks": false + "preset": "ts-jest", + "testEnvironment": "node", + "testPathIgnorePatterns": [ + "/node_modules/", + ".d.ts", + ".js", + "__mocks__", + "mockData" + ], + "clearMocks": true, + "resetMocks": false, + "roots": [ + "" + ], + "collectCoverage": false, + "setupFiles": [ + "/.jest/setup.js" + ], + "moduleNameMapper": { + "^@shapeshiftoss\\/([^/ ]+)": [ + "@shapeshiftoss/$1", + "@shapeshiftoss/$1/src" + ] + }, + "globals": { + "ts-jest": { + "sourceMap": true, + "isolatedModules": true + } + } } -} +} \ No newline at end of file diff --git a/packages/chain-adapters/src/evm/EvmBaseAdapter.ts b/packages/chain-adapters/src/evm/EvmBaseAdapter.ts index 60875bd0d21..a814b32c78a 100644 --- a/packages/chain-adapters/src/evm/EvmBaseAdapter.ts +++ b/packages/chain-adapters/src/evm/EvmBaseAdapter.ts @@ -37,7 +37,6 @@ import type { import { ValidAddressResultType } from '../types' import { chainIdToChainLabel, - convertNumberToHex, getAssetNamespace, toAddressNList, toRootDerivationPath, @@ -548,7 +547,7 @@ export abstract class EvmBaseAdapter implements IChainAdap const bip44Params = this.getBIP44Params({ accountNumber }) const txToSign = { addressNList: toAddressNList(bip44Params), - value: convertNumberToHex(value), + value: numberToHex(value), to, chainId: Number(fromChainId(this.chainId).chainReference), data, diff --git a/packages/unchained-client/src/evm/parser/index.ts b/packages/unchained-client/src/evm/parser/index.ts index f869b80c4df..9c1c0d1c269 100644 --- a/packages/unchained-client/src/evm/parser/index.ts +++ b/packages/unchained-client/src/evm/parser/index.ts @@ -1,6 +1,5 @@ import type { AssetId, ChainId } from '@shapeshiftoss/caip' import { ASSET_NAMESPACE, ASSET_REFERENCE, ethChainId, toAssetId } from '@shapeshiftoss/caip' -import { Logger } from '@shapeshiftoss/logger' import { BigNumber } from 'bignumber.js' import { ethers } from 'ethers' @@ -12,11 +11,6 @@ import type { ParsedTx, SubParser, Tx, TxSpecific } from './types' export * from './types' export * from './utils' -const logger = new Logger({ - namespace: ['unchained-client', 'evm', 'parser'], - level: process.env.LOG_LEVEL, -}) - export interface TransactionParserArgs { chainId: ChainId assetId: AssetId @@ -170,7 +164,6 @@ export class BaseTransactionParser { case 'BEP721': return ASSET_NAMESPACE.bep721 default: - logger.warn(`unsupported asset namespace: ${transfer.type}`) return } })() diff --git a/src/features/defi/helpers/utils.ts b/src/features/defi/helpers/utils.ts index 64e5bae9a9f..386df394559 100644 --- a/src/features/defi/helpers/utils.ts +++ b/src/features/defi/helpers/utils.ts @@ -1,7 +1,16 @@ import type { Asset } from '@shapeshiftoss/asset-service' import type { AccountId, ChainId } from '@shapeshiftoss/caip' import { cosmosChainId, osmosisChainId } from '@shapeshiftoss/caip' -import { bnOrZero } from 'lib/bignumber/bignumber' +import type { + evm, + EvmChainAdapter, + EvmChainId, + FeeData, + FeeDataEstimate, +} from '@shapeshiftoss/chain-adapters' +import type { HDWallet } from '@shapeshiftoss/hdwallet-core' +import { supportsETH } from '@shapeshiftoss/hdwallet-core' +import { BigNumber, bn, bnOrZero } from 'lib/bignumber/bignumber' import { selectPortfolioCryptoPrecisionBalanceByFilter } from 'state/slices/selectors' import { store } from 'state/store' @@ -34,3 +43,84 @@ export const canCoverTxFees = ({ return bnOrZero(feeAssetBalanceCryptoHuman).minus(bnOrZero(estimatedGasCryptoPrecision)).gte(0) } + +export const getFeeDataFromEstimate = ({ + average, + fast, +}: FeeDataEstimate): FeeData => ({ + txFee: BigNumber.max( + average.txFee, + bnOrZero(bn(fast.chainSpecific.gasPrice).times(average.chainSpecific.gasLimit)), + ).toFixed(0), // use worst case average eip1559 vs fast legacy + chainSpecific: { + gasLimit: average.chainSpecific.gasLimit, // average and fast gasLimit values are the same + gasPrice: fast.chainSpecific.gasPrice, // fast gas price since it is underestimated currently + maxFeePerGas: average.chainSpecific.maxFeePerGas, + maxPriorityFeePerGas: average.chainSpecific.maxPriorityFeePerGas, + }, +}) + +type GetFeesFromFeeDataArgs = { + wallet: HDWallet + feeData: evm.FeeData +} + +export const getFeesFromFeeData = async ({ + wallet, + feeData: { gasLimit, gasPrice, maxFeePerGas, maxPriorityFeePerGas }, +}: GetFeesFromFeeDataArgs): Promise => { + if (!supportsETH(wallet)) throw new Error('wallet has no evm support') + if (!gasLimit) throw new Error('gasLimit is required') + + const supportsEip1559 = await wallet.ethSupportsEIP1559() + + // use eip1559 fees if able + if (supportsEip1559 && maxFeePerGas && maxPriorityFeePerGas) { + return { gasLimit, maxFeePerGas, maxPriorityFeePerGas } + } + + // fallback to legacy fees if unable to use eip1559 + if (gasPrice) return { gasLimit, gasPrice } + + throw new Error('legacy gas or eip1559 gas required') +} + +type BuildAndBroadcastArgs = GetFeesFromFeeDataArgs & { + accountNumber: number + adapter: EvmChainAdapter + data: string + to: string + value: string +} + +export const buildAndBroadcast = async ({ + accountNumber, + adapter, + data, + feeData, + to, + value, + wallet, +}: BuildAndBroadcastArgs) => { + const { txToSign } = await adapter.buildCustomTx({ + wallet, + to, + accountNumber, + value, + data, + ...(await getFeesFromFeeData({ wallet, feeData })), + }) + + if (wallet.supportsOfflineSigning()) { + const signedTx = await adapter.signTransaction({ txToSign, wallet }) + const txid = await adapter.broadcastTransaction(signedTx) + return txid + } + + if (wallet.supportsBroadcast() && adapter.signAndBroadcastTransaction) { + const txid = await adapter.signAndBroadcastTransaction({ txToSign, wallet }) + return txid + } + + throw new Error('buildAndBroadcast: no broadcast support') +} diff --git a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Claim/ClaimConfirm.tsx b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Claim/ClaimConfirm.tsx index 3c580eb6307..73eed33267a 100644 --- a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Claim/ClaimConfirm.tsx +++ b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Claim/ClaimConfirm.tsx @@ -70,7 +70,7 @@ export const ClaimConfirm = ({ accountId, assetId, amount, onBack }: ClaimConfir assertIsFoxEthStakingContractAddress(contractAddress) - const { claimRewards, getClaimGasData, foxFarmingContract } = useFoxFarming(contractAddress) + const { claimRewards, getClaimFeeData, foxFarmingContract } = useFoxFarming(contractAddress) const translate = useTranslate() const mixpanel = getMixPanel() const { onOngoingFarmingTxIdChange } = useFoxEth() @@ -156,9 +156,9 @@ export const ClaimConfirm = ({ accountId, assetId, amount, onBack }: ClaimConfir !(walletState.wallet && feeAsset && feeMarketData && foxFarmingContract && accountAddress) ) return - const gasEstimate = await getClaimGasData(accountAddress) - if (!gasEstimate) throw new Error('Gas estimation failed') - const estimatedGasCrypto = bnOrZero(gasEstimate.average.txFee) + const feeData = await getClaimFeeData(accountAddress) + if (!feeData) throw new Error('Gas estimation failed') + const estimatedGasCrypto = bnOrZero(feeData.txFee) .div(`1e${feeAsset.precision}`) .toPrecision() setCanClaim(true) @@ -174,7 +174,7 @@ export const ClaimConfirm = ({ accountId, assetId, amount, onBack }: ClaimConfir feeAsset.precision, feeMarketData, feeMarketData.price, - getClaimGasData, + getClaimFeeData, walletState.wallet, foxFarmingContract, ]) diff --git a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Deposit/components/Approve.tsx b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Deposit/components/Approve.tsx index 71a25f3fdb6..7dd36af1af9 100644 --- a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Deposit/components/Approve.tsx +++ b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Deposit/components/Approve.tsx @@ -68,7 +68,7 @@ export const Approve: React.FC = ({ accountId, onNext }) ) assertIsFoxEthStakingContractAddress(contractAddress) - const { allowance, approve, getStakeGasData } = useFoxFarming(contractAddress) + const { allowance, approve, getStakeFeeData } = useFoxFarming(contractAddress) const assets = useAppSelector(selectAssets) @@ -109,9 +109,9 @@ export const Approve: React.FC = ({ accountId, onNext }) maxAttempts: 30, }) // Get deposit gas estimate - const gasData = await getStakeGasData(state.deposit.cryptoAmount) - if (!gasData) return - const estimatedGasCryptoPrecision = bnOrZero(gasData.average.txFee) + const feeData = await getStakeFeeData(state.deposit.cryptoAmount) + if (!feeData) return + const estimatedGasCryptoPrecision = bnOrZero(feeData.txFee) .div(bn(10).pow(feeAsset.precision)) .toPrecision() dispatch({ @@ -147,7 +147,7 @@ export const Approve: React.FC = ({ accountId, onNext }) wallet, asset, approve, - getStakeGasData, + getStakeFeeData, feeAsset.precision, onNext, assets, diff --git a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Deposit/components/Deposit.tsx b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Deposit/components/Deposit.tsx index 2046d9b47b3..9d7bbd8625d 100644 --- a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Deposit/components/Deposit.tsx +++ b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Deposit/components/Deposit.tsx @@ -95,8 +95,8 @@ export const Deposit: React.FC = ({ const { allowance: foxFarmingAllowance, - getStakeGasData, - getApproveGasData, + getStakeFeeData, + getApproveFeeData, } = useFoxFarming(contractAddress) const feeAssetId = getChainAdapterManager().get(chainId)?.getFeeAssetId() @@ -132,9 +132,9 @@ export const Deposit: React.FC = ({ ): Promise => { if (!assetReference) return try { - const gasData = await getStakeGasData(deposit.cryptoAmount) - if (!gasData) return - return bnOrZero(gasData.average.txFee).div(bn(10).pow(feeAsset.precision)).toPrecision() + const feeData = await getStakeFeeData(deposit.cryptoAmount) + if (!feeData) return + return bnOrZero(feeData.txFee).div(bn(10).pow(feeAsset.precision)).toPrecision() } catch (error) { moduleLogger.error( { fn: 'getDepositGasEstimateCryptoPrecision', error }, @@ -180,12 +180,12 @@ export const Deposit: React.FC = ({ assets, ) } else { - const estimatedGasCryptoBaseUnit = await getApproveGasData() - if (!estimatedGasCryptoBaseUnit) return + const feeData = await getApproveFeeData() + if (!feeData) return dispatch({ type: FoxFarmingDepositActionType.SET_APPROVE, payload: { - estimatedGasCryptoPrecision: bnOrZero(estimatedGasCryptoBaseUnit.average.txFee) + estimatedGasCryptoPrecision: bnOrZero(feeData.txFee) .div(bn(10).pow(feeAsset.precision)) .toPrecision(), }, @@ -210,14 +210,14 @@ export const Deposit: React.FC = ({ feeAsset, foxFarmingOpportunity, assetReference, - getStakeGasData, + getStakeFeeData, toast, translate, asset, foxFarmingAllowance, onNext, assets, - getApproveGasData, + getApproveFeeData, ], ) diff --git a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/Approve.tsx b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/Approve.tsx index 852a50a1968..af092c354a2 100644 --- a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/Approve.tsx +++ b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/Approve.tsx @@ -67,7 +67,7 @@ export const Approve: React.FC = ({ accountId, onNext }) => { assertIsFoxEthStakingContractAddress(contractAddress) - const { allowance, approve, getUnstakeGasData } = useFoxFarming(contractAddress) + const { allowance, approve, getUnstakeFeeData } = useFoxFarming(contractAddress) const toast = useToast() const assets = useAppSelector(selectAssets) @@ -107,9 +107,9 @@ export const Approve: React.FC = ({ accountId, onNext }) => { maxAttempts: 30, }) // Get withdraw gas estimate - const gasData = await getUnstakeGasData(state.withdraw.lpAmount, state.withdraw.isExiting) - if (!gasData) return - const estimatedGasCrypto = bnOrZero(gasData.average.txFee) + const feeData = await getUnstakeFeeData(state.withdraw.lpAmount, state.withdraw.isExiting) + if (!feeData) return + const estimatedGasCrypto = bnOrZero(feeData.txFee) .div(bn(10).pow(underlyingAsset?.precision ?? 0)) .toPrecision() dispatch({ @@ -145,7 +145,7 @@ export const Approve: React.FC = ({ accountId, onNext }) => { opportunity, wallet, approve, - getUnstakeGasData, + getUnstakeFeeData, underlyingAsset?.precision, onNext, assets, diff --git a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/ExpiredWithdraw.tsx b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/ExpiredWithdraw.tsx index 9ec49a4acd8..a168666c48e 100644 --- a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/ExpiredWithdraw.tsx +++ b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/ExpiredWithdraw.tsx @@ -70,7 +70,7 @@ export const ExpiredWithdraw: React.FC = ({ assertIsFoxEthStakingContractAddress(contractAddress) - const { getUnstakeGasData, allowance, getApproveGasData } = useFoxFarming(contractAddress) + const { getUnstakeFeeData, allowance, getApproveFeeData } = useFoxFarming(contractAddress) const methods = useForm({ mode: 'onChange' }) @@ -108,9 +108,9 @@ export const ExpiredWithdraw: React.FC = ({ const getWithdrawGasEstimate = async () => { try { - const fee = await getUnstakeGasData(amountAvailableCryptoPrecision.toFixed(), true) - if (!fee) return - return bnOrZero(fee.average.txFee).div(bn(10).pow(feeAsset.precision)).toPrecision() + const feeData = await getUnstakeFeeData(amountAvailableCryptoPrecision.toFixed(), true) + if (!feeData) return + return bnOrZero(feeData.txFee).div(bn(10).pow(feeAsset.precision)).toPrecision() } catch (error) { // TODO: handle client side errors maybe add a toast? moduleLogger.error( @@ -162,12 +162,12 @@ export const ExpiredWithdraw: React.FC = ({ assets, ) } else { - const estimatedGasCrypto = await getApproveGasData() - if (!estimatedGasCrypto) return + const feeData = await getApproveFeeData() + if (!feeData) return dispatch({ type: FoxFarmingWithdrawActionType.SET_APPROVE, payload: { - estimatedGasCryptoPrecision: bnOrZero(estimatedGasCrypto.average.txFee) + estimatedGasCryptoPrecision: bnOrZero(feeData.txFee) .div(bn(10).pow(feeAsset.precision)) .toPrecision(), }, diff --git a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/Withdraw.tsx b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/Withdraw.tsx index dd06de2ba68..fa2665da183 100644 --- a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/Withdraw.tsx +++ b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Withdraw/components/Withdraw.tsx @@ -64,7 +64,7 @@ export const Withdraw: React.FC = ({ assertIsFoxEthStakingContractAddress(contractAddress) - const { getUnstakeGasData, allowance, getApproveGasData } = useFoxFarming(contractAddress) + const { getUnstakeFeeData, allowance, getApproveFeeData } = useFoxFarming(contractAddress) const methods = useForm({ mode: 'onChange' }) const { setValue } = methods @@ -95,15 +95,15 @@ export const Withdraw: React.FC = ({ const getWithdrawGasEstimateCryptoPrecision = useCallback( async (withdraw: WithdrawValues) => { try { - const fee = await getUnstakeGasData(withdraw.cryptoAmount, isExiting) - if (!fee) return - return bnOrZero(fee.average.txFee).div(bn(10).pow(feeAsset.precision)).toPrecision() + const feeData = await getUnstakeFeeData(withdraw.cryptoAmount, isExiting) + if (!feeData) return + return bnOrZero(feeData.txFee).div(bn(10).pow(feeAsset.precision)).toPrecision() } catch (error) { // TODO: handle client side errors maybe add a toast? moduleLogger.error(error, 'FoxFarmingWithdraw:getWithdrawGasEstimate error:') } }, - [feeAsset.precision, getUnstakeGasData, isExiting], + [feeAsset.precision, getUnstakeFeeData, isExiting], ) const handleContinue = useCallback( @@ -147,12 +147,12 @@ export const Withdraw: React.FC = ({ assets, ) } else { - const estimatedGasCrypto = await getApproveGasData() - if (!estimatedGasCrypto) return + const feeData = await getApproveFeeData() + if (!feeData) return dispatch({ type: FoxFarmingWithdrawActionType.SET_APPROVE, payload: { - estimatedGasCryptoPrecision: bnOrZero(estimatedGasCrypto.average.txFee) + estimatedGasCryptoPrecision: bnOrZero(feeData.txFee) .div(bn(10).pow(feeAsset.precision)) .toPrecision(), }, @@ -166,7 +166,7 @@ export const Withdraw: React.FC = ({ assets, dispatch, feeAsset.precision, - getApproveGasData, + getApproveFeeData, getWithdrawGasEstimateCryptoPrecision, isExiting, onNext, diff --git a/src/features/defi/providers/fox-farming/hooks/useFoxFarming.ts b/src/features/defi/providers/fox-farming/hooks/useFoxFarming.ts index 863427bff6a..fcc36f8533b 100644 --- a/src/features/defi/providers/fox-farming/hooks/useFoxFarming.ts +++ b/src/features/defi/providers/fox-farming/hooks/useFoxFarming.ts @@ -1,16 +1,15 @@ import { MaxUint256 } from '@ethersproject/constants' import { ethAssetId, fromAccountId } from '@shapeshiftoss/caip' -import type { ethereum, EvmChainId, FeeData } from '@shapeshiftoss/chain-adapters' -import { supportsETH } from '@shapeshiftoss/hdwallet-core' +import type { ethereum } from '@shapeshiftoss/chain-adapters' import { ETH_FOX_POOL_CONTRACT_ADDRESS, UNISWAP_V2_ROUTER_02_CONTRACT_ADDRESS, } from 'contracts/constants' import { getOrCreateContractByAddress } from 'contracts/contractManager' +import { buildAndBroadcast, getFeeDataFromEstimate } from 'features/defi/helpers/utils' import { useCallback, useMemo } from 'react' import { useFoxEth } from 'context/FoxEthProvider/FoxEthProvider' import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton' -import { useEvm } from 'hooks/useEvm/useEvm' import { useWallet } from 'hooks/useWallet/useWallet' import { bnOrZero } from 'lib/bignumber/bignumber' import { logger } from 'lib/logger' @@ -35,7 +34,6 @@ export const useFoxFarming = ( { skip }: UseFoxFarmingOptions = {}, ) => { const { farmingAccountId } = useFoxEth() - const { supportedEvmChainIds } = useEvm() const ethAsset = useAppSelector(state => selectAssetById(state, ethAssetId)) const lpAsset = useAppSelector(state => selectAssetById(state, foxEthLpAssetId)) @@ -46,12 +44,12 @@ export const useFoxFarming = ( const accountNumber = useAppSelector(state => selectAccountNumberByAccountId(state, filter)) - const { - state: { wallet }, - } = useWallet() + const wallet = useWallet().state.wallet const chainAdapterManager = getChainAdapterManager() - const adapter = chainAdapterManager.get(ethAsset.chainId) as unknown as ethereum.ChainAdapter + const adapter = chainAdapterManager.get(ethAsset.chainId) as unknown as + | ethereum.ChainAdapter + | undefined const uniswapRouterContract = useMemo( () => (skip ? null : getOrCreateContractByAddress(UNISWAP_V2_ROUTER_02_CONTRACT_ADDRESS)), @@ -71,21 +69,19 @@ export const useFoxFarming = ( const stake = useCallback( async (lpAmount: string) => { try { - if ( - skip || - !farmingAccountId || - !isValidAccountNumber(accountNumber) || - !foxFarmingContract || - !wallet - ) - return - if (!adapter) - throw new Error(`addLiquidityEth: no adapter available for ${ethAsset.chainId}`) + if (skip) return + if (!farmingAccountId) return + if (!isValidAccountNumber(accountNumber)) return + if (!foxFarmingContract) return + if (!wallet) return + + if (!adapter) throw new Error(`no adapter available for ${ethAsset.chainId}`) + const data = foxFarmingContract.interface.encodeFunctionData('stake', [ bnOrZero(lpAmount).times(bnOrZero(10).exponentiatedBy(lpAsset.precision)).toFixed(0), ]) - const adapterType = adapter.getChainId() - const estimatedFees = await adapter.getFeeData({ + + const feeData = await adapter.getFeeData({ to: contractAddress, value: '0', chainSpecific: { @@ -93,63 +89,20 @@ export const useFoxFarming = ( from: fromAccountId(farmingAccountId).account, }, }) - const result = await (async () => { - if (supportedEvmChainIds.includes(adapterType)) { - if (!supportsETH(wallet)) - throw new Error(`addLiquidityEthFox: wallet does not support ethereum`) - const fees = estimatedFees.average as FeeData - const { - chainSpecific: { gasPrice, gasLimit, maxFeePerGas, maxPriorityFeePerGas }, - } = fees - const shouldUseEIP1559Fees = - (await wallet.ethSupportsEIP1559()) && - maxFeePerGas !== undefined && - maxPriorityFeePerGas !== undefined - if (!shouldUseEIP1559Fees && gasPrice === undefined) { - throw new Error(`addLiquidityEthFox: missing gasPrice for non-EIP-1559 tx`) - } - return await adapter.buildCustomTx({ - to: contractAddress, - value: '0', - wallet, - data, - gasLimit, - accountNumber, - ...(shouldUseEIP1559Fees ? { maxFeePerGas, maxPriorityFeePerGas } : { gasPrice }), - }) - } else { - throw new Error(`addLiquidityEthFox: wallet does not support ethereum`) - } - })() - const txToSign = result.txToSign - - const broadcastTXID = await (async () => { - if (wallet.supportsOfflineSigning()) { - const signedTx = await adapter.signTransaction({ - txToSign, - wallet, - }) - return adapter.broadcastTransaction(signedTx) - } else if (wallet.supportsBroadcast()) { - /** - * signAndBroadcastTransaction is an optional method on the HDWallet interface. - * Check and see if it exists; if so, call and make sure a txhash is returned - */ - if (!adapter.signAndBroadcastTransaction) { - throw new Error('signAndBroadcastTransaction undefined for wallet') - } - return adapter.signAndBroadcastTransaction?.({ txToSign, wallet }) - } else { - throw new Error('Bad hdwallet config') - } - })() - - if (!broadcastTXID) { - throw new Error('Broadcast failed') - } - return broadcastTXID - } catch (error) { - moduleLogger.warn(error, 'useFoxFarming:stake error') + + const txid = await buildAndBroadcast({ + accountNumber, + adapter, + feeData: getFeeDataFromEstimate(feeData).chainSpecific, + to: contractAddress, + value: '0', + wallet, + data, + }) + + return txid + } catch (err) { + moduleLogger.error(err, 'failed to stake') } }, [ @@ -160,7 +113,6 @@ export const useFoxFarming = ( ethAsset.chainId, foxFarmingContract, lpAsset.precision, - supportedEvmChainIds, skip, wallet, ], @@ -169,27 +121,21 @@ export const useFoxFarming = ( const unstake = useCallback( async (lpAmount: string, isExiting: boolean) => { try { - if ( - skip || - !farmingAccountId || - !isValidAccountNumber(accountNumber) || - !foxFarmingContract || - !wallet - ) - return - const chainAdapterManager = getChainAdapterManager() - const adapter = chainAdapterManager.get( - ethAsset.chainId, - ) as unknown as ethereum.ChainAdapter - if (!adapter) - throw new Error(`foxFarmingUnstake: no adapter available for ${ethAsset.chainId}`) + if (skip) return + if (!farmingAccountId) return + if (!isValidAccountNumber(accountNumber)) return + if (!foxFarmingContract) return + if (!wallet) return + + if (!adapter) throw new Error(`no adapter available for ${ethAsset.chainId}`) + const data = isExiting ? foxFarmingContract.interface.encodeFunctionData('exit') : foxFarmingContract.interface.encodeFunctionData('withdraw', [ bnOrZero(lpAmount).times(bnOrZero(10).exponentiatedBy(lpAsset.precision)).toFixed(0), ]) - const adapterType = adapter.getChainId() - const estimatedFees = await adapter.getFeeData({ + + const feeData = await adapter.getFeeData({ to: contractAddress, value: '0', chainSpecific: { @@ -197,73 +143,30 @@ export const useFoxFarming = ( from: fromAccountId(farmingAccountId).account, }, }) - const result = await (async () => { - if (supportedEvmChainIds.includes(adapterType)) { - if (!supportsETH(wallet)) - throw new Error(`unstakeEthFoxLp: wallet does not support ethereum`) - const fees = estimatedFees.average as FeeData - const { - chainSpecific: { gasPrice, gasLimit, maxFeePerGas, maxPriorityFeePerGas }, - } = fees - const shouldUseEIP1559Fees = - (await wallet.ethSupportsEIP1559()) && - maxFeePerGas !== undefined && - maxPriorityFeePerGas !== undefined - if (!shouldUseEIP1559Fees && gasPrice === undefined) { - throw new Error(`addLiquidityEthFox: missing gasPrice for non-EIP-1559 tx`) - } - return await adapter.buildCustomTx({ - to: contractAddress, - value: '0', - wallet, - data, - gasLimit, - accountNumber, - ...(shouldUseEIP1559Fees ? { maxFeePerGas, maxPriorityFeePerGas } : { gasPrice }), - }) - } else { - throw new Error(`addLiquidityEthFox: wallet does not support ethereum`) - } - })() - const txToSign = result.txToSign - - const broadcastTXID = await (async () => { - if (wallet.supportsOfflineSigning()) { - const signedTx = await adapter.signTransaction({ - txToSign, - wallet, - }) - return adapter.broadcastTransaction(signedTx) - } else if (wallet.supportsBroadcast()) { - /** - * signAndBroadcastTransaction is an optional method on the HDWallet interface. - * Check and see if it exists; if so, call and make sure a txhash is returned - */ - if (!adapter.signAndBroadcastTransaction) { - throw new Error('signAndBroadcastTransaction undefined for wallet') - } - return adapter.signAndBroadcastTransaction?.({ txToSign, wallet }) - } else { - throw new Error('Bad hdwallet config') - } - })() - - if (!broadcastTXID) { - throw new Error('Broadcast failed') - } - return broadcastTXID - } catch (error) { - moduleLogger.warn(error, 'useFoxFarming:unstake error') + + const txid = await buildAndBroadcast({ + accountNumber, + adapter, + feeData: getFeeDataFromEstimate(feeData).chainSpecific, + to: contractAddress, + value: '0', + wallet, + data, + }) + + return txid + } catch (err) { + moduleLogger.error(err, 'failed to unstake') } }, [ + adapter, farmingAccountId, accountNumber, contractAddress, ethAsset.chainId, foxFarmingContract, lpAsset.precision, - supportedEvmChainIds, wallet, skip, ], @@ -271,39 +174,47 @@ export const useFoxFarming = ( const allowance = useCallback(async () => { if (skip || !farmingAccountId || !uniV2LPContract) return + const userAddress = fromAccountId(farmingAccountId).account const _allowance = await uniV2LPContract.allowance(userAddress, contractAddress) + return _allowance.toString() }, [farmingAccountId, contractAddress, uniV2LPContract, skip]) - const getApproveGasDataCryptoBaseUnit = useCallback(async () => { - if (adapter && farmingAccountId && uniV2LPContract) { - const data = uniV2LPContract.interface.encodeFunctionData('approve', [ - contractAddress, - MaxUint256, - ]) - const farmingAccountAddress = fromAccountId(farmingAccountId).account - const fees = await adapter.getFeeData({ - to: uniV2LPContract.address, - value: '0', - chainSpecific: { - contractData: data, - from: farmingAccountAddress, - contractAddress: uniV2LPContract.address, - }, - }) - return fees - } + const getApproveFeeData = useCallback(async () => { + if (!adapter || !farmingAccountId || !uniV2LPContract) return + + const data = uniV2LPContract.interface.encodeFunctionData('approve', [ + contractAddress, + MaxUint256, + ]) + + const farmingAccountAddress = fromAccountId(farmingAccountId).account + + const feeData = await adapter.getFeeData({ + to: uniV2LPContract.address, + value: '0', + chainSpecific: { + contractData: data, + from: farmingAccountAddress, + contractAddress: uniV2LPContract.address, + }, + }) + + return getFeeDataFromEstimate(feeData) }, [adapter, farmingAccountId, contractAddress, uniV2LPContract]) - const getStakeGasData = useCallback( + const getStakeFeeData = useCallback( async (lpAmount: string) => { - if (skip || !farmingAccountId || !uniswapRouterContract) return + if (skip || !adapter || !farmingAccountId || !uniswapRouterContract) return + const data = foxFarmingContract!.interface.encodeFunctionData('stake', [ bnOrZero(lpAmount).times(bnOrZero(10).exponentiatedBy(lpAsset.precision)).toFixed(0), ]) + const farmingAccountAddress = fromAccountId(farmingAccountId).account - const estimatedFees = await adapter.getFeeData({ + + const feeData = await adapter.getFeeData({ to: contractAddress, value: '0', chainSpecific: { @@ -311,7 +222,8 @@ export const useFoxFarming = ( from: farmingAccountAddress, }, }) - return estimatedFees + + return getFeeDataFromEstimate(feeData) }, [ adapter, @@ -324,16 +236,19 @@ export const useFoxFarming = ( ], ) - const getUnstakeGasData = useCallback( + const getUnstakeFeeData = useCallback( async (lpAmount: string, isExiting: boolean) => { - if (skip || !farmingAccountId || !uniswapRouterContract) return + if (skip || !adapter || !farmingAccountId || !uniswapRouterContract) return + const data = isExiting ? foxFarmingContract!.interface.encodeFunctionData('exit') : foxFarmingContract!.interface.encodeFunctionData('withdraw', [ bnOrZero(lpAmount).times(bnOrZero(10).exponentiatedBy(lpAsset.precision)).toFixed(0), ]) + const farmingAccountAddress = fromAccountId(farmingAccountId).account - const estimatedFees = await adapter.getFeeData({ + + const feeData = await adapter.getFeeData({ to: contractAddress, value: '0', chainSpecific: { @@ -341,7 +256,8 @@ export const useFoxFarming = ( from: farmingAccountAddress, }, }) - return estimatedFees + + return getFeeDataFromEstimate(feeData) }, [ adapter, @@ -354,91 +270,73 @@ export const useFoxFarming = ( ], ) + const getClaimFeeData = useCallback( + async (userAddress: string) => { + if (!adapter || !foxFarmingContract || !userAddress) return + + const data = foxFarmingContract.interface.encodeFunctionData('getReward') + + const feeData = await adapter.getFeeData({ + to: contractAddress, + value: '0', + chainSpecific: { + contractData: data, + from: userAddress, + }, + }) + + return getFeeDataFromEstimate(feeData) + }, + [adapter, contractAddress, foxFarmingContract], + ) + const approve = useCallback(async () => { if (!wallet || !isValidAccountNumber(accountNumber) || !uniV2LPContract) return + + if (!adapter) throw new Error(`no adapter available for ${ethAsset.chainId}`) + const data = uniV2LPContract.interface.encodeFunctionData('approve', [ contractAddress, MaxUint256, ]) - const gasData = await getApproveGasDataCryptoBaseUnit() - if (!gasData) return - const fees = gasData.average as FeeData - const { - chainSpecific: { gasPrice, gasLimit }, - } = fees - if (gasPrice === undefined) { - throw new Error(`approve: missing gasPrice for non-EIP-1559 tx`) - } - const result = await adapter.buildCustomTx({ + + const feeData = await getApproveFeeData() + if (!feeData) return + + const txid = await buildAndBroadcast({ + accountNumber, + adapter, + feeData: feeData.chainSpecific, to: uniV2LPContract.address, value: '0', wallet, data, - gasLimit, - accountNumber, - gasPrice, }) - const txToSign = result.txToSign - const broadcastTXID = await (async () => { - if (wallet.supportsOfflineSigning()) { - const signedTx = await adapter.signTransaction({ - txToSign, - wallet, - }) - return adapter.broadcastTransaction(signedTx) - } else if (wallet.supportsBroadcast()) { - /** - * signAndBroadcastTransaction is an optional method on the HDWallet interface. - * Check and see if it exists; if so, call and make sure a txhash is returned - */ - if (!adapter.signAndBroadcastTransaction) { - throw new Error('signAndBroadcastTransaction undefined for wallet') - } - return adapter.signAndBroadcastTransaction?.({ txToSign, wallet }) - } else { - throw new Error('Bad hdwallet config') - } - })() - return broadcastTXID + return txid }, [ accountNumber, adapter, + ethAsset.chainId, contractAddress, - getApproveGasDataCryptoBaseUnit, + getApproveFeeData, uniV2LPContract, wallet, ]) - const getClaimGasData = useCallback( - async (userAddress: string) => { - if (!foxFarmingContract || !userAddress) return - const data = foxFarmingContract.interface.encodeFunctionData('getReward') - const estimatedFees = await adapter.getFeeData({ - to: contractAddress, - value: '0', - chainSpecific: { - contractData: data, - from: userAddress, - }, - }) - return estimatedFees - }, - [adapter, contractAddress, foxFarmingContract], - ) - const claimRewards = useCallback(async () => { - if ( - skip || - !wallet || - !isValidAccountNumber(accountNumber) || - !foxFarmingContract || - !farmingAccountId - ) - return + if (skip) return + if (!wallet) return + if (!isValidAccountNumber(accountNumber)) return + if (!foxFarmingContract) return + if (!farmingAccountId) return + + if (!adapter) throw new Error(`no adapter available for ${ethAsset.chainId}`) + const data = foxFarmingContract.interface.encodeFunctionData('getReward') const farmingAccountAddress = fromAccountId(farmingAccountId).account - const estimatedFees = await adapter.getFeeData({ + + const feeData = await adapter.getFeeData({ to: contractAddress, value: '0', chainSpecific: { @@ -446,54 +344,36 @@ export const useFoxFarming = ( from: farmingAccountAddress, }, }) - const fees = estimatedFees.average as FeeData - const { - chainSpecific: { gasPrice, gasLimit }, - } = fees - if (gasPrice === undefined) { - throw new Error(`approve: missing gasPrice for non-EIP-1559 tx`) - } - const result = await adapter.buildCustomTx({ + + const txid = await buildAndBroadcast({ + accountNumber, + adapter, + feeData: getFeeDataFromEstimate(feeData).chainSpecific, to: contractAddress, value: '0', wallet, data, - gasLimit, - accountNumber, - gasPrice, }) - const txToSign = result.txToSign - const broadcastTXID = await (async () => { - if (wallet.supportsOfflineSigning()) { - const signedTx = await adapter.signTransaction({ - txToSign, - wallet, - }) - return adapter.broadcastTransaction(signedTx) - } else if (wallet.supportsBroadcast()) { - /** - * signAndBroadcastTransaction is an optional method on the HDWallet interface. - * Check and see if it exists; if so, call and make sure a txhash is returned - */ - if (!adapter.signAndBroadcastTransaction) { - throw new Error('signAndBroadcastTransaction undefined for wallet') - } - return adapter.signAndBroadcastTransaction?.({ txToSign, wallet }) - } else { - throw new Error('Bad hdwallet config') - } - })() - return broadcastTXID - }, [accountNumber, adapter, farmingAccountId, contractAddress, foxFarmingContract, skip, wallet]) + return txid + }, [ + accountNumber, + adapter, + farmingAccountId, + ethAsset.chainId, + contractAddress, + foxFarmingContract, + skip, + wallet, + ]) return { allowance, approve, - getApproveGasData: getApproveGasDataCryptoBaseUnit, - getStakeGasData, - getClaimGasData, - getUnstakeGasData, + getApproveFeeData, + getStakeFeeData, + getClaimFeeData, + getUnstakeFeeData, stake, unstake, claimRewards, diff --git a/src/features/defi/providers/univ2/components/UniV2Manager/Deposit/components/Approve.tsx b/src/features/defi/providers/univ2/components/UniV2Manager/Deposit/components/Approve.tsx index e7e006d69ce..e72d33f3f77 100644 --- a/src/features/defi/providers/univ2/components/UniV2Manager/Deposit/components/Approve.tsx +++ b/src/features/defi/providers/univ2/components/UniV2Manager/Deposit/components/Approve.tsx @@ -113,7 +113,7 @@ export const Approve: React.FC = ({ accountId, onNext }) => { isApprove0Needed && asset0ContractAddress, isApprove1Needed && asset1ContractAddress, ].filter(Boolean) - const { approveAsset, asset0Allowance, asset1Allowance, getDepositGasDataCryptoBaseUnit } = + const { approveAsset, asset0Allowance, asset1Allowance, getDepositFeeData } = useUniV2LiquidityPool({ accountId: accountId ?? '', lpAssetId, @@ -241,13 +241,12 @@ export const Approve: React.FC = ({ accountId, onNext }) => { if (!(state && dispatch && lpOpportunity)) return if (!(isApprove0Needed || isApprove1Needed)) return if (isAsset0AllowanceGranted && isAsset1AllowanceGranted) { - // Get deposit gas estimate - const gasData = await getDepositGasDataCryptoBaseUnit({ + const feeData = await getDepositFeeData({ token0Amount: state.deposit.asset0CryptoAmount, token1Amount: state.deposit.asset1CryptoAmount, }) - if (!gasData) return - const estimatedGasCryptoPrecision = bnOrZero(gasData.average.txFee) + if (!feeData) return + const estimatedGasCryptoPrecision = bnOrZero(feeData.txFee) .div(bn(10).pow(feeAsset.precision)) .toPrecision() dispatch({ @@ -290,7 +289,7 @@ export const Approve: React.FC = ({ accountId, onNext }) => { assets, dispatch, feeAsset.precision, - getDepositGasDataCryptoBaseUnit, + getDepositFeeData, isApprove0Needed, isApprove1Needed, isAsset0AllowanceGranted, diff --git a/src/features/defi/providers/univ2/components/UniV2Manager/Deposit/components/Deposit.tsx b/src/features/defi/providers/univ2/components/UniV2Manager/Deposit/components/Deposit.tsx index 62f4fd0b1f3..0f7aa77cd21 100644 --- a/src/features/defi/providers/univ2/components/UniV2Manager/Deposit/components/Deposit.tsx +++ b/src/features/defi/providers/univ2/components/UniV2Manager/Deposit/components/Deposit.tsx @@ -73,7 +73,7 @@ export const Deposit: React.FC = ({ }) const assetId0 = lpOpportunity?.underlyingAssetIds[0] ?? '' const assetId1 = lpOpportunity?.underlyingAssetIds[1] ?? '' - const { asset0Allowance, asset1Allowance, getApproveGasData, getDepositGasDataCryptoBaseUnit } = + const { asset0Allowance, asset1Allowance, getApproveFeeData, getDepositFeeData } = useUniV2LiquidityPool({ accountId: accountId ?? '', lpAssetId, @@ -118,12 +118,12 @@ export const Deposit: React.FC = ({ if (!feeAsset) return const { cryptoAmount0: token0Amount, cryptoAmount1: token1Amount } = deposit try { - const gasData = await getDepositGasDataCryptoBaseUnit({ + const feeData = await getDepositFeeData({ token0Amount, token1Amount, }) - if (!gasData) return - return bnOrZero(gasData.average.txFee).div(bn(10).pow(feeAsset.precision)).toPrecision() + if (!feeData) return + return bnOrZero(feeData.txFee).div(bn(10).pow(feeAsset.precision)).toPrecision() } catch (error) { moduleLogger.error( { fn: 'getDepositGasEstimateCryptoPrecision', error }, @@ -198,31 +198,33 @@ export const Deposit: React.FC = ({ assetId1 !== ethAssetId ? ethers.utils.getAddress(fromAssetId(assetId1).assetReference) : undefined + // While the naive approach would be to think both assets approve() calls are going to result in the same gas estimation, // this is not necesssarly true. Some ERC-20s approve() might have a bit more logic, and thus require more gas. // e.g https://github.com/Uniswap/governance/blob/eabd8c71ad01f61fb54ed6945162021ee419998e/contracts/Uni.sol#L119 - const asset0EstimatedGasCrypto = - assetId0 !== ethAssetId && (await getApproveGasData(asset0ContractAddress!)) - const asset1EstimatedGasCrypto = - assetId1 !== ethAssetId && (await getApproveGasData(asset1ContractAddress!)) - if (!(asset0EstimatedGasCrypto || asset1EstimatedGasCrypto)) return + const asset0ApprovalFee = + asset0ContractAddress && bnOrZero((await getApproveFeeData(asset0ContractAddress))?.txFee) + const asset1ApprovalFee = + asset1ContractAddress && bnOrZero((await getApproveFeeData(asset1ContractAddress))?.txFee) + + if (!(asset0ApprovalFee || asset1ApprovalFee)) return - if (!isAsset0AllowanceGranted && asset0EstimatedGasCrypto) { + if (!isAsset0AllowanceGranted && asset0ApprovalFee) { dispatch({ type: UniV2DepositActionType.SET_APPROVE_0, payload: { - estimatedGasCryptoPrecision: bnOrZero(asset0EstimatedGasCrypto.average.txFee) + estimatedGasCryptoPrecision: bnOrZero(asset0ApprovalFee) .div(bn(10).pow(feeAsset.precision)) .toPrecision(), }, }) } - if (!isAsset1AllowanceGranted && asset1EstimatedGasCrypto) { + if (!isAsset1AllowanceGranted && asset1ApprovalFee) { dispatch({ type: UniV2DepositActionType.SET_APPROVE_1, payload: { - estimatedGasCryptoPrecision: bnOrZero(asset1EstimatedGasCrypto.average.txFee) + estimatedGasCryptoPrecision: bnOrZero(asset1ApprovalFee) .div(bn(10).pow(feeAsset.precision)) .toPrecision(), }, diff --git a/src/features/defi/providers/univ2/components/UniV2Manager/Withdraw/components/Approve.tsx b/src/features/defi/providers/univ2/components/UniV2Manager/Withdraw/components/Approve.tsx index f6fb68c879e..3d9bfd06d49 100644 --- a/src/features/defi/providers/univ2/components/UniV2Manager/Withdraw/components/Approve.tsx +++ b/src/features/defi/providers/univ2/components/UniV2Manager/Withdraw/components/Approve.tsx @@ -77,7 +77,7 @@ export const Approve: React.FC = ({ accountId, onNext }) => { const assetId0 = lpOpportunity?.underlyingAssetIds[0] ?? '' const assetId1 = lpOpportunity?.underlyingAssetIds[1] ?? '' - const { approveAsset, lpAllowance, getWithdrawGasData } = useUniV2LiquidityPool({ + const { approveAsset, lpAllowance, getWithdrawFeeData } = useUniV2LiquidityPool({ accountId: accountId ?? '', assetId0: lpOpportunity?.underlyingAssetIds[0] ?? '', assetId1: lpOpportunity?.underlyingAssetIds[1] ?? '', @@ -127,14 +127,13 @@ export const Approve: React.FC = ({ accountId, onNext }) => { interval: 15000, maxAttempts: 30, }) - // Get withdraw gas estimate - const gasData = await getWithdrawGasData( + const feeData = await getWithdrawFeeData( state.withdraw.lpAmount, state.withdraw.asset0Amount, state.withdraw.asset1Amount, ) - if (!gasData) return - const estimatedGasCryptoPrecision = bnOrZero(gasData.average.txFee) + if (!feeData) return + const estimatedGasCryptoPrecision = bnOrZero(feeData.txFee) .div(bn(10).pow(feeAsset.precision)) .toPrecision() dispatch({ @@ -174,7 +173,7 @@ export const Approve: React.FC = ({ accountId, onNext }) => { wallet, approveAsset, lpAssetId, - getWithdrawGasData, + getWithdrawFeeData, feeAsset.precision, onNext, assets, diff --git a/src/features/defi/providers/univ2/components/UniV2Manager/Withdraw/components/Withdraw.tsx b/src/features/defi/providers/univ2/components/UniV2Manager/Withdraw/components/Withdraw.tsx index 07fbc6f9035..6295461eb55 100644 --- a/src/features/defi/providers/univ2/components/UniV2Manager/Withdraw/components/Withdraw.tsx +++ b/src/features/defi/providers/univ2/components/UniV2Manager/Withdraw/components/Withdraw.tsx @@ -81,7 +81,7 @@ export const Withdraw: React.FC = ({ const assetId0 = uniV2Opportunity?.underlyingAssetIds[0] ?? '' const assetId1 = uniV2Opportunity?.underlyingAssetIds[1] ?? '' - const { lpAllowance, getApproveGasData, getWithdrawGasData } = useUniV2LiquidityPool({ + const { lpAllowance, getApproveFeeData, getWithdrawFeeData } = useUniV2LiquidityPool({ accountId: accountId ?? '', assetId0: uniV2Opportunity?.underlyingAssetIds[0] ?? '', assetId1: uniV2Opportunity?.underlyingAssetIds[1] ?? '', @@ -136,13 +136,13 @@ export const Withdraw: React.FC = ({ const getWithdrawGasEstimateCryptoPrecision = async (withdraw: WithdrawValues) => { try { - const fee = await getWithdrawGasData( + const feeData = await getWithdrawFeeData( withdraw.cryptoAmount, asset0AmountCryptoPrecision, asset1AmountCryptoPrecision, ) - if (!fee) return - return bnOrZero(fee.average.txFee).div(bn(10).pow(feeAsset.precision)).toPrecision() + if (!feeData) return + return bnOrZero(feeData.txFee).div(bn(10).pow(feeAsset.precision)).toPrecision() } catch (error) { // TODO: handle client side errors maybe add a toast? moduleLogger.error(error, 'UniV2Withdraw:getWithdrawGasEstimate error:') @@ -196,12 +196,12 @@ export const Withdraw: React.FC = ({ dispatch({ type: UniV2WithdrawActionType.SET_LOADING, payload: false }) } else { const lpAssetContractAddress = ethers.utils.getAddress(fromAssetId(lpAssetId).assetReference) - const estimatedGasCryptoPrecision = await getApproveGasData(lpAssetContractAddress) - if (!estimatedGasCryptoPrecision) return + const feeData = await getApproveFeeData(lpAssetContractAddress) + if (!feeData) return dispatch({ type: UniV2WithdrawActionType.SET_APPROVE, payload: { - estimatedGasCryptoPrecision: bnOrZero(estimatedGasCryptoPrecision.average.txFee) + estimatedGasCryptoPrecision: bnOrZero(feeData.txFee) .div(bn(10).pow(feeAsset.precision)) .toPrecision(), }, diff --git a/src/features/defi/providers/univ2/hooks/useUniV2LiquidityPool.ts b/src/features/defi/providers/univ2/hooks/useUniV2LiquidityPool.ts index a7431c9c0fd..a617b750862 100644 --- a/src/features/defi/providers/univ2/hooks/useUniV2LiquidityPool.ts +++ b/src/features/defi/providers/univ2/hooks/useUniV2LiquidityPool.ts @@ -1,8 +1,7 @@ import { MaxUint256 } from '@ethersproject/constants' import type { AccountId, AssetId } from '@shapeshiftoss/caip' import { ethAssetId, ethChainId, fromAccountId, fromAssetId, toAssetId } from '@shapeshiftoss/caip' -import type { ethereum, EvmChainId, FeeData } from '@shapeshiftoss/chain-adapters' -import { supportsETH } from '@shapeshiftoss/hdwallet-core' +import type { ethereum } from '@shapeshiftoss/chain-adapters' import { UNISWAP_V2_ROUTER_02_CONTRACT_ADDRESS, WETH_TOKEN_CONTRACT_ADDRESS, @@ -10,11 +9,11 @@ import { import { getOrCreateContractByAddress, getOrCreateContractByType } from 'contracts/contractManager' import { ContractType } from 'contracts/types' import { ethers } from 'ethers' +import { buildAndBroadcast, getFeeDataFromEstimate } from 'features/defi/helpers/utils' import isNumber from 'lodash/isNumber' import { useCallback, useMemo } from 'react' import type { Address } from 'viem' import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton' -import { useEvm } from 'hooks/useEvm/useEvm' import { useWallet } from 'hooks/useWallet/useWallet' import { bn, bnOrZero } from 'lib/bignumber/bignumber' import { logger } from 'lib/logger' @@ -52,7 +51,6 @@ export const useUniV2LiquidityPool = ({ assetId1: AssetId lpAssetId: AssetId } & UseUniV2LiquidityPoolOptions) => { - const { supportedEvmChainIds } = useEvm() const assetId0OrWeth = assetId0 === ethAssetId ? wethAssetId : assetId0 const assetId1OrWeth = assetId1 === ethAssetId ? wethAssetId : assetId1 @@ -67,16 +65,15 @@ export const useUniV2LiquidityPool = ({ if (!weth) throw new Error(`Asset not found for AssetId ${wethAssetId}`) const filter = useMemo(() => ({ accountId }), [accountId]) - const accountNumber = useAppSelector(state => selectAccountNumberByAccountId(state, filter)) - - const { - state: { wallet }, - } = useWallet() + const wallet = useWallet().state.wallet const asset0Price = useAppSelector(state => selectMarketDataById(state, assetId0OrWeth)).price const chainAdapterManager = getChainAdapterManager() - const adapter = chainAdapterManager.get(ethChainId) as unknown as ethereum.ChainAdapter + const adapter = chainAdapterManager.get(ethChainId) as unknown as + | ethereum.ChainAdapter + | undefined + const uniswapRouterContract = useMemo( () => (skip ? null : getOrCreateContractByAddress(UNISWAP_V2_ROUTER_02_CONTRACT_ADDRESS)), [skip], @@ -193,24 +190,23 @@ export const useUniV2LiquidityPool = ({ try { if (skip || !accountId || !isNumber(accountNumber) || !uniswapRouterContract || !wallet) return - if (!adapter) throw new Error(`addLiquidity: no adapter available for ${asset0.chainId}`) + + if (!adapter) throw new Error(`no adapter available for ${asset0.chainId}`) + const maybeEthAmount = (() => { if (assetId0OrWeth === wethAssetId) return token0Amount if (assetId1OrWeth === wethAssetId) return token1Amount return '0' })() + const value = bnOrZero(maybeEthAmount) .times(bn(10).exponentiatedBy(weth.precision)) .toFixed(0) - const data = makeAddLiquidityData({ - token0Amount, - token1Amount, - }) - - const adapterType = adapter.getChainId() + const data = makeAddLiquidityData({ token0Amount, token1Amount }) const contractAddress = fromAssetId(uniswapV2Router02AssetId).assetReference - const estimatedFees = await adapter.getFeeData({ + + const feeData = await adapter.getFeeData({ to: contractAddress, value, chainSpecific: { @@ -218,71 +214,20 @@ export const useUniV2LiquidityPool = ({ from: fromAccountId(accountId).account, }, }) - const result = await (async () => { - if (supportedEvmChainIds.includes(adapterType)) { - if (!supportsETH(wallet)) - throw new Error(`addLiquidity: wallet does not support ethereum`) - const fees = estimatedFees.fast as FeeData - const { - chainSpecific: { - gasPrice, - gasLimit: gasLimitBase, - maxFeePerGas, - maxPriorityFeePerGas, - }, - } = fees - - const gasLimit = bnOrZero(gasLimitBase).times(1.1).toFixed(0) - const shouldUseEIP1559Fees = - (await wallet.ethSupportsEIP1559()) && - maxFeePerGas !== undefined && - maxPriorityFeePerGas !== undefined - if (!shouldUseEIP1559Fees && gasPrice === undefined) { - throw new Error(`addLiquidity: missing gasPrice for non-EIP-1559 tx`) - } - const contractAddress = fromAssetId(uniswapV2Router02AssetId).assetReference - return await adapter.buildCustomTx({ - to: contractAddress, - value, - wallet, - data, - gasLimit, - accountNumber, - ...(shouldUseEIP1559Fees ? { maxFeePerGas, maxPriorityFeePerGas } : { gasPrice }), - }) - } else { - throw new Error(`addLiquidity: wallet does not support ethereum`) - } - })() - const txToSign = result.txToSign - - const broadcastTXID = await (async () => { - if (wallet.supportsOfflineSigning()) { - const signedTx = await adapter.signTransaction({ - txToSign, - wallet, - }) - return adapter.broadcastTransaction(signedTx) - } else if (wallet.supportsBroadcast()) { - /** - * signAndBroadcastTransaction is an optional method on the HDWallet interface. - * Check and see if it exists; if so, call and make sure a txhash is returned - */ - if (!adapter.signAndBroadcastTransaction) { - throw new Error('signAndBroadcastTransaction undefined for wallet') - } - return adapter.signAndBroadcastTransaction?.({ txToSign, wallet }) - } else { - throw new Error('Bad hdwallet config') - } - })() - if (!broadcastTXID) { - throw new Error('Broadcast failed') - } - return broadcastTXID - } catch (error) { - moduleLogger.warn(error, 'useUniV2LiquidityPool:addLiquidity error') + const txid = await buildAndBroadcast({ + accountNumber, + adapter, + feeData: getFeeDataFromEstimate(feeData).chainSpecific, + to: contractAddress, + value, + wallet, + data, + }) + + return txid + } catch (err) { + moduleLogger.error(err, 'failed to addLiquidity') } }, [ @@ -294,7 +239,6 @@ export const useUniV2LiquidityPool = ({ assetId1OrWeth, makeAddLiquidityData, skip, - supportedEvmChainIds, uniswapRouterContract, wallet, weth, @@ -320,8 +264,10 @@ export const useUniV2LiquidityPool = ({ const lpAmountBaseUnit = bnOrZero(lpAmount) .times(bn(10).exponentiatedBy(lpAsset.precision)) .toFixed(0) + const deadline = Date.now() + 1200000 // 20 minutes from now const to = fromAccountId(accountId).account + if ([assetId0OrWeth, assetId1OrWeth].includes(wethAssetId)) { const otherAssetContractAddress = assetId0OrWeth === wethAssetId ? asset1ContractAddress : asset0ContractAddress @@ -359,6 +305,7 @@ export const useUniV2LiquidityPool = ({ weth.precision, ], ) + const removeLiquidity = useCallback( async ({ lpAmount, @@ -372,9 +319,8 @@ export const useUniV2LiquidityPool = ({ try { if (skip || !accountId || !isNumber(accountNumber) || !uniswapRouterContract || !wallet) return - const chainAdapterManager = getChainAdapterManager() - const adapter = chainAdapterManager.get(asset0.chainId) as unknown as ethereum.ChainAdapter - if (!adapter) throw new Error(`removeLiquidity: no adapter available for ${asset0.chainId}`) + + if (!adapter) throw new Error(`no adapter available for ${asset0.chainId}`) const data = makeRemoveLiquidityData({ asset0ContractAddress, @@ -384,9 +330,9 @@ export const useUniV2LiquidityPool = ({ asset0Amount, }) - const adapterType = adapter.getChainId() const contractAddress = fromAssetId(uniswapV2Router02AssetId).assetReference - const estimatedFees = await adapter.getFeeData({ + + const feeData = await adapter.getFeeData({ to: contractAddress, value: '0', chainSpecific: { @@ -394,75 +340,24 @@ export const useUniV2LiquidityPool = ({ from: fromAccountId(accountId).account, }, }) - const result = await (async () => { - if (supportedEvmChainIds.includes(adapterType)) { - if (!supportsETH(wallet)) - throw new Error(`removeLiquidity: wallet does not support ethereum`) - const fees = estimatedFees.fast as FeeData - const { - chainSpecific: { - gasPrice, - gasLimit: gasLimitBase, - maxFeePerGas, - maxPriorityFeePerGas, - }, - } = fees - // Gas limit tends to be too low and make Txs revert - // So we artificially bump it by 10% to ensure Txs go through - const gasLimit = bnOrZero(gasLimitBase).times(1.1).toFixed(0) - const shouldUseEIP1559Fees = - (await wallet.ethSupportsEIP1559()) && - maxFeePerGas !== undefined && - maxPriorityFeePerGas !== undefined - if (!shouldUseEIP1559Fees && gasPrice === undefined) { - throw new Error(`removeLiquidity: missing gasPrice for non-EIP-1559 tx`) - } - const contractAddress = fromAssetId(uniswapV2Router02AssetId).assetReference - return await adapter.buildCustomTx({ - to: contractAddress, - value: '0', - wallet, - data, - gasLimit, - accountNumber, - ...(shouldUseEIP1559Fees ? { maxFeePerGas, maxPriorityFeePerGas } : { gasPrice }), - }) - } else { - throw new Error(`removeLiquidity: wallet does not support ethereum`) - } - })() - const txToSign = result.txToSign - - const broadcastTXID = await (async () => { - if (wallet.supportsOfflineSigning()) { - const signedTx = await adapter.signTransaction({ - txToSign, - wallet, - }) - return adapter.broadcastTransaction(signedTx) - } else if (wallet.supportsBroadcast()) { - /** - * signAndBroadcastTransaction is an optional method on the HDWallet interface. - * Check and see if it exists; if so, call and make sure a txhash is returned - */ - if (!adapter.signAndBroadcastTransaction) { - throw new Error('signAndBroadcastTransaction undefined for wallet') - } - return adapter.signAndBroadcastTransaction?.({ txToSign, wallet }) - } else { - throw new Error('Bad hdwallet config') - } - })() - if (!broadcastTXID) { - throw new Error('Broadcast failed') - } - return broadcastTXID - } catch (error) { - moduleLogger.warn(error, 'useUniV2LiquidityPool:remoLiquidity error') + const txid = await buildAndBroadcast({ + accountNumber, + adapter, + feeData: getFeeDataFromEstimate(feeData).chainSpecific, + to: contractAddress, + value: '0', + wallet, + data, + }) + + return txid + } catch (err) { + moduleLogger.error(err, 'failed to removeLiquidity') } }, [ + adapter, skip, accountId, accountNumber, @@ -472,12 +367,12 @@ export const useUniV2LiquidityPool = ({ makeRemoveLiquidityData, asset0ContractAddress, asset1ContractAddress, - supportedEvmChainIds, ], ) const calculateHoldings = useCallback(async () => { if (skip || !uniV2LPContract || !accountId) return + const balance = await uniV2LPContract.balanceOf(fromAccountId(accountId).account) const totalSupply = await uniV2LPContract.totalSupply() const reserves = await uniV2LPContract.getReserves() @@ -498,89 +393,97 @@ export const useUniV2LiquidityPool = ({ }, [skip, uniV2LPContract, accountId, asset0.precision, asset1.precision]) const getLpTVL = useCallback(async () => { - if (uniV2LPContract) { - const reserves = await uniV2LPContract.getReserves() - // Amount of Eth in liquidity pool - const ethInReserve = bnOrZero(reserves?.[0]?.toString()).div(bn(10).pow(asset0.precision)) - - // Total market cap of liquidity pool in usdc. - // Multiplied by 2 to show equal amount of eth and fox. - const totalLiquidity = ethInReserve.times(asset0Price).times(2) - return totalLiquidity.toString() - } + if (!uniV2LPContract) return + + const reserves = await uniV2LPContract.getReserves() + // Amount of Eth in liquidity pool + const ethInReserve = bnOrZero(reserves?.[0]?.toString()).div(bn(10).pow(asset0.precision)) + + // Total market cap of liquidity pool in usdc. + // Multiplied by 2 to show equal amount of eth and fox. + const totalLiquidity = ethInReserve.times(asset0Price).times(2) + return totalLiquidity.toString() }, [asset0.precision, asset0Price, uniV2LPContract]) const getLpTokenPrice = useCallback(async () => { - if (!skip && uniV2LPContract) { - const tvl = await getLpTVL() - const totalSupply = await uniV2LPContract.totalSupply() - return bnOrZero(tvl).div(bnOrZero(totalSupply.toString()).div(bn(10).pow(lpAsset.precision))) - } + if (skip || !uniV2LPContract) return + + const tvl = await getLpTVL() + const totalSupply = await uniV2LPContract.totalSupply() + + return bnOrZero(tvl).div(bnOrZero(totalSupply.toString()).div(bn(10).pow(lpAsset.precision))) }, [skip, getLpTVL, lpAsset.precision, uniV2LPContract]) // TODO(gomes): consolidate me const asset0Allowance = useCallback(async () => { - if (skip) return - const contract = asset0Contract - if (!accountId || !contract) return + if (skip || !accountId || !asset0Contract) return + const accountAddress = fromAccountId(accountId).account const contractAddress = fromAssetId(uniswapV2Router02AssetId).assetReference - const _allowance = await contract.allowance(accountAddress, contractAddress) + const _allowance = await asset0Contract.allowance(accountAddress, contractAddress) + return _allowance.toString() }, [skip, asset0Contract, accountId]) const asset1Allowance = useCallback(async () => { - if (skip) return - const contract = asset1Contract - if (!accountId || !contract) return + if (skip || !accountId || !asset1Contract) return + const accountAddress = fromAccountId(accountId).account const contractAddress = fromAssetId(uniswapV2Router02AssetId).assetReference - const _allowance = await contract.allowance(accountAddress, contractAddress) + const _allowance = await asset1Contract.allowance(accountAddress, contractAddress) + return _allowance.toString() }, [skip, asset1Contract, accountId]) const lpAllowance = useCallback(async () => { - if (skip) return - const contract = uniV2LPContract - if (!accountId || !contract) return + if (skip || !accountId || !uniV2LPContract) return + const accountAddress = fromAccountId(accountId).account const contractAddress = fromAssetId(uniswapV2Router02AssetId).assetReference - const _allowance = await contract.allowance(accountAddress, contractAddress) + const _allowance = await uniV2LPContract.allowance(accountAddress, contractAddress) + return _allowance.toString() }, [skip, uniV2LPContract, accountId]) - const getApproveGasData = useCallback( + const getApproveFeeData = useCallback( async (contractAddress: Address) => { - if (skip) return + if (skip || !adapter || !accountId) return + const contract = getOrCreateContractByType({ address: contractAddress, type: ContractType.ERC20, }) - if (adapter && accountId && contract) { - const data = contract.interface.encodeFunctionData('approve', [ - fromAssetId(uniswapV2Router02AssetId).assetReference, - MaxUint256, - ]) - const fees = await adapter.getFeeData({ - to: contract.address, - value: '0', - chainSpecific: { - contractData: data, - from: fromAccountId(accountId).account, - contractAddress: contract.address, - }, - }) - return fees - } + + if (!contract) return + + const data = contract.interface.encodeFunctionData('approve', [ + fromAssetId(uniswapV2Router02AssetId).assetReference, + MaxUint256, + ]) + + const feeData = await adapter.getFeeData({ + to: contract.address, + value: '0', + chainSpecific: { + contractData: data, + from: fromAccountId(accountId).account, + contractAddress: contract.address, + }, + }) + + return getFeeDataFromEstimate(feeData) }, [skip, adapter, accountId], ) - const getDepositGasDataCryptoBaseUnit = useCallback( + const getDepositFeeData = useCallback( async ({ token0Amount, token1Amount }: { token0Amount: string; token1Amount: string }) => { - if (skip || !accountId || !uniswapRouterContract) return + if (skip || !adapter || !accountId || !uniswapRouterContract) return + // https://docs.uniswap.org/contracts/v2/reference/smart-contracts/router-02#addliquidityeth const deadline = Date.now() + 1200000 // 20 minutes from now + + // TODO(gomes): consolidate branching, surely we can do better if ([assetId0OrWeth, assetId1OrWeth].includes(wethAssetId)) { const otherAssetContractAddress = assetId0OrWeth === wethAssetId ? asset1ContractAddress : asset0ContractAddress @@ -605,7 +508,8 @@ export const useUniV2LiquidityPool = ({ accountAddress, deadline, ]) - const estimatedFees = await adapter.getFeeData({ + + const feeData = await adapter.getFeeData({ to: contractAddress, value: ethValueBaseUnit, chainSpecific: { @@ -613,8 +517,8 @@ export const useUniV2LiquidityPool = ({ from: accountAddress, }, }) - return estimatedFees - // TODO(gomes): consolidate branching, surely we can do better + + return getFeeDataFromEstimate(feeData) } else { const accountAddress = fromAccountId(accountId).account const contractAddress = fromAssetId(uniswapV2Router02AssetId).assetReference @@ -632,7 +536,8 @@ export const useUniV2LiquidityPool = ({ accountAddress, deadline, ]) - const estimatedFees = await adapter.getFeeData({ + + const feeData = await adapter.getFeeData({ to: contractAddress, value: '0', // 0 ETH since these are ERC20 <-> ERC20 pools chainSpecific: { @@ -640,7 +545,8 @@ export const useUniV2LiquidityPool = ({ from: accountAddress, }, }) - return estimatedFees + + return getFeeDataFromEstimate(feeData) } }, [ @@ -658,9 +564,10 @@ export const useUniV2LiquidityPool = ({ ], ) - const getWithdrawGasData = useCallback( + const getWithdrawFeeData = useCallback( async (lpAmount: string, asset0Amount: string, asset1Amount: string) => { - if (skip || !accountId || !uniswapRouterContract) return + if (skip || !adapter || !accountId || !uniswapRouterContract) return + const data = makeRemoveLiquidityData({ lpAmount, asset0Amount, @@ -670,7 +577,8 @@ export const useUniV2LiquidityPool = ({ }) const contractAddress = fromAssetId(uniswapV2Router02AssetId).assetReference - const estimatedFees = await adapter.getFeeData({ + + const feeData = await adapter.getFeeData({ to: contractAddress, value: '0', chainSpecific: { @@ -679,11 +587,7 @@ export const useUniV2LiquidityPool = ({ }, }) - const gasLimitBase = estimatedFees.fast.chainSpecific.gasLimit - const gasLimit = bnOrZero(gasLimitBase).times(1.1).toFixed(0) - estimatedFees.fast.chainSpecific.gasLimit = gasLimit - - return estimatedFees + return getFeeDataFromEstimate(feeData) }, [ skip, @@ -699,6 +603,9 @@ export const useUniV2LiquidityPool = ({ const approveAsset = useCallback( async (contractAddress: Address) => { if (skip || !wallet || !isNumber(accountNumber)) return + + if (!adapter) throw new Error(`no adapter available for ${ethChainId}`) + const contract = getOrCreateContractByType({ address: contractAddress, type: ContractType.ERC20, @@ -711,49 +618,23 @@ export const useUniV2LiquidityPool = ({ uniV2ContractAddress, MaxUint256, ]) - const gasData = await getApproveGasData(contractAddress) - if (!gasData) return - const fees = gasData.fast as FeeData - const { - chainSpecific: { gasPrice, gasLimit }, - } = fees - if (gasPrice === undefined) { - throw new Error(`approveAsset: missing gasPrice for non-EIP-1559 tx`) - } - const result = await adapter.buildCustomTx({ - to: contract!.address, + + const feeData = await getApproveFeeData(contractAddress) + if (!feeData) return + + const txid = await buildAndBroadcast({ + accountNumber, + adapter, + feeData: feeData.chainSpecific, + to: contractAddress, value: '0', wallet, data, - gasLimit, - accountNumber, - gasPrice, }) - const txToSign = result.txToSign - - const broadcastTXID = await (async () => { - if (wallet.supportsOfflineSigning()) { - const signedTx = await adapter.signTransaction({ - txToSign, - wallet, - }) - return adapter.broadcastTransaction(signedTx) - } else if (wallet.supportsBroadcast()) { - /** - * signAndBroadcastTransaction is an optional method on the HDWallet interface. - * Check and see if it exists; if so, call and make sure a txhash is returned - */ - if (!adapter.signAndBroadcastTransaction) { - throw new Error('signAndBroadcastTransaction undefined for wallet') - } - return adapter.signAndBroadcastTransaction?.({ txToSign, wallet }) - } else { - throw new Error('Bad hdwallet config') - } - })() - return broadcastTXID + + return txid }, - [accountNumber, adapter, getApproveGasData, skip, wallet], + [accountNumber, adapter, getApproveFeeData, skip, wallet], ) return { @@ -763,10 +644,10 @@ export const useUniV2LiquidityPool = ({ asset1Allowance, approveAsset, calculateHoldings, - getApproveGasData, - getDepositGasDataCryptoBaseUnit, + getApproveFeeData, + getDepositFeeData, getLpTVL, - getWithdrawGasData, + getWithdrawFeeData, removeLiquidity, getLpTokenPrice, } diff --git a/src/lib/swapper/api.ts b/src/lib/swapper/api.ts index ed2f478dff3..36ab3cd5506 100644 --- a/src/lib/swapper/api.ts +++ b/src/lib/swapper/api.ts @@ -34,61 +34,38 @@ export const makeSwapErrorRight = ({ code, }) +export type EvmFeeData = { + estimatedGasCryptoBaseUnit?: string + gasPriceCryptoBaseUnit?: string + approvalFeeCryptoBaseUnit?: string + totalFee?: string + maxFeePerGas?: string + maxPriorityFeePerGas?: string +} + +export type UtxoFeeData = { + byteCount: string + satsPerByte: string +} + +export type CosmosSdkFeeData = { + estimatedGasCryptoBaseUnit: string +} + type ChainSpecificQuoteFeeData = ChainSpecific< T, { - [KnownChainIds.EthereumMainnet]: { - estimatedGasCryptoBaseUnit?: string - gasPriceCryptoBaseUnit?: string - approvalFeeCryptoBaseUnit?: string - totalFee?: string - } - [KnownChainIds.AvalancheMainnet]: { - estimatedGasCryptoBaseUnit?: string - gasPriceCryptoBaseUnit?: string - approvalFeeCryptoBaseUnit?: string - totalFee?: string - } - [KnownChainIds.OptimismMainnet]: { - estimatedGasCryptoBaseUnit?: string - gasPriceCryptoBaseUnit?: string - approvalFeeCryptoBaseUnit?: string - totalFee?: string - } - [KnownChainIds.BnbSmartChainMainnet]: { - estimatedGasCryptoBaseUnit?: string - gasPriceCryptoBaseUnit?: string - approvalFeeCryptoBaseUnit?: string - totalFee?: string - } - [KnownChainIds.PolygonMainnet]: { - estimatedGasCryptoBaseUnit?: string - gasPriceCryptoBaseUnit?: string - approvalFeeCryptoBaseUnit?: string - totalFee?: string - } - [KnownChainIds.BitcoinMainnet]: { - byteCount: string - satsPerByte: string - } - [KnownChainIds.DogecoinMainnet]: { - byteCount: string - satsPerByte: string - } - [KnownChainIds.LitecoinMainnet]: { - byteCount: string - satsPerByte: string - } - [KnownChainIds.BitcoinCashMainnet]: { - byteCount: string - satsPerByte: string - } - [KnownChainIds.CosmosMainnet]: { - estimatedGasCryptoBaseUnit: string - } - [KnownChainIds.ThorchainMainnet]: { - estimatedGasCryptoBaseUnit: string - } + [KnownChainIds.EthereumMainnet]: EvmFeeData + [KnownChainIds.AvalancheMainnet]: EvmFeeData + [KnownChainIds.OptimismMainnet]: EvmFeeData + [KnownChainIds.BnbSmartChainMainnet]: EvmFeeData + [KnownChainIds.PolygonMainnet]: EvmFeeData + [KnownChainIds.BitcoinMainnet]: UtxoFeeData + [KnownChainIds.DogecoinMainnet]: UtxoFeeData + [KnownChainIds.LitecoinMainnet]: UtxoFeeData + [KnownChainIds.BitcoinCashMainnet]: UtxoFeeData + [KnownChainIds.CosmosMainnet]: CosmosSdkFeeData + [KnownChainIds.ThorchainMainnet]: CosmosSdkFeeData } > diff --git a/src/lib/swapper/swappers/CowSwapper/cowApprove/cowApprove.ts b/src/lib/swapper/swappers/CowSwapper/cowApprove/cowApprove.ts index 869f0ba9cb3..c25ca2d4fbd 100644 --- a/src/lib/swapper/swappers/CowSwapper/cowApprove/cowApprove.ts +++ b/src/lib/swapper/swappers/CowSwapper/cowApprove/cowApprove.ts @@ -1,60 +1,30 @@ +import { fromAssetId } from '@shapeshiftoss/caip' import type { KnownChainIds } from '@shapeshiftoss/types' import type { ApproveAmountInput, ApproveInfiniteInput } from 'lib/swapper/api' -import { SwapError, SwapErrorType } from 'lib/swapper/api' import type { CowSwapperDeps } from 'lib/swapper/swappers/CowSwapper/CowSwapper' import { MAX_ALLOWANCE } from 'lib/swapper/swappers/CowSwapper/utils/constants' -import { erc20Abi } from 'lib/swapper/swappers/utils/abi/erc20-abi' import { grantAllowance } from 'lib/swapper/swappers/utils/helpers/helpers' -export async function cowApproveInfinite( - { adapter, web3 }: CowSwapperDeps, - { quote, wallet }: ApproveInfiniteInput, +export function cowApproveAmount( + deps: CowSwapperDeps, + { quote, wallet, amount }: ApproveAmountInput, ) { - try { - const allowanceGrantRequired = await grantAllowance({ - quote: { - ...quote, - sellAmountBeforeFeesCryptoBaseUnit: MAX_ALLOWANCE, - }, - wallet, - adapter, - erc20Abi, - web3, - }) + const { accountNumber, allowanceContract, feeData, sellAsset } = quote - return allowanceGrantRequired - } catch (e) { - if (e instanceof SwapError) throw e - throw new SwapError('[cowApproveInfinite]', { - cause: e, - code: SwapErrorType.APPROVE_INFINITE_FAILED, - }) - } + return grantAllowance({ + ...deps, + accountNumber, + approvalAmount: amount ?? quote.sellAmountBeforeFeesCryptoBaseUnit, + to: fromAssetId(sellAsset.assetId).assetReference, + feeData: feeData.chainSpecific, + spender: allowanceContract, + wallet, + }) } -export async function cowApproveAmount( - { adapter, web3 }: CowSwapperDeps, - { quote, wallet, amount }: ApproveAmountInput, -) { - try { - const approvalAmount = amount ?? quote.sellAmountBeforeFeesCryptoBaseUnit - const allowanceGrantRequired = await grantAllowance({ - quote: { - ...quote, - sellAmountBeforeFeesCryptoBaseUnit: approvalAmount, - }, - wallet, - adapter, - erc20Abi, - web3, - }) - - return allowanceGrantRequired - } catch (e) { - if (e instanceof SwapError) throw e - throw new SwapError('[cowApproveAmount]', { - cause: e, - code: SwapErrorType.APPROVE_AMOUNT_FAILED, - }) - } +export function cowApproveInfinite( + deps: CowSwapperDeps, + input: ApproveInfiniteInput, +): Promise { + return cowApproveAmount(deps, { ...input, amount: MAX_ALLOWANCE }) } diff --git a/src/lib/swapper/swappers/CowSwapper/cowBuildTrade/cowBuildTrade.test.ts b/src/lib/swapper/swappers/CowSwapper/cowBuildTrade/cowBuildTrade.test.ts index 7ed61175dcf..dd4219e07ac 100644 --- a/src/lib/swapper/swappers/CowSwapper/cowBuildTrade/cowBuildTrade.test.ts +++ b/src/lib/swapper/swappers/CowSwapper/cowBuildTrade/cowBuildTrade.test.ts @@ -116,10 +116,7 @@ const expectedApiInputFoxToEth: CowSwapSellQuoteApiInput = { const expectedTradeWethToFox: CowTrade = { rate: '14716.04718939437505555958', // 14716 FOX per WETH feeData: { - chainSpecific: { - estimatedGasCryptoBaseUnit: '100000', - gasPriceCryptoBaseUnit: '79036500000', - }, + chainSpecific: {}, buyAssetTradeFeeUsd: '0', networkFeeCryptoBaseUnit: '0', sellAssetTradeFeeUsd: '17.95954294012756741283729339486489192096', @@ -139,11 +136,7 @@ const expectedTradeQuoteWbtcToWethWithApprovalFeeCryptoBaseUnit: CowTrade = { rate: '0.00004995640398295996', feeData: { - chainSpecific: { - estimatedGasCryptoBaseUnit: '100000', - gasPriceCryptoBaseUnit: '79036500000', - }, + chainSpecific: {}, buyAssetTradeFeeUsd: '0', networkFeeCryptoBaseUnit: '0', sellAssetTradeFeeUsd: '5.3955565850972847808512', diff --git a/src/lib/swapper/swappers/CowSwapper/cowBuildTrade/cowBuildTrade.ts b/src/lib/swapper/swappers/CowSwapper/cowBuildTrade/cowBuildTrade.ts index 3c60d962157..3e419b0853d 100644 --- a/src/lib/swapper/swappers/CowSwapper/cowBuildTrade/cowBuildTrade.ts +++ b/src/lib/swapper/swappers/CowSwapper/cowBuildTrade/cowBuildTrade.ts @@ -2,7 +2,6 @@ import { ethAssetId, fromAssetId } from '@shapeshiftoss/caip' import { KnownChainIds } from '@shapeshiftoss/types' import type { Result } from '@sniptt/monads' import { Err, Ok } from '@sniptt/monads' -import type { AxiosResponse } from 'axios' import { bn, bnOrZero } from 'lib/bignumber/bignumber' import type { BuildTradeInput, SwapErrorRight } from 'lib/swapper/api' import { makeSwapErrorRight, SwapError, SwapErrorType } from 'lib/swapper/api' @@ -10,38 +9,29 @@ import type { CowSwapperDeps } from 'lib/swapper/swappers/CowSwapper/CowSwapper' import type { CowSwapQuoteResponse, CowTrade } from 'lib/swapper/swappers/CowSwapper/types' import { COW_SWAP_ETH_MARKER_ADDRESS, - COW_SWAP_VAULT_RELAYER_ADDRESS, DEFAULT_APP_DATA, DEFAULT_SOURCE, ORDER_KIND_SELL, } from 'lib/swapper/swappers/CowSwapper/utils/constants' import { cowService } from 'lib/swapper/swappers/CowSwapper/utils/cowService' +import type { CowSwapSellQuoteApiInput } from 'lib/swapper/swappers/CowSwapper/utils/helpers/helpers' import { getNowPlusThirtyMinutesTimestamp, getUsdRate, } from 'lib/swapper/swappers/CowSwapper/utils/helpers/helpers' -import { erc20AllowanceAbi } from 'lib/swapper/swappers/utils/abi/erc20Allowance-abi' -import { - getApproveContractData, - isApprovalRequired, -} from 'lib/swapper/swappers/utils/helpers/helpers' export async function cowBuildTrade( deps: CowSwapperDeps, input: BuildTradeInput, ): Promise, SwapErrorRight>> { try { - const { - sellAsset, - buyAsset, - sellAmountBeforeFeesCryptoBaseUnit: sellAmountExcludeFeeCryptoBaseUnit, - accountNumber, - wallet, - } = input - const { adapter, web3 } = deps + const { adapter } = deps + const { sellAsset, buyAsset, accountNumber, wallet } = input + const sellAmount = input.sellAmountBeforeFeesCryptoBaseUnit const { assetReference: sellAssetErc20Address, assetNamespace: sellAssetNamespace } = fromAssetId(sellAsset.assetId) + const { assetReference: buyAssetErc20Address, chainId: buyAssetChainId } = fromAssetId( buyAsset.assetId, ) @@ -62,44 +52,29 @@ export async function cowBuildTrade( const buyToken = buyAsset.assetId !== ethAssetId ? buyAssetErc20Address : COW_SWAP_ETH_MARKER_ADDRESS + const receiveAddress = await adapter.getAddress({ accountNumber, wallet }) - /** - * /v1/quote - * params: { - * sellToken: contract address of token to sell - * buyToken: contractAddress of token to buy - * receiver: receiver address can be defaulted to "0x0000000000000000000000000000000000000000" - * validTo: time duration during which quote is valid (eg : 1654851610 as timestamp) - * appData: appData for the CowSwap quote that can be used later, can be defaulted to "0x0000000000000000000000000000000000000000000000000000000000000000" - * partiallyFillable: false - * from: sender address can be defaulted to "0x0000000000000000000000000000000000000000" - * kind: "sell" or "buy" - * sellAmountBeforeFee / buyAmountAfterFee: amount in base unit - * } - */ - const quoteResponse: AxiosResponse = - await cowService.post(`${deps.apiUrl}/v1/quote/`, { - sellToken: sellAssetErc20Address, - buyToken, - receiver: receiveAddress, - validTo: getNowPlusThirtyMinutesTimestamp(), - appData: DEFAULT_APP_DATA, - partiallyFillable: false, - from: receiveAddress, - kind: ORDER_KIND_SELL, - sellAmountBeforeFee: sellAmountExcludeFeeCryptoBaseUnit, - }) + // https://api.cow.fi/docs/#/default/post_api_v1_quote + const { data } = await cowService.post(`${deps.apiUrl}/v1/quote/`, { + sellToken: sellAssetErc20Address, + buyToken, + receiver: receiveAddress, + validTo: getNowPlusThirtyMinutesTimestamp(), + appData: DEFAULT_APP_DATA, + partiallyFillable: false, + from: receiveAddress, + kind: ORDER_KIND_SELL, + sellAmountBeforeFee: sellAmount, + } as CowSwapSellQuoteApiInput) const { - data: { - quote: { - buyAmount: buyAmountCryptoBaseUnit, - sellAmount: quoteSellAmountExcludeFeeCryptoBaseUnit, - feeAmount: feeAmountInSellTokenCryptoBaseUnit, - }, + quote: { + buyAmount: buyAmountCryptoBaseUnit, + sellAmount: quoteSellAmountExcludeFeeCryptoBaseUnit, + feeAmount: feeAmountInSellTokenCryptoBaseUnit, }, - } = quoteResponse + } = data const sellAssetUsdRate = await getUsdRate(deps, sellAsset) const sellAssetTradeFeeUsd = bnOrZero(feeAmountInSellTokenCryptoBaseUnit) @@ -115,32 +90,15 @@ export async function cowBuildTrade( ) const rate = buyAmountCryptoPrecision.div(quoteSellAmountCryptoPrecision).toString() - const data = getApproveContractData({ - web3, - spenderAddress: COW_SWAP_VAULT_RELAYER_ADDRESS, - contractAddress: sellAssetErc20Address, - }) - - const feeDataOptions = await adapter.getFeeData({ - to: sellAssetErc20Address, - value: '0', - chainSpecific: { from: receiveAddress, contractData: data }, - }) - - const feeData = feeDataOptions['fast'] - const trade: CowTrade = { rate, feeData: { networkFeeCryptoBaseUnit: '0', // no miner fee for CowSwap - chainSpecific: { - estimatedGasCryptoBaseUnit: feeData.chainSpecific.gasLimit, - gasPriceCryptoBaseUnit: feeData.chainSpecific.gasPrice, - }, + chainSpecific: {}, // no on chain fees for CowSwap buyAssetTradeFeeUsd: '0', // Trade fees for buy Asset are always 0 since trade fees are subtracted from sell asset sellAssetTradeFeeUsd, }, - sellAmountBeforeFeesCryptoBaseUnit: sellAmountExcludeFeeCryptoBaseUnit, + sellAmountBeforeFeesCryptoBaseUnit: sellAmount, buyAmountCryptoBaseUnit, sources: DEFAULT_SOURCE, buyAsset, @@ -151,24 +109,6 @@ export async function cowBuildTrade( sellAmountDeductFeeCryptoBaseUnit: quoteSellAmountExcludeFeeCryptoBaseUnit, } - const approvalRequired = await isApprovalRequired({ - adapter, - sellAsset, - allowanceContract: COW_SWAP_VAULT_RELAYER_ADDRESS, - receiveAddress, - sellAmountExcludeFeeCryptoBaseUnit, - web3: deps.web3, - erc20AllowanceAbi, - }) - - if (approvalRequired) { - trade.feeData.chainSpecific.approvalFeeCryptoBaseUnit = bnOrZero( - feeData.chainSpecific.gasLimit, - ) - .multipliedBy(bnOrZero(feeData.chainSpecific.gasPrice)) - .toString() - } - return Ok(trade) } catch (e) { if (e instanceof SwapError) diff --git a/src/lib/swapper/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.test.ts b/src/lib/swapper/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.test.ts index 16af9791d54..c373ae7ca66 100644 --- a/src/lib/swapper/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.test.ts +++ b/src/lib/swapper/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.test.ts @@ -134,7 +134,9 @@ const expectedTradeQuoteWethToFox: TradeQuote = { chainSpecific: { estimatedGasCryptoBaseUnit: '100000', gasPriceCryptoBaseUnit: '79036500000', - approvalFeeCryptoBaseUnit: '7903650000000000', + approvalFeeCryptoBaseUnit: '4080654495000000', + maxFeePerGas: '216214758112', + maxPriorityFeePerGas: '2982734547', }, buyAssetTradeFeeUsd: '0', sellAssetTradeFeeUsd: '17.95954294012756741283729339486489192096', @@ -157,7 +159,9 @@ const expectedTradeQuoteFoxToEth: TradeQuote = { chainSpecific: { estimatedGasCryptoBaseUnit: '100000', gasPriceCryptoBaseUnit: '79036500000', - approvalFeeCryptoBaseUnit: '7903650000000000', + approvalFeeCryptoBaseUnit: '4080654495000000', + maxFeePerGas: '216214758112', + maxPriorityFeePerGas: '2982734547', }, buyAssetTradeFeeUsd: '0', sellAssetTradeFeeUsd: '5.3955565850972847808512', @@ -180,7 +184,9 @@ const expectedTradeQuoteSmallAmountWethToFox: TradeQuote, SwapErrorRight>> { try { - const { - sellAsset, - buyAsset, - sellAmountBeforeFeesCryptoBaseUnit, - accountNumber, - receiveAddress, - } = input const { adapter, web3 } = deps + const { sellAsset, buyAsset, accountNumber, receiveAddress } = input + const sellAmount = input.sellAmountBeforeFeesCryptoBaseUnit const { assetReference: sellAssetErc20Address, assetNamespace: sellAssetNamespace } = fromAssetId(sellAsset.assetId) + const { assetReference: buyAssetErc20Address, chainId: buyAssetChainId } = fromAssetId( buyAsset.assetId, ) @@ -68,6 +64,7 @@ export async function getCowSwapTradeQuote( const buyToken = buyAsset.assetId !== ethAssetId ? buyAssetErc20Address : COW_SWAP_ETH_MARKER_ADDRESS + const { minimumAmountCryptoHuman, maximumAmountCryptoHuman } = await getCowSwapMinMax( deps, sellAsset, @@ -77,16 +74,15 @@ export async function getCowSwapTradeQuote( const minQuoteSellAmount = bnOrZero(minimumAmountCryptoHuman).times( bn(10).exponentiatedBy(sellAsset.precision), ) - const isSellAmountBelowMinimum = bnOrZero(sellAmountBeforeFeesCryptoBaseUnit).lt( - minQuoteSellAmount, - ) + const isSellAmountBelowMinimum = bnOrZero(sellAmount).lt(minQuoteSellAmount) // making sure we do not have decimals for cowswap api (can happen at least from minQuoteSellAmount) const normalizedSellAmountCryptoBaseUnit = normalizeIntegerAmount( - isSellAmountBelowMinimum ? minQuoteSellAmount : sellAmountBeforeFeesCryptoBaseUnit, + isSellAmountBelowMinimum ? minQuoteSellAmount : sellAmount, ) - const apiInput: CowSwapSellQuoteApiInput = { + // https://api.cow.fi/docs/#/default/post_api_v1_quote + const { data } = await cowService.post(`${deps.apiUrl}/v1/quote/`, { sellToken: sellAssetErc20Address, buyToken, receiver: DEFAULT_ADDRESS, @@ -96,34 +92,15 @@ export async function getCowSwapTradeQuote( from: DEFAULT_ADDRESS, kind: ORDER_KIND_SELL, sellAmountBeforeFee: normalizedSellAmountCryptoBaseUnit, - } - - /** - * /v1/quote - * params: { - * sellToken: contract address of token to sell - * buyToken: contractAddress of token to buy - * receiver: receiver address can be defaulted to "0x0000000000000000000000000000000000000000" - * validTo: time duration during which quote is valid (eg : 1654851610 as timestamp) - * appData: appData for the CowSwap quote that can be used later, can be defaulted to "0x0000000000000000000000000000000000000000000000000000000000000000" - * partiallyFillable: false - * from: sender address can be defaulted to "0x0000000000000000000000000000000000000000" - * kind: "sell" or "buy" - * sellAmountBeforeFee / buyAmountAfterFee: amount in base unit - * } - */ - const quoteResponse: AxiosResponse = - await cowService.post(`${deps.apiUrl}/v1/quote/`, apiInput) + } as CowSwapSellQuoteApiInput) const { - data: { - quote: { - buyAmount: buyAmountCryptoBaseUnit, - sellAmount: sellAmountCryptoBaseUnit, - feeAmount: feeAmountInSellTokenCryptoBaseUnit, - }, + quote: { + buyAmount: buyAmountCryptoBaseUnit, + sellAmount: sellAmountCryptoBaseUnit, + feeAmount: feeAmountInSellTokenCryptoBaseUnit, }, - } = quoteResponse + } = data const quoteSellAmountPlusFeesCryptoBaseUnit = bnOrZero(sellAmountCryptoBaseUnit).plus( feeAmountInSellTokenCryptoBaseUnit, @@ -137,17 +114,17 @@ export async function getCowSwapTradeQuote( ) const rate = buyCryptoAmount.div(sellCryptoAmount).toString() - const data = getApproveContractData({ + const approveData = getApproveContractData({ web3, spenderAddress: COW_SWAP_VAULT_RELAYER_ADDRESS, contractAddress: sellAssetErc20Address, }) - const [feeDataOptions, sellAssetUsdRate] = await Promise.all([ + const [feeData, sellAssetUsdRate] = await Promise.all([ adapter.getFeeData({ to: sellAssetErc20Address, value: '0', - chainSpecific: { from: receiveAddress, contractData: data }, + chainSpecific: { from: receiveAddress, contractData: approveData }, }), getUsdRate(deps, sellAsset), ]) @@ -157,15 +134,13 @@ export async function getCowSwapTradeQuote( .multipliedBy(bnOrZero(sellAssetUsdRate)) .toString() - const feeData = feeDataOptions['fast'] - const isQuoteSellAmountBelowMinimum = bnOrZero(quoteSellAmountPlusFeesCryptoBaseUnit).lt( minQuoteSellAmount, ) // If isQuoteSellAmountBelowMinimum we don't want to replace it with normalizedSellAmount // The purpose of this was to get a quote from CowSwap even with small amounts const quoteSellAmountCryptoBaseUnit = isQuoteSellAmountBelowMinimum - ? sellAmountBeforeFeesCryptoBaseUnit + ? sellAmount : normalizedSellAmountCryptoBaseUnit // Similarly, if isQuoteSellAmountBelowMinimum we can't use the buy amount from the quote @@ -174,6 +149,8 @@ export async function getCowSwapTradeQuote( ? '0' : buyAmountCryptoBaseUnit + const { average, fast } = feeData + return Ok({ rate, minimumCryptoHuman: minimumAmountCryptoHuman, @@ -181,11 +158,11 @@ export async function getCowSwapTradeQuote( feeData: { networkFeeCryptoBaseUnit: '0', // no miner fee for CowSwap chainSpecific: { - estimatedGasCryptoBaseUnit: feeData.chainSpecific.gasLimit, - gasPriceCryptoBaseUnit: feeData.chainSpecific.gasPrice, - approvalFeeCryptoBaseUnit: bnOrZero(feeData.chainSpecific.gasLimit) - .multipliedBy(bnOrZero(feeData.chainSpecific.gasPrice)) - .toString(), + estimatedGasCryptoBaseUnit: average.chainSpecific.gasLimit, + gasPriceCryptoBaseUnit: fast.chainSpecific.gasPrice, // fast gas price since it is underestimated currently + maxFeePerGas: average.chainSpecific.maxFeePerGas, + maxPriorityFeePerGas: average.chainSpecific.maxPriorityFeePerGas, + approvalFeeCryptoBaseUnit: fast.txFee, // use worst case fast fee }, buyAssetTradeFeeUsd: '0', // Trade fees for buy Asset are always 0 since trade fees are subtracted from sell asset sellAssetTradeFeeUsd, diff --git a/src/lib/swapper/swappers/LifiSwapper/approve/approve.ts b/src/lib/swapper/swappers/LifiSwapper/approve/approve.ts index cbadd006625..d2dc5a72be3 100644 --- a/src/lib/swapper/swappers/LifiSwapper/approve/approve.ts +++ b/src/lib/swapper/swappers/LifiSwapper/approve/approve.ts @@ -1,31 +1,25 @@ +import { fromAssetId } from '@shapeshiftoss/caip' import type { EvmChainId } from '@shapeshiftoss/chain-adapters' -import { isEvmChainId } from '@shapeshiftoss/chain-adapters' import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton' -import type { ApproveAmountInput, ApproveInfiniteInput, TradeQuote } from 'lib/swapper/api' +import type { ApproveAmountInput, ApproveInfiniteInput } from 'lib/swapper/api' import { SwapError, SwapErrorType } from 'lib/swapper/api' import { MAX_ALLOWANCE } from 'lib/swapper/swappers/LifiSwapper/utils/constants' -import { erc20Abi } from 'lib/swapper/swappers/utils/abi/erc20-abi' import { grantAllowance } from 'lib/swapper/swappers/utils/helpers/helpers' import { isEvmChainAdapter } from 'lib/utils' import { getWeb3InstanceByChainId } from 'lib/web3-instance' -const grantAllowanceForAmount = async ( +const grantAllowanceForAmount = ( { quote, wallet }: ApproveAmountInput, approvalAmountCryptoBaseUnit: string, ) => { - const chainId = quote.sellAsset.chainId + const { accountNumber, allowanceContract, feeData, sellAsset } = quote + + const chainId = sellAsset.chainId const adapterManager = getChainAdapterManager() const adapter = adapterManager.get(chainId) const web3 = getWeb3InstanceByChainId(chainId) - if (!isEvmChainId(chainId)) { - throw new SwapError('[grantAllowanceForAmount] - only EVM chains are supported', { - code: SwapErrorType.UNSUPPORTED_CHAIN, - details: { chainId }, - }) - } - - if (adapter === undefined) { + if (!adapter) { throw new SwapError('[grantAllowanceForAmount] - getChainAdapterManager returned undefined', { code: SwapErrorType.UNSUPPORTED_CHAIN, details: { chainId }, @@ -42,16 +36,14 @@ const grantAllowanceForAmount = async ( }) } - const approvalQuote: TradeQuote = { - ...quote, - sellAmountBeforeFeesCryptoBaseUnit: approvalAmountCryptoBaseUnit, - } - - return await grantAllowance({ - quote: approvalQuote, + return grantAllowance({ + accountNumber, + spender: allowanceContract, + feeData: feeData.chainSpecific, + approvalAmount: approvalAmountCryptoBaseUnit, + to: fromAssetId(sellAsset.assetId).assetReference, wallet, adapter, - erc20Abi, web3, }) } diff --git a/src/lib/swapper/swappers/ThorchainSwapper/buildThorTrade/buildThorTrade.ts b/src/lib/swapper/swappers/ThorchainSwapper/buildThorTrade/buildThorTrade.ts index 01b1bf2492d..d29d3ddfe10 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/buildThorTrade/buildThorTrade.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/buildThorTrade/buildThorTrade.ts @@ -10,6 +10,7 @@ import { Err, Ok } from '@sniptt/monads' import type { BuildTradeInput, GetUtxoTradeQuoteInput, + QuoteFeeData, SwapErrorRight, TradeQuote, } from 'lib/swapper/api' @@ -75,14 +76,8 @@ export const buildTrade = async ({ adapter: sellAdapter as unknown as EvmBaseAdapter, sellAmountCryptoBaseUnit, destinationAddress, + feeData: quote.feeData as QuoteFeeData, deps, - gasPriceCryptoBaseUnit: - (quote as TradeQuote).feeData.chainSpecific - ?.gasPriceCryptoBaseUnit ?? '0', - gasLimit: - (quote as TradeQuote).feeData.chainSpecific - ?.estimatedGasCryptoBaseUnit ?? '0', - buyAssetTradeFeeUsd: quote.feeData.buyAssetTradeFeeUsd, }) return maybeEthTradeTx.map(ethTradeTx => ({ diff --git a/src/lib/swapper/swappers/ThorchainSwapper/evm/makeTradeTx.ts b/src/lib/swapper/swappers/ThorchainSwapper/evm/makeTradeTx.ts index 05d95361052..8897ac8cded 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/evm/makeTradeTx.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/evm/makeTradeTx.ts @@ -1,16 +1,18 @@ import type { Asset } from '@shapeshiftoss/asset-service' import { fromAssetId } from '@shapeshiftoss/caip' -import type { EvmBaseAdapter } from '@shapeshiftoss/chain-adapters' +import type { EvmBaseAdapter, EvmChainId } from '@shapeshiftoss/chain-adapters' import type { ETHSignTx, HDWallet } from '@shapeshiftoss/hdwallet-core' import type { Result } from '@sniptt/monads' import { Err, Ok } from '@sniptt/monads' -import type { SwapErrorRight } from 'lib/swapper/api' +import type { QuoteFeeData, SwapErrorRight } from 'lib/swapper/api' import { makeSwapErrorRight, SwapError, SwapErrorType } from 'lib/swapper/api' import { getThorTxInfo } from 'lib/swapper/swappers/ThorchainSwapper/evm/utils/getThorTxData' import type { ThorEvmSupportedChainId } from 'lib/swapper/swappers/ThorchainSwapper/ThorchainSwapper' import type { ThorchainSwapperDeps } from 'lib/swapper/swappers/ThorchainSwapper/types' -type MakeTradeTxArgs = { +import { getFeesFromFeeData } from '../../utils/helpers/helpers' + +type MakeTradeTxArgs = { wallet: HDWallet accountNumber: number sellAmountCryptoBaseUnit: string @@ -19,21 +21,9 @@ type MakeTradeTxArgs = { destinationAddress: string adapter: EvmBaseAdapter slippageTolerance: string + feeData: QuoteFeeData deps: ThorchainSwapperDeps - gasLimit: string - buyAssetTradeFeeUsd: string -} & ( - | { - gasPriceCryptoBaseUnit: string - maxFeePerGas?: never - maxPriorityFeePerGas?: never - } - | { - gasPriceCryptoBaseUnit?: never - maxFeePerGas: string - maxPriorityFeePerGas: string - } -) +} export const makeTradeTx = async ({ wallet, @@ -43,14 +33,10 @@ export const makeTradeTx = async ({ sellAsset, destinationAddress, adapter, - maxFeePerGas, - maxPriorityFeePerGas, - gasPriceCryptoBaseUnit, slippageTolerance, + feeData, deps, - gasLimit, - buyAssetTradeFeeUsd, -}: MakeTradeTxArgs): Promise< +}: MakeTradeTxArgs): Promise< Result< { txToSign: ETHSignTx @@ -69,7 +55,7 @@ export const makeTradeTx = async ({ sellAmountCryptoBaseUnit, slippageTolerance, destinationAddress, - buyAssetTradeFeeUsd, + buyAssetTradeFeeUsd: feeData.buyAssetTradeFeeUsd, }) if (maybeThorTxInfo.isErr()) return Err(maybeThorTxInfo.unwrapErr()) @@ -83,12 +69,9 @@ export const makeTradeTx = async ({ wallet, accountNumber, to: router, - gasLimit, - ...(gasPriceCryptoBaseUnit !== undefined - ? { gasPrice: gasPriceCryptoBaseUnit } - : { maxFeePerGas, maxPriorityFeePerGas }), value: isErc20Trade ? '0' : sellAmountCryptoBaseUnit, data, + ...(await getFeesFromFeeData({ wallet, feeData: feeData.chainSpecific })), }), ) } catch (e) { diff --git a/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts b/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts index 4feddaeb9b9..22a09c23dbe 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts @@ -28,6 +28,8 @@ const expectedQuoteResponse: TradeQuote = { estimatedGasCryptoBaseUnit: '100000', approvalFeeCryptoBaseUnit: '700000', gasPriceCryptoBaseUnit: '7', + maxFeePerGas: '5', + maxPriorityFeePerGas: '6', }, buyAssetTradeFeeUsd: '7.656', sellAssetTradeFeeUsd: '0', diff --git a/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts b/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts index 7c8e59bd170..0fb522d1122 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts @@ -19,6 +19,7 @@ import { makeSwapErrorRight, SwapError, SwapErrorType, SwapperName } from 'lib/s import { RUNE_OUTBOUND_TRANSACTION_FEE_CRYPTO_HUMAN } from 'lib/swapper/swappers/ThorchainSwapper/constants' import { getSlippage } from 'lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getSlippage' import type { + ThorChainId, ThorCosmosSdkSupportedChainId, ThorEvmSupportedChainId, ThorUtxoSupportedChainId, @@ -48,7 +49,7 @@ type GetThorTradeQuoteInput = { input: GetTradeQuoteInput } -type GetThorTradeQuoteReturn = Promise, SwapErrorRight>> +type GetThorTradeQuoteReturn = Promise, SwapErrorRight>> type GetThorTradeQuote = (args: GetThorTradeQuoteInput) => GetThorTradeQuoteReturn diff --git a/src/lib/swapper/swappers/ThorchainSwapper/thorTradeApproveInfinite/thorTradeApproveInfinite.ts b/src/lib/swapper/swappers/ThorchainSwapper/thorTradeApproveInfinite/thorTradeApproveInfinite.ts index 9407c28d116..26a958b741d 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/thorTradeApproveInfinite/thorTradeApproveInfinite.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/thorTradeApproveInfinite/thorTradeApproveInfinite.ts @@ -1,66 +1,38 @@ +import { fromAssetId } from '@shapeshiftoss/caip' import type { ethereum } from '@shapeshiftoss/chain-adapters' import { KnownChainIds } from '@shapeshiftoss/types' import type { ApproveInfiniteInput } from 'lib/swapper/api' import { SwapError, SwapErrorType } from 'lib/swapper/api' import type { ThorchainSwapperDeps } from 'lib/swapper/swappers/ThorchainSwapper/types' import { MAX_ALLOWANCE } from 'lib/swapper/swappers/ThorchainSwapper/utils/constants' -import { erc20Abi } from 'lib/swapper/swappers/utils/abi/erc20-abi' -import { APPROVAL_GAS_LIMIT } from 'lib/swapper/swappers/utils/constants' import { grantAllowance } from 'lib/swapper/swappers/utils/helpers/helpers' -export const thorTradeApproveInfinite = async ({ - deps, - input, +export const thorTradeApproveInfinite = ({ + deps: { adapterManager, web3 }, + input: { quote, wallet }, }: { deps: ThorchainSwapperDeps input: ApproveInfiniteInput }): Promise => { - try { - const { adapterManager, web3 } = deps - const { quote, wallet } = input + const adapter = adapterManager.get(KnownChainIds.EthereumMainnet) as unknown as + | ethereum.ChainAdapter + | undefined - const approvalQuote = { - ...quote, - sellAmount: MAX_ALLOWANCE, - feeData: { - ...quote.feeData, - chainSpecific: { - ...quote.feeData.chainSpecific, - // Thor approvals are cheaper than trades, but we don't have dynamic quote data for them. - // Instead, we use a hardcoded gasLimit estimate in place of the estimatedGas in the Thor quote response. - estimatedGas: APPROVAL_GAS_LIMIT, - }, - }, - } - - const sellAssetChainId = approvalQuote.sellAsset.chainId - const adapter = adapterManager.get(KnownChainIds.EthereumMainnet) as unknown as - | ethereum.ChainAdapter - | undefined - - if (!adapter) - throw new SwapError( - `[thorTradeApproveInfinite] - No chain adapter found for ${sellAssetChainId}.`, - { - code: SwapErrorType.UNSUPPORTED_CHAIN, - details: { sellAssetChainId }, - }, - ) - - const allowanceGrantRequired = await grantAllowance({ - quote: approvalQuote, - wallet, - adapter, - erc20Abi, - web3, - }) - - return allowanceGrantRequired - } catch (e) { - if (e instanceof SwapError) throw e - throw new SwapError('[zrxApproveInfinite]', { - cause: e, - code: SwapErrorType.APPROVE_INFINITE_FAILED, - }) + if (!adapter) { + throw new SwapError( + `[thorTradeApproveInfinite] - No chain adapter found for ${quote.sellAsset.chainId}.`, + { code: SwapErrorType.UNSUPPORTED_CHAIN }, + ) } + + return grantAllowance({ + accountNumber: quote.accountNumber, + spender: quote.allowanceContract, + feeData: quote.feeData.chainSpecific, + approvalAmount: MAX_ALLOWANCE, + to: fromAssetId(quote.sellAsset.assetId).assetReference, + wallet, + adapter, + web3, + }) } diff --git a/src/lib/swapper/swappers/ThorchainSwapper/utils/txFeeHelpers/evmTxFees/getEvmTxFees.ts b/src/lib/swapper/swappers/ThorchainSwapper/utils/txFeeHelpers/evmTxFees/getEvmTxFees.ts index 48fcd90ca7d..8720d2603df 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/utils/txFeeHelpers/evmTxFees/getEvmTxFees.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/utils/txFeeHelpers/evmTxFees/getEvmTxFees.ts @@ -1,7 +1,6 @@ import type { AssetReference } from '@shapeshiftoss/caip' import type { EvmBaseAdapter } from '@shapeshiftoss/chain-adapters' -import { FeeDataKey } from '@shapeshiftoss/chain-adapters' -import { bn, bnOrZero } from 'lib/bignumber/bignumber' +import { BigNumber, bn, bnOrZero } from 'lib/bignumber/bignumber' import type { QuoteFeeData } from 'lib/swapper/api' import { SwapError, SwapErrorType } from 'lib/swapper/api' import type { ThorEvmSupportedChainId } from 'lib/swapper/swappers/ThorchainSwapper/ThorchainSwapper' @@ -20,35 +19,26 @@ export const getEvmTxFees = async ({ buyAssetTradeFeeUsd, }: GetEvmTxFeesArgs): Promise> => { try { - const gasFeeData = await adapter.getGasFeeData() + const { average, fast } = await adapter.getGasFeeData() + + // use worst case average eip1559 vs fast legacy + const maxGasPrice = bnOrZero(BigNumber.max(average.maxFeePerGas ?? 0, fast.gasPrice)) // this is a good value to cover all thortrades out of EVMs // in the future we may want to look at doing this more precisely and in a future-proof way // TODO: calculate this dynamically - const gasLimit = THOR_EVM_GAS_LIMIT - - const feeDataOptions = { - fast: { - txFee: bn(gasLimit).times(gasFeeData[FeeDataKey.Fast].gasPrice).toString(), - chainSpecific: { - gasPrice: gasFeeData[FeeDataKey.Fast].gasPrice, - gasLimit, - }, - }, - } + const txFee = bn(THOR_EVM_GAS_LIMIT).times(maxGasPrice) - const feeData = feeDataOptions['fast'] + const approvalFee = sellAssetReference && bn(APPROVAL_GAS_LIMIT).times(maxGasPrice).toFixed(0) return { - networkFeeCryptoBaseUnit: feeData.txFee, + networkFeeCryptoBaseUnit: txFee.toFixed(0), chainSpecific: { - estimatedGasCryptoBaseUnit: feeData.chainSpecific.gasLimit, - gasPriceCryptoBaseUnit: feeData.chainSpecific.gasPrice, - approvalFeeCryptoBaseUnit: - sellAssetReference && - bnOrZero(APPROVAL_GAS_LIMIT) - .multipliedBy(bnOrZero(feeData.chainSpecific.gasPrice)) - .toString(), + estimatedGasCryptoBaseUnit: THOR_EVM_GAS_LIMIT, + gasPriceCryptoBaseUnit: fast.gasPrice, // fast gas price since it is underestimated currently + maxFeePerGas: average.maxFeePerGas, + maxPriorityFeePerGas: average.maxPriorityFeePerGas, + approvalFeeCryptoBaseUnit: approvalFee, }, buyAssetTradeFeeUsd, sellAssetTradeFeeUsd: '0', diff --git a/src/lib/swapper/swappers/ZrxSwapper/ZrxSwapper.ts b/src/lib/swapper/swappers/ZrxSwapper/ZrxSwapper.ts index 33a80763bae..c5db960c7e3 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/ZrxSwapper.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/ZrxSwapper.ts @@ -86,12 +86,12 @@ export class ZrxSwapper implements Swapper { } } - buildTrade(args: BuildTradeInput): Promise, SwapErrorRight>> { - return zrxBuildTrade(this.deps, args) + buildTrade(input: BuildTradeInput): Promise, SwapErrorRight>> { + return zrxBuildTrade(this.deps, input) } getTradeQuote(input: GetEvmTradeQuoteInput): Promise, SwapErrorRight>> { - return getZrxTradeQuote(input) + return getZrxTradeQuote(this.deps, input) } getUsdRate(input: Asset): Promise { diff --git a/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.test.ts b/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.test.ts index 448e80fb5c6..1b1c4e35b9b 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.test.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.test.ts @@ -1,15 +1,16 @@ import { btcChainId, ethChainId } from '@shapeshiftoss/caip' import type { ethereum } from '@shapeshiftoss/chain-adapters' +import { isEvmChainId } from '@shapeshiftoss/chain-adapters' import { KnownChainIds } from '@shapeshiftoss/types' import type { AxiosStatic } from 'axios' import type Web3 from 'web3' import { bn, bnOrZero } from 'lib/bignumber/bignumber' import { normalizeAmount } from '../../utils/helpers/helpers' +import { gasFeeData } from '../../utils/test-data/setupDeps' import { setupQuote } from '../../utils/test-data/setupSwapQuote' -import { baseUrlFromChainId } from '../utils/helpers/helpers' import { zrxServiceFactory } from '../utils/zrxService' -import { ZrxSwapper } from '../ZrxSwapper' +import { getZrxTradeQuote } from './getZrxTradeQuote' jest.mock('lib/swapper/swappers/ZrxSwapper/utils/zrxService', () => { const axios: AxiosStatic = jest.createMockFromModule('axios') @@ -22,40 +23,47 @@ jest.mock('lib/swapper/swappers/ZrxSwapper/utils/zrxService', () => { const zrxService = zrxServiceFactory('https://api.0x.org/') -jest.mock('../utils/helpers/helpers') +jest.mock('../utils/helpers/helpers', () => ({ + ...jest.requireActual('../utils/helpers/helpers'), + getUsdRate: jest.fn(), + baseUrlFromChainId: () => 'https://api.0x.org/', +})) jest.mock('../../utils/helpers/helpers') jest.mock('../utils/zrxService') +jest.mock('@shapeshiftoss/chain-adapters') +jest.mocked(isEvmChainId).mockReturnValue(true) describe('getZrxTradeQuote', () => { const sellAmount = '1000000000000000000' ;(normalizeAmount as jest.Mock).mockReturnValue(sellAmount) - ;(baseUrlFromChainId as jest.Mock).mockReturnValue('https://api.0x.org/') const zrxSwapperDeps = { web3: {} as Web3, adapter: { getChainId: () => KnownChainIds.EthereumMainnet, + getGasFeeData: () => Promise.resolve(gasFeeData), } as ethereum.ChainAdapter, } it('returns quote with fee data', async () => { const { quoteInput } = setupQuote() - const swapper = new ZrxSwapper(zrxSwapperDeps) ;(zrxService.get as jest.Mock).mockReturnValue( Promise.resolve({ - data: { price: '100', gasPrice: '1000', estimatedGas: '1000000' }, + data: { price: '100', gasPrice: '1000', gas: '1000000' }, }), ) - const maybeQuote = await swapper.getTradeQuote(quoteInput) + const maybeQuote = await getZrxTradeQuote(zrxSwapperDeps, quoteInput) expect(maybeQuote.isErr()).toBe(false) const quote = maybeQuote.unwrap() expect(quote.feeData).toStrictEqual({ chainSpecific: { - estimatedGasCryptoBaseUnit: '1500000', - gasPriceCryptoBaseUnit: '1000', - approvalFeeCryptoBaseUnit: '100000000', + estimatedGasCryptoBaseUnit: '1000000', + gasPriceCryptoBaseUnit: '79036500000', + approvalFeeCryptoBaseUnit: '21621475811200000', + maxFeePerGas: '216214758112', + maxPriorityFeePerGas: '2982734547', }, buyAssetTradeFeeUsd: '0', - networkFeeCryptoBaseUnit: '1500000000', + networkFeeCryptoBaseUnit: '216214758112000000', sellAssetTradeFeeUsd: '0', }) expect(quote.rate).toBe('100') @@ -63,17 +71,13 @@ describe('getZrxTradeQuote', () => { it('returns an Err with a bad zrx response with no error indicated', async () => { const { quoteInput } = setupQuote() - const swapper = new ZrxSwapper(zrxSwapperDeps) ;(zrxService.get as jest.Mock).mockReturnValue(Promise.resolve({})) - const maybeTradeQuote = await swapper.getTradeQuote({ - ...quoteInput, - }) + const maybeTradeQuote = await getZrxTradeQuote(zrxSwapperDeps, quoteInput) expect(maybeTradeQuote.isErr()).toBe(true) expect(maybeTradeQuote.unwrapErr()).toMatchObject({ cause: undefined, code: 'TRADE_QUOTE_FAILED', - details: undefined, message: '[getZrxTradeQuote] Bad ZRX response, no data was returned', name: 'SwapError', }) @@ -81,14 +85,11 @@ describe('getZrxTradeQuote', () => { it('returns an Err with on errored zrx response', async () => { const { quoteInput } = setupQuote() - const swapper = new ZrxSwapper(zrxSwapperDeps) ;(zrxService.get as jest.Mock).mockRejectedValue({ response: { data: { code: 502, reason: 'Failed to do some stuff' } }, } as never) - const maybeTradeQuote = await swapper.getTradeQuote({ - ...quoteInput, - }) + const maybeTradeQuote = await getZrxTradeQuote(zrxSwapperDeps, quoteInput) expect(maybeTradeQuote.isErr()).toBe(true) expect(maybeTradeQuote.unwrapErr()).toMatchObject({ @@ -100,23 +101,24 @@ describe('getZrxTradeQuote', () => { }) }) - it('returns quote without fee data', async () => { + it('returns quote without gas limit', async () => { const { quoteInput } = setupQuote() - const swapper = new ZrxSwapper(zrxSwapperDeps) ;(zrxService.get as jest.Mock).mockReturnValue( Promise.resolve({ data: { price: '100' }, }), ) - const maybeQuote = await swapper.getTradeQuote(quoteInput) + const maybeQuote = await getZrxTradeQuote(zrxSwapperDeps, quoteInput) expect(maybeQuote.isErr()).toBe(false) const quote = maybeQuote.unwrap() expect(quote?.feeData).toStrictEqual({ chainSpecific: { estimatedGasCryptoBaseUnit: '0', - approvalFeeCryptoBaseUnit: '0', - gasPriceCryptoBaseUnit: undefined, + approvalFeeCryptoBaseUnit: '21621475811200000', + gasPriceCryptoBaseUnit: '79036500000', + maxFeePerGas: '216214758112', + maxPriorityFeePerGas: '2982734547', }, sellAssetTradeFeeUsd: '0', buyAssetTradeFeeUsd: '0', @@ -126,9 +128,8 @@ describe('getZrxTradeQuote', () => { it('returns an Err on non ethereum chain for buyAsset', async () => { const { quoteInput, buyAsset } = setupQuote() - const swapper = new ZrxSwapper(zrxSwapperDeps) ;(zrxService.get as jest.Mock).mockReturnValue(Promise.resolve()) - const maybeTradeQuote = await swapper.getTradeQuote({ + const maybeTradeQuote = await getZrxTradeQuote(zrxSwapperDeps, { ...quoteInput, buyAsset: { ...buyAsset, chainId: btcChainId }, }) @@ -141,43 +142,38 @@ describe('getZrxTradeQuote', () => { buyAssetChainId: btcChainId, sellAssetChainId: ethChainId, }, - message: - '[getZrxTradeQuote] - Both assets need to be on the same supported EVM chain to use Zrx', + message: `[assertValidTradePair] - both assets must be on chainId eip155:1`, name: 'SwapError', }) }) it('returns an Err on non ethereum chain for sellAsset', async () => { const { quoteInput, sellAsset } = setupQuote() - const swapper = new ZrxSwapper(zrxSwapperDeps) ;(zrxService.get as jest.Mock).mockReturnValue(Promise.resolve()) - const maybeTradeQuote = await swapper.getTradeQuote({ + const maybeTradeQuote = await getZrxTradeQuote(zrxSwapperDeps, { ...quoteInput, sellAsset: { ...sellAsset, chainId: btcChainId }, }) expect(maybeTradeQuote.isErr()).toBe(true) expect(maybeTradeQuote.unwrapErr()).toMatchObject({ - cause: undefined, code: 'UNSUPPORTED_PAIR', details: { buyAssetChainId: ethChainId, sellAssetChainId: btcChainId, }, - message: - '[getZrxTradeQuote] - Both assets need to be on the same supported EVM chain to use Zrx', + message: '[assertValidTradePair] - both assets must be on chainId eip155:1', name: 'SwapError', }) }) it('use minQuoteSellAmount when sellAmount is 0', async () => { const { quoteInput, sellAsset } = setupQuote() - const swapper = new ZrxSwapper(zrxSwapperDeps) ;(zrxService.get as jest.Mock).mockReturnValue( Promise.resolve({ data: { sellAmount: '20000000000000000000' } }), ) const minimum = '20' - const maybeQuote = await swapper.getTradeQuote({ + const maybeQuote = await getZrxTradeQuote(zrxSwapperDeps, { ...quoteInput, sellAmountBeforeFeesCryptoBaseUnit: '0', }) diff --git a/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts b/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts index 002649c5106..1eebabcc759 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts @@ -1,15 +1,15 @@ import type { Result } from '@sniptt/monads' import { Err, Ok } from '@sniptt/monads' -import type { AxiosResponse } from 'axios' -import { bn, bnOrZero } from 'lib/bignumber/bignumber' -import type { GetEvmTradeQuoteInput, SwapErrorRight, SwapSource, TradeQuote } from 'lib/swapper/api' +import { BigNumber, bn, bnOrZero } from 'lib/bignumber/bignumber' +import type { GetEvmTradeQuoteInput, SwapErrorRight, TradeQuote } from 'lib/swapper/api' import { makeSwapErrorRight, SwapError, SwapErrorType } from 'lib/swapper/api' import { APPROVAL_GAS_LIMIT } from 'lib/swapper/swappers/utils/constants' import { normalizeAmount } from 'lib/swapper/swappers/utils/helpers/helpers' import { getZrxMinMax } from 'lib/swapper/swappers/ZrxSwapper/getZrxMinMax/getZrxMinMax' -import type { ZrxPriceResponse } from 'lib/swapper/swappers/ZrxSwapper/types' -import { DEFAULT_SOURCE } from 'lib/swapper/swappers/ZrxSwapper/utils/constants' +import type { ZrxPriceResponse, ZrxSwapperDeps } from 'lib/swapper/swappers/ZrxSwapper/types' +import { AFFILIATE_ADDRESS, DEFAULT_SOURCE } from 'lib/swapper/swappers/ZrxSwapper/utils/constants' import { + assertValidTradePair, assetToToken, baseUrlFromChainId, } from 'lib/swapper/swappers/ZrxSwapper/utils/helpers/helpers' @@ -17,27 +17,18 @@ import { zrxServiceFactory } from 'lib/swapper/swappers/ZrxSwapper/utils/zrxServ import type { ZrxSupportedChainId } from 'lib/swapper/swappers/ZrxSwapper/ZrxSwapper' export async function getZrxTradeQuote( + { adapter }: ZrxSwapperDeps, input: GetEvmTradeQuoteInput, ): Promise, SwapErrorRight>> { try { - const { - sellAsset, - buyAsset, - sellAmountBeforeFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, - accountNumber, - } = input - if (buyAsset.chainId !== input.chainId || sellAsset.chainId !== input.chainId) { - throw new SwapError( - '[getZrxTradeQuote] - Both assets need to be on the same supported EVM chain to use Zrx', - { - code: SwapErrorType.UNSUPPORTED_PAIR, - details: { buyAssetChainId: buyAsset.chainId, sellAssetChainId: sellAsset.chainId }, - }, - ) - } + const { sellAsset, buyAsset, accountNumber, receiveAddress } = input + const sellAmount = input.sellAmountBeforeFeesCryptoBaseUnit - const buyToken = assetToToken(buyAsset) - const sellToken = assetToToken(sellAsset) + const assertion = assertValidTradePair({ adapter, buyAsset, sellAsset }) + if (assertion.isErr()) return Err(assertion.unwrapErr()) + + const baseUrl = baseUrlFromChainId(buyAsset.chainId) + const zrxService = zrxServiceFactory(baseUrl) const { minimumAmountCryptoHuman, maximumAmountCryptoHuman } = await getZrxMinMax( sellAsset, @@ -48,87 +39,69 @@ export async function getZrxTradeQuote( ) const normalizedSellAmount = normalizeAmount( - bnOrZero(sellAmountCryptoBaseUnit).eq(0) - ? minQuoteSellAmountCryptoBaseUnit - : sellAmountCryptoBaseUnit, + bnOrZero(sellAmount).eq(0) ? minQuoteSellAmountCryptoBaseUnit : sellAmount, ) - const baseUrl = baseUrlFromChainId(buyAsset.chainId) - const zrxService = zrxServiceFactory(baseUrl) - /** - * /swap/v1/price - * params: { - * sellToken: contract address (or symbol) of token to sell - * buyToken: contractAddress (or symbol) of token to buy - * sellAmount?: integer string value of the smallest increment of the sell token - * buyAmount?: integer string value of the smallest increment of the buy token - * } - */ - const quoteResponse: AxiosResponse = await zrxService.get( - '/swap/v1/price', - { - params: { - sellToken, - buyToken, - sellAmount: normalizedSellAmount, - }, + // https://docs.0x.org/0x-swap-api/api-references/get-swap-v1-price + const { data } = await zrxService.get('/swap/v1/price', { + params: { + buyToken: assetToToken(buyAsset), + sellToken: assetToToken(sellAsset), + sellAmount: normalizedSellAmount, + takerAddress: receiveAddress, + affiliateAddress: AFFILIATE_ADDRESS, + skipValidation: true, }, - ) + }) - if (!quoteResponse.data) + if (!data) { return Err( makeSwapErrorRight({ message: '[getZrxTradeQuote] Bad ZRX response, no data was returned', code: SwapErrorType.TRADE_QUOTE_FAILED, }), ) + } - const { - data: { - estimatedGas: estimatedGasResponse, - gasPrice: gasPriceCryptoBaseUnit, - price, - sellAmount: sellAmountResponse, - buyAmount, - sources, - allowanceTarget, - }, - } = quoteResponse - - const useSellAmount = !!sellAmountCryptoBaseUnit - const rate = useSellAmount ? price : bn(1).div(price).toString() + const { average, fast } = await adapter.getGasFeeData() - const estimatedGas = bnOrZero(estimatedGasResponse).times(1.5) - const fee = estimatedGas.multipliedBy(bnOrZero(gasPriceCryptoBaseUnit)).toString() + // use worst case average eip1559 vs fast legacy + const maxGasPrice = bnOrZero(BigNumber.max(average.maxFeePerGas ?? 0, fast.gasPrice)) // 0x approvals are cheaper than trades, but we don't have dynamic quote data for them. // Instead, we use a hardcoded gasLimit estimate in place of the estimatedGas in the 0x quote response. - const approvalFeeCryptoBaseUnit = bnOrZero(APPROVAL_GAS_LIMIT) - .multipliedBy(bnOrZero(gasPriceCryptoBaseUnit)) - .toFixed() + const approvalFeeCryptoBaseUnit = bn(APPROVAL_GAS_LIMIT).times(maxGasPrice).toFixed(0) + + const useSellAmount = !!sellAmount + const rate = useSellAmount ? data.price : bn(1).div(data.price).toString() + const gasLimit = bnOrZero(data.gas) + const txFee = gasLimit.times(maxGasPrice) const tradeQuote: TradeQuote = { + buyAsset, + sellAsset, + accountNumber, rate, minimumCryptoHuman: minimumAmountCryptoHuman, maximumCryptoHuman: maximumAmountCryptoHuman, feeData: { chainSpecific: { - estimatedGasCryptoBaseUnit: estimatedGas.toString(), - gasPriceCryptoBaseUnit, + estimatedGasCryptoBaseUnit: gasLimit.toFixed(0), + gasPriceCryptoBaseUnit: fast.gasPrice, // fast gas price since it is underestimated currently + maxFeePerGas: average.maxFeePerGas, + maxPriorityFeePerGas: average.maxPriorityFeePerGas, approvalFeeCryptoBaseUnit, }, - networkFeeCryptoBaseUnit: fee, + networkFeeCryptoBaseUnit: txFee.toFixed(0), buyAssetTradeFeeUsd: '0', sellAssetTradeFeeUsd: '0', }, - sellAmountBeforeFeesCryptoBaseUnit: sellAmountResponse, - buyAmountCryptoBaseUnit: buyAmount, - sources: sources?.filter((s: SwapSource) => parseFloat(s.proportion) > 0) || DEFAULT_SOURCE, - allowanceContract: allowanceTarget, - buyAsset, - sellAsset, - accountNumber, + allowanceContract: data.allowanceTarget, + buyAmountCryptoBaseUnit: data.buyAmount, + sellAmountBeforeFeesCryptoBaseUnit: data.sellAmount, + sources: data.sources?.filter(s => parseFloat(s.proportion) > 0) || DEFAULT_SOURCE, } + return Ok(tradeQuote as TradeQuote) } catch (e) { // TODO(gomes): scrutinize what can throw above and don't throw, because monads diff --git a/src/lib/swapper/swappers/ZrxSwapper/types.ts b/src/lib/swapper/swappers/ZrxSwapper/types.ts index 88a2a38cedd..a11ec3deaca 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/types.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/types.ts @@ -8,6 +8,8 @@ import type { export type ZrxCommonResponse = { price: string + estimatedGas: string + gas: string gasPrice: string buyAmount: string sellAmount: string @@ -15,14 +17,12 @@ export type ZrxCommonResponse = { sources: SwapSource[] } -export type ZrxPriceResponse = ZrxCommonResponse & { - estimatedGas: string -} +export type ZrxPriceResponse = ZrxCommonResponse export type ZrxQuoteResponse = ZrxCommonResponse & { to: string data: string - gas: string + value: string } export interface ZrxTrade extends Trade { diff --git a/src/lib/swapper/swappers/ZrxSwapper/utils/helpers/helpers.ts b/src/lib/swapper/swappers/ZrxSwapper/utils/helpers/helpers.ts index d22b2630fbc..eb63adf5723 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/utils/helpers/helpers.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/utils/helpers/helpers.ts @@ -9,12 +9,17 @@ import { polygonAssetId, } from '@shapeshiftoss/caip' import { KnownChainIds } from '@shapeshiftoss/types' +import type { Result } from '@sniptt/monads' +import { Err, Ok } from '@sniptt/monads' import type { AxiosResponse } from 'axios' import { bn, bnOrZero } from 'lib/bignumber/bignumber' -import { SwapError, SwapErrorType } from 'lib/swapper/api' +import type { SwapErrorRight } from 'lib/swapper/api' +import { makeSwapErrorRight, SwapError, SwapErrorType } from 'lib/swapper/api' import type { ZrxPriceResponse } from 'lib/swapper/swappers/ZrxSwapper/types' import { zrxServiceFactory } from 'lib/swapper/swappers/ZrxSwapper/utils/zrxService' +import type { ZrxSupportedChainAdapter } from '../../ZrxSwapper' + export const baseUrlFromChainId = (chainId: string): string => { switch (chainId) { case KnownChainIds.EthereumMainnet: @@ -112,3 +117,28 @@ export const getUsdRate = async (sellAsset: Asset): Promise => { }) } } + +export const assertValidTradePair = ({ + buyAsset, + sellAsset, + adapter, +}: { + buyAsset: Asset + sellAsset: Asset + adapter: ZrxSupportedChainAdapter +}): Result => { + const chainId = adapter.getChainId() + + if (buyAsset.chainId === chainId && sellAsset.chainId === chainId) return Ok(true) + + return Err( + makeSwapErrorRight({ + message: `[assertValidTradePair] - both assets must be on chainId ${chainId}`, + code: SwapErrorType.UNSUPPORTED_PAIR, + details: { + buyAssetChainId: buyAsset.chainId, + sellAssetChainId: sellAsset.chainId, + }, + }), + ) +} diff --git a/src/lib/swapper/swappers/ZrxSwapper/utils/test-data/setupZrxSwapQuote.ts b/src/lib/swapper/swappers/ZrxSwapper/utils/test-data/setupZrxSwapQuote.ts index 9b4ff1c1eef..ced9bf213fe 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/utils/test-data/setupZrxSwapQuote.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/utils/test-data/setupZrxSwapQuote.ts @@ -12,7 +12,9 @@ export const setupZrxTradeQuoteResponse = () => { to: '0x123', data: '0x1234', gas: '1235', + estimatedGas: '1235', gasPrice: '1236', + value: '0', sources: [], buyAmount: '', } diff --git a/src/lib/swapper/swappers/ZrxSwapper/zrxApprovalNeeded/zrxApprovalNeeded.test.ts b/src/lib/swapper/swappers/ZrxSwapper/zrxApprovalNeeded/zrxApprovalNeeded.test.ts index 9b7fdb3ce42..fb1310548ab 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/zrxApprovalNeeded/zrxApprovalNeeded.test.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/zrxApprovalNeeded/zrxApprovalNeeded.test.ts @@ -59,7 +59,7 @@ describe('zrxApprovalNeeded', () => { wallet, } - await expect(zrxApprovalNeeded(deps, input)).rejects.toThrow('[zrxApprovalNeeded]') + await expect(zrxApprovalNeeded(deps, input)).rejects.toThrow() }) it('returns false if allowanceOnChain is greater than quote.sellAmount', async () => { diff --git a/src/lib/swapper/swappers/ZrxSwapper/zrxApprovalNeeded/zrxApprovalNeeded.ts b/src/lib/swapper/swappers/ZrxSwapper/zrxApprovalNeeded/zrxApprovalNeeded.ts index af7af68edf8..5d265be421c 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/zrxApprovalNeeded/zrxApprovalNeeded.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/zrxApprovalNeeded/zrxApprovalNeeded.ts @@ -7,19 +7,22 @@ import { getERC20Allowance } from 'lib/swapper/swappers/utils/helpers/helpers' import type { ZrxSwapperDeps } from 'lib/swapper/swappers/ZrxSwapper/types' import type { ZrxSupportedChainId } from 'lib/swapper/swappers/ZrxSwapper/ZrxSwapper' +import { assertValidTradePair } from '../utils/helpers/helpers' + export async function zrxApprovalNeeded( { adapter, web3 }: ZrxSwapperDeps, { quote, wallet }: ApprovalNeededInput, ): Promise { - const { sellAsset } = quote - - const { assetReference: sellAssetErc20Address } = fromAssetId(sellAsset.assetId) - try { - if (sellAsset.chainId !== adapter.getChainId()) { - throw new SwapError('[zrxApprovalNeeded] - sellAsset chainId is not supported', { - code: SwapErrorType.UNSUPPORTED_CHAIN, - details: { chainId: sellAsset.chainId }, + const { accountNumber, allowanceContract, buyAsset, sellAsset } = quote + const sellAmount = quote.sellAmountBeforeFeesCryptoBaseUnit + + const assertion = assertValidTradePair({ adapter, buyAsset, sellAsset }) + if (assertion.isErr()) { + const { message, code, details } = assertion.unwrapErr() + throw new SwapError(message, { + code, + details: details as Record | undefined, }) } @@ -28,33 +31,25 @@ export async function zrxApprovalNeeded( return { approvalNeeded: false } } - const { accountNumber } = quote - - const receiveAddress = await adapter.getAddress({ accountNumber, wallet }) - - if (!quote.allowanceContract) { - throw new SwapError('[zrxApprovalNeeded] - allowanceTarget is required', { + if (!allowanceContract) { + throw new SwapError('[zrxApprovalNeeded] - quote contains no allowanceContract', { code: SwapErrorType.VALIDATION_FAILED, - details: { chainId: sellAsset.chainId }, + details: { quote }, }) } - const allowanceResult = await getERC20Allowance({ + const receiveAddress = await adapter.getAddress({ accountNumber, wallet }) + + const allowance = await getERC20Allowance({ web3, erc20AllowanceAbi, - sellAssetErc20Address, - spenderAddress: quote.allowanceContract, + sellAssetErc20Address: fromAssetId(sellAsset.assetId).assetReference, + spenderAddress: allowanceContract, ownerAddress: receiveAddress, }) - const allowanceOnChain = bnOrZero(allowanceResult) - if (!quote.feeData.chainSpecific?.gasPriceCryptoBaseUnit) - throw new SwapError('[zrxApprovalNeeded] - no gas price with quote', { - code: SwapErrorType.RESPONSE_ERROR, - details: { feeData: quote.feeData }, - }) return { - approvalNeeded: allowanceOnChain.lt(bnOrZero(quote.sellAmountBeforeFeesCryptoBaseUnit)), + approvalNeeded: bnOrZero(allowance).lt(bnOrZero(sellAmount)), } } catch (e) { if (e instanceof SwapError) throw e diff --git a/src/lib/swapper/swappers/ZrxSwapper/zrxApprove/zrxApprove.ts b/src/lib/swapper/swappers/ZrxSwapper/zrxApprove/zrxApprove.ts index 095de5d9fc7..63902ea9968 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/zrxApprove/zrxApprove.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/zrxApprove/zrxApprove.ts @@ -1,67 +1,30 @@ -import type { ApproveAmountInput, ApproveInfiniteInput, TradeQuote } from 'lib/swapper/api' -import { SwapError, SwapErrorType } from 'lib/swapper/api' -import { erc20Abi } from 'lib/swapper/swappers/utils/abi/erc20-abi' -import { APPROVAL_GAS_LIMIT } from 'lib/swapper/swappers/utils/constants' +import { fromAssetId } from '@shapeshiftoss/caip' +import type { ApproveAmountInput, ApproveInfiniteInput } from 'lib/swapper/api' import { grantAllowance } from 'lib/swapper/swappers/utils/helpers/helpers' import type { ZrxSwapperDeps } from 'lib/swapper/swappers/ZrxSwapper/types' import { MAX_ALLOWANCE } from 'lib/swapper/swappers/ZrxSwapper/utils/constants' import type { ZrxSupportedChainId } from 'lib/swapper/swappers/ZrxSwapper/ZrxSwapper' -const grantAllowanceForAmount = ( - { adapter, web3 }: ZrxSwapperDeps, - { quote, wallet }: ApproveInfiniteInput, - approvalAmount: string, -) => { - const approvalQuote: TradeQuote = { - ...quote, - sellAmountBeforeFeesCryptoBaseUnit: approvalAmount, - feeData: { - ...quote.feeData, - chainSpecific: { - ...quote.feeData.chainSpecific, - // 0x approvals are cheaper than trades, but we don't have dynamic quote data for them. - // Instead, we use a hardcoded gasLimit estimate in place of the estimatedGas in the 0x quote response. - estimatedGas: APPROVAL_GAS_LIMIT, - }, - }, - } - return grantAllowance({ - quote: approvalQuote, - wallet, - adapter, - erc20Abi, - web3, - }) -} - export function zrxApproveAmount( deps: ZrxSwapperDeps, - args: ApproveAmountInput, -) { - try { - // If no amount is specified we use the quotes sell amount - const approvalAmount = args.amount ?? args.quote.sellAmountBeforeFeesCryptoBaseUnit - return grantAllowanceForAmount(deps, args, approvalAmount) - } catch (e) { - if (e instanceof SwapError) throw e - throw new SwapError('[zrxApproveAmount]', { - cause: e, - code: SwapErrorType.APPROVE_AMOUNT_FAILED, - }) - } + { quote, wallet, amount }: ApproveAmountInput, +): Promise { + const { accountNumber, allowanceContract, feeData, sellAsset } = quote + + return grantAllowance({ + ...deps, + accountNumber, + spender: allowanceContract, + feeData: feeData.chainSpecific, + approvalAmount: amount ?? quote.sellAmountBeforeFeesCryptoBaseUnit, + to: fromAssetId(sellAsset.assetId).assetReference, + wallet, + }) } export function zrxApproveInfinite( deps: ZrxSwapperDeps, - args: ApproveInfiniteInput, -) { - try { - return grantAllowanceForAmount(deps, args, MAX_ALLOWANCE) - } catch (e) { - if (e instanceof SwapError) throw e - throw new SwapError('[zrxApproveInfinite]', { - cause: e, - code: SwapErrorType.APPROVE_INFINITE_FAILED, - }) - } + input: ApproveInfiniteInput, +): Promise { + return zrxApproveAmount(deps, { ...input, amount: MAX_ALLOWANCE }) } diff --git a/src/lib/swapper/swappers/ZrxSwapper/zrxBuildTrade/zrxBuildTrade.test.ts b/src/lib/swapper/swappers/ZrxSwapper/zrxBuildTrade/zrxBuildTrade.test.ts index 485578edd75..bd6e733abb4 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/zrxBuildTrade/zrxBuildTrade.test.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/zrxBuildTrade/zrxBuildTrade.test.ts @@ -4,10 +4,9 @@ import { KnownChainIds } from '@shapeshiftoss/types' import * as unchained from '@shapeshiftoss/unchained-client' import type { AxiosStatic } from 'axios' import Web3 from 'web3' -import { bnOrZero } from 'lib/bignumber/bignumber' import type { BuildTradeInput, QuoteFeeData } from '../../../api' -import { APPROVAL_GAS_LIMIT } from '../../utils/constants' +import { feeData } from '../../utils/test-data/setupDeps' import type { ZrxTrade } from '../types' import { setupZrxTradeQuoteResponse } from '../utils/test-data/setupZrxSwapQuote' import { zrxServiceFactory } from '../utils/zrxService' @@ -53,6 +52,7 @@ const setup = () => { }, rpcUrl: ethNodeUrl, }) + adapter.getFeeData = () => Promise.resolve(feeData) const zrxService = zrxServiceFactory('https://api.0x.org/') return { web3Instance, adapter, zrxService } @@ -93,13 +93,12 @@ describe('zrxBuildTrade', () => { rate: quoteResponse.price, feeData: { chainSpecific: { - approvalFeeCryptoBaseUnit: '123600000', estimatedGasCryptoBaseUnit: '1235', - gasPriceCryptoBaseUnit: '1236', + gasPriceCryptoBaseUnit: '79036500000', + maxFeePerGas: '216214758112', + maxPriorityFeePerGas: '2982734547', }, - networkFeeCryptoBaseUnit: ( - Number(quoteResponse.gas) * Number(quoteResponse.gasPrice) - ).toString(), + networkFeeCryptoBaseUnit: '21621475811200000', sellAssetTradeFeeUsd: '0', buyAssetTradeFeeUsd: '0', }, @@ -154,29 +153,24 @@ describe('zrxBuildTrade', () => { }) it('should return a quote response with gasPrice multiplied by estimatedGas', async () => { - const gasPriceCryptoBaseUnit = '10000' - const estimatedGas = '100' const data = { ...quoteResponse, allowanceTarget: 'allowanceTargetAddress', - gas: estimatedGas, - gasPrice: gasPriceCryptoBaseUnit, + gas: '100', + gasPrice: '10000', } ;(zrxService.get as jest.Mock).mockReturnValue(Promise.resolve({ data })) const expectedFeeData: QuoteFeeData = { chainSpecific: { - approvalFeeCryptoBaseUnit: bnOrZero(APPROVAL_GAS_LIMIT) - .multipliedBy(gasPriceCryptoBaseUnit) - .toString(), - gasPriceCryptoBaseUnit, - estimatedGasCryptoBaseUnit: estimatedGas, + gasPriceCryptoBaseUnit: '79036500000', + estimatedGasCryptoBaseUnit: '100', + maxFeePerGas: '216214758112', + maxPriorityFeePerGas: '2982734547', }, buyAssetTradeFeeUsd: '0', sellAssetTradeFeeUsd: '0', - networkFeeCryptoBaseUnit: bnOrZero(gasPriceCryptoBaseUnit) - .multipliedBy(estimatedGas) - .toString(), + networkFeeCryptoBaseUnit: '21621475811200000', } const maybeBuiltTrade = await zrxBuildTrade(deps, { ...buildTradeInput, wallet }) diff --git a/src/lib/swapper/swappers/ZrxSwapper/zrxBuildTrade/zrxBuildTrade.ts b/src/lib/swapper/swappers/ZrxSwapper/zrxBuildTrade/zrxBuildTrade.ts index f9b4638003a..169476bbf5a 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/zrxBuildTrade/zrxBuildTrade.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/zrxBuildTrade/zrxBuildTrade.ts @@ -2,12 +2,11 @@ import type { Result } from '@sniptt/monads' import { Err, Ok } from '@sniptt/monads' import type { AxiosResponse } from 'axios' import * as rax from 'retry-axios' -import { bnOrZero } from 'lib/bignumber/bignumber' +import { BigNumber, bn, bnOrZero } from 'lib/bignumber/bignumber' import type { BuildTradeInput, SwapErrorRight } from 'lib/swapper/api' import { makeSwapErrorRight, SwapError, SwapErrorType } from 'lib/swapper/api' -import { erc20AllowanceAbi } from 'lib/swapper/swappers/utils/abi/erc20Allowance-abi' -import { APPROVAL_GAS_LIMIT, DEFAULT_SLIPPAGE } from 'lib/swapper/swappers/utils/constants' -import { isApprovalRequired, normalizeAmount } from 'lib/swapper/swappers/utils/helpers/helpers' +import { DEFAULT_SLIPPAGE } from 'lib/swapper/swappers/utils/constants' +import { normalizeAmount } from 'lib/swapper/swappers/utils/helpers/helpers' import type { ZrxQuoteResponse, ZrxSwapperDeps, @@ -16,6 +15,7 @@ import type { import { applyAxiosRetry } from 'lib/swapper/swappers/ZrxSwapper/utils/applyAxiosRetry' import { AFFILIATE_ADDRESS, DEFAULT_SOURCE } from 'lib/swapper/swappers/ZrxSwapper/utils/constants' import { + assertValidTradePair, assetToToken, baseUrlFromChainId, } from 'lib/swapper/swappers/ZrxSwapper/utils/helpers/helpers' @@ -23,45 +23,18 @@ import { zrxServiceFactory } from 'lib/swapper/swappers/ZrxSwapper/utils/zrxServ import type { ZrxSupportedChainId } from 'lib/swapper/swappers/ZrxSwapper/ZrxSwapper' export async function zrxBuildTrade( - { adapter, web3 }: ZrxSwapperDeps, + { adapter }: ZrxSwapperDeps, input: BuildTradeInput, ): Promise, SwapErrorRight>> { - const { - sellAsset, - buyAsset, - sellAmountBeforeFeesCryptoBaseUnit: sellAmountExcludeFeeCryptoBaseUnit, - slippage, - accountNumber, - receiveAddress, - } = input try { - const adapterChainId = adapter.getChainId() + const { sellAsset, buyAsset, slippage, accountNumber, receiveAddress } = input + const sellAmount = input.sellAmountBeforeFeesCryptoBaseUnit - if (buyAsset.chainId !== adapterChainId) { - return Err( - makeSwapErrorRight({ - message: `[zrxBuildTrade] - buyAsset must be on chainId ${adapterChainId}`, - code: SwapErrorType.VALIDATION_FAILED, - details: { chainId: sellAsset.chainId }, - }), - ) - } - - const slippagePercentage = slippage ? bnOrZero(slippage).toString() : DEFAULT_SLIPPAGE + const assertion = assertValidTradePair({ adapter, buyAsset, sellAsset }) + if (assertion.isErr()) return Err(assertion.unwrapErr()) const baseUrl = baseUrlFromChainId(buyAsset.chainId) - const zrxService = zrxServiceFactory(baseUrl) - - /** - * /swap/v1/quote - * params: { - * sellToken: contract address (or symbol) of token to sell - * buyToken: contractAddress (or symbol) of token to buy - * sellAmount?: integer string value of the smallest increment of the sell token - * } - */ - - const zrxRetry = applyAxiosRetry(zrxService, { + const zrxService = applyAxiosRetry(zrxServiceFactory(baseUrl), { statusCodesToRetry: [[400, 400]], shouldRetry: err => { const cfg = rax.getConfig(err) @@ -76,76 +49,65 @@ export async function zrxBuildTrade( return rax.shouldRetryRequest(err) }, }) - const quoteResponse: AxiosResponse = await zrxRetry.get( + + // https://docs.0x.org/0x-swap-api/api-references/get-swap-v1-quote + const { data: quote }: AxiosResponse = await zrxService.get( '/swap/v1/quote', { params: { buyToken: assetToToken(buyAsset), sellToken: assetToToken(sellAsset), - sellAmount: normalizeAmount(sellAmountExcludeFeeCryptoBaseUnit), + sellAmount: normalizeAmount(sellAmount), takerAddress: receiveAddress, - slippagePercentage, - skipValidation: false, + slippagePercentage: slippage ? bnOrZero(slippage).toString() : DEFAULT_SLIPPAGE, affiliateAddress: AFFILIATE_ADDRESS, + skipValidation: false, }, }, ) - const { - data: { - allowanceTarget, - sellAmount, - gasPrice: gasPriceCryptoBaseUnit, - gas: gasCryptoBaseUnit, - price, - to, - buyAmount: buyAmountCryptoBaseUnit, - data: txData, - sources, + const { average, fast } = await adapter.getFeeData({ + to: quote.to, + value: quote.value, + chainSpecific: { + from: receiveAddress, + contractAddress: quote.to, + contractData: quote.data, }, - } = quoteResponse - - const estimatedGas = bnOrZero(gasCryptoBaseUnit || 0) - const networkFee = bnOrZero(estimatedGas) - .multipliedBy(bnOrZero(gasPriceCryptoBaseUnit)) - .toString() - - const approvalRequired = await isApprovalRequired({ - adapter, - sellAsset, - allowanceContract: allowanceTarget, - receiveAddress, - sellAmountExcludeFeeCryptoBaseUnit, - web3, - erc20AllowanceAbi, }) - const approvalFee = bnOrZero(APPROVAL_GAS_LIMIT) - .multipliedBy(bnOrZero(gasPriceCryptoBaseUnit)) - .toString() + // use worst case average eip1559 vs fast legacy + const maxGasPrice = BigNumber.max( + average.chainSpecific.maxFeePerGas ?? 0, + fast.chainSpecific.gasPrice, + ) + + const txFee = bnOrZero(bn(fast.chainSpecific.gasLimit).times(maxGasPrice)) const trade: ZrxTrade = { sellAsset, buyAsset, accountNumber, receiveAddress, - rate: price, - depositAddress: to, + rate: quote.price, + depositAddress: quote.to, feeData: { chainSpecific: { - estimatedGasCryptoBaseUnit: estimatedGas.toString(), - gasPriceCryptoBaseUnit, - approvalFeeCryptoBaseUnit: approvalRequired ? approvalFee : undefined, + estimatedGasCryptoBaseUnit: quote.gas, + gasPriceCryptoBaseUnit: fast.chainSpecific.gasPrice, // fast gas price since it is underestimated currently + maxFeePerGas: average.chainSpecific.maxFeePerGas, + maxPriorityFeePerGas: average.chainSpecific.maxPriorityFeePerGas, }, - networkFeeCryptoBaseUnit: networkFee, + networkFeeCryptoBaseUnit: txFee.toFixed(0), buyAssetTradeFeeUsd: '0', sellAssetTradeFeeUsd: '0', }, - txData, - sellAmountBeforeFeesCryptoBaseUnit: sellAmount, - buyAmountCryptoBaseUnit, - sources: sources?.filter(s => parseFloat(s.proportion) > 0) || DEFAULT_SOURCE, + txData: quote.data, + buyAmountCryptoBaseUnit: quote.buyAmount, + sellAmountBeforeFeesCryptoBaseUnit: quote.sellAmount, + sources: quote.sources?.filter(s => parseFloat(s.proportion) > 0) || DEFAULT_SOURCE, } + return Ok(trade as ZrxTrade) } catch (e) { if (e instanceof SwapError) @@ -158,7 +120,7 @@ export async function zrxBuildTrade( ) return Err( makeSwapErrorRight({ - message: '[[zrxBuildTrade]]', + message: '[zrxBuildTrade]', cause: e, code: SwapErrorType.BUILD_TRADE_FAILED, }), diff --git a/src/lib/swapper/swappers/ZrxSwapper/zrxExecuteTrade/zrxExecuteTrade.test.ts b/src/lib/swapper/swappers/ZrxSwapper/zrxExecuteTrade/zrxExecuteTrade.test.ts index de786cb5859..109a1c22180 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/zrxExecuteTrade/zrxExecuteTrade.test.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/zrxExecuteTrade/zrxExecuteTrade.test.ts @@ -2,6 +2,7 @@ import type { ChainAdapter } from '@shapeshiftoss/chain-adapters' import type { HDWallet } from '@shapeshiftoss/hdwallet-core' import type { KnownChainIds } from '@shapeshiftoss/types' +import { gasFeeData } from '../../utils/test-data/setupDeps' import { setupQuote } from '../../utils/test-data/setupSwapQuote' import type { ZrxExecuteTradeInput, ZrxSwapperDeps, ZrxTrade } from '../types' import { zrxExecuteTrade } from './zrxExecuteTrade' @@ -10,14 +11,17 @@ describe('ZrxExecuteTrade', () => { const { sellAsset, buyAsset } = setupQuote() const txid = '0xffaac3dd529171e8a9a2adaf36b0344877c4894720d65dfd86e4b3a56c5a857e' let wallet = { + _supportsETH: true, supportsOfflineSigning: jest.fn(() => true), + ethSupportsEIP1559: jest.fn(() => false), } as unknown as HDWallet const adapter = { - buildSendTransaction: jest.fn(() => Promise.resolve({ txToSign: '0000000000000000' })), + buildCustomTx: jest.fn(() => Promise.resolve({ txToSign: '0000000000000000' })), signTransaction: jest.fn(() => Promise.resolve('0000000000000000000')), broadcastTransaction: jest.fn(() => Promise.resolve(txid)), signAndBroadcastTransaction: jest.fn(() => Promise.resolve(txid)), + getGasFeeData: jest.fn(() => Promise.resolve(gasFeeData)), } as unknown as ChainAdapter<'eip155:1'> const deps = { adapter } as unknown as ZrxSwapperDeps diff --git a/src/lib/swapper/swappers/ZrxSwapper/zrxExecuteTrade/zrxExecuteTrade.ts b/src/lib/swapper/swappers/ZrxSwapper/zrxExecuteTrade/zrxExecuteTrade.ts index da855891373..83fb949f13e 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/zrxExecuteTrade/zrxExecuteTrade.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/zrxExecuteTrade/zrxExecuteTrade.ts @@ -1,9 +1,8 @@ import type { Result } from '@sniptt/monads' import { Err, Ok } from '@sniptt/monads' -import { numberToHex } from 'web3-utils' import type { SwapErrorRight, TradeResult } from 'lib/swapper/api' import { makeSwapErrorRight, SwapError, SwapErrorType } from 'lib/swapper/api' -import { isNativeEvmAsset } from 'lib/swapper/swappers/utils/helpers/helpers' +import { buildAndBroadcast, isNativeEvmAsset } from 'lib/swapper/swappers/utils/helpers/helpers' import type { ZrxExecuteTradeInput, ZrxSwapperDeps } from 'lib/swapper/swappers/ZrxSwapper/types' import type { ZrxSupportedChainId } from 'lib/swapper/swappers/ZrxSwapper/ZrxSwapper' @@ -11,56 +10,24 @@ export async function zrxExecuteTrade( { adapter }: ZrxSwapperDeps, { trade, wallet }: ZrxExecuteTradeInput, ): Promise> { - const { accountNumber, sellAsset } = trade + const { accountNumber, depositAddress, feeData, sellAsset, txData } = trade + const { sellAmountBeforeFeesCryptoBaseUnit } = trade try { - // value is 0 for erc20s - const value = isNativeEvmAsset(sellAsset.assetId) - ? trade.sellAmountBeforeFeesCryptoBaseUnit - : '0' - - const buildTxResponse = await adapter.buildSendTransaction({ - value, - wallet, - to: trade.depositAddress, - chainSpecific: { - gasPrice: numberToHex(trade.feeData?.chainSpecific?.gasPriceCryptoBaseUnit || 0), - gasLimit: numberToHex(trade.feeData?.chainSpecific?.estimatedGasCryptoBaseUnit || 0), - }, + const txid = await buildAndBroadcast({ accountNumber, + adapter, + feeData: feeData.chainSpecific, + to: depositAddress, + value: isNativeEvmAsset(sellAsset.assetId) ? sellAmountBeforeFeesCryptoBaseUnit : '0', + wallet, + data: txData, }) - const { txToSign } = buildTxResponse - - const txWithQuoteData = { ...txToSign, data: trade.txData ?? '' } - - if (wallet.supportsOfflineSigning()) { - const signedTx = await adapter.signTransaction({ txToSign: txWithQuoteData, wallet }) - - const txid = await adapter.broadcastTransaction(signedTx) - - return Ok({ tradeId: txid }) - } else if (wallet.supportsBroadcast() && adapter.signAndBroadcastTransaction) { - const txid = await adapter.signAndBroadcastTransaction?.({ - txToSign: txWithQuoteData, - wallet, - }) - - return Ok({ tradeId: txid }) - } else { - throw new SwapError('[zrxExecuteTrade]', { - code: SwapErrorType.SIGN_AND_BROADCAST_FAILED, - }) - } + return Ok({ tradeId: txid }) } catch (e) { if (e instanceof SwapError) - return Err( - makeSwapErrorRight({ - message: e.message, - code: e.code, - details: e.details, - }), - ) + return Err(makeSwapErrorRight({ message: e.message, code: e.code, details: e.details })) return Err( makeSwapErrorRight({ message: '[zrxExecuteTrade]', diff --git a/src/lib/swapper/swappers/utils/helpers/helpers.test.ts b/src/lib/swapper/swappers/utils/helpers/helpers.test.ts index e92d02d4bea..3313c674885 100644 --- a/src/lib/swapper/swappers/utils/helpers/helpers.test.ts +++ b/src/lib/swapper/swappers/utils/helpers/helpers.test.ts @@ -1,8 +1,8 @@ +import { fromAssetId } from '@shapeshiftoss/caip' import type { HDWallet } from '@shapeshiftoss/hdwallet-core' import Web3 from 'web3' import { bn } from '../../../../bignumber/bignumber' -import { erc20Abi } from '../abi/erc20-abi' import { erc20AllowanceAbi } from '../abi/erc20Allowance-abi' import { setupDeps } from '../test-data/setupDeps' import { setupQuote } from '../test-data/setupSwapQuote' @@ -97,14 +97,14 @@ describe('utils', () => { describe('grantAllowance', () => { const walletAddress = '0xc770eefad204b5180df6a14ee197d99d808ee52d' const wallet = { + _supportsETH: true, + ethSupportsEIP1559: jest.fn(() => false), supportsOfflineSigning: jest.fn(() => true), ethGetAddress: jest.fn(() => Promise.resolve(walletAddress)), } as unknown as HDWallet it('should return a txid', async () => { - const quote = { - ...tradeQuote, - } + const { accountNumber, allowanceContract: spender, feeData, sellAsset } = tradeQuote ;(web3.eth.Contract as jest.Mock).mockImplementation(() => ({ methods: { approve: jest.fn(() => ({ @@ -114,11 +114,21 @@ describe('utils', () => { })), }, })) - ;(adapter.buildSendTransaction as jest.Mock).mockResolvedValueOnce({ txToSign: {} }) + ;(adapter.buildCustomTx as jest.Mock).mockResolvedValueOnce({ txToSign: {} }) + ;(adapter.signTransaction as jest.Mock).mockResolvedValueOnce('signedTx') ;(adapter.broadcastTransaction as jest.Mock).mockResolvedValueOnce('broadcastedTx') - expect(await grantAllowance({ quote, wallet, adapter, erc20Abi, web3 })).toEqual( - 'broadcastedTx', - ) + expect( + await grantAllowance({ + accountNumber, + feeData: feeData.chainSpecific, + spender, + to: fromAssetId(sellAsset.assetId).assetReference, + approvalAmount: tradeQuote.sellAmountBeforeFeesCryptoBaseUnit, + wallet, + adapter, + web3, + }), + ).toEqual('broadcastedTx') }) }) diff --git a/src/lib/swapper/swappers/utils/helpers/helpers.ts b/src/lib/swapper/swappers/utils/helpers/helpers.ts index d4790847a9e..24c5a3b125f 100644 --- a/src/lib/swapper/swappers/utils/helpers/helpers.ts +++ b/src/lib/swapper/swappers/utils/helpers/helpers.ts @@ -8,18 +8,20 @@ import { optimismAssetId, polygonAssetId, } from '@shapeshiftoss/caip' -import type { EvmChainAdapter, EvmChainId } from '@shapeshiftoss/chain-adapters' +import type { evm, EvmChainAdapter } from '@shapeshiftoss/chain-adapters' import type { HDWallet } from '@shapeshiftoss/hdwallet-core' +import { supportsETH } from '@shapeshiftoss/hdwallet-core' import { KnownChainIds } from '@shapeshiftoss/types' import type Web3 from 'web3' import type { AbiItem } from 'web3-utils' -import { numberToHex } from 'web3-utils' import type { BigNumber } from 'lib/bignumber/bignumber' import { bn, bnOrZero } from 'lib/bignumber/bignumber' -import type { TradeQuote } from 'lib/swapper/api' +import type { EvmFeeData } from 'lib/swapper/api' import { SwapError, SwapErrorType } from 'lib/swapper/api' import { MAX_ALLOWANCE } from 'lib/swapper/swappers/CowSwapper/utils/constants' -import { erc20Abi as erc20AbiImported } from 'lib/swapper/swappers/utils/abi/erc20-abi' +import { erc20Abi } from 'lib/swapper/swappers/utils/abi/erc20-abi' + +import { APPROVAL_GAS_LIMIT } from '../constants' export type IsApprovalRequiredArgs = { adapter: EvmChainAdapter @@ -45,11 +47,22 @@ export type GetApproveContractDataArgs = { contractAddress: string } -type GrantAllowanceArgs = { - quote: TradeQuote +type GetFeesFromFeeDataArgs = { wallet: HDWallet + feeData: EvmFeeData +} + +type BuildAndBroadcastArgs = GetFeesFromFeeDataArgs & { + accountNumber: number adapter: EvmChainAdapter - erc20Abi: AbiItem[] + data: string + to: string + value: string +} + +type GrantAllowanceArgs = Omit & { + approvalAmount: string + spender: string web3: Web3 } @@ -110,57 +123,109 @@ export const isApprovalRequired = async ({ } } -export const grantAllowance = async ({ - quote, +export const getFeesFromFeeData = async ({ wallet, - adapter, - erc20Abi, - web3, -}: GrantAllowanceArgs): Promise => { - try { - const { assetReference: sellAssetErc20Address } = fromAssetId(quote.sellAsset.assetId) + feeData, +}: GetFeesFromFeeDataArgs): Promise => { + if (!supportsETH(wallet)) { + throw new SwapError('[getFeesFromFeeData]', { + cause: 'eth wallet required', + code: SwapErrorType.SIGN_AND_BROADCAST_FAILED, + details: { wallet }, + }) + } - const erc20Contract = new web3.eth.Contract(erc20Abi, sellAssetErc20Address) - const approveTx = erc20Contract.methods - .approve(quote.allowanceContract, quote.sellAmountBeforeFeesCryptoBaseUnit) - .encodeABI() + const gasLimit = feeData.estimatedGasCryptoBaseUnit + const gasPrice = feeData.gasPriceCryptoBaseUnit + const maxFeePerGas = feeData.maxFeePerGas + const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas - const { accountNumber } = quote + if (!gasLimit) { + throw new SwapError('[getFeesFromFeeData]', { + cause: 'gasLimit is required', + code: SwapErrorType.SIGN_AND_BROADCAST_FAILED, + }) + } + + const eip1559Support = await wallet.ethSupportsEIP1559() + + if (eip1559Support && maxFeePerGas && maxPriorityFeePerGas) + return { gasLimit, maxFeePerGas, maxPriorityFeePerGas } + if (gasPrice) return { gasLimit, gasPrice } + + throw new SwapError('[getFeesFromFeeData]', { + cause: 'legacy gas or eip1559 gas required', + code: SwapErrorType.SIGN_AND_BROADCAST_FAILED, + }) +} - const { txToSign } = await adapter.buildSendTransaction({ +export const buildAndBroadcast = async ({ + accountNumber, + adapter, + data, + feeData, + to, + value, + wallet, +}: BuildAndBroadcastArgs) => { + try { + const { txToSign } = await adapter.buildCustomTx({ wallet, - to: sellAssetErc20Address, + to, accountNumber, - value: '0', - chainSpecific: { - tokenContractAddress: sellAssetErc20Address, - gasPrice: numberToHex(quote.feeData?.chainSpecific?.gasPriceCryptoBaseUnit || 0), - gasLimit: numberToHex(quote.feeData?.chainSpecific?.estimatedGasCryptoBaseUnit || 0), - }, + value, + data, + ...(await getFeesFromFeeData({ wallet, feeData })), }) - const grantAllowanceTxToSign = { - ...txToSign, - data: approveTx, - } if (wallet.supportsOfflineSigning()) { - const signedTx = await adapter.signTransaction({ txToSign: grantAllowanceTxToSign, wallet }) + const signedTx = await adapter.signTransaction({ txToSign, wallet }) + const txid = await adapter.broadcastTransaction(signedTx) + return txid + } - const broadcastedTxId = await adapter.broadcastTransaction(signedTx) + if (wallet.supportsBroadcast() && adapter.signAndBroadcastTransaction) { + const txid = await adapter.signAndBroadcastTransaction({ txToSign, wallet }) + return txid + } - return broadcastedTxId - } else if (wallet.supportsBroadcast() && adapter.signAndBroadcastTransaction) { - const broadcastedTxId = await adapter.signAndBroadcastTransaction?.({ - txToSign: grantAllowanceTxToSign, - wallet, - }) + throw new SwapError('[buildAndBroadcast]', { + cause: 'no broadcast support', + code: SwapErrorType.SIGN_AND_BROADCAST_FAILED, + }) + } catch (e) { + if (e instanceof SwapError) throw e + throw new SwapError('[buildAndBroadcast]', { + cause: e, + code: SwapErrorType.SIGN_AND_BROADCAST_FAILED, + }) + } +} - return broadcastedTxId - } else { - throw new SwapError('[grantAllowance] - invalid HDWallet config', { - code: SwapErrorType.SIGN_AND_BROADCAST_FAILED, - }) - } +export const grantAllowance = async ({ + feeData, + accountNumber, + approvalAmount, + spender, + to, + wallet, + adapter, + web3, +}: GrantAllowanceArgs): Promise => { + const erc20Contract = new web3.eth.Contract(erc20Abi, to) + const inputData = erc20Contract.methods.approve(spender, approvalAmount).encodeABI() + + try { + const txid = await buildAndBroadcast({ + accountNumber, + adapter, + feeData: { ...feeData, estimatedGasCryptoBaseUnit: APPROVAL_GAS_LIMIT }, + to, + value: '0', + wallet, + data: inputData, + }) + return txid } catch (e) { if (e instanceof SwapError) throw e throw new SwapError('[grantAllowance]', { @@ -191,7 +256,7 @@ export const getApproveContractData = ({ spenderAddress, contractAddress, }: GetApproveContractDataArgs): string => { - const contract = new web3.eth.Contract(erc20AbiImported, contractAddress) + const contract = new web3.eth.Contract(erc20Abi, contractAddress) return contract.methods.approve(spenderAddress, MAX_ALLOWANCE).encodeABI() } diff --git a/src/lib/swapper/swappers/utils/test-data/setupDeps.ts b/src/lib/swapper/swappers/utils/test-data/setupDeps.ts index a3bba3e6a34..2c4ea4ddbbf 100644 --- a/src/lib/swapper/swappers/utils/test-data/setupDeps.ts +++ b/src/lib/swapper/swappers/utils/test-data/setupDeps.ts @@ -1,4 +1,5 @@ import { ethAssetId } from '@shapeshiftoss/caip' +import type { evm, EvmChainId, FeeDataEstimate } from '@shapeshiftoss/chain-adapters' import { ethereum } from '@shapeshiftoss/chain-adapters' import * as unchained from '@shapeshiftoss/unchained-client' import Web3 from 'web3' @@ -7,6 +8,48 @@ import { WETH } from 'lib/swapper/swappers/utils/test-data/assets' jest.mock('@shapeshiftoss/chain-adapters') jest.mock('web3') +export const gasFeeData: evm.GasFeeDataEstimate = { + fast: { + gasPrice: '79036500000', + maxFeePerGas: '216214758112', + maxPriorityFeePerGas: '2982734547', + }, + slow: { + gasPrice: '79036500000', + maxFeePerGas: '216214758112', + maxPriorityFeePerGas: '2982734547', + }, + average: { + gasPrice: '79036500000', + maxFeePerGas: '216214758112', + maxPriorityFeePerGas: '2982734547', + }, +} + +export const feeData: FeeDataEstimate = { + fast: { + txFee: '4080654495000000', + chainSpecific: { + gasLimit: '100000', + ...gasFeeData.fast, + }, + }, + average: { + txFee: '4080654495000000', + chainSpecific: { + gasLimit: '100000', + ...gasFeeData.average, + }, + }, + slow: { + txFee: '4080654495000000', + chainSpecific: { + gasLimit: '100000', + ...gasFeeData.slow, + }, + }, +} + export const setupDeps = () => { const ethChainAdapter = new ethereum.ChainAdapter({ providers: { @@ -21,6 +64,7 @@ export const setupDeps = () => { }) ethChainAdapter.getFeeAssetId = () => ethAssetId + ethChainAdapter.getGasFeeData = () => Promise.resolve(gasFeeData) const ethNodeUrl = 'http://localhost:1000' const web3Provider = new Web3.providers.HttpProvider(ethNodeUrl) diff --git a/src/lib/swapper/swappers/utils/test-data/setupSwapQuote.ts b/src/lib/swapper/swappers/utils/test-data/setupSwapQuote.ts index d8637e5efd9..ab349c06f22 100644 --- a/src/lib/swapper/swappers/utils/test-data/setupSwapQuote.ts +++ b/src/lib/swapper/swappers/utils/test-data/setupSwapQuote.ts @@ -17,7 +17,11 @@ export const setupQuote = () => { minimumCryptoHuman: '0', maximumCryptoHuman: '999999999999', feeData: { - chainSpecific: {}, + chainSpecific: { + gasPriceCryptoBaseUnit: '5', + maxFeePerGas: '6', + maxPriorityFeePerGas: '1', + }, sellAssetTradeFeeUsd: '0', networkFeeCryptoBaseUnit: '0', buyAssetTradeFeeUsd: '0', diff --git a/src/plugins/walletConnectToDapps/v1/useApprovalHandler.tsx b/src/plugins/walletConnectToDapps/v1/useApprovalHandler.tsx index 71536c6a821..7932f8745b9 100644 --- a/src/plugins/walletConnectToDapps/v1/useApprovalHandler.tsx +++ b/src/plugins/walletConnectToDapps/v1/useApprovalHandler.tsx @@ -111,9 +111,8 @@ export const useApprovalHandler = (wcAccountId: AccountId | undefined) => { to: tx.to, data: tx.data, value: tx.value ?? '0', - gasLimit: - (approveData.gasLimit ? convertNumberToHex(approveData.gasLimit) : tx.gas) ?? - convertNumberToHex(90000), // https://docs.walletconnect.com/1.0/json-rpc-api-methods/ethereum#eth_sendtransaction + // https://docs.walletconnect.com/1.0/json-rpc-api-methods/ethereum#eth_sendtransaction + gasLimit: approveData.gasLimit ?? tx.gas ?? '90000', ...gasData, }) const txToSign = { diff --git a/src/plugins/walletConnectToDapps/v2/utils/EIP155RequestHandlerUtil.ts b/src/plugins/walletConnectToDapps/v2/utils/EIP155RequestHandlerUtil.ts index 7dd52cca827..5d27e311127 100644 --- a/src/plugins/walletConnectToDapps/v2/utils/EIP155RequestHandlerUtil.ts +++ b/src/plugins/walletConnectToDapps/v2/utils/EIP155RequestHandlerUtil.ts @@ -95,10 +95,8 @@ export const approveEIP155Request = async ({ to: sendTransaction.to, data: sendTransaction.data, value: sendTransaction.value ?? '0', - gasLimit: - (customTransactionData.gasLimit - ? convertNumberToHex(customTransactionData.gasLimit) - : sendTransaction.gasLimit) ?? convertNumberToHex(90000), // https://docs.walletconnect.com/2.0/advanced/rpc-reference/ethereum-rpc#eth_sendtransaction + // https://docs.walletconnect.com/2.0/advanced/rpc-reference/ethereum-rpc#eth_sendtransaction + gasLimit: customTransactionData.gasLimit ?? sendTransaction.gasLimit ?? '90000', ...gasData, }) const txToSign = {