diff --git a/README.md b/README.md index 8a8577f0479..bc225aa1518 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ If you are using Linux and macOS it works out of the box following these steps: 5. Build Packages: ```sh - yarn build + yarn build:packages ``` 6. Run `yarn env dev` to generate a `.env` file. diff --git a/__mocks__/ethers.ts b/__mocks__/ethers.ts index 7da501f0f23..bdf627aea50 100644 --- a/__mocks__/ethers.ts +++ b/__mocks__/ethers.ts @@ -3,6 +3,7 @@ const ethers = { ...jest.requireActual('ethers').ethers, providers: { JsonRpcProvider: jest.fn(), + JsonRpcBatchProvider: jest.fn(), }, Contract: jest.fn().mockImplementation(address => ({ decimals: () => { 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..d1a5347fb40 100644 --- a/package.json +++ b/package.json @@ -134,7 +134,7 @@ "embla-carousel-react": "^7.0.5", "envalid": "^7.3.1", "eth-url-parser": "^1.0.4", - "ethers": "^5.5.3", + "ethers": "^5.7.2", "framer-motion": "^6.3.11", "friendly-challenge": "0.9.2", "grapheme-splitter": "^1.0.4", @@ -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/asset-service/package.json b/packages/asset-service/package.json index b93b0b4912c..b742328d827 100644 --- a/packages/asset-service/package.json +++ b/packages/asset-service/package.json @@ -29,7 +29,6 @@ "js-pixel-fonts": "^1.5.0" }, "devDependencies": { - "@ethersproject/providers": "^5.5.3", "@yfi/sdk": "^1.2.0", "colorthief": "^2.3.2" } diff --git a/packages/asset-service/src/generateAssetData/ethereum/index.ts b/packages/asset-service/src/generateAssetData/ethereum/index.ts index db006c0f536..6bc050d2d72 100644 --- a/packages/asset-service/src/generateAssetData/ethereum/index.ts +++ b/packages/asset-service/src/generateAssetData/ethereum/index.ts @@ -13,7 +13,8 @@ import { ethereum } from '../baseAssets' import * as coingecko from '../coingecko' import { getIdleTokens } from './idleVaults' import { getUniswapV2Pools } from './uniswapV2Pools' -import { getUnderlyingVaultTokens, getYearnVaults, getZapperTokens } from './yearnVaults' +// Yearn SDK is currently rugged upstream +// import { getUnderlyingVaultTokens, getYearnVaults, getZapperTokens } from './yearnVaults' const foxyToken: Asset = { assetId: toAssetId({ @@ -33,22 +34,21 @@ const foxyToken: Asset = { } export const getAssets = async (): Promise => { - const [ethTokens, yearnVaults, zapperTokens, underlyingTokens, uniV2PoolTokens, idleTokens] = - await Promise.all([ - coingecko.getAssets(ethChainId), - getYearnVaults(), - getZapperTokens(), - getUnderlyingVaultTokens(), - getUniswapV2Pools(), - getIdleTokens(), - ]) + const [ethTokens, uniV2PoolTokens, idleTokens] = await Promise.all([ + coingecko.getAssets(ethChainId), + // getYearnVaults(), + // getZapperTokens(), + // getUnderlyingVaultTokens(), + getUniswapV2Pools(), + getIdleTokens(), + ]) const ethAssets = [ foxyToken, ...ethTokens, - ...yearnVaults, - ...zapperTokens, - ...underlyingTokens, + // ...yearnVaults, + // ...zapperTokens, + // ...underlyingTokens, ...uniV2PoolTokens, ...idleTokens, ] diff --git a/packages/asset-service/src/generateAssetData/ethereum/yearnVaults.ts b/packages/asset-service/src/generateAssetData/ethereum/yearnVaults.ts index 17603da3c27..fc74776baec 100644 --- a/packages/asset-service/src/generateAssetData/ethereum/yearnVaults.ts +++ b/packages/asset-service/src/generateAssetData/ethereum/yearnVaults.ts @@ -1,7 +1,7 @@ -import { JsonRpcProvider } from '@ethersproject/providers' import { ethChainId as chainId, toAssetId } from '@shapeshiftoss/caip' import type { Token, Vault } from '@yfi/sdk' import { Yearn } from '@yfi/sdk' +import { ethers } from 'ethers' import toLower from 'lodash/toLower' import type { Asset } from '../../service/AssetService' @@ -9,7 +9,7 @@ import { ethereum } from '../baseAssets' import { colorMap } from '../colorMap' const network = 1 // 1 for mainnet -const provider = new JsonRpcProvider(process.env.ETHEREUM_NODE_URL) +const provider = new ethers.providers.JsonRpcBatchProvider(process.env.ETHEREUM_NODE_URL) export const yearnSdk = new Yearn(network, { provider }) const explorerData = { diff --git a/packages/caip/src/adapters/yearn/utils.ts b/packages/caip/src/adapters/yearn/utils.ts index bd2f7a5aff4..9882d69cb09 100644 --- a/packages/caip/src/adapters/yearn/utils.ts +++ b/packages/caip/src/adapters/yearn/utils.ts @@ -1,7 +1,7 @@ /* eslint-disable @shapeshiftoss/logger/no-native-console */ -import { JsonRpcProvider } from '@ethersproject/providers' import type { Token, Vault } from '@yfi/sdk' import { Yearn } from '@yfi/sdk' +import { ethers } from 'ethers' import fs from 'fs' import toLower from 'lodash/toLower' import uniqBy from 'lodash/uniqBy' @@ -11,7 +11,7 @@ import { toChainId } from '../../chainId/chainId' import { CHAIN_NAMESPACE, CHAIN_REFERENCE } from '../../constants' const network = 1 // 1 for mainnet -const provider = new JsonRpcProvider(process.env.REACT_APP_ETHEREUM_NODE_URL) +const provider = new ethers.providers.JsonRpcBatchProvider(process.env.REACT_APP_ETHEREUM_NODE_URL) const yearnSdk = new Yearn(network, { provider }) export const writeFiles = async (data: Record>) => { 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/investor-foxy/package.json b/packages/investor-foxy/package.json index 8b1fe8ec98f..26bf7a9cb21 100644 --- a/packages/investor-foxy/package.json +++ b/packages/investor-foxy/package.json @@ -21,14 +21,11 @@ "cli": "yarn build && yarn node dist/foxycli.js" }, "dependencies": { - "@ethersproject/providers": "^5.5.3", "@shapeshiftoss/caip": "workspace:^", "@shapeshiftoss/chain-adapters": "workspace:^", "@shapeshiftoss/logger": "workspace:^", "@shapeshiftoss/types": "workspace:^", - "readline-sync": "^1.4.10", - "web3-core": "1.7.4", - "web3-utils": "1.7.4" + "readline-sync": "^1.4.10" }, "devDependencies": { "@types/readline-sync": "^1.4.4" diff --git a/packages/investor-foxy/src/abi/erc20-abi.ts b/packages/investor-foxy/src/abi/erc20-abi.ts index bc1723c6421..ae7e19c78ef 100644 --- a/packages/investor-foxy/src/abi/erc20-abi.ts +++ b/packages/investor-foxy/src/abi/erc20-abi.ts @@ -1,6 +1,6 @@ -import type { AbiItem } from 'web3-utils' +import type { ContractInterface } from 'ethers' -export const erc20Abi: AbiItem[] = [ +export const erc20Abi: ContractInterface = [ { constant: true, inputs: [], diff --git a/packages/investor-foxy/src/abi/foxy-abi.ts b/packages/investor-foxy/src/abi/foxy-abi.ts index cc8e6ccdbcb..99522a88a09 100644 --- a/packages/investor-foxy/src/abi/foxy-abi.ts +++ b/packages/investor-foxy/src/abi/foxy-abi.ts @@ -1,6 +1,6 @@ -import type { AbiItem } from 'web3-utils' +import type { ContractInterface } from 'ethers' -export const foxyAbi: AbiItem[] = [ +export const foxyAbi: ContractInterface = [ { inputs: [], stateMutability: 'nonpayable', diff --git a/packages/investor-foxy/src/abi/foxy-staking-abi.ts b/packages/investor-foxy/src/abi/foxy-staking-abi.ts index 44d3ee93bed..0d62f0aae71 100644 --- a/packages/investor-foxy/src/abi/foxy-staking-abi.ts +++ b/packages/investor-foxy/src/abi/foxy-staking-abi.ts @@ -1,6 +1,6 @@ -import type { AbiItem } from 'web3-utils' +import type { ContractInterface } from 'ethers' -export const foxyStakingAbi: AbiItem[] = [ +export const foxyStakingAbi: ContractInterface = [ { inputs: [ { diff --git a/packages/investor-foxy/src/abi/liquidity-reserve-abi.ts b/packages/investor-foxy/src/abi/liquidity-reserve-abi.ts index b2e9615dece..4cea3c40602 100644 --- a/packages/investor-foxy/src/abi/liquidity-reserve-abi.ts +++ b/packages/investor-foxy/src/abi/liquidity-reserve-abi.ts @@ -1,6 +1,6 @@ -import type { AbiItem } from 'web3-utils' +import type { ContractInterface } from 'ethers' -export const liquidityReserveAbi: AbiItem[] = [ +export const liquidityReserveAbi: ContractInterface = [ { inputs: [ { diff --git a/packages/investor-foxy/src/abi/toke-manager-abi.ts b/packages/investor-foxy/src/abi/toke-manager-abi.ts index c6ee537cef2..a4a5250015e 100644 --- a/packages/investor-foxy/src/abi/toke-manager-abi.ts +++ b/packages/investor-foxy/src/abi/toke-manager-abi.ts @@ -1,6 +1,6 @@ -import type { AbiItem } from 'web3-utils' +import type { ContractInterface } from 'ethers' -export const tokeManagerAbi: AbiItem[] = [ +export const tokeManagerAbi: ContractInterface = [ { inputs: [], stateMutability: 'nonpayable', type: 'constructor' }, { anonymous: false, diff --git a/packages/investor-foxy/src/abi/toke-pool-abi.ts b/packages/investor-foxy/src/abi/toke-pool-abi.ts index c8c9018fa3a..4ef28248b05 100644 --- a/packages/investor-foxy/src/abi/toke-pool-abi.ts +++ b/packages/investor-foxy/src/abi/toke-pool-abi.ts @@ -1,6 +1,6 @@ -import type { AbiItem } from 'web3-utils' +import type { ContractInterface } from 'ethers' -export const tokePoolAbi: AbiItem[] = [ +export const tokePoolAbi: ContractInterface = [ { anonymous: false, inputs: [ diff --git a/packages/investor-foxy/src/abi/toke-reward-hash-abi.ts b/packages/investor-foxy/src/abi/toke-reward-hash-abi.ts index 758f382dcf1..c2d62f9274c 100644 --- a/packages/investor-foxy/src/abi/toke-reward-hash-abi.ts +++ b/packages/investor-foxy/src/abi/toke-reward-hash-abi.ts @@ -1,6 +1,6 @@ -import type { AbiItem } from 'web3-utils' +import type { ContractInterface } from 'ethers' -export const tokeRewardHashAbi: AbiItem[] = [ +export const tokeRewardHashAbi: ContractInterface = [ { inputs: [], stateMutability: 'nonpayable', type: 'constructor' }, { anonymous: false, diff --git a/packages/investor-foxy/src/api/api.ts b/packages/investor-foxy/src/api/api.ts index d77125ce5cb..50ab70f9fac 100644 --- a/packages/investor-foxy/src/api/api.ts +++ b/packages/investor-foxy/src/api/api.ts @@ -1,16 +1,12 @@ -import { JsonRpcProvider } from '@ethersproject/providers' import type { ChainReference } from '@shapeshiftoss/caip' import { CHAIN_NAMESPACE, CHAIN_REFERENCE, toAssetId } from '@shapeshiftoss/caip' -import type { ChainAdapter } from '@shapeshiftoss/chain-adapters' +import type { EvmBaseAdapter, FeeDataEstimate } from '@shapeshiftoss/chain-adapters' import { Logger } from '@shapeshiftoss/logger' import { KnownChainIds, WithdrawType } from '@shapeshiftoss/types' import axios from 'axios' import type { BigNumber } from 'bignumber.js' +import { ethers } from 'ethers' import { toLower } from 'lodash' -import Web3 from 'web3' -import type { HttpProvider, TransactionReceipt } from 'web3-core/types' -import type { Contract } from 'web3-eth-contract' -import { numberToHex } from 'web3-utils' import { erc20Abi } from '../abi/erc20-abi' import { foxyAbi } from '../abi/foxy-abi' @@ -26,7 +22,7 @@ import { tokePoolAddress, tokeRewardHashAddress, } from '../constants' -import { bn, bnOrZero, buildTxToSign } from '../utils' +import { bn, bnOrZero } from '../utils' import type { AllowanceInput, ApproveInput, @@ -34,8 +30,9 @@ import type { CanClaimWithdrawParams, ClaimWithdrawal, ContractAddressInput, - EstimateGasApproveInput, - EstimateGasTxInput, + EstimateApproveFeesInput, + EstimateFeesTxInput, + EstimateWithdrawFeesInput, FoxyAddressesType, FoxyOpportunityInputData, GetTokeRewardAmount, @@ -49,7 +46,6 @@ import type { TxInputWithoutAmount, TxInputWithoutAmountAndWallet, TxReceipt, - WithdrawEstimateGasInput, WithdrawInfo, WithdrawInput, } from './foxy-types' @@ -64,7 +60,7 @@ type EthereumChainReference = | typeof CHAIN_REFERENCE.EthereumRopsten export type ConstructorArgs = { - adapter: ChainAdapter + adapter: EvmBaseAdapter providerUrl: string foxyAddresses: FoxyAddressesType chainReference?: EthereumChainReference @@ -88,13 +84,12 @@ export const transformData = ({ tvl, apy, expired, ...contractData }: FoxyOpport const TOKE_IPFS_URL = 'https://ipfs.tokemaklabs.xyz/ipfs' export class FoxyApi { - public adapter: ChainAdapter - public provider: HttpProvider + public adapter: EvmBaseAdapter + public provider: ethers.providers.JsonRpcBatchProvider private providerUrl: string - public jsonRpcProvider: JsonRpcProvider - public web3: Web3 - private foxyStakingContracts: Contract[] - private liquidityReserveContracts: Contract[] + public jsonRpcProvider: ethers.providers.JsonRpcBatchProvider + private foxyStakingContracts: ethers.Contract[] + private liquidityReserveContracts: ethers.Contract[] private readonly ethereumChainReference: ChainReference private foxyAddresses: FoxyAddressesType @@ -105,14 +100,14 @@ export class FoxyApi { chainReference = CHAIN_REFERENCE.EthereumMainnet, }: ConstructorArgs) { this.adapter = adapter - this.provider = new Web3.providers.HttpProvider(providerUrl) - this.jsonRpcProvider = new JsonRpcProvider(providerUrl) - this.web3 = new Web3(this.provider) + this.provider = new ethers.providers.JsonRpcBatchProvider(providerUrl) + this.jsonRpcProvider = new ethers.providers.JsonRpcBatchProvider(providerUrl) this.foxyStakingContracts = foxyAddresses.map( - addresses => new this.web3.eth.Contract(foxyStakingAbi, addresses.staking), + addresses => new ethers.Contract(addresses.staking, foxyStakingAbi, this.provider), ) this.liquidityReserveContracts = foxyAddresses.map( - addresses => new this.web3.eth.Contract(liquidityReserveAbi, addresses.liquidityReserve), + addresses => + new ethers.Contract(addresses.liquidityReserve, liquidityReserveAbi, this.provider), ) this.ethereumChainReference = chainReference this.providerUrl = providerUrl @@ -124,24 +119,42 @@ export class FoxyApi { * to exponential notation ('1.6e+21') in javascript. * @param amount */ - private normalizeAmount(amount: BigNumber) { - return this.web3.utils.toBN(amount.toFixed()) + private normalizeAmount(amount: BigNumber): ethers.BigNumber { + return ethers.BigNumber.from(amount.toFixed()) } + // TODO(gomes): This is rank and should really belong in web for sanity sake. private async signAndBroadcastTx(input: SignAndBroadcastTx): Promise { const { payload, wallet, dryRun } = input - const txToSign = buildTxToSign(payload) + + const { + chainSpecific: { gasPrice, gasLimit, maxFeePerGas, maxPriorityFeePerGas }, + } = payload.estimatedFees.fast + const shouldUseEIP1559Fees = + (await wallet.ethSupportsEIP1559()) && + maxFeePerGas !== undefined && + maxPriorityFeePerGas !== undefined + + const { txToSign } = await this.adapter.buildCustomTx({ + to: payload.to, + value: payload.value, + gasLimit, + wallet, + data: payload.data, + accountNumber: payload.bip44Params.accountNumber, + ...(shouldUseEIP1559Fees ? { maxFeePerGas, maxPriorityFeePerGas } : { gasPrice }), + }) if (wallet.supportsOfflineSigning()) { const signedTx = await this.adapter.signTransaction({ txToSign, wallet }) if (dryRun) return signedTx try { if (this.providerUrl.includes('localhost') || this.providerUrl.includes('127.0.0.1')) { - const sendSignedTx = await this.web3.eth.sendSignedTransaction(signedTx) - return sendSignedTx?.blockHash + const sendSignedTx = await this.provider.sendTransaction(signedTx) + return sendSignedTx?.blockHash ?? '' } return this.adapter.broadcastTransaction(signedTx) - } catch (err) { - throw new Error(`Failed to broadcast: ${err}`) + } catch (e) { + throw new Error(`Failed to broadcast: ${e}`) } } else if (wallet.supportsBroadcast() && this.adapter.signAndBroadcastTransaction) { if (dryRun) { @@ -154,71 +167,52 @@ export class FoxyApi { } checksumAddress(address: string): string { - return this.web3.utils.toChecksumAddress(address) + // ethers always returns checksum addresses from getAddress() calls + return ethers.utils.getAddress(address) } private verifyAddresses(addresses: string[]) { - try { - addresses.forEach(address => { - this.checksumAddress(address) - }) - } catch (err) { - throw new Error(`Verify Address: ${err}`) - } + addresses.forEach(address => { + this.checksumAddress(address) + }) } - private getStakingContract(contractAddress: string): Contract { + private getStakingContract(contractAddress: string): ethers.Contract { const stakingContract = this.foxyStakingContracts.find( - item => toLower(item.options.address) === toLower(contractAddress), + item => toLower(item.address) === toLower(contractAddress), ) if (!stakingContract) throw new Error('Not a valid contract address') return stakingContract } - private getLiquidityReserveContract(liquidityReserveAddress: string): Contract { + private getLiquidityReserveContract(liquidityReserveAddress: string): ethers.Contract { const liquidityReserveContract = this.liquidityReserveContracts.find( - item => toLower(item.options.address) === toLower(liquidityReserveAddress), + item => toLower(item.address) === toLower(liquidityReserveAddress), ) if (!liquidityReserveContract) throw new Error('Not a valid reserve contract address') return liquidityReserveContract } - private async getGasPriceAndNonce(userAddress: string) { - let nonce: number - try { - nonce = await this.web3.eth.getTransactionCount(userAddress) - } catch (err) { - throw new Error(`Get nonce Error: ${err}`) - } - let gasPrice: string - try { - gasPrice = await this.web3.eth.getGasPrice() - } catch (err) { - throw new Error(`Get gasPrice Error: ${err}`) - } - return { nonce: String(nonce), gasPrice } - } - async getFoxyOpportunities() { try { const opportunities = await Promise.all( this.foxyAddresses.map(async addresses => { const stakingContract = this.foxyStakingContracts.find( - item => toLower(item.options.address) === toLower(addresses.staking), + item => toLower(item.address) === toLower(addresses.staking), ) try { - const expired = await stakingContract?.methods.pauseStaking().call() + const expired = await stakingContract?.pauseStaking() const tvl = await this.tvl({ tokenContractAddress: addresses.foxy }) const apy = this.apy() return transformData({ ...addresses, expired, tvl, apy }) - } catch (err) { - throw new Error(`Failed to get contract data ${err}`) + } catch (e) { + throw new Error(`Failed to get contract data ${e}`) } }), ) return opportunities - } catch (err) { - throw new Error(`getFoxyOpportunities Error: ${err}`) + } catch (e) { + throw new Error(`getFoxyOpportunities Error: ${e}`) } } @@ -232,26 +226,23 @@ export class FoxyApi { const stakingContract = this.getStakingContract(addresses.staking) try { - const expired = await stakingContract.methods.pauseStaking().call() + const expired = await stakingContract.pauseStaking() const tvl = await this.tvl({ tokenContractAddress: addresses.foxy }) const apy = this.apy() return transformData({ ...addresses, tvl, apy, expired }) - } catch (err) { - throw new Error(`Failed to get contract data ${err}`) + } catch (e) { + throw new Error(`Failed to get contract data ${e}`) } } - async getGasPrice() { - const gasPrice = await this.web3.eth.getGasPrice() - return bnOrZero(gasPrice) - } - - getTxReceipt({ txid }: TxReceipt): Promise { + getTxReceipt({ txid }: TxReceipt): Promise { if (!txid) throw new Error('Must pass txid') - return this.web3.eth.getTransactionReceipt(txid) + return this.provider.getTransactionReceipt(txid) } - async estimateClaimWithdrawGas(input: ClaimWithdrawal): Promise { + async estimateClaimWithdrawFees( + input: ClaimWithdrawal, + ): Promise> { const { claimAddress, userAddress, contractAddress } = input const addressToClaim = claimAddress ?? userAddress this.verifyAddresses([addressToClaim, userAddress, contractAddress]) @@ -259,34 +250,62 @@ export class FoxyApi { const stakingContract = this.getStakingContract(contractAddress) try { - const estimatedGas = await stakingContract.methods.claimWithdraw(addressToClaim).estimateGas({ - from: userAddress, + const data = stakingContract.interface.encodeFunctionData('claimWithdraw', [addressToClaim]) + const feeData = await this.adapter.getFeeData({ + to: contractAddress, + value: '0', + chainSpecific: { + contractData: data, + from: userAddress, + }, }) - return bnOrZero(estimatedGas) - } catch (err) { - throw new Error(`Failed to get gas ${err}`) + + const { + chainSpecific: { gasLimit: gasLimitBase }, + } = feeData.fast + const safeGasLimit = bnOrZero(gasLimitBase).times('1.05').toFixed(0) + feeData.fast.chainSpecific.gasLimit = safeGasLimit + + return feeData + } catch (e) { + throw new Error(`Failed to get gas ${e}`) } } - async estimateSendWithdrawalRequestsGas( + async estimateSendWithdrawalRequestsFees( input: TxInputWithoutAmountAndWallet, - ): Promise { + ): Promise> { const { userAddress, contractAddress } = input this.verifyAddresses([userAddress, contractAddress]) const stakingContract = this.getStakingContract(contractAddress) try { - const estimatedGas = await stakingContract.methods.sendWithdrawalRequests().estimateGas({ - from: userAddress, + const data = stakingContract.interface.encodeFunctionData('sendWithdrawalRequests', []) + const feeData = await this.adapter.getFeeData({ + to: contractAddress, + value: '0', + chainSpecific: { + contractData: data, + from: userAddress, + }, }) - return bnOrZero(estimatedGas) - } catch (err) { - throw new Error(`Failed to get gas ${err}`) + + const { + chainSpecific: { gasLimit: gasLimitBase }, + } = feeData.fast + const safeGasLimit = bnOrZero(gasLimitBase).times('1.05').toFixed(0) + feeData.fast.chainSpecific.gasLimit = safeGasLimit + + return feeData + } catch (e) { + throw new Error(`Failed to get gas ${e}`) } } - async estimateAddLiquidityGas(input: EstimateGasTxInput): Promise { + async estimateAddLiquidityFees( + input: EstimateFeesTxInput, + ): Promise> { const { amountDesired, userAddress, contractAddress } = input this.verifyAddresses([userAddress, contractAddress]) if (!amountDesired.gt(0)) throw new Error('Must send valid amount') @@ -294,18 +313,33 @@ export class FoxyApi { const liquidityReserveContract = this.getLiquidityReserveContract(contractAddress) try { - const estimatedGas = await liquidityReserveContract.methods - .addLiquidity(this.normalizeAmount(amountDesired)) - .estimateGas({ + const data = liquidityReserveContract.interface.encodeFunctionData('addLiquidity', [ + this.normalizeAmount(amountDesired), + ]) + const feeData = await this.adapter.getFeeData({ + to: contractAddress, + value: '0', + chainSpecific: { + contractData: data, from: userAddress, - }) - return bnOrZero(estimatedGas) - } catch (err) { - throw new Error(`Failed to get gas ${err}`) + }, + }) + + const { + chainSpecific: { gasLimit: gasLimitBase }, + } = feeData.fast + const safeGasLimit = bnOrZero(gasLimitBase).times('1.05').toFixed(0) + feeData.fast.chainSpecific.gasLimit = safeGasLimit + + return feeData + } catch (e) { + throw new Error(`Failed to get gas ${e}`) } } - async estimateRemoveLiquidityGas(input: EstimateGasTxInput): Promise { + async estimateRemoveLiquidityFees( + input: EstimateFeesTxInput, + ): Promise> { const { amountDesired, userAddress, contractAddress } = input this.verifyAddresses([userAddress, contractAddress]) if (!amountDesired.gt(0)) throw new Error('Must send valid amount') @@ -313,18 +347,34 @@ export class FoxyApi { const liquidityReserveContract = this.getLiquidityReserveContract(contractAddress) try { - const estimatedGas = await liquidityReserveContract.methods - .removeLiquidity(this.normalizeAmount(amountDesired)) - .estimateGas({ + const data = liquidityReserveContract.encodeFunctionData('removeLiquidity', [ + this.normalizeAmount(amountDesired), + ]) + + const feeData = await this.adapter.getFeeData({ + to: contractAddress, + value: '0', + chainSpecific: { + contractData: data, from: userAddress, - }) - return bnOrZero(estimatedGas) - } catch (err) { - throw new Error(`Failed to get gas ${err}`) + }, + }) + + const { + chainSpecific: { gasLimit: gasLimitBase }, + } = feeData.fast + const safeGasLimit = bnOrZero(gasLimitBase).times('1.05').toFixed(0) + feeData.fast.chainSpecific.gasLimit = safeGasLimit + + return feeData + } catch (e) { + throw new Error(`Failed to get gas ${e}`) } } - async estimateWithdrawGas(input: WithdrawEstimateGasInput): Promise { + async estimateWithdrawFees( + input: EstimateWithdrawFeesInput, + ): Promise> { const { amountDesired, userAddress, contractAddress, type } = input this.verifyAddresses([userAddress, contractAddress]) @@ -334,40 +384,71 @@ export class FoxyApi { if (isDelayed && !amountDesired.gt(0)) throw new Error('Must send valid amount') try { - const estimatedGas = isDelayed - ? await stakingContract.methods - .unstake(this.normalizeAmount(amountDesired), true) - .estimateGas({ - from: userAddress, - }) - : await stakingContract.methods.instantUnstake(true).estimateGas({ - from: userAddress, - }) - return bnOrZero(estimatedGas) - } catch (err) { - throw new Error(`Failed to get gas ${err}`) + const data = isDelayed + ? stakingContract.interface.encodeFunctionData('unstake(uint256,bool)', [ + this.normalizeAmount(amountDesired), + true, + ]) + : stakingContract.interface.encodeFunctionData('instantUnstake', [true]) + + const feeData = await this.adapter.getFeeData({ + to: contractAddress, + value: '0', + chainSpecific: { + contractData: data, + from: userAddress, + }, + }) + + const { + chainSpecific: { gasLimit: gasLimitBase }, + } = feeData.fast + const safeGasLimit = bnOrZero(gasLimitBase).times('1.05').toFixed(0) + feeData.fast.chainSpecific.gasLimit = safeGasLimit + + return feeData + } catch (e) { + throw new Error(`Failed to get gas ${e}`) } } - async estimateApproveGas(input: EstimateGasApproveInput): Promise { + async estimateApproveFees( + input: EstimateApproveFeesInput, + ): Promise> { const { userAddress, tokenContractAddress, contractAddress } = input this.verifyAddresses([userAddress, contractAddress, tokenContractAddress]) - const depositTokenContract = new this.web3.eth.Contract(erc20Abi, tokenContractAddress) + const depositTokenContract = new ethers.Contract(tokenContractAddress, erc20Abi, this.provider) try { - const estimatedGas = await depositTokenContract.methods - .approve(contractAddress, MAX_ALLOWANCE) - .estimateGas({ + const data = depositTokenContract.interface.encodeFunctionData('approve', [ + contractAddress, + MAX_ALLOWANCE, + ]) + const feeData = await this.adapter.getFeeData({ + to: tokenContractAddress, + value: '0', + chainSpecific: { + contractData: data, from: userAddress, - }) - return bnOrZero(estimatedGas) - } catch (err) { - throw new Error(`Failed to get gas ${err}`) + }, + }) + + const { + chainSpecific: { gasLimit: gasLimitBase }, + } = feeData.fast + const safeGasLimit = bnOrZero(gasLimitBase).times('1.05').toFixed(0) + feeData.fast.chainSpecific.gasLimit = safeGasLimit + + return feeData + } catch (e) { + throw new Error(`Failed to get gas ${e}`) } } - async estimateDepositGas(input: EstimateGasTxInput): Promise { + async estimateDepositFees( + input: EstimateFeesTxInput, + ): Promise> { const { amountDesired, userAddress, contractAddress } = input this.verifyAddresses([userAddress, contractAddress]) if (!amountDesired.gt(0)) throw new Error('Must send valid amount') @@ -375,14 +456,28 @@ export class FoxyApi { const stakingContract = this.getStakingContract(contractAddress) try { - const estimatedGas = await stakingContract.methods - .stake(this.normalizeAmount(amountDesired), userAddress) - .estimateGas({ + const data = stakingContract.interface.encodeFunctionData('stake(uint256)', [ + this.normalizeAmount(amountDesired), + ]) + + const feeData = await this.adapter.getFeeData({ + to: contractAddress, + value: '0', + chainSpecific: { + contractData: data, from: userAddress, - }) - return bnOrZero(estimatedGas) - } catch (err) { - throw new Error(`Failed to get gas ${err}`) + }, + }) + + const { + chainSpecific: { gasLimit: gasLimitBase }, + } = feeData.fast + const safeGasLimit = bnOrZero(gasLimitBase).times('1.05').toFixed(0) + feeData.fast.chainSpecific.gasLimit = safeGasLimit + + return feeData + } catch (e) { + throw new Error(`Failed to get gas ${e}`) } } @@ -399,32 +494,19 @@ export class FoxyApi { this.verifyAddresses([userAddress, contractAddress, tokenContractAddress]) if (!wallet) throw new Error('Missing inputs') - let estimatedGasBN: BigNumber - try { - estimatedGasBN = await this.estimateApproveGas(input) - } catch (err) { - throw new Error(`Estimate Gas Error: ${err}`) - } - const depositTokenContract = new this.web3.eth.Contract(erc20Abi, tokenContractAddress) - const data: string = depositTokenContract.methods - .approve( - contractAddress, - amount ? numberToHex(this.normalizeAmount(bnOrZero(amount))) : MAX_ALLOWANCE, - ) - .encodeABI({ - from: userAddress, - }) + const estimatedFees = await this.estimateApproveFees(input) + const depositTokenContract = new ethers.Contract(tokenContractAddress, erc20Abi, this.provider) + const data: string = depositTokenContract.interface.encodeFunctionData('approve', [ + contractAddress, + amount ? this.normalizeAmount(bnOrZero(amount)) : MAX_ALLOWANCE, + ]) - const { nonce, gasPrice } = await this.getGasPriceAndNonce(userAddress) const chainReferenceAsNumber = Number(this.ethereumChainReference) - const estimatedGas = estimatedGasBN.toString() const payload = { bip44Params, chainId: chainReferenceAsNumber, data, - estimatedGas, - gasPrice, - nonce, + estimatedFees, to: tokenContractAddress, value: '0', } @@ -435,18 +517,14 @@ export class FoxyApi { const { userAddress, tokenContractAddress, contractAddress } = input this.verifyAddresses([userAddress, contractAddress, tokenContractAddress]) - const depositTokenContract: Contract = new this.web3.eth.Contract( - erc20Abi, + const depositTokenContract: ethers.Contract = new ethers.Contract( tokenContractAddress, + erc20Abi, + this.provider, ) - let allowance - try { - allowance = await depositTokenContract.methods.allowance(userAddress, contractAddress).call() - } catch (err) { - throw new Error(`Failed to get allowance ${err}`) - } - return allowance + const allowance = await depositTokenContract.allowance(userAddress, contractAddress) + return allowance.toString() } async deposit(input: TxInput): Promise { @@ -462,33 +540,21 @@ export class FoxyApi { if (!amountDesired.gt(0)) throw new Error('Must send valid amount') if (!wallet) throw new Error('Missing inputs') - let estimatedGasBN: BigNumber - try { - estimatedGasBN = await this.estimateDepositGas(input) - } catch (err) { - throw new Error(`Estimate Gas Error: ${err}`) - } + const estimatedFees = await this.estimateDepositFees(input) const stakingContract = this.getStakingContract(contractAddress) - const userChecksum = this.web3.utils.toChecksumAddress(userAddress) - const data: string = await stakingContract.methods - .stake(this.normalizeAmount(amountDesired), userAddress) - .encodeABI({ - value: 0, - from: userChecksum, - }) + const data = stakingContract.interface.encodeFunctionData('stake(uint256,address)', [ + this.normalizeAmount(amountDesired), + userAddress, + ]) - const { nonce, gasPrice } = await this.getGasPriceAndNonce(userAddress) - const estimatedGas = estimatedGasBN.toString() const chainReferenceAsNumber = Number(this.ethereumChainReference) const payload = { bip44Params, chainId: chainReferenceAsNumber, data, - estimatedGas, - gasPrice, - nonce, + estimatedFees, to: contractAddress, value: '0', } @@ -508,36 +574,26 @@ export class FoxyApi { this.verifyAddresses([userAddress, contractAddress]) if (!wallet) throw new Error('Missing inputs') - let estimatedGasBN: BigNumber - try { - estimatedGasBN = await this.estimateWithdrawGas(input) - } catch (err) { - throw new Error(`Estimate Gas Error: ${err}`) - } + const estimatedFees = await this.estimateWithdrawFees(input) const stakingContract = this.getStakingContract(contractAddress) const isDelayed = type === WithdrawType.DELAYED && amountDesired if (isDelayed && !amountDesired.gt(0)) throw new Error('Must send valid amount') - const data: string = isDelayed - ? stakingContract.methods.unstake(this.normalizeAmount(amountDesired), true).encodeABI({ - from: userAddress, - }) - : stakingContract.methods.instantUnstake(true).encodeABI({ - from: userAddress, - }) + const stakingContractCallInput: Parameters< + typeof stakingContract.interface.encodeFunctionData + > = isDelayed + ? ['unstake(uint256,bool)', [this.normalizeAmount(amountDesired), true]] + : ['instantUnstake', ['true']] + const data: string = stakingContract.interface.encodeFunctionData(...stakingContractCallInput) - const { nonce, gasPrice } = await this.getGasPriceAndNonce(userAddress) - const estimatedGas = estimatedGasBN.toString() const chainReferenceAsNumber = Number(this.ethereumChainReference) const payload = { bip44Params, chainId: chainReferenceAsNumber, data, - estimatedGas, - gasPrice, - nonce, + estimatedFees, to: contractAddress, value: '0', } @@ -546,67 +602,68 @@ export class FoxyApi { async canClaimWithdraw(input: CanClaimWithdrawParams): Promise { const { userAddress, contractAddress } = input - const tokeManagerContract = new this.web3.eth.Contract(tokeManagerAbi, tokeManagerAddress) - const tokePoolContract = new this.web3.eth.Contract(tokePoolAbi, tokePoolAddress) + const tokeManagerContract = new ethers.Contract( + tokeManagerAddress, + tokeManagerAbi, + this.provider, + ) + const tokePoolContract = new ethers.Contract(tokePoolAddress, tokePoolAbi, this.provider) const stakingContract = this.getStakingContract(contractAddress) const coolDownInfo = await (async () => { try { - const coolDown = await stakingContract.methods.coolDownInfo(userAddress).call() + const coolDown = await stakingContract.coolDownInfo(userAddress) return { ...coolDown, endEpoch: coolDown.expiry, } - } catch (err) { - logger.error(err, 'failed to get coolDowninfo') + } catch (e) { + logger.error(e, 'failed to get coolDowninfo') } })() const epoch = await (() => { try { - return stakingContract.methods.epoch().call() - } catch (err) { - logger.error(err, 'failed to get epoch') + return stakingContract.epoch() + } catch (e) { + logger.error(e, 'failed to get epoch') return {} } })() const requestedWithdrawals = await (() => { try { - return tokePoolContract.methods.requestedWithdrawals(stakingContract.options.address).call() - } catch (err) { - logger.error(err, 'failed to get requestedWithdrawals') + return tokePoolContract.requestedWithdrawals(stakingContract.address) + } catch (e) { + logger.error(e, 'failed to get requestedWithdrawals') return {} } })() const currentCycleIndex = await (() => { try { - return tokeManagerContract.methods.getCurrentCycleIndex().call() - } catch (err) { - logger.error(err, 'failed to get currentCycleIndex') + return tokeManagerContract.getCurrentCycleIndex() + } catch (e) { + logger.error(e, 'failed to get currentCycleIndex') return 0 } })() const withdrawalAmount = await (() => { try { - return stakingContract.methods.withdrawalAmount().call() - } catch (err) { - logger.error(err, 'failed to get currentCycleIndex') + return stakingContract.withdrawalAmount() + } catch (e) { + logger.error(e, 'failed to get currentCycleIndex') return 0 } })() - const epochExpired = epoch.number >= coolDownInfo.endEpoch - const coolDownValid = - !bnOrZero(coolDownInfo.endEpoch).eq(0) && !bnOrZero(coolDownInfo.amount).eq(0) + const epochExpired = epoch.number.gte(coolDownInfo.endEpoch) + const coolDownValid = !coolDownInfo.endEpoch.isZero() && !coolDownInfo.amount.isZero() - const pastTokeCycleIndex = bnOrZero(requestedWithdrawals.minCycle).lte(currentCycleIndex) - const stakingTokenAvailableWithTokemak = bnOrZero(requestedWithdrawals.amount).plus( - withdrawalAmount, - ) - const stakingTokenAvailable = bnOrZero(withdrawalAmount).gte(coolDownInfo.amount) + const pastTokeCycleIndex = requestedWithdrawals.minCycle.lte(currentCycleIndex) + const stakingTokenAvailableWithTokemak = requestedWithdrawals.amount.add(withdrawalAmount) + const stakingTokenAvailable = withdrawalAmount.gte(coolDownInfo.amount) const validCycleAndAmount = (pastTokeCycleIndex && stakingTokenAvailableWithTokemak.gte(coolDownInfo.amount)) || stakingTokenAvailable @@ -627,32 +684,23 @@ export class FoxyApi { this.verifyAddresses([userAddress, contractAddress, addressToClaim]) if (!wallet) throw new Error('Missing inputs') - let estimatedGasBN: BigNumber - try { - estimatedGasBN = await this.estimateClaimWithdrawGas(input) - } catch (err) { - throw new Error(`Estimate Gas Error: ${err}`) - } + const estimatedFees = await this.estimateClaimWithdrawFees(input) const stakingContract = this.getStakingContract(contractAddress) const canClaim = await this.canClaimWithdraw({ userAddress, contractAddress }) if (!canClaim) throw new Error('Not ready to claim') - const data: string = stakingContract.methods.claimWithdraw(addressToClaim).encodeABI({ - from: userAddress, - }) + const data: string = stakingContract.interface.encodeFunctionData('claimWithdraw', [ + addressToClaim, + ]) - const { nonce, gasPrice } = await this.getGasPriceAndNonce(userAddress) - const estimatedGas = estimatedGasBN.toString() const chainReferenceAsNumber = Number(this.ethereumChainReference) const payload = { bip44Params, chainId: chainReferenceAsNumber, data, - estimatedGas, - gasPrice, - nonce, + estimatedFees, to: contractAddress, value: '0', } @@ -661,66 +709,70 @@ export class FoxyApi { async canSendWithdrawalRequest(input: StakingContract): Promise { const { stakingContract } = input - const tokeManagerContract = new this.web3.eth.Contract(tokeManagerAbi, tokeManagerAddress) + const tokeManagerContract = new ethers.Contract( + tokeManagerAddress, + tokeManagerAbi, + this.provider, + ) - const requestWithdrawalAmount = await (() => { + const requestWithdrawalAmount = await (async () => { try { - return stakingContract.methods.requestWithdrawalAmount().call() - } catch (err) { - logger.error(err, 'failed to get requestWithdrawalAmount') + return (await stakingContract.requestWithdrawalAmount()).toString() + } catch (e) { + logger.error(e, 'failed to get requestWithdrawalAmount') return 0 } })() - const timeLeftToRequestWithdrawal = await (() => { + const timeLeftToRequestWithdrawal: string = await (async () => { try { - return stakingContract.methods.timeLeftToRequestWithdrawal().call() - } catch (err) { - logger.error(err, 'failed to get timeLeftToRequestWithdrawal') - return 0 + return (await stakingContract.timeLeftToRequestWithdrawal()).toString() + } catch (e) { + logger.error(e, 'failed to get timeLeftToRequestWithdrawal') + return '0' } })() - const lastTokeCycleIndex = await (() => { + const lastTokeCycleIndex: string = await (async () => { try { - return stakingContract.methods.lastTokeCycleIndex().call() + return (await stakingContract.lastTokeCycleIndex()).toString() } catch (err) { logger.error(err, 'failed to get lastTokeCycleIndex') - return 0 + return '0' } })() - const duration = await (() => { + const duration: string = await (async () => { try { - return tokeManagerContract.methods.getCycleDuration().call() - } catch (err) { - logger.error(err, 'failed to get cycleDuration') - return 0 + return (await tokeManagerContract.getCycleDuration()).toString() + } catch (e) { + logger.error(e, 'failed to get cycleDuration') + return '0' } })() - const currentCycleIndex = await (() => { + const currentCycleIndex: string = await (async () => { try { - return tokeManagerContract.methods.getCurrentCycleIndex().call() - } catch (err) { - logger.error(err, 'failed to get currentCycleIndex') - return 0 + return (await tokeManagerContract.getCurrentCycleIndex()).toString() + } catch (e) { + logger.error(e, 'failed to get currentCycleIndex') + return '0' } })() - const currentCycleStart = await (() => { + const currentCycleStart: string = await (async () => { try { - return tokeManagerContract.methods.getCurrentCycle().call() - } catch (err) { - logger.error(err, 'failed to get currentCycle') - return 0 + return (await tokeManagerContract.getCurrentCycle()).toString() + } catch (e) { + logger.error(e, 'failed to get currentCycle') + return '0' } })() const nextCycleStart = bnOrZero(currentCycleStart).plus(duration) - const blockNumber = await this.web3.eth.getBlockNumber() - const timestamp = (await this.web3.eth.getBlock(blockNumber)).timestamp + const blockNumber = await this.provider.getBlockNumber() + const timestamp = (await this.provider.getBlock(blockNumber)).timestamp const isTimeToRequest = bnOrZero(timestamp) .plus(timeLeftToRequestWithdrawal) @@ -736,32 +788,20 @@ export class FoxyApi { this.verifyAddresses([userAddress, contractAddress]) if (!wallet || !contractAddress) throw new Error('Missing inputs') - let estimatedGasBN: BigNumber - try { - estimatedGasBN = await this.estimateSendWithdrawalRequestsGas(input) - } catch (err) { - throw new Error(`Estimate Gas Error: ${err}`) - } + const estimatedFees = await this.estimateSendWithdrawalRequestsFees(input) const stakingContract = this.getStakingContract(contractAddress) const canSendRequest = await this.canSendWithdrawalRequest({ stakingContract }) if (!canSendRequest) throw new Error('Not ready to send request') - const data: string = stakingContract.methods.sendWithdrawalRequests().encodeABI({ - from: userAddress, - }) - - const { nonce, gasPrice } = await this.getGasPriceAndNonce(userAddress) - const estimatedGas = estimatedGasBN.toString() + const data: string = stakingContract.interface.encodeFunctionData('sendWithdrawalRequests') const chainReferenceAsNumber = Number(this.ethereumChainReference) const payload = { bip44Params, chainId: chainReferenceAsNumber, data, - estimatedGas, - gasPrice, - nonce, + estimatedFees, to: contractAddress, value: '0', } @@ -784,31 +824,19 @@ export class FoxyApi { if (!wallet) throw new Error('Missing inputs') - let estimatedGasBN: BigNumber - try { - estimatedGasBN = await this.estimateAddLiquidityGas(input) - } catch (err) { - throw new Error(`Estimate Gas Error: ${err}`) - } - + const estimatedFees = await this.estimateAddLiquidityFees(input) const liquidityReserveContract = this.getLiquidityReserveContract(contractAddress) - const data: string = liquidityReserveContract.methods - .addLiquidity(this.normalizeAmount(amountDesired)) - .encodeABI({ - from: userAddress, - }) + const data: string = liquidityReserveContract.interface.encodeFunctionData('addLiquidity', [ + this.normalizeAmount(amountDesired), + ]) - const { nonce, gasPrice } = await this.getGasPriceAndNonce(userAddress) - const estimatedGas = estimatedGasBN.toString() const chainReferenceAsNumber = Number(this.ethereumChainReference) const payload = { bip44Params, chainId: chainReferenceAsNumber, data, - estimatedGas, - gasPrice, - nonce, + estimatedFees, to: contractAddress, value: '0', } @@ -830,31 +858,20 @@ export class FoxyApi { if (!amountDesired.gt(0)) throw new Error('Must send valid amount') if (!wallet) throw new Error('Missing inputs') - let estimatedGasBN: BigNumber - try { - estimatedGasBN = await this.estimateRemoveLiquidityGas(input) - } catch (err) { - throw new Error(`Estimate Gas Error: ${err}`) - } + const estimatedFees = await this.estimateRemoveLiquidityFees(input) const liquidityReserveContract = this.getLiquidityReserveContract(contractAddress) - const data: string = liquidityReserveContract.methods - .removeLiquidity(this.normalizeAmount(amountDesired)) - .encodeABI({ - from: userAddress, - }) + const data: string = liquidityReserveContract.interface.encodeFunctionData('removeLiquidity', [ + this.normalizeAmount(amountDesired), + ]) - const { nonce, gasPrice } = await this.getGasPriceAndNonce(userAddress) - const estimatedGas = estimatedGasBN.toString() const chainReferenceAsNumber = Number(this.ethereumChainReference) const payload = { bip44Params, chainId: chainReferenceAsNumber, data, - estimatedGas, - gasPrice, - nonce, + estimatedFees, to: contractAddress, value: '0', } @@ -870,32 +887,34 @@ export class FoxyApi { let coolDownInfo try { - const coolDown = await stakingContract.methods.coolDownInfo(userAddress).call() + const coolDown = await stakingContract.coolDownInfo(userAddress) coolDownInfo = { ...coolDown, endEpoch: coolDown.expiry, } - } catch (err) { - throw new Error(`Failed to get coolDowninfo: ${err}`) + } catch (e) { + throw new Error(`Failed to get coolDowninfo: ${e}`) } let epoch try { - epoch = await stakingContract.methods.epoch().call() - } catch (err) { - throw new Error(`Failed to get epoch: ${err}`) + epoch = await stakingContract.epoch() + } catch (e) { + throw new Error(`Failed to get epoch: ${e}`) } let currentBlock try { - currentBlock = await this.web3.eth.getBlockNumber() - } catch (err) { - throw new Error(`Failed to get block number: ${err}`) + currentBlock = await this.provider.getBlockNumber() + } catch (e) { + throw new Error(`Failed to get block number: ${e}`) } - const epochsLeft = bnOrZero(coolDownInfo.endEpoch).minus(epoch.number) // epochs left until can claim + const epochsLeft = bnOrZero(coolDownInfo.endEpoch.toString()).minus(epoch.number.toString()) // epochs left until can claim const blocksLeftInCurrentEpoch = - epochsLeft.gt(0) && epoch.endBlock > currentBlock ? epoch.endBlock - currentBlock : 0 // calculate time remaining in current epoch + epochsLeft.gt(0) && epoch.endBlock.gt(currentBlock) + ? epoch.endBlock.sub(currentBlock).toString() + : '0' // calculate time remaining in current epoch const blocksLeftInFutureEpochs = epochsLeft.minus(1).gt(0) - ? epochsLeft.minus(1).times(epoch.length) - : 0 // don't count current epoch + ? epochsLeft.minus(1).times(epoch.length).toString() + : '0' // don't count current epoch const blocksUntilClaimable = bnOrZero(blocksLeftInCurrentEpoch).plus(blocksLeftInFutureEpochs) // total blocks left until can claim const secondsUntilClaimable = blocksUntilClaimable.times(13) // average block time is 13 seconds to get total seconds const currentDate = new Date() @@ -908,12 +927,12 @@ export class FoxyApi { const { tokenContractAddress, userAddress } = input this.verifyAddresses([userAddress, tokenContractAddress]) - const contract = new this.web3.eth.Contract(erc20Abi, tokenContractAddress) + const contract = new ethers.Contract(tokenContractAddress, erc20Abi, this.provider) try { - const balance = await contract.methods.balanceOf(userAddress).call() - return bnOrZero(balance) - } catch (err) { - throw new Error(`Failed to get balance: ${err}`) + const balance = await contract.balanceOf(userAddress) + return bnOrZero(balance.toString()) + } catch (e) { + throw new Error(`Failed to get balance: ${e}`) } } @@ -924,28 +943,30 @@ export class FoxyApi { let liquidityReserveAddress try { - liquidityReserveAddress = await stakingContract.methods.LIQUIDITY_RESERVE().call() - } catch (err) { - throw new Error(`Failed to get liquidityReserve address ${err}`) + liquidityReserveAddress = await stakingContract.LIQUIDITY_RESERVE() + } catch (e) { + throw new Error(`Failed to get liquidityReserve address ${e}`) } const liquidityReserveContract = this.getLiquidityReserveContract(liquidityReserveAddress) try { - const feeInBasisPoints = await liquidityReserveContract.methods.fee().call() - return bnOrZero(feeInBasisPoints).div(10000) // convert from basis points to decimal percentage - } catch (err) { - throw new Error(`Failed to get instantUnstake fee ${err}`) + // ethers BigNumber doesn't support floats, so we have to convert it to a regular bn first + // to be able to get a float bignumber.js as an output + const feeInBasisPoints = bnOrZero((await liquidityReserveContract.fee()).toString()) + return feeInBasisPoints.div(10000) // convert from basis points to decimal percentage + } catch (e) { + throw new Error(`Failed to get instantUnstake fee ${e}`) } } async totalSupply({ tokenContractAddress }: TokenAddressInput): Promise { this.verifyAddresses([tokenContractAddress]) - const contract = new this.web3.eth.Contract(erc20Abi, tokenContractAddress) + const contract = new ethers.Contract(tokenContractAddress, erc20Abi, this.provider) try { - const totalSupply = await contract.methods.totalSupply().call() - return bnOrZero(totalSupply) - } catch (err) { - throw new Error(`Failed to get totalSupply: ${err}`) + const totalSupply = await contract.totalSupply() + return bnOrZero(totalSupply.toString()) + } catch (e) { + throw new Error(`Failed to get totalSupply: ${e}`) } } @@ -961,13 +982,13 @@ export class FoxyApi { async tvl(input: TokenAddressInput): Promise { const { tokenContractAddress } = input this.verifyAddresses([tokenContractAddress]) - const contract = new this.web3.eth.Contract(foxyAbi, tokenContractAddress) + const contract = new ethers.Contract(tokenContractAddress, foxyAbi, this.provider) try { - const balance = await contract.methods.circulatingSupply().call() - return bnOrZero(balance) - } catch (err) { - throw new Error(`Failed to get tvl: ${err}`) + const balance = await contract.circulatingSupply() + return bnOrZero(balance.toString()) + } catch (e) { + throw new Error(`Failed to get tvl: ${e}`) } } @@ -976,39 +997,39 @@ export class FoxyApi { this.verifyAddresses([userAddress, contractAddress]) const stakingContract = this.getStakingContract(contractAddress) - let coolDownInfo - try { - coolDownInfo = await stakingContract.methods.coolDownInfo(userAddress).call() - } catch (err) { - throw new Error(`Failed to get coolDowninfo: ${err}`) - } - let releaseTime - try { - releaseTime = await this.getTimeUntilClaimable(input) - } catch (err) { - throw new Error(`Failed to getTimeUntilClaimable: ${err}`) - } + const coolDownInfo: [amount: string, gons: string, expiry: string] = ( + await stakingContract.coolDownInfo(userAddress) + ).map((info: ethers.BigNumber) => info.toString()) + const releaseTime = await this.getTimeUntilClaimable(input) + + const [amount, gons, expiry] = coolDownInfo return { - ...coolDownInfo, + amount, + gons, + expiry, releaseTime, } } async getClaimFromTokemakArgs(input: ContractAddressInput): Promise { const { contractAddress } = input - const rewardHashContract = new this.web3.eth.Contract(tokeRewardHashAbi, tokeRewardHashAddress) + const rewardHashContract = new ethers.Contract( + tokeRewardHashAddress, + tokeRewardHashAbi, + this.provider, + ) const latestCycleIndex = await (() => { try { - return rewardHashContract.methods.latestCycleIndex().call() - } catch (err) { - throw new Error(`Failed to get latestCycleIndex, ${err}`) + return rewardHashContract.latestCycleIndex() + } catch (e) { + throw new Error(`Failed to get latestCycleIndex, ${e}`) } })() const cycleHashes = await (() => { try { - return rewardHashContract.methods.cycleHashes(latestCycleIndex).call() - } catch (err) { - throw new Error(`Failed to get latestCycleIndex, ${err}`) + return rewardHashContract.cycleHashes(latestCycleIndex) + } catch (e) { + throw new Error(`Failed to get latestCycleIndex, ${e}`) } })() @@ -1030,8 +1051,8 @@ export class FoxyApi { s, recipient: payload, } - } catch (err) { - throw new Error(`Failed to get information from Tokemak ipfs ${err}`) + } catch (e) { + throw new Error(`Failed to get information from Tokemak ipfs ${e}`) } } @@ -1039,20 +1060,19 @@ export class FoxyApi { const { tokenContractAddress, userAddress } = input this.verifyAddresses([tokenContractAddress]) - const foxyContract = new this.web3.eth.Contract(foxyAbi, tokenContractAddress) + const foxyContract = new ethers.Contract(tokenContractAddress, foxyAbi, this.provider) const fromBlock = 14381454 // genesis rebase const rebaseEvents = await (async () => { try { - const events = ( - await foxyContract.getPastEvents('LogRebase', { - fromBlock, - toBlock: 'latest', - }) - ).filter(rebase => rebase.returnValues.rebase !== '0') - return events - } catch (err) { - logger.error(err, 'failed to get rebase events') + const filter = foxyContract.filters.LogRebase() + const events = await foxyContract.queryFilter(filter, fromBlock, 'latest') + const filteredEvents = events.filter( + rebase => rebase.args?.rebase && !rebase.args.rebase.isZero(), + ) + return filteredEvents + } catch (e) { + logger.error(e, 'failed to get rebase events') return undefined } })() @@ -1061,22 +1081,17 @@ export class FoxyApi { const transferEvents = await (async () => { try { - const events = await foxyContract.getPastEvents('Transfer', { - fromBlock, - toBlock: 'latest', - }) + const filter = foxyContract.filters.Transfer() + const events = await foxyContract.queryFilter(filter, fromBlock, 'latest') return events - } catch (err) { - logger.error(err, 'failed to get transfer events') + } catch (e) { + logger.error(e, 'failed to get transfer events') return undefined } })() const events: RebaseEvent[] = rebaseEvents.map(rebaseEvent => { - const { - blockNumber, - returnValues: { epoch }, - } = rebaseEvent + const { blockNumber, args: { epoch } = { epoch: '' } } = rebaseEvent return { blockNumber, epoch, @@ -1097,36 +1112,33 @@ export class FoxyApi { // check transfer events to see if a user triggered a rebase through unstake or stake const unstakedTransferInfo = transferEvents?.filter( e => - e.blockNumber === event.blockNumber && - e.returnValues.from.toLowerCase() === userAddress, + e.blockNumber === event.blockNumber && e.args?.from.toLowerCase() === userAddress, ) - const unstakedTransferAmount = unstakedTransferInfo?.[0]?.returnValues?.value ?? 0 + const unstakedTransferAmount = unstakedTransferInfo?.[0]?.args?.value ?? 0 const stakedTransferInfo = transferEvents?.filter( - e => - e.blockNumber === event.blockNumber && - e.returnValues.to.toLowerCase() === userAddress, + e => e.blockNumber === event.blockNumber && e.args?.to.toLowerCase() === userAddress, ) - const stakedTransferAmount = stakedTransferInfo?.[0]?.returnValues?.value ?? 0 + const stakedTransferAmount = stakedTransferInfo?.[0]?.args?.value ?? 0 - const postRebaseBalanceResult = await foxyContract.methods - .balanceOf(userAddress) - .call(null, event.blockNumber) - const unadjustedPreRebaseBalance = await foxyContract.methods - .balanceOf(userAddress) - .call(null, event.blockNumber - 1) + const postRebaseBalanceResult = await foxyContract.balanceOf(userAddress, { + blockTag: event.blockNumber, + }) + const unadjustedPreRebaseBalance = await foxyContract.balanceOf(userAddress, { + blockTag: event.blockNumber - 1, + }) // unstake events can trigger rebases, if they do, adjust the amount to not include that unstake's transfer amount - const preRebaseBalanceResult = bnOrZero(unadjustedPreRebaseBalance) - .minus(unstakedTransferAmount) - .plus(stakedTransferAmount) + const preRebaseBalanceResult = bnOrZero(unadjustedPreRebaseBalance.toString()) + .minus(unstakedTransferAmount.toString()) + .plus(stakedTransferAmount.toString()) .toString() return { preRebaseBalance: preRebaseBalanceResult, - postRebaseBalance: postRebaseBalanceResult, + postRebaseBalance: postRebaseBalanceResult.toString() as string, } - } catch (err) { - logger.error(err, 'failed to get balance of address') + } catch (e) { + logger.error(e, 'failed to get balance of address') return { preRebaseBalance: bn(0).toString(), postRebaseBalance: bn(0).toString(), @@ -1136,10 +1148,10 @@ export class FoxyApi { const blockTime = await (async () => { try { - const block = await this.web3.eth.getBlock(event.blockNumber) + const block = await this.provider.getBlock(event.blockNumber) return bnOrZero(block.timestamp).toNumber() - } catch (err) { - logger.error(err, 'failed to get timestamp of block') + } catch (e) { + logger.error(e, 'failed to get timestamp of block') return 0 } })() diff --git a/packages/investor-foxy/src/api/foxy-types.ts b/packages/investor-foxy/src/api/foxy-types.ts index ef81dde1806..ce166d5ef06 100644 --- a/packages/investor-foxy/src/api/foxy-types.ts +++ b/packages/investor-foxy/src/api/foxy-types.ts @@ -1,8 +1,9 @@ import type { AssetId } from '@shapeshiftoss/caip' -import type { HDWallet } from '@shapeshiftoss/hdwallet-core' -import type { BIP44Params, WithdrawType } from '@shapeshiftoss/types' +import type { FeeDataEstimate } from '@shapeshiftoss/chain-adapters' +import type { ETHWallet } from '@shapeshiftoss/hdwallet-core' +import type { BIP44Params, KnownChainIds, WithdrawType } from '@shapeshiftoss/types' import type { BigNumber } from 'bignumber.js' -import type { Contract } from 'web3-eth-contract' +import type { Contract } from 'ethers' export type FoxyAddressesType = { staking: string @@ -27,10 +28,10 @@ export type ApproveInput = { tokenContractAddress: string contractAddress: string userAddress: string - wallet: HDWallet + wallet: ETHWallet } -export type EstimateGasApproveInput = Pick< +export type EstimateApproveFeesInput = Pick< ApproveInput, 'userAddress' | 'tokenContractAddress' | 'contractAddress' > @@ -41,7 +42,7 @@ export type TxInput = { tokenContractAddress?: string userAddress: string contractAddress: string - wallet: HDWallet + wallet: ETHWallet amountDesired: BigNumber } @@ -57,7 +58,7 @@ export type WithdrawInput = Omit & { amountDesired?: BigNumber } -export type WithdrawEstimateGasInput = Omit +export type EstimateWithdrawFeesInput = Omit export type FoxyOpportunityInputData = { tvl: BigNumber @@ -69,7 +70,7 @@ export type FoxyOpportunityInputData = { liquidityReserve: string } -export type EstimateGasTxInput = Pick< +export type EstimateFeesTxInput = Pick< TxInput, 'tokenContractAddress' | 'contractAddress' | 'userAddress' | 'amountDesired' > @@ -102,16 +103,14 @@ export type SignAndBroadcastPayload = { bip44Params: BIP44Params chainId: number data: string - estimatedGas: string - gasPrice: string - nonce: string + estimatedFees: FeeDataEstimate to: string value: string } export type SignAndBroadcastTx = { payload: SignAndBroadcastPayload - wallet: HDWallet + wallet: ETHWallet dryRun: boolean } diff --git a/packages/investor-foxy/src/utils/buildTxToSign.ts b/packages/investor-foxy/src/utils/buildTxToSign.ts deleted file mode 100644 index 37c3843b67f..00000000000 --- a/packages/investor-foxy/src/utils/buildTxToSign.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { toAddressNList } from '@shapeshiftoss/chain-adapters' -import type { ETHSignTx } from '@shapeshiftoss/hdwallet-core' -import type { BIP44Params } from '@shapeshiftoss/types' -import { numberToHex } from 'web3-utils' - -type BuildTxToSignInput = { - bip44Params: BIP44Params - chainId: number - data: string - estimatedGas: string - gasPrice: string - nonce: string - value: string - to: string -} - -export const buildTxToSign = ({ - bip44Params, - chainId = 1, - data, - estimatedGas, - gasPrice, - nonce, - to, - value, -}: BuildTxToSignInput): ETHSignTx => ({ - addressNList: toAddressNList(bip44Params), - value: numberToHex(value), - to, - chainId, // TODO: implement for multiple chains - data, - nonce: numberToHex(nonce), - gasPrice: numberToHex(gasPrice), - gasLimit: numberToHex(estimatedGas), -}) diff --git a/packages/investor-foxy/src/utils/index.ts b/packages/investor-foxy/src/utils/index.ts index dfd9ab73da5..fcd8f6c533e 100644 --- a/packages/investor-foxy/src/utils/index.ts +++ b/packages/investor-foxy/src/utils/index.ts @@ -1,2 +1 @@ export * from './bignumber' -export * from './buildTxToSign' diff --git a/packages/investor-idle/package.json b/packages/investor-idle/package.json index 6c498e03ec4..62d528edaa0 100644 --- a/packages/investor-idle/package.json +++ b/packages/investor-idle/package.json @@ -21,7 +21,6 @@ "cli": "yarn build && yarn node dist/idlecli.js" }, "dependencies": { - "@ethersproject/providers": "^5.5.3", "@shapeshiftoss/caip": "workspace:^", "@shapeshiftoss/chain-adapters": "workspace:^", "@shapeshiftoss/investor": "workspace:^", diff --git a/packages/investor-yearn/package.json b/packages/investor-yearn/package.json index 3ff0b9af293..94156bb281e 100644 --- a/packages/investor-yearn/package.json +++ b/packages/investor-yearn/package.json @@ -21,7 +21,6 @@ "cli": "yarn build && yarn node dist/yearncli.js" }, "dependencies": { - "@ethersproject/providers": "^5.5.3", "@shapeshiftoss/caip": "workspace:^", "@shapeshiftoss/chain-adapters": "workspace:^", "@shapeshiftoss/investor": "workspace:^", diff --git a/packages/investor-yearn/src/YearnInvestor.ts b/packages/investor-yearn/src/YearnInvestor.ts index f31329da6ab..f0dc75c1497 100644 --- a/packages/investor-yearn/src/YearnInvestor.ts +++ b/packages/investor-yearn/src/YearnInvestor.ts @@ -1,8 +1,8 @@ -import { JsonRpcProvider } from '@ethersproject/providers' import type { ChainAdapter } from '@shapeshiftoss/chain-adapters' import type { Investor } from '@shapeshiftoss/investor' import type { KnownChainIds } from '@shapeshiftoss/types' import { type ChainId, type VaultMetadata, Yearn } from '@yfi/sdk' +import { ethers } from 'ethers' import { find } from 'lodash' import filter from 'lodash/filter' import Web3 from 'web3' @@ -31,7 +31,7 @@ export class YearnInvestor implements Investor implements SubParser { - provider: ethers.providers.JsonRpcProvider + provider: ethers.providers.JsonRpcBatchProvider readonly chainId: ChainId readonly abiInterface = new ethers.utils.Interface(bep20) diff --git a/packages/unchained-client/src/evm/ethereum/parser/uniV2.ts b/packages/unchained-client/src/evm/ethereum/parser/uniV2.ts index 4006b88c755..02929d69afb 100644 --- a/packages/unchained-client/src/evm/ethereum/parser/uniV2.ts +++ b/packages/unchained-client/src/evm/ethereum/parser/uniV2.ts @@ -23,12 +23,11 @@ export interface TxMetadata extends BaseTxMetadata { export interface ParserArgs { chainId: ChainId - provider: ethers.providers.JsonRpcProvider + provider: ethers.providers.JsonRpcBatchProvider } export class Parser implements SubParser { - provider: ethers.providers.JsonRpcProvider - + provider: ethers.providers.JsonRpcBatchProvider readonly chainId: ChainId readonly wethContract: string readonly abiInterface = new ethers.utils.Interface(UNIV2_ABI) diff --git a/packages/unchained-client/src/evm/ethereum/parser/weth.ts b/packages/unchained-client/src/evm/ethereum/parser/weth.ts index 1310dd86e12..704cc270d00 100644 --- a/packages/unchained-client/src/evm/ethereum/parser/weth.ts +++ b/packages/unchained-client/src/evm/ethereum/parser/weth.ts @@ -16,12 +16,11 @@ export interface TxMetadata extends BaseTxMetadata { export interface ParserArgs { chainId: ChainId - provider: ethers.providers.JsonRpcProvider + provider: ethers.providers.JsonRpcBatchProvider } export class Parser implements SubParser { - provider: ethers.providers.JsonRpcProvider - + provider: ethers.providers.JsonRpcBatchProvider readonly chainId: ChainId readonly wethContract: string readonly abiInterface = new ethers.utils.Interface(WETH_ABI) diff --git a/packages/unchained-client/src/evm/parser/erc20.ts b/packages/unchained-client/src/evm/parser/erc20.ts index 7f799af86a7..51a17cfa4b6 100644 --- a/packages/unchained-client/src/evm/parser/erc20.ts +++ b/packages/unchained-client/src/evm/parser/erc20.ts @@ -16,11 +16,11 @@ export interface TxMetadata extends BaseTxMetadata { interface ParserArgs { chainId: ChainId - provider: ethers.providers.JsonRpcProvider + provider: ethers.providers.JsonRpcBatchProvider } export class Parser implements SubParser { - provider: ethers.providers.JsonRpcProvider + provider: ethers.providers.JsonRpcBatchProvider readonly chainId: ChainId readonly abiInterface = new ethers.utils.Interface(ERC20_ABI) diff --git a/packages/unchained-client/src/evm/parser/index.ts b/packages/unchained-client/src/evm/parser/index.ts index f869b80c4df..982c8f1e801 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 @@ -27,14 +21,13 @@ export class BaseTransactionParser { chainId: ChainId assetId: AssetId - protected readonly provider: ethers.providers.JsonRpcProvider - + protected readonly provider: ethers.providers.JsonRpcBatchProvider private parsers: SubParser[] = [] constructor(args: TransactionParserArgs) { this.chainId = args.chainId this.assetId = args.assetId - this.provider = new ethers.providers.JsonRpcProvider(args.rpcUrl) + this.provider = new ethers.providers.JsonRpcBatchProvider(args.rpcUrl) } /** @@ -170,7 +163,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..9696dae278f 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,46 @@ 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, + }, + }) + + 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 +221,8 @@ export const useFoxFarming = ( from: farmingAccountAddress, }, }) - return estimatedFees + + return getFeeDataFromEstimate(feeData) }, [ adapter, @@ -324,16 +235,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 +255,8 @@ export const useFoxFarming = ( from: farmingAccountAddress, }, }) - return estimatedFees + + return getFeeDataFromEstimate(feeData) }, [ adapter, @@ -354,91 +269,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 +343,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/foxy/components/FoxyManager/Deposit/FoxyDeposit.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Deposit/FoxyDeposit.tsx index 25414b8f7f1..ba4c110db25 100644 --- a/src/features/defi/providers/foxy/components/FoxyManager/Deposit/FoxyDeposit.tsx +++ b/src/features/defi/providers/foxy/components/FoxyManager/Deposit/FoxyDeposit.tsx @@ -2,6 +2,7 @@ import { Center } from '@chakra-ui/react' import type { AccountId } from '@shapeshiftoss/caip' import { toAssetId } from '@shapeshiftoss/caip' import { KnownChainIds } from '@shapeshiftoss/types' +import { ethers } from 'ethers' import { DefiModalContent } from 'features/defi/components/DefiModal/DefiModalContent' import { DefiModalHeader } from 'features/defi/components/DefiModal/DefiModalHeader' import type { @@ -53,9 +54,14 @@ export const FoxyDeposit: React.FC<{ const translate = useTranslate() const [state, dispatch] = useReducer(reducer, initialState) const { query, history, location } = useBrowserRouter() - const { chainId, contractAddress, assetReference, assetNamespace } = query + const { + chainId, + contractAddress: foxyContractAddress, + assetReference: foxyStakingContractAddress, + assetNamespace, + } = query // ContractAssetId - const assetId = toAssetId({ chainId, assetNamespace, assetReference }) + const assetId = toAssetId({ chainId, assetNamespace, assetReference: foxyStakingContractAddress }) const opportunityMetadataFilter = useMemo(() => ({ stakingId: assetId as StakingId }), [assetId]) const opportunityMetadata = useAppSelector(state => @@ -83,7 +89,7 @@ export const FoxyDeposit: React.FC<{ if ( !( walletState.wallet && - contractAddress && + foxyStakingContractAddress && !isFoxyAprLoading && chainAdapter && foxyApi && @@ -91,7 +97,9 @@ export const FoxyDeposit: React.FC<{ ) ) return - const foxyOpportunity = await foxyApi.getFoxyOpportunityByStakingAddress(contractAddress) + const foxyOpportunity = await foxyApi.getFoxyOpportunityByStakingAddress( + ethers.utils.getAddress(foxyStakingContractAddress), + ) dispatch({ type: FoxyDepositActionType.SET_OPPORTUNITY, payload: { ...foxyOpportunity, apy: foxyAprData?.foxyApr ?? '' }, @@ -105,10 +113,11 @@ export const FoxyDeposit: React.FC<{ foxyApi, bip44Params, chainAdapterManager, - contractAddress, + foxyContractAddress, walletState.wallet, foxyAprData?.foxyApr, isFoxyAprLoading, + foxyStakingContractAddress, ]) const handleBack = () => { @@ -136,7 +145,7 @@ export const FoxyDeposit: React.FC<{ label: translate('defi.steps.approve.title'), component: ownProps => , props: { - contractAddress, + contractAddress: foxyContractAddress, }, }, [DefiStep.Confirm]: { @@ -148,7 +157,7 @@ export const FoxyDeposit: React.FC<{ component: Status, }, } - }, [accountId, handleAccountIdChange, contractAddress, translate, stakingAsset.symbol]) + }, [accountId, handleAccountIdChange, foxyContractAddress, translate, stakingAsset.symbol]) if (loading || !stakingAsset || !marketData) { return ( diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Approve.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Approve.tsx index a65168ab6fc..c30474b284d 100644 --- a/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Approve.tsx +++ b/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Approve.tsx @@ -1,6 +1,7 @@ import { useToast } from '@chakra-ui/react' import type { AccountId } from '@shapeshiftoss/caip' import { fromAccountId } from '@shapeshiftoss/caip' +import { supportsETH } from '@shapeshiftoss/hdwallet-core' import { Approve as ReusableApprove } from 'features/defi/components/Approve/Approve' import { ApprovePreFooter } from 'features/defi/components/Approve/ApprovePreFooter' import type { DepositValues } from 'features/defi/components/Deposit/Deposit' @@ -68,17 +69,18 @@ export const Approve: React.FC = ({ accountId, onNext }) => { async (deposit: DepositValues) => { if (!accountAddress || !assetReference || !foxyApi) return try { - const [gasLimit, gasPrice] = await Promise.all([ - foxyApi.estimateDepositGas({ - tokenContractAddress: assetReference, - contractAddress, - amountDesired: bnOrZero(deposit.cryptoAmount) - .times(bn(10).pow(asset.precision)) - .decimalPlaces(0), - userAddress: accountAddress, - }), - foxyApi.getGasPrice(), - ]) + const feeDataEstimate = await foxyApi.estimateDepositFees({ + tokenContractAddress: assetReference, + contractAddress, + amountDesired: bnOrZero(deposit.cryptoAmount) + .times(bn(10).pow(asset.precision)) + .decimalPlaces(0), + userAddress: accountAddress, + }) + + const { + chainSpecific: { gasPrice, gasLimit }, + } = feeDataEstimate.fast return bnOrZero(gasPrice).times(gasLimit).toFixed(0) } catch (error) { moduleLogger.error( @@ -111,6 +113,10 @@ export const Approve: React.FC = ({ accountId, onNext }) => { return try { dispatch({ type: FoxyDepositActionType.SET_LOADING, payload: true }) + + if (!supportsETH(walletState.wallet)) + throw new Error(`handleApprove: wallet does not support ethereum`) + await foxyApi.approve({ tokenContractAddress: assetReference, contractAddress, diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Confirm.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Confirm.tsx index ccea6722c07..d9b89ad4e2c 100644 --- a/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Confirm.tsx +++ b/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Confirm.tsx @@ -1,6 +1,8 @@ import { Alert, AlertIcon, Box, Stack, useToast } from '@chakra-ui/react' import type { AccountId } from '@shapeshiftoss/caip' import { fromAccountId } from '@shapeshiftoss/caip' +import { supportsETH } from '@shapeshiftoss/hdwallet-core' +import type { ethers } from 'ethers' import { Confirm as ReusableConfirm } from 'features/defi/components/Confirm/Confirm' import { Summary } from 'features/defi/components/Summary' import { DefiStep } from 'features/defi/contexts/DefiManagerProvider/DefiCommon' @@ -8,7 +10,6 @@ import { useFoxyQuery } from 'features/defi/providers/foxy/components/FoxyManage import isNil from 'lodash/isNil' import { useCallback, useContext, useMemo } from 'react' import { useTranslate } from 'react-polyglot' -import type { TransactionReceipt } from 'web3-core/types' import { Amount } from 'components/Amount/Amount' import { AssetIcon } from 'components/AssetIcon' import type { StepComponentProps } from 'components/DeFi/components/Steps' @@ -81,33 +82,36 @@ export const Confirm: React.FC = ({ onNext, accountId }) => { return try { dispatch({ type: FoxyDepositActionType.SET_LOADING, payload: true }) - const [txid, gasPrice] = await Promise.all([ - foxyApi.deposit({ - amountDesired: bnOrZero(state?.deposit.cryptoAmount) - .times(bn(10).pow(asset.precision)) - .decimalPlaces(0), - tokenContractAddress: assetReference, - userAddress: accountAddress, - contractAddress, - wallet: walletState.wallet, - bip44Params, - }), - foxyApi.getGasPrice(), - ]) + + if (!supportsETH(walletState.wallet)) + throw new Error(`handleDeposit: wallet does not support ethereum`) + + const txid = await foxyApi.deposit({ + amountDesired: bnOrZero(state?.deposit.cryptoAmount) + .times(bn(10).pow(asset.precision)) + .decimalPlaces(0), + tokenContractAddress: assetReference, + userAddress: accountAddress, + contractAddress, + wallet: walletState.wallet, + bip44Params, + }) dispatch({ type: FoxyDepositActionType.SET_TXID, payload: txid }) onNext(DefiStep.Status) const transactionReceipt = await poll({ fn: () => foxyApi.getTxReceipt({ txid }), - validate: (result: TransactionReceipt) => !isNil(result), + validate: (result: ethers.providers.TransactionReceipt) => !isNil(result), interval: 15000, maxAttempts: 30, }) dispatch({ type: FoxyDepositActionType.SET_DEPOSIT, payload: { - txStatus: transactionReceipt.status === true ? 'success' : 'failed', - usedGasFeeCryptoBaseUnit: bnOrZero(gasPrice).times(transactionReceipt.gasUsed).toFixed(0), + txStatus: transactionReceipt.status ? 'success' : 'failed', + usedGasFeeCryptoBaseUnit: transactionReceipt.effectiveGasPrice + .mul(transactionReceipt.gasUsed) + .toString(), }, }) } catch (error) { diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Deposit.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Deposit.tsx index 2ba9a77c8c8..03082cb70aa 100644 --- a/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Deposit.tsx +++ b/src/features/defi/providers/foxy/components/FoxyManager/Deposit/components/Deposit.tsx @@ -69,14 +69,16 @@ export const Deposit: React.FC = ({ const getApproveGasEstimate = async () => { if (!accountAddress || !assetReference || !foxyApi) return try { - const [gasLimit, gasPrice] = await Promise.all([ - foxyApi.estimateApproveGas({ - tokenContractAddress: assetReference, - contractAddress, - userAddress: accountAddress, - }), - foxyApi.getGasPrice(), - ]) + const feeDataEstimate = await foxyApi.estimateApproveFees({ + tokenContractAddress: assetReference, + contractAddress, + userAddress: accountAddress, + }) + + const { + chainSpecific: { gasPrice, gasLimit }, + } = feeDataEstimate.fast + return bnOrZero(gasPrice).times(gasLimit).toFixed(0) } catch (error) { moduleLogger.error( @@ -95,17 +97,19 @@ export const Deposit: React.FC = ({ const getDepositGasEstimateCryptoBaseUnit = async (deposit: DepositValues) => { if (!accountAddress || !assetReference || !foxyApi) return try { - const [gasLimit, gasPrice] = await Promise.all([ - foxyApi.estimateDepositGas({ - tokenContractAddress: assetReference, - contractAddress, - amountDesired: bnOrZero(deposit.cryptoAmount) - .times(`1e+${asset.precision}`) - .decimalPlaces(0), - userAddress: accountAddress, - }), - foxyApi.getGasPrice(), - ]) + const feeDataEstimate = await foxyApi.estimateDepositFees({ + tokenContractAddress: assetReference, + contractAddress, + amountDesired: bnOrZero(deposit.cryptoAmount) + .times(`1e+${asset.precision}`) + .decimalPlaces(0), + userAddress: accountAddress, + }) + + const { + chainSpecific: { gasPrice, gasLimit }, + } = feeDataEstimate.fast + return bnOrZero(gasPrice).times(gasLimit).toFixed(0) } catch (error) { moduleLogger.error( diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimConfirm.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimConfirm.tsx index 8ed9f9f8a69..e5feb757e27 100644 --- a/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimConfirm.tsx +++ b/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimConfirm.tsx @@ -9,8 +9,10 @@ import { useToast, } from '@chakra-ui/react' import type { AccountId, AssetId, ChainId } from '@shapeshiftoss/caip' -import { ASSET_REFERENCE, toAssetId } from '@shapeshiftoss/caip' +import { ASSET_NAMESPACE, ASSET_REFERENCE, toAssetId } from '@shapeshiftoss/caip' +import { supportsETH } from '@shapeshiftoss/hdwallet-core' import { KnownChainIds } from '@shapeshiftoss/types' +import dayjs from 'dayjs' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslate } from 'react-polyglot' import { useHistory } from 'react-router' @@ -25,16 +27,22 @@ import { useWallet } from 'hooks/useWallet/useWallet' import { bnOrZero } from 'lib/bignumber/bignumber' import { logger } from 'lib/logger' import { getFoxyApi } from 'state/apis/foxy/foxyApiSingleton' +import type { StakingId } from 'state/slices/opportunitiesSlice/types' +import { + serializeUserStakingId, + supportsUndelegations, +} from 'state/slices/opportunitiesSlice/utils' import { selectAssetById, selectBIP44ParamsByAccountId, + selectEarnUserStakingOpportunityByUserStakingId, selectMarketDataById, } from 'state/slices/selectors' import { useAppSelector } from 'state/store' type ClaimConfirmProps = { accountId: AccountId | undefined - assetId: AssetId + stakingAssetId: AssetId amount?: string contractAddress: string chainId: ChainId @@ -47,7 +55,7 @@ const moduleLogger = logger.child({ export const ClaimConfirm = ({ accountId, - assetId, + stakingAssetId, amount, contractAddress, chainId, @@ -56,7 +64,6 @@ export const ClaimConfirm = ({ const [userAddress, setUserAddress] = useState('') const [estimatedGas, setEstimatedGas] = useState('0') const [loading, setLoading] = useState(false) - const [canClaim, setCanClaim] = useState(false) const foxyApi = getFoxyApi() const { state: walletState } = useWallet() const translate = useTranslate() @@ -66,8 +73,8 @@ export const ClaimConfirm = ({ const chainAdapterManager = getChainAdapterManager() // Asset Info - const asset = useAppSelector(state => selectAssetById(state, assetId)) - const assetMarketData = useAppSelector(state => selectMarketDataById(state, assetId)) + const stakingAsset = useAppSelector(state => selectAssetById(state, stakingAssetId)) + const assetMarketData = useAppSelector(state => selectMarketDataById(state, stakingAssetId)) const feeAssetId = toAssetId({ chainId, assetNamespace: 'slip44', @@ -76,7 +83,7 @@ export const ClaimConfirm = ({ const feeAsset = useAppSelector(state => selectAssetById(state, feeAssetId)) const feeMarketData = useAppSelector(state => selectMarketDataById(state, feeAssetId)) - if (!asset) throw new Error(`Asset not found for AssetId ${assetId}`) + if (!stakingAsset) throw new Error(`Asset not found for AssetId ${stakingAssetId}`) if (!feeAsset) throw new Error(`Fee asset not found for AssetId ${feeAssetId}`) const toast = useToast() @@ -85,14 +92,49 @@ export const ClaimConfirm = ({ const bip44Params = useAppSelector(state => selectBIP44ParamsByAccountId(state, accountFilter)) const cryptoHumanBalance = useMemo( - () => bnOrZero(claimAmount).div(`1e+${asset.precision}`), - [asset.precision, claimAmount], + () => bnOrZero(claimAmount).div(`1e+${stakingAsset.precision}`), + [stakingAsset.precision, claimAmount], + ) + // The highest level AssetId/OpportunityId, in this case of the single FOXy contract + const assetId = toAssetId({ + chainId, + assetNamespace: ASSET_NAMESPACE.erc20, + assetReference: contractAddress, + }) + const opportunityDataFilter = useMemo(() => { + if (!accountId) return undefined + return { + userStakingId: serializeUserStakingId(accountId, assetId as StakingId), + } + }, [accountId, assetId]) + + const foxyEarnOpportunityData = useAppSelector(state => + opportunityDataFilter + ? selectEarnUserStakingOpportunityByUserStakingId(state, opportunityDataFilter) + : undefined, + ) + + const undelegations = useMemo( + () => + foxyEarnOpportunityData && supportsUndelegations(foxyEarnOpportunityData) + ? foxyEarnOpportunityData.undelegations + : undefined, + [foxyEarnOpportunityData], + ) + + const hasPendingUndelegation = Boolean( + undelegations && + undelegations.some(undelegation => + dayjs().isAfter(dayjs(undelegation.completionTime).unix()), + ), ) const handleConfirm = useCallback(async () => { if (!(walletState.wallet && contractAddress && userAddress && foxyApi && bip44Params)) return setLoading(true) try { + if (!supportsETH(walletState.wallet)) + throw new Error(`handleConfirm: wallet does not support ethereum`) const txid = await foxyApi.claimWithdraw({ claimAddress: userAddress, userAddress, @@ -102,7 +144,7 @@ export const ClaimConfirm = ({ }) history.push('/status', { txid, - assetId, + assetId: stakingAssetId, amount, userAddress, estimatedGas, @@ -121,7 +163,7 @@ export const ClaimConfirm = ({ } }, [ amount, - assetId, + stakingAssetId, bip44Params, chainId, contractAddress, @@ -140,25 +182,27 @@ export const ClaimConfirm = ({ try { const chainAdapter = await chainAdapterManager.get(KnownChainIds.EthereumMainnet) if (!(walletState.wallet && contractAddress && foxyApi && chainAdapter)) return + if (!supportsETH(walletState.wallet)) + throw new Error(`ClaimConfirm::useEffect: wallet does not support ethereum`) + const { accountNumber } = bip44Params const userAddress = await chainAdapter.getAddress({ wallet: walletState.wallet, accountNumber, }) setUserAddress(userAddress) - const [gasLimit, gasPrice, canClaimWithdraw] = await Promise.all([ - foxyApi.estimateClaimWithdrawGas({ - claimAddress: userAddress, - userAddress, - contractAddress, - wallet: walletState.wallet, - bip44Params, - }), - foxyApi.getGasPrice(), - foxyApi.canClaimWithdraw({ contractAddress, userAddress }), - ]) + const feeDataEstimate = await foxyApi.estimateClaimWithdrawFees({ + claimAddress: userAddress, + userAddress, + contractAddress, + wallet: walletState.wallet, + bip44Params, + }) + + const { + chainSpecific: { gasPrice, gasLimit }, + } = feeDataEstimate.fast - setCanClaim(canClaimWithdraw) const gasEstimate = bnOrZero(gasPrice).times(gasLimit).toFixed(0) setEstimatedGas(gasEstimate) } catch (error) { @@ -182,12 +226,12 @@ export const ClaimConfirm = ({ - + @@ -251,7 +295,7 @@ export const ClaimConfirm = ({