diff --git a/src/entities/gas.ts b/src/entities/gas.ts index 7b436d4af01..2f3702b4a42 100644 --- a/src/entities/gas.ts +++ b/src/entities/gas.ts @@ -1,3 +1,5 @@ +import { GasFeeLegacyParams } from '@/__swaps__/types/gas'; + type Numberish = number | string; export interface Fee { diff --git a/src/entities/transactions/transaction.ts b/src/entities/transactions/transaction.ts index 9f5a7d16936..d95869313ba 100644 --- a/src/entities/transactions/transaction.ts +++ b/src/entities/transactions/transaction.ts @@ -5,7 +5,7 @@ import { EthereumAddress } from '../wallet'; import { Network } from '@/helpers/networkTypes'; import { AddCashCurrencyAsset } from '@/references'; import { ChainId, SwapType } from '@rainbow-me/swaps'; -import { SwapMetadata } from '@/raps/common'; +import { SwapMetadata } from '@/raps/references'; import { UniqueAsset } from '../uniqueAssets'; import { ParsedAsset } from '@/resources/assets/types'; import { TransactionStatus, TransactionType } from '@/resources/transactions/types'; diff --git a/src/handlers/ens.ts b/src/handlers/ens.ts index 5f1b857e9a1..cfb31cf92c3 100644 --- a/src/handlers/ens.ts +++ b/src/handlers/ens.ts @@ -9,7 +9,7 @@ import { debounce, isEmpty, sortBy } from 'lodash'; import { fetchENSAvatar, prefetchENSAvatar } from '../hooks/useENSAvatar'; import { prefetchENSCover } from '../hooks/useENSCover'; import { prefetchENSRecords } from '../hooks/useENSRecords'; -import { ENSActionParameters, RapActionTypes } from '../raps/common'; +import { ENSActionParameters, ENSRapActionType } from '@/raps/common'; import { getENSData, getNameFromLabelhash, saveENSData } from './localstorage/ens'; import { estimateGasWithPadding, getProviderForNetwork, TokenStandard } from './web3'; import { ENSRegistrationRecords, Records, UniqueAsset } from '@/entities'; @@ -26,8 +26,8 @@ import { prefetchENSAddress } from '@/resources/ens/ensAddressQuery'; import { ENS_MARQUEE_QUERY_KEY } from '@/resources/metadata/ensMarqueeQuery'; import { queryClient } from '@/react-query'; import { EnsMarqueeAccount } from '@/graphql/__generated__/metadata'; -import { getEnsMarqueeFallback } from '@/components/ens-registration/IntroMarquee/IntroMarquee'; import { MimeType, handleNFTImages } from '@/utils/handleNFTImages'; +import store from '@/redux/store'; const DUMMY_RECORDS = { description: 'description', @@ -655,12 +655,15 @@ export const estimateENSRegistrationGasLimit = async ( records: Records = DUMMY_RECORDS ) => { const salt = generateSalt(); + const { selectedGasFee, gasFeeParamsBySpeed } = store.getState().gas; const commitGasLimitPromise = estimateENSCommitGasLimit({ duration, name, ownerAddress, rentPrice, salt, + selectedGasFee, + gasFeeParamsBySpeed, }); const setRecordsGasLimitPromise = estimateENSSetRecordsGasLimit({ @@ -888,13 +891,13 @@ export const getTransactionTypeForRecords = (registrationRecords: ENSRegistratio export const getRapActionTypeForTxType = (txType: ENSRegistrationTransactionType) => { switch (txType) { case ENSRegistrationTransactionType.MULTICALL: - return RapActionTypes.multicallENS; + return ENSRapActionType.multicallENS; case ENSRegistrationTransactionType.SET_ADDR: - return RapActionTypes.setAddrENS; + return ENSRapActionType.setAddrENS; case ENSRegistrationTransactionType.SET_TEXT: - return RapActionTypes.setTextENS; + return ENSRapActionType.setTextENS; case ENSRegistrationTransactionType.SET_CONTENTHASH: - return RapActionTypes.setContenthashENS; + return ENSRapActionType.setContenthashENS; default: return null; } diff --git a/src/hooks/useENSRegistrationActionHandler.ts b/src/hooks/useENSRegistrationActionHandler.ts index b63e56edfc7..037f3e17bb0 100644 --- a/src/hooks/useENSRegistrationActionHandler.ts +++ b/src/hooks/useENSRegistrationActionHandler.ts @@ -5,7 +5,7 @@ import { Image } from 'react-native-image-crop-picker'; import { useRecoilValue } from 'recoil'; import { avatarMetadataAtom } from '../components/ens-registration/RegistrationAvatar/RegistrationAvatar'; import { coverMetadataAtom } from '../components/ens-registration/RegistrationCover/RegistrationCover'; -import { ENSActionParameters, RapActionTypes } from '../raps/common'; +import { ENSActionParameters, ENSRapActionType } from '@/raps/common'; import usePendingTransactions from './usePendingTransactions'; import { useAccountSettings, useENSRegistration, useWalletENSAvatar, useWallets } from '.'; import { Records, RegistrationParameters } from '@/entities'; @@ -15,16 +15,20 @@ import { uploadImage } from '@/handlers/pinata'; import { getProviderForNetwork } from '@/handlers/web3'; import { ENS_DOMAIN, generateSalt, getRentPrice, REGISTRATION_STEPS } from '@/helpers/ens'; import { loadWallet } from '@/model/wallet'; -import { executeRap } from '@/raps'; import { timeUnits } from '@/references'; import Routes from '@/navigation/routesNames'; import { labelhash, logger } from '@/utils'; import { getNextNonce } from '@/state/nonces'; import { Network } from '@/networks/types'; +import { Hex } from 'viem'; +import { executeENSRap } from '@/raps/actions/ens'; +import store from '@/redux/store'; const NOOP = () => null; const formatENSActionParams = (registrationParameters: RegistrationParameters): ENSActionParameters => { + const { selectedGasFee, gasFeeParamsBySpeed } = store.getState().gas; + return { duration: registrationParameters?.duration, mode: registrationParameters?.mode, @@ -34,6 +38,8 @@ const formatENSActionParams = (registrationParameters: RegistrationParameters): rentPrice: registrationParameters?.rentPrice, salt: registrationParameters?.salt, setReverseRecord: registrationParameters?.setReverseRecord, + gasFeeParamsBySpeed, + selectedGasFee, }; }; @@ -110,7 +116,7 @@ export default function useENSRegistrationActionHandler( salt, }; - await executeRap(wallet, RapActionTypes.commitENS, commitEnsRegistrationParameters, () => { + await executeENSRap(wallet, ENSRapActionType.commitENS, commitEnsRegistrationParameters, () => { if (isHardwareWallet) { goBack(); } @@ -166,7 +172,7 @@ export default function useENSRegistrationActionHandler( setReverseRecord: sendReverseRecord, }; - await executeRap(wallet, RapActionTypes.registerENS, registerEnsRegistrationParameters, callback); + await executeENSRap(wallet, ENSRapActionType.registerENS, registerEnsRegistrationParameters, callback); updateAvatarsOnNextBlock.current = true; }, @@ -193,7 +199,7 @@ export default function useENSRegistrationActionHandler( rentPrice: rentPrice.toString(), }; - await executeRap(wallet, RapActionTypes.renewENS, registerEnsRegistrationParameters, callback); + await executeENSRap(wallet, ENSRapActionType.renewENS, registerEnsRegistrationParameters, callback); }, [accountAddress, duration, registrationParameters] ); @@ -217,7 +223,7 @@ export default function useENSRegistrationActionHandler( ownerAddress: accountAddress, }; - await executeRap(wallet, RapActionTypes.setNameENS, registerEnsRegistrationParameters, callback); + await executeENSRap(wallet, ENSRapActionType.setNameENS, registerEnsRegistrationParameters, callback); }, [accountAddress, registrationParameters] ); @@ -244,11 +250,11 @@ export default function useENSRegistrationActionHandler( nonce, ownerAddress: accountAddress, records: changedRecords, - resolverAddress: resolver?.address, + resolverAddress: resolver?.address as Hex, setReverseRecord: sendReverseRecord, }; - await executeRap(wallet, RapActionTypes.setRecordsENS, setRecordsEnsRegistrationParameters, callback); + await executeENSRap(wallet, ENSRapActionType.setRecordsENS, setRecordsEnsRegistrationParameters, callback); updateAvatarsOnNextBlock.current = true; }, @@ -285,7 +291,7 @@ export default function useENSRegistrationActionHandler( transferControl, }; - const { nonce: newNonce } = await executeRap(wallet, RapActionTypes.transferENS, transferEnsParameters, callback); + const { nonce: newNonce } = await executeENSRap(wallet, ENSRapActionType.transferENS, transferEnsParameters, callback); return { nonce: newNonce }; }, diff --git a/src/hooks/useENSRegistrationCosts.ts b/src/hooks/useENSRegistrationCosts.ts index 3f802b28d05..dd291856be1 100644 --- a/src/hooks/useENSRegistrationCosts.ts +++ b/src/hooks/useENSRegistrationCosts.ts @@ -60,6 +60,7 @@ export default function useENSRegistrationCosts({ const duration = yearsDuration * timeUnits.secs.year; const name = inputName.replace(ENS_DOMAIN, ''); const { + selectedGasFee, gasFeeParamsBySpeed: useGasGasFeeParamsBySpeed, currentBlockParams: useGasCurrentBlockParams, updateTxFee, @@ -107,9 +108,11 @@ export default function useENSRegistrationCosts({ ownerAddress: accountAddress, rentPrice: rentPriceInWei as string, salt, + selectedGasFee, + gasFeeParamsBySpeed: useGasGasFeeParamsBySpeed, }); return newCommitGasLimit || ''; - }, [accountAddress, duration, name, rentPriceInWei]); + }, [accountAddress, duration, name, rentPriceInWei, selectedGasFee, useGasGasFeeParamsBySpeed]); const getRegisterRapGasLimit = useCallback(async () => { const newRegisterRapGasLimit = await estimateENSRegisterSetRecordsAndNameGasLimit({ @@ -120,9 +123,21 @@ export default function useENSRegistrationCosts({ rentPrice: registrationParameters?.rentPrice, salt: registrationParameters?.salt, setReverseRecord: sendReverseRecord, + selectedGasFee: selectedGasFee, + gasFeeParamsBySpeed: useGasGasFeeParamsBySpeed, }); return newRegisterRapGasLimit || ''; - }, [accountAddress, duration, name, registrationParameters?.rentPrice, registrationParameters?.salt, sendReverseRecord, changedRecords]); + }, [ + duration, + name, + accountAddress, + changedRecords, + registrationParameters?.rentPrice, + registrationParameters?.salt, + sendReverseRecord, + selectedGasFee, + useGasGasFeeParamsBySpeed, + ]); const getSetRecordsGasLimit = useCallback(async () => { const newSetRecordsGasLimit = await estimateENSSetRecordsGasLimit({ diff --git a/src/hooks/useParamsForExchangeModal.ts b/src/hooks/useParamsForExchangeModal.ts index 7f39ab432b5..d332490f8b6 100644 --- a/src/hooks/useParamsForExchangeModal.ts +++ b/src/hooks/useParamsForExchangeModal.ts @@ -1,7 +1,7 @@ import { SwapModalField, updateSwapSlippage, updateSwapSource } from '@/redux/swap'; import { MutableRefObject, useEffect, useState } from 'react'; import { useSwapInputHandlers } from '@/hooks/index'; -import { SwapMetadata } from '@/raps/common'; +import { SwapMetadata } from '@/raps/references'; import { useDispatch } from 'react-redux'; import { useRoute } from '@react-navigation/native'; import { TextInput } from 'react-native'; diff --git a/src/hooks/useSwapSettings.ts b/src/hooks/useSwapSettings.ts index 310e431a3ee..8e9d5780414 100644 --- a/src/hooks/useSwapSettings.ts +++ b/src/hooks/useSwapSettings.ts @@ -1,7 +1,8 @@ import { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { AppState } from '@/redux/store'; -import { Source, updateSwapSlippage as updateSwapSlippageRedux, updateSwapSource as updateSwapSourceRedux } from '@/redux/swap'; +import { Source } from '@/raps/references'; +import { updateSwapSlippage as updateSwapSlippageRedux, updateSwapSource as updateSwapSourceRedux } from '@/redux/swap'; export default function useSwapSettings() { const dispatch = useDispatch(); diff --git a/src/raps/actions/crosschainSwap.ts b/src/raps/actions/crosschainSwap.ts index b0581fe221d..440fd2ddf5c 100644 --- a/src/raps/actions/crosschainSwap.ts +++ b/src/raps/actions/crosschainSwap.ts @@ -1,78 +1,114 @@ -import { Wallet } from '@ethersproject/wallet'; import { Signer } from '@ethersproject/abstract-signer'; -import { ChainId, CrosschainQuote, fillCrosschainQuote, SwapType } from '@rainbow-me/swaps'; -import { captureException } from '@sentry/react-native'; -import { CrosschainSwapActionParameters, Rap, RapExchangeActionParameters } from '../common'; -import { NewTransaction } from '@/entities'; - -import { toHex } from '@/handlers/web3'; -import { parseGasParamAmounts } from '@/parsers'; -import store from '@/redux/store'; -import { ethereumUtils } from '@/utils'; -import logger from '@/utils/logger'; -import { estimateCrosschainSwapGasLimit } from '@/handlers/swap'; -import { swapMetadataStorage } from './swap'; -import { REFERRER } from '@/references'; -import { overrideWithFastSpeedIfNeeded } from '../utils'; +import { CrosschainQuote, fillCrosschainQuote } from '@rainbow-me/swaps'; +import { Address } from 'viem'; +import { getProviderForNetwork, estimateGasWithPadding } from '@/handlers/web3'; + +import { REFERRER, gasUnits } from '@/references'; +import { ChainId } from '@/__swaps__/types/chains'; +import { NewTransaction } from '@/entities/transactions'; +import { TxHash } from '@/resources/transactions/types'; import { addNewTransaction } from '@/state/pendingTransactions'; +import { RainbowError, logger } from '@/logger'; + +import { TransactionGasParams, TransactionLegacyGasParams } from '@/__swaps__/types/gas'; +import { toHex } from '@/__swaps__/utils/hex'; +import { ActionProps, RapActionResult } from '../references'; +import { + CHAIN_IDS_WITH_TRACE_SUPPORT, + SWAP_GAS_PADDING, + estimateSwapGasLimitWithFakeApproval, + getDefaultGasLimitForTrade, + overrideWithFastSpeedIfNeeded, +} from '../utils'; +import { ethereumUtils } from '@/utils'; +import { TokenColors } from '@/graphql/__generated__/metadata'; +import { ParsedAsset } from '@/resources/assets/types'; +import { parseGasParamAmounts } from '@/parsers'; -const actionName = 'crosschainSwap'; +const getCrosschainSwapDefaultGasLimit = (quote: CrosschainQuote) => quote?.routes?.[0]?.userTxs?.[0]?.gasFees?.gasLimit; -export const executeCrosschainSwap = async ({ +export const estimateCrosschainSwapGasLimit = async ({ chainId, + requiresApprove, + quote, +}: { + chainId: ChainId; + requiresApprove?: boolean; + quote: CrosschainQuote; +}): Promise => { + // TODO: MARK - Replace this once we migrate network => chainId + const provider = await getProviderForNetwork(ethereumUtils.getNetworkFromChainId(chainId)); + if (!provider || !quote) { + return gasUnits.basic_swap[chainId]; + } + try { + if (requiresApprove) { + if (CHAIN_IDS_WITH_TRACE_SUPPORT.includes(chainId)) { + try { + const gasLimitWithFakeApproval = await estimateSwapGasLimitWithFakeApproval(chainId, provider, quote); + return gasLimitWithFakeApproval; + } catch (e) { + const routeGasLimit = getCrosschainSwapDefaultGasLimit(quote); + if (routeGasLimit) return routeGasLimit; + } + } + + return getCrosschainSwapDefaultGasLimit(quote) || getDefaultGasLimitForTrade(quote, chainId); + } + + const gasLimit = await estimateGasWithPadding( + { + data: quote.data, + from: quote.from, + to: quote.to, + value: quote.value, + }, + undefined, + null, + provider, + SWAP_GAS_PADDING + ); + + return gasLimit || getCrosschainSwapDefaultGasLimit(quote); + } catch (error) { + return getCrosschainSwapDefaultGasLimit(quote); + } +}; + +export const executeCrosschainSwap = async ({ gasLimit, - maxFeePerGas, - maxPriorityFeePerGas, - gasPrice, + gasParams, nonce, - tradeDetails, + quote, wallet, - permit = false, - flashbots = false, }: { - chainId: ChainId; - gasLimit: string | number; - maxFeePerGas: string; - maxPriorityFeePerGas: string; - gasPrice: string; + gasLimit: string; + gasParams: TransactionGasParams | TransactionLegacyGasParams; nonce?: number; - tradeDetails: CrosschainQuote | null; - wallet: Wallet | Signer | null; - permit: boolean; - flashbots: boolean; + quote: CrosschainQuote; + wallet: Signer; }) => { - if (!wallet || !tradeDetails) return null; - const walletAddress = await wallet.getAddress(); + if (!wallet || !quote) return null; const transactionParams = { gasLimit: toHex(gasLimit) || undefined, - // In case it's an L2 with legacy gas price like arbitrum - ...(gasPrice ? { gasPrice } : {}), - // EIP-1559 like networks - ...(maxFeePerGas ? { maxFeePerGas } : {}), - ...(maxPriorityFeePerGas ? { maxPriorityFeePerGas } : {}), - nonce: nonce ? toHex(nonce) : undefined, + nonce: nonce ? toHex(String(nonce)) : undefined, + ...gasParams, }; - - logger.debug('FILLCROSSCHAINSWAP', tradeDetails, transactionParams, walletAddress, permit, chainId); - return fillCrosschainQuote(tradeDetails, transactionParams, wallet, REFERRER); + return fillCrosschainQuote(quote, transactionParams, wallet, REFERRER); }; -const crosschainSwap = async ( - wallet: Signer, - currentRap: Rap, - index: number, - parameters: RapExchangeActionParameters, - baseNonce?: number -): Promise => { - logger.log(`[${actionName}] base nonce`, baseNonce, 'index:', index); - const { inputAmount, tradeDetails, chainId, requiresApprove } = parameters as CrosschainSwapActionParameters; - const { dispatch } = store; - const { accountAddress } = store.getState().settings; - const { inputCurrency, outputCurrency } = store.getState().swap; - const { gasFeeParamsBySpeed, selectedGasFee } = store.getState().gas; +export const crosschainSwap = async ({ + wallet, + currentRap, + index, + parameters, + baseNonce, + selectedGasFee, + gasFeeParamsBySpeed, +}: ActionProps<'crosschainSwap'>): Promise => { + const { quote, chainId, requiresApprove } = parameters; let gasParams = parseGasParamAmounts(selectedGasFee); - if (currentRap.actions.length - 1 > index) { gasParams = overrideWithFastSpeedIfNeeded({ gasParams, @@ -80,97 +116,99 @@ const crosschainSwap = async ( gasFeeParamsBySpeed, }); } + let gasLimit; try { - const newGasLimit = await estimateCrosschainSwapGasLimit({ - chainId: Number(chainId), + gasLimit = await estimateCrosschainSwapGasLimit({ + chainId, requiresApprove, - tradeDetails: tradeDetails as CrosschainQuote, + quote, }); - gasLimit = newGasLimit; } catch (e) { - logger.sentry(`[${actionName}] error estimateSwapGasLimit`); - captureException(e); + logger.error(new RainbowError('crosschainSwap: error estimateCrosschainSwapGasLimit'), { + message: (e as Error)?.message, + }); throw e; } + const nonce = baseNonce ? baseNonce + index : undefined; + + const swapParams = { + chainId, + gasLimit, + nonce, + quote, + wallet, + gasParams, + }; + let swap; try { - logger.sentry(`[${actionName}] executing rap`, { - ...gasParams, - gasLimit, - }); - const nonce = baseNonce ? baseNonce + index : undefined; - - const swapParams = { - ...gasParams, - chainId, - flashbots: !!parameters.flashbots, - gasLimit, - nonce, - tradeDetails, - wallet, - }; - - // @ts-ignore swap = await executeCrosschainSwap(swapParams); } catch (e) { - logger.sentry('Error', e); - const fakeError = new Error('Failed to execute swap'); - captureException(fakeError); + logger.error(new RainbowError('crosschainSwap: error executeCrosschainSwap'), { message: (e as Error)?.message }); throw e; } - logger.log(`[${actionName}] response`, swap); + if (!swap) throw new RainbowError('crosschainSwap: error executeCrosschainSwap'); - const isBridge = inputCurrency.symbol === outputCurrency.symbol; - if (!swap?.hash) return; + // TODO: MARK - Replace this once we migrate network => chainId + const network = ethereumUtils.getNetworkFromChainId(parameters.chainId); - const newTransaction: NewTransaction = { - data: swap?.data, - from: accountAddress, - to: swap?.to ?? null, - value: tradeDetails?.value?.toString() || '', - asset: outputCurrency, + const transaction = { + data: parameters.quote.data, + value: parameters.quote.value?.toString(), + asset: { + ...parameters.assetToBuy, + network: ethereumUtils.getNetworkFromChainId(parameters.assetToBuy.chainId), + colors: parameters.assetToBuy.colors as TokenColors, + } as ParsedAsset, changes: [ { direction: 'out', - asset: inputCurrency, - value: tradeDetails.sellAmount.toString(), + // TODO: MARK - Replace this once we migrate network => chainId + // asset: parameters.assetToSell, + asset: { + ...parameters.assetToSell, + network: ethereumUtils.getNetworkFromChainId(parameters.assetToSell.chainId), + colors: parameters.assetToSell.colors as TokenColors, + }, + value: quote.sellAmount.toString(), }, { direction: 'in', - asset: outputCurrency, - value: tradeDetails.buyAmount.toString(), + // TODO: MARK - Replace this once we migrate network => chainId + // asset: parameters.assetToBuy, + asset: { + ...parameters.assetToBuy, + network: ethereumUtils.getNetworkFromChainId(parameters.assetToBuy.chainId), + colors: parameters.assetToBuy.colors as TokenColors, + }, + value: quote.buyAmount.toString(), }, ], - hash: swap.hash, - network: inputCurrency.network, - nonce: swap?.nonce, + from: parameters.quote.from as Address, + to: parameters.quote.to as Address, + hash: swap.hash as TxHash, + // TODO: MARK - Replace this once we migrate network => chainId + network, + // chainId: parameters.chainId, + nonce: swap.nonce, status: 'pending', type: 'swap', flashbots: parameters.flashbots, - swap: { - type: SwapType.crossChain, - fromChainId: ethereumUtils.getChainIdFromNetwork(inputCurrency?.network), - toChainId: ethereumUtils.getChainIdFromNetwork(outputCurrency?.network), - isBridge, - }, ...gasParams, - }; + } satisfies NewTransaction; addNewTransaction({ - address: accountAddress, - transaction: newTransaction, - network: inputCurrency.network, + address: parameters.quote.from as Address, + // chainId: parameters.chainId as ChainId, + network, + transaction, }); - logger.log(`[${actionName}] adding new txn`, newTransaction); - if (parameters.meta && swap?.hash) { - swapMetadataStorage.set(swap.hash.toLowerCase(), JSON.stringify({ type: 'swap', data: parameters.meta })); - } - - return swap?.nonce; + return { + nonce: swap.nonce, + hash: swap.hash, + }; }; - -export { crosschainSwap }; diff --git a/src/raps/actions/ens.ts b/src/raps/actions/ens.ts index 66f16b5ddbf..97a368b2a7e 100644 --- a/src/raps/actions/ens.ts +++ b/src/raps/actions/ens.ts @@ -1,24 +1,34 @@ import { Signer } from '@ethersproject/abstract-signer'; import { captureException } from '@sentry/react-native'; -import { - // @ts-ignore - IS_TESTING, -} from 'react-native-dotenv'; -import { Rap, RapActionTypes, RapENSActionParameters } from '../common'; +import { IS_TESTING } from 'react-native-dotenv'; +import { ENSActionParameters, ENSRap, ENSRapActionType, RapENSAction, RapENSActionParameters } from '@/raps/common'; import { analytics } from '@/analytics'; import { ENSRegistrationRecords, NewTransaction, TransactionGasParamAmounts } from '@/entities'; import { estimateENSTransactionGasLimit, formatRecordsForTransaction } from '@/handlers/ens'; import { toHex } from '@/handlers/web3'; import { NetworkTypes } from '@/helpers'; import { ENSRegistrationTransactionType, getENSExecutionDetails, REGISTRATION_MODES } from '@/helpers/ens'; - +import * as i18n from '@/languages'; import { saveCommitRegistrationParameters, updateTransactionRegistrationParameters } from '@/redux/ensRegistration'; import store from '@/redux/store'; -import { ethereumUtils } from '@/utils'; import logger from '@/utils/logger'; import { parseGasParamAmounts } from '@/parsers'; import { addNewTransaction } from '@/state/pendingTransactions'; import { Network } from '@/networks/types'; +import { + createRegisterENSRap, + createRenewENSRap, + createCommitENSRap, + createSetNameENSRap, + createSetRecordsENSRap, + createTransferENSRap, +} from '../registerENS'; +import { Logger } from '@ethersproject/logger'; + +export interface ENSRapActionResponse { + baseNonce?: number | null; + errorMessage: string | null; +} const executeCommit = async ( name?: string, @@ -299,7 +309,6 @@ const ensAction = async ( logger.log(`[${actionName}] base nonce`, baseNonce, 'index:', index); const { dispatch } = store; const { accountAddress: ownerAddress } = store.getState().settings; - const { selectedGasFee } = store.getState().gas; const { name, duration, rentPrice, records, salt, toAddress, mode } = parameters; @@ -343,7 +352,7 @@ const ensAction = async ( let maxFeePerGas; let maxPriorityFeePerGas; try { - const gasParams = parseGasParamAmounts(selectedGasFee) as TransactionGasParamAmounts; + const gasParams = parseGasParamAmounts(parameters.selectedGasFee) as TransactionGasParamAmounts; maxFeePerGas = gasParams.maxFeePerGas; maxPriorityFeePerGas = gasParams.maxPriorityFeePerGas; @@ -484,34 +493,34 @@ const ensAction = async ( const commitENS = async ( wallet: Signer, - currentRap: Rap, + currentRap: ENSRap, index: number, parameters: RapENSActionParameters, baseNonce?: number ): Promise => { - return ensAction(wallet, RapActionTypes.commitENS, index, parameters, ENSRegistrationTransactionType.COMMIT, baseNonce); + return ensAction(wallet, ENSRapActionType.commitENS, index, parameters, ENSRegistrationTransactionType.COMMIT, baseNonce); }; const multicallENS = async ( wallet: Signer, - currentRap: Rap, + currentRap: ENSRap, index: number, parameters: RapENSActionParameters, baseNonce?: number ): Promise => { - return ensAction(wallet, RapActionTypes.multicallENS, index, parameters, ENSRegistrationTransactionType.MULTICALL, baseNonce); + return ensAction(wallet, ENSRapActionType.multicallENS, index, parameters, ENSRegistrationTransactionType.MULTICALL, baseNonce); }; const registerWithConfig = async ( wallet: Signer, - currentRap: Rap, + currentRap: ENSRap, index: number, parameters: RapENSActionParameters, baseNonce?: number ): Promise => { return ensAction( wallet, - RapActionTypes.registerWithConfigENS, + ENSRapActionType.registerWithConfigENS, index, parameters, ENSRegistrationTransactionType.REGISTER_WITH_CONFIG, @@ -521,62 +530,212 @@ const registerWithConfig = async ( const renewENS = async ( wallet: Signer, - currentRap: Rap, + currentRap: ENSRap, index: number, parameters: RapENSActionParameters, baseNonce?: number ): Promise => { - return ensAction(wallet, RapActionTypes.renewENS, index, parameters, ENSRegistrationTransactionType.RENEW, baseNonce); + return ensAction(wallet, ENSRapActionType.renewENS, index, parameters, ENSRegistrationTransactionType.RENEW, baseNonce); }; const setNameENS = async ( wallet: Signer, - currentRap: Rap, + currentRap: ENSRap, index: number, parameters: RapENSActionParameters, baseNonce?: number ): Promise => { - return ensAction(wallet, RapActionTypes.setNameENS, index, parameters, ENSRegistrationTransactionType.SET_NAME, baseNonce); + return ensAction(wallet, ENSRapActionType.setNameENS, index, parameters, ENSRegistrationTransactionType.SET_NAME, baseNonce); }; const setAddrENS = async ( wallet: Signer, - currentRap: Rap, + currentRap: ENSRap, index: number, parameters: RapENSActionParameters, baseNonce?: number ): Promise => { - return ensAction(wallet, RapActionTypes.setAddrENS, index, parameters, ENSRegistrationTransactionType.SET_ADDR, baseNonce); + return ensAction(wallet, ENSRapActionType.setAddrENS, index, parameters, ENSRegistrationTransactionType.SET_ADDR, baseNonce); }; const reclaimENS = async ( wallet: Signer, - currentRap: Rap, + currentRap: ENSRap, index: number, parameters: RapENSActionParameters, baseNonce?: number ): Promise => { - return ensAction(wallet, RapActionTypes.reclaimENS, index, parameters, ENSRegistrationTransactionType.RECLAIM, baseNonce); + return ensAction(wallet, ENSRapActionType.reclaimENS, index, parameters, ENSRegistrationTransactionType.RECLAIM, baseNonce); }; const setTextENS = async ( wallet: Signer, - currentRap: Rap, + currentRap: ENSRap, index: number, parameters: RapENSActionParameters, baseNonce?: number ): Promise => { - return ensAction(wallet, RapActionTypes.setTextENS, index, parameters, ENSRegistrationTransactionType.SET_TEXT, baseNonce); + return ensAction(wallet, ENSRapActionType.setTextENS, index, parameters, ENSRegistrationTransactionType.SET_TEXT, baseNonce); }; const setContenthashENS = async ( wallet: Signer, - currentRap: Rap, + currentRap: ENSRap, index: number, parameters: RapENSActionParameters, baseNonce?: number ): Promise => { - return ensAction(wallet, RapActionTypes.setContenthashENS, index, parameters, ENSRegistrationTransactionType.SET_CONTENTHASH, baseNonce); + return ensAction( + wallet, + ENSRapActionType.setContenthashENS, + index, + parameters, + ENSRegistrationTransactionType.SET_CONTENTHASH, + baseNonce + ); +}; + +const createENSRapByType = (type: string, ensRegistrationParameters: ENSActionParameters) => { + switch (type) { + case ENSRapActionType.registerENS: + return createRegisterENSRap(ensRegistrationParameters); + case ENSRapActionType.renewENS: + return createRenewENSRap(ensRegistrationParameters); + case ENSRapActionType.setNameENS: + return createSetNameENSRap(ensRegistrationParameters); + case ENSRapActionType.setRecordsENS: + return createSetRecordsENSRap(ensRegistrationParameters); + case ENSRapActionType.transferENS: + return createTransferENSRap(ensRegistrationParameters); + case ENSRapActionType.commitENS: + default: + return createCommitENSRap(ensRegistrationParameters); + } +}; + +const getRapFullName = (actions: RapENSAction[]) => { + const actionTypes = actions.map(action => action.type); + return actionTypes.join(' + '); +}; + +const findENSActionByType = (type: ENSRapActionType) => { + switch (type) { + case ENSRapActionType.commitENS: + return commitENS; + case ENSRapActionType.registerWithConfigENS: + return registerWithConfig; + case ENSRapActionType.multicallENS: + return multicallENS; + case ENSRapActionType.setAddrENS: + return setAddrENS; + case ENSRapActionType.setContenthashENS: + return setContenthashENS; + case ENSRapActionType.setTextENS: + return setTextENS; + case ENSRapActionType.setNameENS: + return setNameENS; + case ENSRapActionType.reclaimENS: + return reclaimENS; + case ENSRapActionType.renewENS: + return renewENS; + default: + return () => Promise.resolve(undefined); + } +}; + +interface EthersError extends Error { + code?: string | null; +} + +const parseError = (error: EthersError): string => { + const errorCode = error?.code; + switch (errorCode) { + case Logger.errors.UNPREDICTABLE_GAS_LIMIT: + return i18n.t(i18n.l.wallet.transaction.errors.unpredictable_gas); + case Logger.errors.INSUFFICIENT_FUNDS: + return i18n.t(i18n.l.wallet.transaction.errors.insufficient_funds); + default: + return i18n.t(i18n.l.wallet.transaction.errors.generic); + } +}; + +const executeAction = async ( + action: RapENSAction, + wallet: Signer, + rap: ENSRap, + index: number, + rapName: string, + baseNonce?: number +): Promise => { + logger.log('[1 INNER] index', index); + const { type, parameters } = action; + let nonce; + try { + logger.log('[2 INNER] executing type', type); + const actionPromise = findENSActionByType(type); + nonce = await actionPromise(wallet, rap, index, parameters as RapENSActionParameters, baseNonce); + return { baseNonce: nonce, errorMessage: null }; + } catch (error: any) { + logger.debug('Rap blew up', error); + logger.sentry('[3 INNER] error running action, code:', error?.code); + captureException(error); + analytics.track('Rap failed', { + category: 'raps', + failed_action: type, + label: rapName, + }); + // If the first action failed, return an error message + if (index === 0) { + const errorMessage = parseError(error); + logger.log('[4 INNER] displaying error message', errorMessage); + return { baseNonce: null, errorMessage }; + } + return { baseNonce: null, errorMessage: null }; + } +}; + +export const executeENSRap = async ( + wallet: Signer, + type: ENSRapActionType, + parameters: ENSActionParameters, + callback: (success?: boolean, errorMessage?: string | null) => void +) => { + const rap = await createENSRapByType(type, parameters as ENSActionParameters); + const { actions } = rap; + const rapName = getRapFullName(actions); + + analytics.track('Rap started', { + category: 'raps', + label: rapName, + }); + + let nonce = parameters?.nonce; + + logger.log('[common - executing rap]: actions', actions); + if (actions.length) { + const firstAction = actions[0]; + const { baseNonce, errorMessage } = await executeAction(firstAction, wallet, rap, 0, rapName, nonce); + + if (typeof baseNonce === 'number') { + for (let index = 1; index < actions.length; index++) { + const action = actions[index]; + await executeAction(action, wallet, rap, index, rapName, baseNonce); + } + nonce = baseNonce + actions.length - 1; + callback(true); + } else { + // Callback with failure state + callback(false, errorMessage); + } + } + + analytics.track('Rap completed', { + category: 'raps', + label: rapName, + }); + logger.log('[common - executing rap] finished execute rap function'); + + return { nonce }; }; export default { diff --git a/src/raps/actions/index.ts b/src/raps/actions/index.ts index 2d80bb37ffb..2ce01224887 100644 --- a/src/raps/actions/index.ts +++ b/src/raps/actions/index.ts @@ -1,4 +1,2 @@ -export { swap } from './swap'; -export { crosschainSwap } from './crosschainSwap'; -export { default as unlock, assetNeedsUnlocking, estimateApprove } from './unlock'; -export { default as ens } from './ens'; +export { estimateSwapGasLimit, executeSwap, swap } from './swap'; +export { assetNeedsUnlocking, estimateApprove, executeApprove, unlock } from './unlock'; diff --git a/src/raps/actions/swap.ts b/src/raps/actions/swap.ts index e41f6155e65..8724ca53605 100644 --- a/src/raps/actions/swap.ts +++ b/src/raps/actions/swap.ts @@ -1,94 +1,252 @@ import { Signer } from '@ethersproject/abstract-signer'; -import { ChainId, ETH_ADDRESS, fillQuote, Quote, unwrapNativeAsset, wrapNativeAsset, WRAPPED_ASSET } from '@rainbow-me/swaps'; -import { captureException } from '@sentry/react-native'; -import { toLower } from 'lodash'; -import { Rap, RapExchangeActionParameters, SwapActionParameters } from '../common'; -import { NewTransaction } from '@/entities'; +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { Transaction } from '@ethersproject/transactions'; +import { + CrosschainQuote, + ETH_ADDRESS as ETH_ADDRESS_AGGREGATORS, + Quote, + ChainId as SwapChainId, + WRAPPED_ASSET, + fillQuote, + getQuoteExecutionDetails, + getRainbowRouterContractAddress, + getWrappedAssetMethod, + unwrapNativeAsset, + wrapNativeAsset, +} from '@rainbow-me/swaps'; +import { getProviderForNetwork, estimateGasWithPadding } from '@/handlers/web3'; +import { Address } from 'viem'; -import { toHex } from '@/handlers/web3'; -import { parseGasParamAmounts } from '@/parsers'; -import store from '@/redux/store'; -import { AllowancesCache } from '@/utils'; -import logger from '@/utils/logger'; -import { estimateSwapGasLimit } from '@/handlers/swap'; -import { MMKV } from 'react-native-mmkv'; -import { STORAGE_IDS } from '@/model/mmkv'; -import { REFERRER } from '@/references'; -import { overrideWithFastSpeedIfNeeded } from '../utils'; +import { metadataPOSTClient } from '@/graphql'; +import { ChainId } from '@/__swaps__/types/chains'; +import { NewTransaction } from '@/entities/transactions'; +import { TxHash } from '@/resources/transactions/types'; +import { add } from '@/helpers/utilities'; +import { isLowerCaseMatch } from '@/__swaps__/utils/strings'; +import { isUnwrapNative, isWrapNative } from '@/handlers/swap'; import { addNewTransaction } from '@/state/pendingTransactions'; +import { RainbowError, logger } from '@/logger'; +import { ethereumUtils } from '@/utils'; + +import { gasUnits, REFERRER } from '@/references'; +import { TransactionGasParams, TransactionLegacyGasParams } from '@/__swaps__/types/gas'; +import { toHex } from '@/__swaps__/utils/hex'; +import { ActionProps, RapActionResult } from '../references'; +import { + CHAIN_IDS_WITH_TRACE_SUPPORT, + SWAP_GAS_PADDING, + estimateSwapGasLimitWithFakeApproval, + getDefaultGasLimitForTrade, + overrideWithFastSpeedIfNeeded, + populateSwap, +} from '../utils'; + +import { populateApprove } from './unlock'; +import { TokenColors } from '@/graphql/__generated__/metadata'; +import { swapMetadataStorage } from '../common'; +import { ParsedAsset } from '@/resources/assets/types'; +import { parseGasParamAmounts } from '@/parsers'; + +const WRAP_GAS_PADDING = 1.002; + +export const estimateSwapGasLimit = async ({ + chainId, + requiresApprove, + quote, +}: { + chainId: ChainId; + requiresApprove?: boolean; + quote: Quote; +}): Promise => { + // TODO: MARK - Replace this once we migrate network => chainId + const provider = await getProviderForNetwork(ethereumUtils.getNetworkFromChainId(chainId)); + if (!provider || !quote) { + return gasUnits.basic_swap[chainId]; + } + + const { sellTokenAddress, buyTokenAddress } = quote; + const isWrapNativeAsset = + isLowerCaseMatch(sellTokenAddress, ETH_ADDRESS_AGGREGATORS) && isLowerCaseMatch(buyTokenAddress, WRAPPED_ASSET[chainId]); + + const isUnwrapNativeAsset = + isLowerCaseMatch(sellTokenAddress, WRAPPED_ASSET[chainId]) && isLowerCaseMatch(buyTokenAddress, ETH_ADDRESS_AGGREGATORS); + + // Wrap / Unwrap Eth + if (isWrapNativeAsset || isUnwrapNativeAsset) { + const default_estimate = isWrapNativeAsset ? gasUnits.weth_wrap : gasUnits.weth_unwrap; + try { + const gasLimit = await estimateGasWithPadding( + { + from: quote.from, + value: isWrapNativeAsset ? quote.buyAmount : '0', + }, + getWrappedAssetMethod( + isWrapNativeAsset ? 'deposit' : 'withdraw', + provider as StaticJsonRpcProvider, + chainId as unknown as SwapChainId + ), + null, + provider, + WRAP_GAS_PADDING + ); + + return gasLimit || String(quote?.defaultGasLimit) || String(default_estimate); + } catch (e) { + return String(quote?.defaultGasLimit) || String(default_estimate); + } + // Swap + } else { + try { + const { params, method, methodArgs } = getQuoteExecutionDetails(quote, { from: quote.from }, provider as StaticJsonRpcProvider); + + if (requiresApprove) { + if (CHAIN_IDS_WITH_TRACE_SUPPORT.includes(chainId)) { + try { + const gasLimitWithFakeApproval = await estimateSwapGasLimitWithFakeApproval(chainId, provider, quote); + return gasLimitWithFakeApproval; + } catch (e) { + // + } + } + + return getDefaultGasLimitForTrade(quote, chainId); + } + + const gasLimit = await estimateGasWithPadding(params, method, methodArgs, provider, SWAP_GAS_PADDING); + + return gasLimit || getDefaultGasLimitForTrade(quote, chainId); + } catch (error) { + return getDefaultGasLimitForTrade(quote, chainId); + } + } +}; + +export const estimateUnlockAndSwapFromMetadata = async ({ + swapAssetNeedsUnlocking, + chainId, + accountAddress, + sellTokenAddress, + quote, +}: { + swapAssetNeedsUnlocking: boolean; + chainId: ChainId; + accountAddress: Address; + sellTokenAddress: Address; + quote: Quote | CrosschainQuote; +}) => { + try { + const approveTransaction = await populateApprove({ + owner: accountAddress, + tokenAddress: sellTokenAddress, + spender: getRainbowRouterContractAddress(chainId as number), + chainId, + }); + + // TODO: MARK - Replace this once we migrate network => chainId + const provider = await getProviderForNetwork(ethereumUtils.getNetworkFromChainId(chainId)); + const swapTransaction = await populateSwap({ + provider, + quote, + }); + if ( + approveTransaction?.to && + approveTransaction?.data && + approveTransaction?.from && + swapTransaction?.to && + swapTransaction?.data && + swapTransaction?.from + ) { + const transactions = swapAssetNeedsUnlocking + ? [ + { + to: approveTransaction?.to, + data: approveTransaction?.data || '0x0', + from: approveTransaction?.from, + value: approveTransaction?.value?.toString() || '0x0', + }, + { + to: swapTransaction?.to, + data: swapTransaction?.data || '0x0', + from: swapTransaction?.from, + value: swapTransaction?.value?.toString() || '0x0', + }, + ] + : [ + { + to: swapTransaction?.to, + data: swapTransaction?.data || '0x0', + from: swapTransaction?.from, + value: swapTransaction?.value?.toString() || '0x0', + }, + ]; -export const swapMetadataStorage = new MMKV({ - id: STORAGE_IDS.SWAPS_METADATA_STORAGE, -}); -const actionName = 'swap'; + const response = await metadataPOSTClient.simulateTransactions({ + chainId, + transactions, + }); + const gasLimit = response.simulateTransactions + ?.map(res => res?.gas?.estimate) + .reduce((acc, limit) => (acc && limit ? add(acc, limit) : acc), '0'); + return gasLimit; + } + } catch (e) { + return null; + } + return null; +}; export const executeSwap = async ({ chainId, gasLimit, - maxFeePerGas, - maxPriorityFeePerGas, - gasPrice, nonce, - tradeDetails, + quote, + gasParams, wallet, permit = false, - flashbots = false, }: { chainId: ChainId; - gasLimit: string | number; - maxFeePerGas: string; - maxPriorityFeePerGas: string; - gasPrice: string; + gasLimit: string; + gasParams: TransactionGasParams | TransactionLegacyGasParams; nonce?: number; - tradeDetails: Quote | null; - wallet: Signer | null; + quote: Quote; + wallet: Signer; permit: boolean; - flashbots: boolean; -}) => { - if (!wallet || !tradeDetails) return null; - const walletAddress = await wallet.getAddress(); +}): Promise => { + if (!wallet || !quote) return null; - const { sellTokenAddress, buyTokenAddress } = tradeDetails; + const { sellTokenAddress, buyTokenAddress } = quote; const transactionParams = { gasLimit: toHex(gasLimit) || undefined, - // In case it's an L2 with legacy gas price like arbitrum - gasPrice, - // EIP-1559 like networks - maxFeePerGas, - maxPriorityFeePerGas, - nonce: nonce ? toHex(nonce) : undefined, + nonce: nonce ? toHex(`${nonce}`) : undefined, + ...gasParams, }; // Wrap Eth - if (sellTokenAddress === ETH_ADDRESS && buyTokenAddress === WRAPPED_ASSET[chainId]) { - logger.debug('wrapping native asset', tradeDetails.buyAmount, walletAddress, chainId); - return wrapNativeAsset(tradeDetails.buyAmount, wallet, chainId, transactionParams); + if (isWrapNative({ buyTokenAddress, sellTokenAddress, chainId: chainId as unknown as SwapChainId })) { + return wrapNativeAsset(quote.buyAmount, wallet, chainId as unknown as SwapChainId, transactionParams); // Unwrap Weth - } else if (sellTokenAddress === WRAPPED_ASSET[chainId] && buyTokenAddress === ETH_ADDRESS) { - logger.debug('unwrapping native asset', tradeDetails.sellAmount, walletAddress, chainId); - return unwrapNativeAsset(tradeDetails.sellAmount, wallet, chainId, transactionParams); + } else if (isUnwrapNative({ buyTokenAddress, sellTokenAddress, chainId: chainId as unknown as SwapChainId })) { + return unwrapNativeAsset(quote.sellAmount, wallet, chainId as unknown as SwapChainId, transactionParams); // Swap } else { - logger.debug('FILLQUOTE', tradeDetails, transactionParams, walletAddress, permit, chainId); - return fillQuote(tradeDetails, transactionParams, wallet, permit, chainId, REFERRER); + return fillQuote(quote, transactionParams, wallet, permit, chainId as unknown as SwapChainId, REFERRER); } }; -const swap = async ( - wallet: Signer, - currentRap: Rap, - index: number, - parameters: RapExchangeActionParameters, - baseNonce?: number -): Promise => { - logger.log(`[${actionName}] base nonce`, baseNonce, 'index:', index); - const { inputAmount, tradeDetails, permit, chainId, requiresApprove } = parameters as SwapActionParameters; - const { dispatch } = store; - const { accountAddress } = store.getState().settings; - const { inputCurrency, outputCurrency } = store.getState().swap; - const { gasFeeParamsBySpeed, selectedGasFee } = store.getState().gas; +export const swap = async ({ + currentRap, + wallet, + index, + parameters, + baseNonce, + selectedGasFee, + gasFeeParamsBySpeed, +}: ActionProps<'swap'>): Promise => { let gasParams = parseGasParamAmounts(selectedGasFee); + const { quote, permit, chainId, requiresApprove } = parameters; + // if swap isn't the last action, use fast gas or custom (whatever is faster) + if (currentRap.actions.length - 1 > index) { gasParams = overrideWithFastSpeedIfNeeded({ gasParams, @@ -96,99 +254,107 @@ const swap = async ( gasFeeParamsBySpeed, }); } + let gasLimit; try { - const newGasLimit = await estimateSwapGasLimit({ - chainId: Number(chainId), + gasLimit = await estimateSwapGasLimit({ + chainId, requiresApprove, - tradeDetails, + quote, }); - gasLimit = newGasLimit; } catch (e) { - logger.sentry(`[${actionName}] error estimateSwapGasLimit`); - captureException(e); + logger.error(new RainbowError('swap: error estimateSwapGasLimit'), { + message: (e as Error)?.message, + }); + throw e; } let swap; try { - logger.sentry(`[${actionName}] executing rap`, { - ...gasParams, - gasLimit, - }); const nonce = baseNonce ? baseNonce + index : undefined; - const swapParams = { - ...gasParams, + gasParams, chainId, - flashbots: !!parameters.flashbots, gasLimit, nonce, permit: !!permit, - tradeDetails, + quote, wallet, }; - - // @ts-ignore swap = await executeSwap(swapParams); - - if (permit) { - const walletAddress = await wallet.getAddress(); - // Clear the allowance - const cacheKey = toLower(`${walletAddress}|${tradeDetails.sellTokenAddress}|${tradeDetails.to}`); - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete AllowancesCache.cache[cacheKey]; - } } catch (e) { - logger.sentry('Error', e); - const fakeError = new Error('Failed to execute swap'); - captureException(fakeError); + logger.error(new RainbowError('swap: error executeSwap'), { + message: (e as Error)?.message, + }); throw e; } - logger.log(`[${actionName}] response`, swap); - - if (!swap || !swap?.hash) throw Error; + if (!swap) throw new RainbowError('swap: error executeSwap'); - const newTransaction: NewTransaction = { - data: swap?.data, - from: accountAddress, - to: swap?.to ?? null, - value: tradeDetails?.value?.toString() || '', - asset: outputCurrency, + const transaction = { + data: swap.data, + from: swap.from as Address, + to: swap.to as Address, + value: quote.value?.toString(), + // TODO: MARK - Replace this once we migrate network => chainId + // asset: parameters.assetToBuy, + asset: { + ...parameters.assetToBuy, + network: ethereumUtils.getNetworkFromChainId(parameters.assetToBuy.chainId), + colors: parameters.assetToBuy.colors as TokenColors, + } as ParsedAsset, changes: [ { direction: 'out', - asset: inputCurrency, - value: tradeDetails.sellAmount.toString(), + // TODO: MARK - Replace this once we migrate network => chainId + // asset: parameters.assetToSell, + asset: { + ...parameters.assetToSell, + network: ethereumUtils.getNetworkFromChainId(parameters.assetToSell.chainId), + colors: parameters.assetToSell.colors as TokenColors, + }, + value: quote.sellAmount.toString(), }, { direction: 'in', - asset: outputCurrency, - value: tradeDetails.buyAmount.toString(), + // TODO: MARK - Replace this once we migrate network => chainId + // asset: parameters.assetToBuy, + asset: { + ...parameters.assetToBuy, + network: ethereumUtils.getNetworkFromChainId(parameters.assetToBuy.chainId), + colors: parameters.assetToSell.colors as TokenColors, + }, + value: quote.buyAmount.toString(), }, ], - hash: swap.hash, - network: inputCurrency.network, + hash: swap.hash as TxHash, + // TODO: MARK - Replace this once we migrate network => chainId + network: ethereumUtils.getNetworkFromChainId(parameters.chainId), + // chainId: parameters.chainId, nonce: swap.nonce, status: 'pending', type: 'swap', flashbots: parameters.flashbots, ...gasParams, - }; - logger.log(`[${actionName}] adding new txn`, newTransaction); + } satisfies NewTransaction; - if (parameters.meta && swap?.hash) { + // TODO: MARK - Replace this once we migrate network => chainId + const network = ethereumUtils.getNetworkFromChainId(parameters.chainId); + + if (parameters.meta && swap.hash) { swapMetadataStorage.set(swap.hash.toLowerCase(), JSON.stringify({ type: 'swap', data: parameters.meta })); } addNewTransaction({ - address: accountAddress, - transaction: newTransaction, - network: inputCurrency.network, + address: parameters.quote.from as Address, + // chainId: parameters.chainId as ChainId, + network, + transaction, }); - return swap?.nonce; + return { + nonce: swap.nonce, + hash: swap.hash, + }; }; - -export { swap }; diff --git a/src/raps/actions/unlock.ts b/src/raps/actions/unlock.ts index 6686103960f..140d5ca166d 100644 --- a/src/raps/actions/unlock.ts +++ b/src/raps/actions/unlock.ts @@ -1,90 +1,204 @@ -import { MaxUint256 } from '@ethersproject/constants'; -import { Contract } from '@ethersproject/contracts'; import { Signer } from '@ethersproject/abstract-signer'; -import { ALLOWS_PERMIT, PermitSupportedTokenList, getRainbowRouterContractAddress } from '@rainbow-me/swaps'; -import { captureException } from '@sentry/react-native'; -import { isNull } from 'lodash'; -import { alwaysRequireApprove } from '../../config/debug'; -import { Rap, RapExchangeActionParameters, UnlockActionParameters } from '../common'; -import { Asset, NewTransaction } from '@/entities'; -import { getProviderForNetwork, toHex } from '@/handlers/web3'; -import { parseGasParamAmounts } from '@/parsers'; +import { MaxUint256 } from '@ethersproject/constants'; +import { Contract, PopulatedTransaction } from '@ethersproject/contracts'; +import { parseUnits } from '@ethersproject/units'; +import { getProviderForNetwork } from '@/handlers/web3'; +import { Address, erc20Abi, erc721Abi } from 'viem'; + +import { ChainId } from '@/__swaps__/types/chains'; +import { TransactionGasParams, TransactionLegacyGasParams } from '@/__swaps__/types/gas'; +import { NewTransaction } from '@/entities/transactions'; +import { TxHash } from '@/resources/transactions/types'; +import { addNewTransaction } from '@/state/pendingTransactions'; +import { RainbowError, logger } from '@/logger'; -import store from '@/redux/store'; -import { erc20ABI, ETH_ADDRESS, ethUnits } from '@/references'; +import { ETH_ADDRESS, gasUnits } from '@/references'; +import { ParsedAsset as SwapsParsedAsset } from '@/__swaps__/types/assets'; import { convertAmountToRawAmount, greaterThan } from '@/helpers/utilities'; -import { AllowancesCache, ethereumUtils } from '@/utils'; -import { overrideWithFastSpeedIfNeeded } from '../utils'; -import logger from '@/utils/logger'; +import { ActionProps, RapActionResult } from '../references'; + +import { overrideWithFastSpeedIfNeeded } from './../utils'; +import { ethereumUtils } from '@/utils'; +import { toHex } from '@/__swaps__/utils/hex'; +import { TokenColors } from '@/graphql/__generated__/metadata'; import { ParsedAsset } from '@/resources/assets/types'; -import { addNewTransaction } from '@/state/pendingTransactions'; +import { parseGasParamAmounts } from '@/parsers'; -export const estimateApprove = async ( - owner: string, - tokenAddress: string, - spender: string, - chainId = 1, - allowsPermit = true -): Promise => { +export const getAssetRawAllowance = async ({ + owner, + assetAddress, + spender, + chainId, +}: { + owner: Address; + assetAddress: Address; + spender: Address; + chainId: ChainId; +}) => { try { - if (allowsPermit && ALLOWS_PERMIT[tokenAddress?.toLowerCase() as keyof PermitSupportedTokenList]) { - return '0'; - } - - const network = ethereumUtils.getNetworkFromChainId(chainId); - const provider = await getProviderForNetwork(network); - logger.sentry('exchange estimate approve', { - owner, - spender, - tokenAddress, + // TODO: MARK - Replace this once we migrate network => chainId + const provider = await getProviderForNetwork(ethereumUtils.getNetworkFromChainId(chainId)); + const tokenContract = new Contract(assetAddress, erc20Abi, provider); + const allowance = await tokenContract.allowance(owner, spender); + return allowance.toString(); + } catch (error) { + logger.error(new RainbowError('getRawAllowance: error'), { + message: (error as Error)?.message, }); - const tokenContract = new Contract(tokenAddress, erc20ABI, provider); + return null; + } +}; + +export const assetNeedsUnlocking = async ({ + owner, + amount, + assetToUnlock, + spender, + chainId, +}: { + owner: Address; + amount: string; + assetToUnlock: SwapsParsedAsset; + spender: Address; + chainId: ChainId; +}) => { + if (assetToUnlock.isNativeAsset || assetToUnlock.address === ETH_ADDRESS) return false; + + const allowance = await getAssetRawAllowance({ + owner, + assetAddress: assetToUnlock.address, + spender, + chainId, + }); + + const rawAmount = convertAmountToRawAmount(amount, assetToUnlock.decimals); + const needsUnlocking = !greaterThan(allowance, rawAmount); + return needsUnlocking; +}; + +export const estimateApprove = async ({ + owner, + tokenAddress, + spender, + chainId, +}: { + owner: Address; + tokenAddress: Address; + spender: Address; + chainId: ChainId; +}): Promise => { + try { + // TODO: MARK - Replace this once we migrate network => chainId + const provider = await getProviderForNetwork(ethereumUtils.getNetworkFromChainId(chainId)); + const tokenContract = new Contract(tokenAddress, erc20Abi, provider); const gasLimit = await tokenContract.estimateGas.approve(spender, MaxUint256, { from: owner, }); - return gasLimit ? gasLimit.toString() : ethUnits.basic_approval; + return gasLimit ? gasLimit.toString() : `${gasUnits.basic_approval}`; } catch (error) { - logger.sentry('error estimateApproveWithExchange'); - captureException(error); - return ethUnits.basic_approval; + logger.error(new RainbowError('unlock: error estimateApprove'), { + message: (error as Error)?.message, + }); + return `${gasUnits.basic_approval}`; } }; -const getRawAllowance = async (owner: string, token: Asset, spender: string, chainId = 1) => { +export const populateApprove = async ({ + owner, + tokenAddress, + spender, + chainId, +}: { + owner: Address; + tokenAddress: Address; + spender: Address; + chainId: ChainId; +}): Promise => { try { - const network = ethereumUtils.getNetworkFromChainId(chainId); - const provider = await getProviderForNetwork(network); - const { address: tokenAddress } = token; - const tokenContract = new Contract(tokenAddress, erc20ABI, provider); - const allowance = await tokenContract.allowance(owner, spender); - return allowance.toString(); + // TODO: MARK - Replace this once we migrate network => chainId + const provider = await getProviderForNetwork(ethereumUtils.getNetworkFromChainId(chainId)); + const tokenContract = new Contract(tokenAddress, erc20Abi, provider); + const approveTransaction = await tokenContract.populateTransaction.approve(spender, MaxUint256, { + from: owner, + }); + return approveTransaction; } catch (error) { - logger.sentry('error getRawAllowance'); - captureException(error); + logger.error(new RainbowError(' error populateApprove'), { + message: (error as Error)?.message, + }); return null; } }; -const executeApprove = async ( - tokenAddress: string, - spender: string, - gasLimit: number | string, - gasParams: - | { - gasPrice: string; - maxFeePerGas?: undefined; - maxPriorityFeePerGas?: undefined; - } - | { - maxFeePerGas: string; - maxPriorityFeePerGas: string; - gasPrice?: undefined; - }, - wallet: Signer, - nonce: number | null = null, - chainId = 1 -) => { - const exchange = new Contract(tokenAddress, erc20ABI, wallet); +export const estimateERC721Approval = async ({ + owner, + tokenAddress, + spender, + chainId, +}: { + owner: Address; + tokenAddress: Address; + spender: Address; + chainId: ChainId; +}): Promise => { + try { + // TODO: MARK - Replace this once we migrate network => chainId + const provider = await getProviderForNetwork(ethereumUtils.getNetworkFromChainId(chainId)); + const tokenContract = new Contract(tokenAddress, erc721Abi, provider); + const gasLimit = await tokenContract.estimateGas.setApprovalForAll(spender, false, { + from: owner, + }); + return gasLimit ? gasLimit.toString() : `${gasUnits.basic_approval}`; + } catch (error) { + logger.error(new RainbowError('estimateERC721Approval: error estimateApproval'), { + message: (error as Error)?.message, + }); + return `${gasUnits.basic_approval}`; + } +}; + +export const populateRevokeApproval = async ({ + tokenAddress, + spenderAddress, + chainId, + type = 'erc20', +}: { + tokenAddress?: Address; + spenderAddress?: Address; + chainId?: ChainId; + type: 'erc20' | 'nft'; +}): Promise => { + if (!tokenAddress || !spenderAddress || !chainId) return {}; + // TODO: MARK - Replace this once we migrate network => chainId + const provider = await getProviderForNetwork(ethereumUtils.getNetworkFromChainId(chainId)); + const tokenContract = new Contract(tokenAddress, erc721Abi, provider); + if (type === 'erc20') { + const amountToApprove = parseUnits('0', 'ether'); + const txObject = await tokenContract.populateTransaction.approve(spenderAddress, amountToApprove); + return txObject; + } else { + const txObject = await tokenContract.populateTransaction.setApprovalForAll(spenderAddress, false); + return txObject; + } +}; + +export const executeApprove = async ({ + gasLimit, + gasParams, + nonce, + spender, + tokenAddress, + wallet, +}: { + chainId: ChainId; + gasLimit: string; + gasParams: Partial; + nonce?: number; + spender: Address; + tokenAddress: Address; + wallet: Signer; +}) => { + const exchange = new Contract(tokenAddress, erc20Abi, wallet); return exchange.approve(spender, MaxUint256, { gasLimit: toHex(gasLimit) || undefined, // In case it's an L2 with legacy gas price like arbitrum @@ -92,120 +206,101 @@ const executeApprove = async ( // EIP-1559 like networks maxFeePerGas: gasParams.maxFeePerGas, maxPriorityFeePerGas: gasParams.maxPriorityFeePerGas, - nonce: nonce ? toHex(nonce) : undefined, + nonce: nonce ? toHex(nonce.toString()) : undefined, }); }; -const actionName = 'unlock'; - -const unlock = async ( - wallet: Signer, - currentRap: Rap, - index: number, - parameters: RapExchangeActionParameters, - baseNonce?: number -): Promise => { - logger.log(`[${actionName}] base nonce`, baseNonce, 'index:', index); - const { dispatch } = store; - const { accountAddress } = store.getState().settings; - const { gasFeeParamsBySpeed, selectedGasFee } = store.getState().gas; - const { assetToUnlock, contractAddress, chainId } = parameters as UnlockActionParameters; +export const unlock = async ({ + baseNonce, + index, + parameters, + wallet, + selectedGasFee, + gasFeeParamsBySpeed, +}: ActionProps<'unlock'>): Promise => { + const { assetToUnlock, contractAddress, chainId } = parameters; + const { address: assetAddress } = assetToUnlock; - logger.log(`[${actionName}] rap for`, assetToUnlock); + if (assetAddress === ETH_ADDRESS) throw new RainbowError('unlock: Native ETH cannot be unlocked'); let gasLimit; try { - logger.sentry(`[${actionName}] estimate gas`, { - assetAddress, - contractAddress, + gasLimit = await estimateApprove({ + owner: parameters.fromAddress, + tokenAddress: assetAddress, + spender: contractAddress, + chainId, }); - const contractAllowsPermit = contractAddress === getRainbowRouterContractAddress(chainId); - gasLimit = await estimateApprove(accountAddress, assetAddress, contractAddress, chainId, contractAllowsPermit); } catch (e) { - logger.sentry(`[${actionName}] Error estimating gas`); - captureException(e); + logger.error(new RainbowError('unlock: error estimateApprove'), { + message: (e as Error)?.message, + }); throw e; } - let approval; + let gasParams = parseGasParamAmounts(selectedGasFee); + gasParams = overrideWithFastSpeedIfNeeded({ + gasParams, + chainId, + gasFeeParamsBySpeed, + }); + + const nonce = baseNonce ? baseNonce + index : undefined; + let approval; try { - gasParams = overrideWithFastSpeedIfNeeded({ + approval = await executeApprove({ + tokenAddress: assetAddress, + spender: contractAddress, + gasLimit, gasParams, + wallet, + nonce, chainId, - gasFeeParamsBySpeed, }); - - logger.sentry(`[${actionName}] about to approve`, { - assetAddress, - contractAddress, - gasLimit, - }); - const nonce = baseNonce ? baseNonce + index : null; - approval = await executeApprove(assetAddress, contractAddress, gasLimit, gasParams, wallet, nonce, chainId); } catch (e) { - logger.sentry(`[${actionName}] Error approving`); - captureException(e); + logger.error(new RainbowError('unlock: error executeApprove'), { + message: (e as Error)?.message, + }); throw e; } - const walletAddress = await wallet.getAddress(); - const cacheKey = `${walletAddress}|${assetAddress}|${contractAddress}`.toLowerCase(); - // Cache the approved value - AllowancesCache.cache[cacheKey] = MaxUint256.toString(); + if (!approval) throw new RainbowError('unlock: error executeApprove'); - logger.log(`[${actionName}] response`, approval); - - const newTransaction: NewTransaction = { - asset: assetToUnlock as ParsedAsset, + const transaction = { + asset: { + ...assetToUnlock, + network: ethereumUtils.getNetworkFromChainId(assetToUnlock.chainId), + colors: assetToUnlock.colors as TokenColors, + } as ParsedAsset, data: approval.data, - from: accountAddress, - gasLimit, - hash: approval?.hash, - type: 'approve', - network: ethereumUtils.getNetworkFromChainId(Number(chainId)), - nonce: approval?.nonce, - to: approval?.to, - value: toHex(approval.value), + value: approval.value?.toString(), + changes: [], + from: parameters.fromAddress, + to: assetAddress, + hash: approval.hash as TxHash, + // TODO: MARK - Replace this once we migrate network => chainId + network: ethereumUtils.getNetworkFromChainId(chainId), + // chainId: approval.chainId, + nonce: approval.nonce, status: 'pending', + type: 'approve', + approvalAmount: 'UNLIMITED', ...gasParams, - }; - logger.log(`[${actionName}] adding new txn`, newTransaction); + } satisfies NewTransaction; + + // TODO: MARK - Replace this once we migrate network => chainId + const network = ethereumUtils.getNetworkFromChainId(approval.chainId); + addNewTransaction({ - address: accountAddress, - transaction: newTransaction, - network: newTransaction.network, + address: parameters.fromAddress as Address, + network, + transaction, }); - return approval?.nonce; -}; -export const assetNeedsUnlocking = async ( - accountAddress: string, - amount: string, - assetToUnlock: Asset, - contractAddress: string, - chainId = 1 -) => { - logger.log('checking asset needs unlocking'); - const { address } = assetToUnlock; - if (address === ETH_ADDRESS) return false; - if (alwaysRequireApprove) return true; - - const cacheKey = `${accountAddress}|${address}|${contractAddress}`.toLowerCase(); - - const allowance = await getRawAllowance(accountAddress, assetToUnlock, contractAddress, chainId); - - logger.log('raw allowance', allowance.toString()); - // Cache that value - if (!isNull(allowance)) { - AllowancesCache.cache[cacheKey] = allowance; - } - - const rawAmount = convertAmountToRawAmount(amount, assetToUnlock.decimals); - const needsUnlocking = !greaterThan(allowance, rawAmount); - logger.log('asset needs unlocking?', needsUnlocking, allowance.toString()); - return needsUnlocking; + return { + nonce: approval?.nonce, + hash: approval?.hash, + }; }; - -export default unlock; diff --git a/src/raps/common.ts b/src/raps/common.ts index c800cb248ac..82282d68678 100644 --- a/src/raps/common.ts +++ b/src/raps/common.ts @@ -1,40 +1,11 @@ -import { Provider } from '@ethersproject/abstract-provider'; -import { Logger } from '@ethersproject/logger'; -import { Signer } from '@ethersproject/abstract-signer'; -import { CrosschainQuote, Quote, SwapType } from '@rainbow-me/swaps'; -import { captureException } from '@sentry/react-native'; -import { ens, swap, crosschainSwap, unlock } from './actions'; -import { - createCommitENSRap, - createRegisterENSRap, - createRenewENSRap, - createSetNameENSRap, - createSetRecordsENSRap, - createTransferENSRap, -} from './registerENS'; -import { createUnlockAndSwapRap, estimateUnlockAndSwap } from './unlockAndSwap'; -import { analytics } from '@/analytics'; -import { Asset, EthereumAddress, Records, SwappableAsset } from '@/entities'; -import { - estimateENSCommitGasLimit, - estimateENSRegisterSetRecordsAndNameGasLimit, - estimateENSRenewGasLimit, - estimateENSSetNameGasLimit, - estimateENSSetRecordsGasLimit, -} from '@/handlers/ens'; -import { ExchangeModalTypes } from '@/helpers'; +import { MMKV } from 'react-native-mmkv'; +import { RapAction, RapActionParameterMap, RapActionTypes } from './references'; +import { STORAGE_IDS } from '@/model/mmkv'; +import { logger } from '@/logger'; +import { EthereumAddress, LegacyGasFeeParamsBySpeed, LegacySelectedGasFee, Records, SelectedGasFee, GasFeeParamsBySpeed } from '@/entities'; import { REGISTRATION_MODES } from '@/helpers/ens'; -import logger from '@/utils/logger'; -import { createUnlockAndCrosschainSwapRap, estimateUnlockAndCrosschainSwap } from './unlockAndCrosschainSwap'; -import { Source, SwapModalField } from '@/redux/swap'; -import * as i18n from '@/languages'; -const { commitENS, registerWithConfig, multicallENS, setAddrENS, reclaimENS, setContenthashENS, setTextENS, setNameENS, renewENS } = ens; - -export enum RapActionType { - swap = 'swap', - crosschainSwap = 'crosschainSwap', - unlock = 'unlock', +export enum ENSRapActionType { commitENS = 'commitENS', registerENS = 'registerENS', multicallENS = 'multicallENS', @@ -44,69 +15,13 @@ export enum RapActionType { setContenthashENS = 'setContenthashENS', setTextENS = 'setTextENS', setNameENS = 'setNameENS', + setRecordsENS = 'setRecordsENS', + transferENS = 'transferENS', + registerWithConfigENS = 'registerWithConfigENS', } -export interface RapExchangeActionParameters { - amount?: string | null; - assetToUnlock?: Asset; - contractAddress?: string; - inputAmount?: string | null; - outputAmount?: string | null; - tradeDetails?: Quote; - permit?: boolean; - flashbots?: boolean; - chainId?: number; - requiresApprove?: boolean; - meta?: SwapMetadata; -} - -export interface RapENSActionParameters { - duration: number; - name: string; - ownerAddress: EthereumAddress; - rentPrice: string; - records?: Records; - salt: string; - toAddress?: string; - mode?: keyof typeof REGISTRATION_MODES; -} - -export interface UnlockActionParameters { - amount: string; - assetToUnlock: Asset; - contractAddress: string; - chainId: number; -} - -export type SwapMetadata = { - flashbots: boolean; - slippage: number; - route: Source; - inputAsset: SwappableAsset; - outputAsset: SwappableAsset; - independentField: SwapModalField; - independentValue: string; -}; - -export interface BaseSwapActionParameters { - inputAmount: string; - nonce?: number; - outputAmount: string; - permit?: boolean; - flashbots?: boolean; - provider?: Provider; - chainId: number; - requiresApprove?: boolean; - swapType?: SwapType; - meta?: SwapMetadata; -} - -export interface SwapActionParameters extends BaseSwapActionParameters { - tradeDetails: Quote; -} - -export interface CrosschainSwapActionParameters extends BaseSwapActionParameters { - tradeDetails: CrosschainQuote; +export interface ENSRap { + actions: RapENSAction[]; } export interface ENSActionParameters { @@ -123,332 +38,70 @@ export interface ENSActionParameters { clearRecords?: boolean; setAddress?: boolean; transferControl?: boolean; + selectedGasFee: SelectedGasFee | LegacySelectedGasFee; + gasFeeParamsBySpeed: GasFeeParamsBySpeed | LegacyGasFeeParamsBySpeed; mode?: keyof typeof REGISTRATION_MODES; } -export interface RapActionTransaction { - hash: string | null; -} - -enum RAP_TYPE { - EXCHANGE = 'EXCHANGE', - ENS = 'ENS', +export interface RapENSActionParameters { + duration: number; + name: string; + ownerAddress: EthereumAddress; + rentPrice: string; + records?: Records; + salt: string; + toAddress?: string; + selectedGasFee: SelectedGasFee | LegacySelectedGasFee; + gasFeeParamsBySpeed: GasFeeParamsBySpeed | LegacyGasFeeParamsBySpeed; + mode?: keyof typeof REGISTRATION_MODES; } -export type RapAction = RapSwapAction | RapENSAction; - -export interface RapSwapAction { - parameters: RapExchangeActionParameters; - transaction: RapActionTransaction; - type: RapActionType; +export interface RapActionTransaction { + hash: string | null; } export interface RapENSAction { parameters: RapENSActionParameters; transaction: RapActionTransaction; - type: RapActionType; -} - -export interface Rap { - actions: RapAction[]; + type: ENSRapActionType; } -export interface ENSRap { - actions: RapENSAction[]; -} - -interface RapActionResponse { - baseNonce?: number | null; - errorMessage: string | null; -} - -interface EthersError extends Error { - code?: string | null; +export function createNewAction(type: T, parameters: RapActionParameterMap[T]): RapAction { + const newAction = { + parameters, + transaction: { confirmed: null, hash: null }, + type, + }; + return newAction; } -const NOOP = () => null; - -export const RapActionTypes = { - commitENS: 'commitENS' as RapActionType, - multicallENS: 'multicallENS' as RapActionType, - reclaimENS: 'reclaimENS' as RapActionType, - registerENS: 'registerENS' as RapActionType, - registerWithConfigENS: 'registerWithConfigENS' as RapActionType, - renewENS: 'renewENS' as RapActionType, - setAddrENS: 'setAddrENS' as RapActionType, - setContenthashENS: 'setContenthashENS' as RapActionType, - setNameENS: 'setNameENS' as RapActionType, - setRecordsENS: 'setRecordsENS' as RapActionType, - setTextENS: 'setTextENS' as RapActionType, - swap: 'swap' as RapActionType, - crosschainSwap: 'crosschainSwap' as RapActionType, - transferENS: 'transferENS' as RapActionType, - unlock: 'unlock' as RapActionType, -}; - -export const getSwapRapTypeByExchangeType = (isCrosschainSwap: boolean) => { - if (isCrosschainSwap) { - return RapActionTypes.crosschainSwap; - } - return RapActionTypes.swap; -}; - -const createSwapRapByType = (type: keyof typeof RapActionTypes, swapParameters: SwapActionParameters | CrosschainSwapActionParameters) => { - switch (type) { - case RapActionTypes.crosschainSwap: - return createUnlockAndCrosschainSwapRap(swapParameters as CrosschainSwapActionParameters); - default: - return createUnlockAndSwapRap(swapParameters); - } -}; - -const createENSRapByType = (type: string, ensRegistrationParameters: ENSActionParameters) => { - switch (type) { - case RapActionTypes.registerENS: - return createRegisterENSRap(ensRegistrationParameters); - case RapActionTypes.renewENS: - return createRenewENSRap(ensRegistrationParameters); - case RapActionTypes.setNameENS: - return createSetNameENSRap(ensRegistrationParameters); - case RapActionTypes.setRecordsENS: - return createSetRecordsENSRap(ensRegistrationParameters); - case RapActionTypes.transferENS: - return createTransferENSRap(ensRegistrationParameters); - case RapActionTypes.commitENS: - default: - return createCommitENSRap(ensRegistrationParameters); - } -}; - -export const getSwapRapEstimationByType = ( - type: keyof typeof RapActionTypes, - swapParameters: SwapActionParameters | CrosschainSwapActionParameters -) => { - switch (type) { - case RapActionTypes.swap: - return estimateUnlockAndSwap(swapParameters); - case RapActionTypes.crosschainSwap: - return estimateUnlockAndCrosschainSwap(swapParameters as CrosschainSwapActionParameters); - default: - return null; - } -}; - -export const getENSRapEstimationByType = (type: string, ensRegistrationParameters: ENSActionParameters) => { - switch (type) { - case RapActionTypes.commitENS: - return estimateENSCommitGasLimit(ensRegistrationParameters); - case RapActionTypes.registerENS: - return estimateENSRegisterSetRecordsAndNameGasLimit(ensRegistrationParameters); - case RapActionTypes.renewENS: - return estimateENSRenewGasLimit(ensRegistrationParameters); - case RapActionTypes.setNameENS: - return estimateENSSetNameGasLimit(ensRegistrationParameters); - case RapActionTypes.setRecordsENS: - return estimateENSSetRecordsGasLimit(ensRegistrationParameters); - default: - return null; - } -}; - -const findSwapActionByType = (type: RapActionType) => { - switch (type) { - case RapActionTypes.unlock: - return unlock; - case RapActionTypes.swap: - return swap; - case RapActionTypes.crosschainSwap: - return crosschainSwap; - default: - return NOOP; - } -}; - -const findENSActionByType = (type: RapActionType) => { - switch (type) { - case RapActionTypes.commitENS: - return commitENS; - case RapActionTypes.registerWithConfigENS: - return registerWithConfig; - case RapActionTypes.multicallENS: - return multicallENS; - case RapActionTypes.setAddrENS: - return setAddrENS; - case RapActionTypes.setContenthashENS: - return setContenthashENS; - case RapActionTypes.setTextENS: - return setTextENS; - case RapActionTypes.setNameENS: - return setNameENS; - case RapActionTypes.reclaimENS: - return reclaimENS; - case RapActionTypes.renewENS: - return renewENS; - default: - return NOOP; - } -}; - -const getRapFullName = (actions: RapAction[]) => { - const actionTypes = actions.map(action => action.type); - return actionTypes.join(' + '); -}; - -// i18n -const parseError = (error: EthersError): string => { - const errorCode = error?.code; - switch (errorCode) { - case Logger.errors.UNPREDICTABLE_GAS_LIMIT: - return i18n.t(i18n.l.wallet.transaction.errors.unpredictable_gas); - case Logger.errors.INSUFFICIENT_FUNDS: - return i18n.t(i18n.l.wallet.transaction.errors.insufficient_funds); - default: - return i18n.t(i18n.l.wallet.transaction.errors.generic); - } -}; - -const executeAction = async ( - action: RapAction, - wallet: Signer, - rap: Rap, - index: number, - rapName: string, - baseNonce?: number -): Promise => { - logger.log('[1 INNER] index', index); - const { type, parameters } = action; - let nonce; - try { - logger.log('[2 INNER] executing type', type); - const rapType = getRapTypeFromActionType(type); - if (rapType === RAP_TYPE.ENS) { - const actionPromise = findENSActionByType(type); - nonce = await actionPromise(wallet, rap, index, parameters as RapENSActionParameters, baseNonce); - return { baseNonce: nonce, errorMessage: null }; - } else { - const actionPromise = findSwapActionByType(type); - nonce = await actionPromise(wallet, rap, index, parameters as RapExchangeActionParameters, baseNonce); - return { baseNonce: nonce, errorMessage: null }; - } - } catch (error: any) { - logger.debug('Rap blew up', error); - logger.sentry('[3 INNER] error running action, code:', error?.code); - captureException(error); - analytics.track('Rap failed', { - category: 'raps', - failed_action: type, - label: rapName, - }); - // If the first action failed, return an error message - if (index === 0) { - const errorMessage = parseError(error); - logger.log('[4 INNER] displaying error message', errorMessage); - return { baseNonce: null, errorMessage }; - } - return { baseNonce: null, errorMessage: null }; - } -}; - -const getRapTypeFromActionType = (actionType: RapActionType) => { - switch (actionType) { - case RapActionTypes.swap: - case RapActionTypes.crosschainSwap: - case RapActionTypes.unlock: - return RAP_TYPE.EXCHANGE; - case RapActionTypes.commitENS: - case RapActionTypes.registerENS: - case RapActionTypes.registerWithConfigENS: - case RapActionTypes.multicallENS: - case RapActionTypes.renewENS: - case RapActionTypes.setNameENS: - case RapActionTypes.setAddrENS: - case RapActionTypes.reclaimENS: - case RapActionTypes.setContenthashENS: - case RapActionTypes.setTextENS: - case RapActionTypes.setRecordsENS: - case RapActionTypes.transferENS: - return RAP_TYPE.ENS; - } - return ''; -}; - -export const executeRap = async ( - wallet: Signer, - type: RapActionType, - parameters: SwapActionParameters | ENSActionParameters, - callback: (success?: boolean, errorMessage?: string | null) => void -) => { - const rapType = getRapTypeFromActionType(type); - - let rap: Rap = { actions: [] }; - if (rapType === RAP_TYPE.EXCHANGE) { - rap = await createSwapRapByType(type, parameters as SwapActionParameters); - } else if (rapType === RAP_TYPE.ENS) { - rap = await createENSRapByType(type, parameters as ENSActionParameters); - } - - const { actions } = rap; - const rapName = getRapFullName(actions); - - analytics.track('Rap started', { - category: 'raps', - label: rapName, - }); - - let nonce = parameters?.nonce; - - logger.log('[common - executing rap]: actions', actions); - if (actions.length) { - const firstAction = actions[0]; - const { baseNonce, errorMessage } = await executeAction(firstAction, wallet, rap, 0, rapName, nonce); - - if (typeof baseNonce === 'number') { - for (let index = 1; index < actions.length; index++) { - const action = actions[index]; - await executeAction(action, wallet, rap, index, rapName, baseNonce); - } - nonce = baseNonce + actions.length - 1; - callback(true); - } else { - // Callback with failure state - callback(false, errorMessage); - } - } - - analytics.track('Rap completed', { - category: 'raps', - label: rapName, - }); - logger.log('[common - executing rap] finished execute rap function'); - - return { nonce }; -}; - -export const createNewRap = (actions: RapAction[]) => { +export function createNewRap(actions: RapAction[]) { return { actions, }; -}; +} -export const createNewAction = (type: RapActionType, parameters: RapExchangeActionParameters): RapSwapAction => { - const newAction = { - parameters, - rapType: RAP_TYPE.EXCHANGE, - transaction: { confirmed: null, hash: null }, - type, +export function createNewENSRap(actions: RapENSAction[]) { + return { + actions, }; +} - logger.log('[common] Creating a new action', newAction); - return newAction; -}; +export const swapMetadataStorage = new MMKV({ + id: STORAGE_IDS.SWAPS_METADATA_STORAGE, +}); -export const createNewENSAction = (type: RapActionType, parameters: ENSActionParameters): RapENSAction => { +export const createNewENSAction = (type: ENSRapActionType, parameters: ENSActionParameters): RapENSAction => { const newAction = { parameters, transaction: { confirmed: null, hash: null }, type, }; - logger.log('[common] Creating a new action', newAction); + logger.log('[common] Creating a new action', { + extra: { + ...newAction, + }, + }); return newAction; }; diff --git a/src/raps/execute.ts b/src/raps/execute.ts new file mode 100644 index 00000000000..5b86ba03150 --- /dev/null +++ b/src/raps/execute.ts @@ -0,0 +1,166 @@ +/* eslint-disable no-await-in-loop */ +/* eslint-disable no-async-promise-executor */ +/* eslint-disable no-promise-executor-return */ +import { Signer } from '@ethersproject/abstract-signer'; + +import { RainbowError, logger } from '@/logger'; + +import { swap, unlock } from './actions'; +import { crosschainSwap } from './actions/crosschainSwap'; +import { + ActionProps, + Rap, + RapAction, + RapActionResponse, + RapActionResult, + RapActionTypes, + RapSwapActionParameters, + RapTypes, +} from './references'; +import { createUnlockAndCrosschainSwapRap } from './unlockAndCrosschainSwap'; +import { createUnlockAndSwapRap } from './unlockAndSwap'; +import { GasFeeParamsBySpeed, LegacyGasFeeParamsBySpeed, LegacySelectedGasFee, SelectedGasFee } from '@/entities'; + +export function createSwapRapByType(type: T, swapParameters: RapSwapActionParameters) { + switch (type) { + case 'crosschainSwap': + return createUnlockAndCrosschainSwapRap(swapParameters as RapSwapActionParameters<'crosschainSwap'>); + case 'swap': + return createUnlockAndSwapRap(swapParameters as RapSwapActionParameters<'swap'>); + default: + return { actions: [] }; + } +} + +function typeAction(type: T, props: ActionProps) { + switch (type) { + case 'unlock': + return () => unlock(props as ActionProps<'unlock'>); + case 'swap': + return () => swap(props as ActionProps<'swap'>); + case 'crosschainSwap': + return () => crosschainSwap(props as ActionProps<'crosschainSwap'>); + default: + // eslint-disable-next-line react/display-name + return () => null; + } +} + +export async function executeAction({ + action, + wallet, + rap, + index, + baseNonce, + rapName, + flashbots, + selectedGasFee, + gasFeeParamsBySpeed, +}: { + action: RapAction; + wallet: Signer; + rap: Rap; + index: number; + baseNonce?: number; + rapName: string; + flashbots?: boolean; + selectedGasFee: SelectedGasFee | LegacySelectedGasFee; + gasFeeParamsBySpeed: GasFeeParamsBySpeed | LegacyGasFeeParamsBySpeed; +}): Promise { + const { type, parameters } = action; + try { + const actionProps = { + wallet, + currentRap: rap, + index, + parameters: { ...parameters, flashbots }, + baseNonce, + selectedGasFee, + gasFeeParamsBySpeed, + }; + const { nonce, hash } = (await typeAction(type, actionProps)()) as RapActionResult; + return { baseNonce: nonce, errorMessage: null, hash }; + } catch (error) { + logger.error(new RainbowError(`rap: ${rapName} - error execute action`), { + message: (error as Error)?.message, + }); + if (index === 0) { + return { baseNonce: null, errorMessage: String(error) }; + } + return { baseNonce: null, errorMessage: null }; + } +} + +function getRapFullName(actions: RapAction[]) { + const actionTypes = actions.map(action => action.type); + return actionTypes.join(' + '); +} + +const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); + +const waitForNodeAck = async (hash: string, provider: Signer['provider']): Promise => { + return new Promise(async resolve => { + const tx = await provider?.getTransaction(hash); + // This means the node is aware of the tx, we're good to go + if ((tx && tx.blockNumber === null) || (tx && tx?.blockNumber && tx?.blockNumber > 0)) { + resolve(); + } else { + // Wait for 1 second and try again + await delay(1000); + return waitForNodeAck(hash, provider); + } + }); +}; + +export const walletExecuteRap = async ( + wallet: Signer, + type: RapTypes, + parameters: RapSwapActionParameters<'swap' | 'crosschainSwap'> +): Promise<{ nonce: number | undefined; errorMessage: string | null }> => { + const rap: Rap = await createSwapRapByType(type, parameters); + + const { actions } = rap; + const rapName = getRapFullName(rap.actions); + let nonce = parameters?.nonce; + let errorMessage = null; + if (actions.length) { + const firstAction = actions[0]; + const actionParams = { + action: firstAction, + wallet, + rap, + index: 0, + baseNonce: nonce, + rapName, + flashbots: parameters?.flashbots, + selectedGasFee: parameters?.selectedGasFee, + gasFeeParamsBySpeed: parameters?.gasFeeParamsBySpeed, + }; + + const { baseNonce, errorMessage: error, hash } = await executeAction(actionParams); + + if (typeof baseNonce === 'number') { + actions.length > 1 && hash && (await waitForNodeAck(hash, wallet.provider)); + for (let index = 1; index < actions.length; index++) { + const action = actions[index]; + const actionParams = { + action, + wallet, + rap, + index, + baseNonce, + rapName, + flashbots: parameters?.flashbots, + selectedGasFee: parameters?.selectedGasFee, + gasFeeParamsBySpeed: parameters?.gasFeeParamsBySpeed, + }; + const { hash } = await executeAction(actionParams); + hash && (await waitForNodeAck(hash, wallet.provider)); + } + nonce = baseNonce + actions.length - 1; + } else { + errorMessage = error; + } + } + return { nonce, errorMessage }; +}; diff --git a/src/raps/index.ts b/src/raps/index.ts deleted file mode 100644 index 42c60afba04..00000000000 --- a/src/raps/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { createUnlockAndSwapRap, estimateUnlockAndSwap } from './unlockAndSwap'; -export { executeRap, getSwapRapEstimationByType, getSwapRapTypeByExchangeType } from './common'; -export { createRegisterENSRap, createCommitENSRap, createSetRecordsENSRap } from './registerENS'; diff --git a/src/raps/references.ts b/src/raps/references.ts new file mode 100644 index 00000000000..af51692fc42 --- /dev/null +++ b/src/raps/references.ts @@ -0,0 +1,127 @@ +import { Signer } from '@ethersproject/abstract-signer'; +import { CrosschainQuote, Quote } from '@rainbow-me/swaps'; +import { Address } from 'viem'; + +import { ParsedAsset } from '@/__swaps__/types/assets'; +import { GasFeeParamsBySpeed, LegacyGasFeeParamsBySpeed, LegacySelectedGasFee, SelectedGasFee } from '@/entities'; + +export enum SwapModalField { + input = 'inputAmount', + native = 'nativeAmount', + output = 'outputAmount', +} + +export enum Source { + AggregatorRainbow = 'rainbow', + Aggregator0x = '0x', + Aggregator1inch = '1inch', + Socket = 'socket', +} + +export interface UnlockActionParameters { + amount: string; + assetToUnlock: ParsedAsset; + contractAddress: Address; + chainId: number; +} + +export type SwapMetadata = { + slippage: number; + route: Source; + inputAsset: ParsedAsset; + outputAsset: ParsedAsset; + independentField: SwapModalField; + independentValue: string; +}; + +export type QuoteTypeMap = { + swap: Quote; + crosschainSwap: CrosschainQuote; +}; + +export interface RapSwapActionParameters { + amount?: string | null; + sellAmount: string; + buyAmount?: string; + permit?: boolean; + chainId: number; + requiresApprove?: boolean; + meta?: SwapMetadata; + assetToSell: ParsedAsset; + assetToBuy: ParsedAsset; + selectedGasFee: SelectedGasFee | LegacySelectedGasFee; + gasFeeParamsBySpeed: GasFeeParamsBySpeed | LegacyGasFeeParamsBySpeed; + nonce?: number; + flashbots?: boolean; + quote: QuoteTypeMap[T]; +} + +export interface RapUnlockActionParameters { + fromAddress: Address; + assetToUnlock: ParsedAsset; + contractAddress: Address; + chainId: number; +} + +export type RapActionParameters = RapSwapActionParameters<'swap'> | RapSwapActionParameters<'crosschainSwap'> | RapUnlockActionParameters; + +export interface RapActionTransaction { + hash: string | null; +} + +export type RapActionParameterMap = { + swap: RapSwapActionParameters<'swap'>; + crosschainSwap: RapSwapActionParameters<'crosschainSwap'>; + unlock: RapUnlockActionParameters; +}; + +export interface RapAction { + parameters: RapActionParameterMap[T]; + transaction: RapActionTransaction; + type: T; +} + +export interface Rap { + actions: RapAction<'swap' | 'crosschainSwap' | 'unlock'>[]; +} + +export enum rapActions { + swap = 'swap', + crosschainSwap = 'crosschainSwap', + unlock = 'unlock', +} + +export type RapActionTypes = keyof typeof rapActions; + +export enum rapTypes { + swap = 'swap', + crosschainSwap = 'crosschainSwap', +} + +export type RapTypes = keyof typeof rapTypes; + +export interface RapActionResponse { + baseNonce?: number | null; + errorMessage: string | null; + hash?: string | null; +} + +export interface RapActionResult { + nonce?: number | undefined; + hash?: string | undefined; +} + +export interface ActionProps { + baseNonce?: number; + index: number; + parameters: RapActionParameterMap[T]; + wallet: Signer; + currentRap: Rap; + selectedGasFee: SelectedGasFee | LegacySelectedGasFee; + gasFeeParamsBySpeed: GasFeeParamsBySpeed | LegacyGasFeeParamsBySpeed; +} + +export interface WalletExecuteRapProps { + rapActionParameters: RapSwapActionParameters<'swap' | 'crosschainSwap'>; + type: RapTypes; +} diff --git a/src/raps/registerENS.ts b/src/raps/registerENS.ts index 3ec5823c5ec..4f47f2bd33f 100644 --- a/src/raps/registerENS.ts +++ b/src/raps/registerENS.ts @@ -1,6 +1,6 @@ import { AddressZero } from '@ethersproject/constants'; import { isEmpty } from 'lodash'; -import { createNewENSAction, createNewRap, ENSActionParameters, RapActionTypes, RapENSAction } from './common'; +import { createNewENSAction, createNewENSRap, ENSActionParameters, RapENSAction, ENSRapActionType } from './common'; import { Records } from '@/entities'; import { formatRecordsForTransaction, @@ -27,19 +27,19 @@ export const createSetRecordsENSRap = async (ensActionParameters: ENSActionParam } if (ensActionParameters.setReverseRecord) { - const setName = createNewENSAction(RapActionTypes.setNameENS, ensActionParameters); + const setName = createNewENSAction(ENSRapActionType.setNameENS, ensActionParameters); actions = actions.concat(setName); } // create the overall rap - const newRap = createNewRap(actions); + const newRap = createNewENSRap(actions); return newRap; }; export const createRegisterENSRap = async (ensActionParameters: ENSActionParameters) => { let actions: RapENSAction[] = []; - const register = createNewENSAction(RapActionTypes.registerWithConfigENS, ensActionParameters); + const register = createNewENSAction(ENSRapActionType.registerWithConfigENS, ensActionParameters); actions = actions.concat(register); const ensRegistrationRecords = formatRecordsForTransaction(ensActionParameters.records); @@ -56,34 +56,34 @@ export const createRegisterENSRap = async (ensActionParameters: ENSActionParamet } if (ensActionParameters.setReverseRecord) { - const setName = createNewENSAction(RapActionTypes.setNameENS, ensActionParameters); + const setName = createNewENSAction(ENSRapActionType.setNameENS, ensActionParameters); actions = actions.concat(setName); } // create the overall rap - const newRap = createNewRap(actions); + const newRap = createNewENSRap(actions); return newRap; }; export const createRenewENSRap = async (ENSActionParameters: ENSActionParameters) => { let actions: RapENSAction[] = []; // // commit rap - const commit = createNewENSAction(RapActionTypes.renewENS, ENSActionParameters); + const commit = createNewENSAction(ENSRapActionType.renewENS, ENSActionParameters); actions = actions.concat(commit); // create the overall rap - const newRap = createNewRap(actions); + const newRap = createNewENSRap(actions); return newRap; }; export const createSetNameENSRap = async (ENSActionParameters: ENSActionParameters) => { let actions: RapENSAction[] = []; // // commit rap - const commit = createNewENSAction(RapActionTypes.setNameENS, ENSActionParameters); + const commit = createNewENSAction(ENSRapActionType.setNameENS, ENSActionParameters); actions = actions.concat(commit); // create the overall rap - const newRap = createNewRap(actions); + const newRap = createNewENSRap(actions); return newRap; }; @@ -126,14 +126,14 @@ export const createTransferENSRap = async (ensActionParameters: ENSActionParamet } } } else if (setAddress) { - const setName = createNewENSAction(RapActionTypes.setAddrENS, { + const setName = createNewENSAction(ENSRapActionType.setAddrENS, { ...ensActionParameters, records: { ETH: toAddress }, }); actions = actions.concat(setName); } if (transferControl && toAddress) { - const transferControl = createNewENSAction(RapActionTypes.reclaimENS, { + const transferControl = createNewENSAction(ENSRapActionType.reclaimENS, { ...ensActionParameters, toAddress, }); @@ -141,17 +141,17 @@ export const createTransferENSRap = async (ensActionParameters: ENSActionParamet } // create the overall rap - const newRap = createNewRap(actions); + const newRap = createNewENSRap(actions); return newRap; }; export const createCommitENSRap = async (ENSActionParameters: ENSActionParameters) => { let actions: RapENSAction[] = []; // // commit rap - const commit = createNewENSAction(RapActionTypes.commitENS, ENSActionParameters); + const commit = createNewENSAction(ENSRapActionType.commitENS, ENSActionParameters); actions = actions.concat(commit); // create the overall rap - const newRap = createNewRap(actions); + const newRap = createNewENSRap(actions); return newRap; }; diff --git a/src/raps/unlockAndCrosschainSwap.ts b/src/raps/unlockAndCrosschainSwap.ts index 4f27f449431..02118751b4e 100644 --- a/src/raps/unlockAndCrosschainSwap.ts +++ b/src/raps/unlockAndCrosschainSwap.ts @@ -1,88 +1,147 @@ -import { ETH_ADDRESS as ETH_ADDRESS_AGGREGATOR } from '@rainbow-me/swaps'; -import { assetNeedsUnlocking, estimateApprove } from './actions'; -import { createNewAction, createNewRap, CrosschainSwapActionParameters, RapAction, RapActionTypes } from './common'; +import { ALLOWS_PERMIT, ChainId, ETH_ADDRESS as ETH_ADDRESS_AGGREGATOR, PermitSupportedTokenList, WRAPPED_ASSET } from '@rainbow-me/swaps'; +import { Address } from 'viem'; + +import { ETH_ADDRESS } from '../references'; import { isNativeAsset } from '@/handlers/assets'; -import store from '@/redux/store'; import { add } from '@/helpers/utilities'; -import { ethereumUtils } from '@/utils'; -import { estimateCrosschainSwapGasLimit } from '@/handlers/swap'; +import { ethereumUtils, isLowerCaseMatch } from '@/utils'; -export const estimateUnlockAndCrosschainSwap = async (swapParameters: CrosschainSwapActionParameters) => { - const { inputAmount, tradeDetails, chainId } = swapParameters; - const { inputCurrency, outputCurrency } = store.getState().swap; +import { assetNeedsUnlocking, estimateApprove } from './actions'; +import { estimateCrosschainSwapGasLimit } from './actions/crosschainSwap'; +import { createNewAction, createNewRap } from './common'; +import { RapAction, RapSwapActionParameters, RapUnlockActionParameters } from './references'; - if (!inputCurrency || !outputCurrency || !inputAmount) { - return ethereumUtils.getBasicSwapGasLimit(Number(chainId)); - } - const { accountAddress } = store.getState().settings; +export const estimateUnlockAndCrosschainSwap = async (swapParameters: RapSwapActionParameters<'crosschainSwap'>) => { + const { sellAmount, quote, chainId, assetToSell } = swapParameters; + + const { + from: accountAddress, + sellTokenAddress, + buyTokenAddress, + allowanceTarget, + no_approval, + } = quote as { + from: Address; + sellTokenAddress: Address; + buyTokenAddress: Address; + allowanceTarget: Address; + no_approval: boolean; + }; - const routeAllowanceTargetAddress = tradeDetails?.allowanceTarget; + const isNativeAssetUnwrapping = + (isLowerCaseMatch(sellTokenAddress, WRAPPED_ASSET?.[chainId]) && isLowerCaseMatch(buyTokenAddress, ETH_ADDRESS)) || + isLowerCaseMatch(buyTokenAddress, ETH_ADDRESS_AGGREGATOR); let gasLimits: (string | number)[] = []; let swapAssetNeedsUnlocking = false; + + // TODO: MARK - Replace this once we migrate network => chainId + const network = ethereumUtils.getNetworkFromChainId(chainId); // Aggregators represent native asset as 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE - const nativeAsset = - ETH_ADDRESS_AGGREGATOR.toLowerCase() === inputCurrency.address?.toLowerCase() || - isNativeAsset(inputCurrency.address, ethereumUtils.getNetworkFromChainId(Number(chainId))); - - const shouldNotHaveApproval = tradeDetails.no_approval !== undefined && tradeDetails.no_approval; - if (!nativeAsset && routeAllowanceTargetAddress && !shouldNotHaveApproval) { - swapAssetNeedsUnlocking = await assetNeedsUnlocking(accountAddress, inputAmount, inputCurrency, routeAllowanceTargetAddress, chainId); - if (swapAssetNeedsUnlocking) { - const unlockGasLimit = await estimateApprove(accountAddress, inputCurrency.address, routeAllowanceTargetAddress, chainId, false); - gasLimits = gasLimits.concat(unlockGasLimit); - } + const nativeAsset = isLowerCaseMatch(ETH_ADDRESS_AGGREGATOR, sellTokenAddress) || isNativeAsset(assetToSell.address, network); + + const shouldNotHaveApproval = no_approval !== undefined && no_approval; + + if (!isNativeAssetUnwrapping && !nativeAsset && allowanceTarget && !shouldNotHaveApproval) { + swapAssetNeedsUnlocking = await assetNeedsUnlocking({ + owner: accountAddress, + amount: sellAmount, + assetToUnlock: assetToSell, + spender: allowanceTarget, + chainId, + }); + } + + let unlockGasLimit; + + if (swapAssetNeedsUnlocking) { + unlockGasLimit = await estimateApprove({ + owner: accountAddress, + tokenAddress: sellTokenAddress, + spender: allowanceTarget, + chainId, + }); + gasLimits = gasLimits.concat(unlockGasLimit); } const swapGasLimit = await estimateCrosschainSwapGasLimit({ - chainId: Number(chainId), + chainId, requiresApprove: swapAssetNeedsUnlocking, - tradeDetails, + quote, }); - gasLimits = gasLimits.concat(swapGasLimit); + const gasLimit = gasLimits.concat(swapGasLimit).reduce((acc, limit) => add(acc, limit), '0'); - return gasLimits.reduce((acc, limit) => add(acc, limit), '0'); + return gasLimit.toString(); }; -export const createUnlockAndCrosschainSwapRap = async (swapParameters: CrosschainSwapActionParameters) => { - let actions: RapAction[] = []; +export const createUnlockAndCrosschainSwapRap = async (swapParameters: RapSwapActionParameters<'crosschainSwap'>) => { + let actions: RapAction<'crosschainSwap' | 'unlock'>[] = []; + const { sellAmount, assetToBuy, quote, chainId, assetToSell } = swapParameters; - const { inputAmount, tradeDetails, flashbots, chainId } = swapParameters; - const { inputCurrency } = store.getState().swap; - const { accountAddress } = store.getState().settings; + const { + from: accountAddress, + sellTokenAddress, + buyTokenAddress, + allowanceTarget, + no_approval, + } = quote as { + from: Address; + sellTokenAddress: Address; + buyTokenAddress: Address; + allowanceTarget: Address; + no_approval: boolean; + }; - // this will probably need refactor - const routeAllowanceTargetAddress = tradeDetails?.allowanceTarget; + const isNativeAssetUnwrapping = + isLowerCaseMatch(sellTokenAddress, WRAPPED_ASSET[`${chainId}`]) && + isLowerCaseMatch(buyTokenAddress, ETH_ADDRESS) && + chainId === ChainId.mainnet; // Aggregators represent native asset as 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE - const nativeAsset = - ETH_ADDRESS_AGGREGATOR.toLowerCase() === inputCurrency?.address?.toLowerCase() || - isNativeAsset(inputCurrency?.address, ethereumUtils.getNetworkFromChainId(Number(chainId))); + const nativeAsset = isLowerCaseMatch(ETH_ADDRESS_AGGREGATOR, sellTokenAddress) || assetToSell?.isNativeAsset; + + const shouldNotHaveApproval = no_approval !== undefined && no_approval; let swapAssetNeedsUnlocking = false; - const shouldNotHaveApproval = tradeDetails.no_approval !== undefined && tradeDetails.no_approval; - if (!nativeAsset && routeAllowanceTargetAddress && !shouldNotHaveApproval) { - swapAssetNeedsUnlocking = await assetNeedsUnlocking(accountAddress, inputAmount, inputCurrency, routeAllowanceTargetAddress, chainId); - if (swapAssetNeedsUnlocking) { - const unlock = createNewAction(RapActionTypes.unlock, { - amount: inputAmount, - assetToUnlock: inputCurrency, - chainId, - contractAddress: routeAllowanceTargetAddress, - }); - actions = actions.concat(unlock); - } + + if (!isNativeAssetUnwrapping && !nativeAsset && allowanceTarget && !shouldNotHaveApproval) { + swapAssetNeedsUnlocking = await assetNeedsUnlocking({ + owner: accountAddress, + amount: sellAmount, + assetToUnlock: assetToSell, + spender: allowanceTarget, + chainId, + }); } - const crosschainSwap = createNewAction(RapActionTypes.crosschainSwap, { + const allowsPermit = + !nativeAsset && chainId === ChainId.mainnet && ALLOWS_PERMIT[assetToSell.address?.toLowerCase() as keyof PermitSupportedTokenList]; + + if (swapAssetNeedsUnlocking && !allowsPermit) { + const unlock = createNewAction('unlock', { + fromAddress: accountAddress, + amount: sellAmount, + assetToUnlock: assetToSell, + chainId, + contractAddress: quote.to, + } as RapUnlockActionParameters); + actions = actions.concat(unlock); + } + + // create a swap rap + const swap = createNewAction('crosschainSwap', { chainId, - flashbots, - inputAmount, - requiresApprove: swapAssetNeedsUnlocking, - tradeDetails, + permit: swapAssetNeedsUnlocking && allowsPermit, + requiresApprove: swapAssetNeedsUnlocking && !allowsPermit, + quote, meta: swapParameters.meta, - }); - actions = actions.concat(crosschainSwap); + assetToSell, + sellAmount, + assetToBuy, + selectedGasFee: swapParameters.selectedGasFee, + gasFeeParamsBySpeed: swapParameters.gasFeeParamsBySpeed, + } satisfies RapSwapActionParameters<'crosschainSwap'>); + actions = actions.concat(swap); // create the overall rap const newRap = createNewRap(actions); diff --git a/src/raps/unlockAndSwap.ts b/src/raps/unlockAndSwap.ts index 71fbea3337d..bd4f6f05832 100644 --- a/src/raps/unlockAndSwap.ts +++ b/src/raps/unlockAndSwap.ts @@ -1,120 +1,162 @@ import { ALLOWS_PERMIT, - ChainId, ETH_ADDRESS as ETH_ADDRESS_AGGREGATOR, PermitSupportedTokenList, - getRainbowRouterContractAddress, WRAPPED_ASSET, + getRainbowRouterContractAddress, } from '@rainbow-me/swaps'; -import { assetNeedsUnlocking, estimateApprove } from './actions'; -import { createNewAction, createNewRap, RapAction, RapActionTypes, SwapActionParameters } from './common'; +import { Address } from 'viem'; + +import { ETH_ADDRESS } from '../references'; +import { ChainId } from '@/__swaps__/types/chains'; import { isNativeAsset } from '@/handlers/assets'; -import store from '@/redux/store'; -import { ETH_ADDRESS } from '@/references'; import { add } from '@/helpers/utilities'; -import { ethereumUtils } from '@/utils'; -import { estimateSwapGasLimit } from '@/handlers/swap'; - -export const estimateUnlockAndSwap = async (swapParameters: SwapActionParameters) => { - const { inputAmount, tradeDetails, chainId } = swapParameters; - const { inputCurrency, outputCurrency } = store.getState().swap; - - if (!inputCurrency || !outputCurrency || !inputAmount) { - return ethereumUtils.getBasicSwapGasLimit(Number(chainId)); - } - const { accountAddress } = store.getState().settings; +import { ethereumUtils, isLowerCaseMatch } from '@/utils'; + +import { assetNeedsUnlocking, estimateApprove, estimateSwapGasLimit } from './actions'; +import { estimateUnlockAndSwapFromMetadata } from './actions/swap'; +import { createNewAction, createNewRap } from './common'; +import { RapAction, RapSwapActionParameters, RapUnlockActionParameters } from './references'; +import { isWrapNative } from '@/handlers/swap'; + +export const estimateUnlockAndSwap = async (swapParameters: RapSwapActionParameters<'swap'>) => { + const { sellAmount, quote, chainId, assetToSell } = swapParameters; + + const { + from: accountAddress, + sellTokenAddress, + buyTokenAddress, + } = quote as { + from: Address; + sellTokenAddress: Address; + buyTokenAddress: Address; + }; const isNativeAssetUnwrapping = - inputCurrency?.address?.toLowerCase() === WRAPPED_ASSET?.[Number(chainId)]?.toLowerCase() && - (outputCurrency?.address?.toLowerCase() === ETH_ADDRESS.toLowerCase() || - outputCurrency?.address?.toLowerCase() === ETH_ADDRESS_AGGREGATOR.toLowerCase()); + isLowerCaseMatch(sellTokenAddress, WRAPPED_ASSET?.[chainId]) && + (isLowerCaseMatch(buyTokenAddress, ETH_ADDRESS?.[chainId]) || isLowerCaseMatch(buyTokenAddress, ETH_ADDRESS_AGGREGATOR?.[chainId])); let gasLimits: (string | number)[] = []; let swapAssetNeedsUnlocking = false; - // Aggregators represent native asset as 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE - const nativeAsset = - ETH_ADDRESS_AGGREGATOR.toLowerCase() === inputCurrency.address?.toLowerCase() || - isNativeAsset(inputCurrency.address, ethereumUtils.getNetworkFromChainId(Number(chainId))); + + // TODO: MARK - replace this when we migrate from network => chainId + const network = ethereumUtils.getNetworkFromChainId(chainId); + + const nativeAsset = isLowerCaseMatch(ETH_ADDRESS_AGGREGATOR, sellTokenAddress) || isNativeAsset(sellTokenAddress, network); if (!isNativeAssetUnwrapping && !nativeAsset) { - swapAssetNeedsUnlocking = await assetNeedsUnlocking( + swapAssetNeedsUnlocking = await assetNeedsUnlocking({ + owner: accountAddress, + amount: sellAmount, + assetToUnlock: assetToSell, + spender: getRainbowRouterContractAddress(chainId), + chainId, + }); + } + + if (swapAssetNeedsUnlocking) { + const gasLimitFromMetadata = await estimateUnlockAndSwapFromMetadata({ + swapAssetNeedsUnlocking, + chainId, accountAddress, - inputAmount, - inputCurrency, - getRainbowRouterContractAddress(chainId), - chainId - ); + sellTokenAddress, + quote, + }); + if (gasLimitFromMetadata) { + return gasLimitFromMetadata; + } } let unlockGasLimit; - let swapGasLimit; if (swapAssetNeedsUnlocking) { - unlockGasLimit = await estimateApprove(accountAddress, inputCurrency.address, getRainbowRouterContractAddress(chainId), chainId); + unlockGasLimit = await estimateApprove({ + owner: accountAddress, + tokenAddress: sellTokenAddress, + spender: getRainbowRouterContractAddress(chainId), + chainId, + }); gasLimits = gasLimits.concat(unlockGasLimit); } - swapGasLimit = await estimateSwapGasLimit({ - chainId: Number(chainId), + const swapGasLimit = await estimateSwapGasLimit({ + chainId, requiresApprove: swapAssetNeedsUnlocking, - tradeDetails, + quote, }); - gasLimits = gasLimits.concat(swapGasLimit); + const gasLimit = gasLimits.concat(swapGasLimit).reduce((acc, limit) => add(acc, limit), '0'); - return gasLimits.reduce((acc, limit) => add(acc, limit), '0'); + return gasLimit.toString(); }; -export const createUnlockAndSwapRap = async (swapParameters: SwapActionParameters) => { - let actions: RapAction[] = []; +export const createUnlockAndSwapRap = async (swapParameters: RapSwapActionParameters<'swap'>) => { + let actions: RapAction<'swap' | 'unlock'>[] = []; + + const { sellAmount, quote, chainId, assetToSell, assetToBuy } = swapParameters; + + const { + from: accountAddress, + sellTokenAddress, + buyTokenAddress, + } = quote as { + from: Address; + sellTokenAddress: Address; + buyTokenAddress: Address; + }; - const { inputAmount, tradeDetails, flashbots, chainId } = swapParameters; - const { inputCurrency, outputCurrency } = store.getState().swap; - const { accountAddress } = store.getState().settings; const isNativeAssetUnwrapping = - inputCurrency.address?.toLowerCase() === WRAPPED_ASSET[`${chainId}`]?.toLowerCase() && - outputCurrency.address?.toLowerCase() === ETH_ADDRESS?.toLowerCase() && - chainId === ChainId.mainnet; + isWrapNative({ + chainId, + sellTokenAddress, + buyTokenAddress, + }) && chainId === ChainId.mainnet; + + // TODO: MARK - replace this when we migrate from network => chainId + const network = ethereumUtils.getNetworkFromChainId(chainId); // Aggregators represent native asset as 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE - const nativeAsset = - ETH_ADDRESS_AGGREGATOR.toLowerCase() === inputCurrency?.address?.toLowerCase() || - isNativeAsset(inputCurrency?.address, ethereumUtils.getNetworkFromChainId(Number(chainId))); + const nativeAsset = isLowerCaseMatch(ETH_ADDRESS_AGGREGATOR, sellTokenAddress) || isNativeAsset(sellTokenAddress, network); let swapAssetNeedsUnlocking = false; if (!isNativeAssetUnwrapping && !nativeAsset) { - swapAssetNeedsUnlocking = await assetNeedsUnlocking( - accountAddress, - inputAmount, - inputCurrency, - getRainbowRouterContractAddress(chainId), - chainId - ); + swapAssetNeedsUnlocking = await assetNeedsUnlocking({ + owner: accountAddress, + amount: sellAmount as string, + assetToUnlock: assetToSell, + spender: getRainbowRouterContractAddress(chainId), + chainId, + }); } + const allowsPermit = - !nativeAsset && chainId === ChainId.mainnet && ALLOWS_PERMIT[inputCurrency.address?.toLowerCase() as keyof PermitSupportedTokenList]; + !nativeAsset && chainId === ChainId.mainnet && ALLOWS_PERMIT[assetToSell.address?.toLowerCase() as keyof PermitSupportedTokenList]; if (swapAssetNeedsUnlocking && !allowsPermit) { - const unlock = createNewAction(RapActionTypes.unlock, { - amount: inputAmount, - assetToUnlock: inputCurrency, + const unlock = createNewAction('unlock', { + fromAddress: accountAddress, + amount: sellAmount, + assetToUnlock: assetToSell, chainId, contractAddress: getRainbowRouterContractAddress(chainId), - }); + } as RapUnlockActionParameters); actions = actions.concat(unlock); } // create a swap rap - const swap = createNewAction(RapActionTypes.swap, { + const swap = createNewAction('swap', { chainId, - flashbots, - inputAmount, + sellAmount, permit: swapAssetNeedsUnlocking && allowsPermit, requiresApprove: swapAssetNeedsUnlocking && !allowsPermit, - tradeDetails, + quote, meta: swapParameters.meta, - }); + assetToSell, + assetToBuy, + selectedGasFee: swapParameters.selectedGasFee, + gasFeeParamsBySpeed: swapParameters.gasFeeParamsBySpeed, + } satisfies RapSwapActionParameters<'swap'>); actions = actions.concat(swap); // create the overall rap diff --git a/src/raps/utils.ts b/src/raps/utils.ts index 45128f4ce4b..b91b60cb99b 100644 --- a/src/raps/utils.ts +++ b/src/raps/utils.ts @@ -1,7 +1,24 @@ +import { Block, Provider } from '@ethersproject/abstract-provider'; +import { MaxUint256 } from '@ethersproject/constants'; +import { Contract, PopulatedTransaction } from '@ethersproject/contracts'; +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { ALLOWS_PERMIT, CrosschainQuote, Quote, getQuoteExecutionDetails, getRainbowRouterContractAddress } from '@rainbow-me/swaps'; +import { mainnet } from 'viem/chains'; +import { Chain, erc20Abi } from 'viem'; import { Network } from '@/helpers'; import { GasFeeParamsBySpeed, LegacyGasFeeParamsBySpeed, LegacyTransactionGasParamAmounts, TransactionGasParamAmounts } from '@/entities'; import { ethereumUtils, gasUtils } from '@/utils'; -import { add, greaterThan } from '@/helpers/utilities'; +import { add, greaterThan, multiply } from '@/helpers/utilities'; +import { ChainId } from '@/__swaps__/types/chains'; +import { gasUnits } from '@/references'; +import { toHexNoLeadingZeros } from '@/handlers/web3'; + +export const CHAIN_IDS_WITH_TRACE_SUPPORT: ChainId[] = [mainnet.id]; +export const SWAP_GAS_PADDING = 1.1; + +const GAS_LIMIT_INCREMENT = 50000; +const EXTRA_GAS_PADDING = 1.5; +const TRACE_CALL_BLOCK_NUMBER_OFFSET = 20; export const overrideWithFastSpeedIfNeeded = ({ gasParams, @@ -32,3 +49,170 @@ export const overrideWithFastSpeedIfNeeded = ({ return transactionGasParams; }; + +const getStateDiff = async (provider: Provider, quote: Quote | CrosschainQuote): Promise => { + const tokenAddress = quote.sellTokenAddress; + const fromAddr = quote.from; + const { chainId } = await provider.getNetwork(); + const toAddr = quote.swapType === 'normal' ? getRainbowRouterContractAddress(chainId) : (quote as CrosschainQuote).allowanceTarget; + const tokenContract = new Contract(tokenAddress, erc20Abi, provider); + + const { number: blockNumber } = await (provider.getBlock as () => Promise)(); + + // Get data + const { data } = await tokenContract.populateTransaction.approve(toAddr, MaxUint256.toHexString()); + + // trace_call default params + const callParams = [ + { + data, + from: fromAddr, + to: tokenAddress, + value: '0x0', + }, + ['stateDiff'], + blockNumber - TRACE_CALL_BLOCK_NUMBER_OFFSET, + ]; + + const trace = await (provider as StaticJsonRpcProvider).send('trace_call', callParams); + + if (trace.stateDiff) { + const slotAddress = Object.keys(trace.stateDiff[tokenAddress]?.storage)?.[0]; + if (slotAddress) { + const formattedStateDiff = { + [tokenAddress]: { + stateDiff: { + [slotAddress]: MaxUint256.toHexString(), + }, + }, + }; + return formattedStateDiff; + } + } +}; + +const getClosestGasEstimate = async (estimationFn: (gasEstimate: number) => Promise): Promise => { + // From 200k to 1M + const gasEstimates = Array.from(Array(21).keys()) + .filter(x => x > 3) + .map(x => x * GAS_LIMIT_INCREMENT); + + let start = 0; + let end = gasEstimates.length - 1; + + let highestFailedGuess = null; + let lowestSuccessfulGuess = null; + let lowestFailureGuess = null; + // guess is typically middle of array + let guessIndex = Math.floor((end - start) / 2); + while (end > start) { + // eslint-disable-next-line no-await-in-loop + const gasEstimationSucceded = await estimationFn(gasEstimates[guessIndex]); + if (gasEstimationSucceded) { + if (!lowestSuccessfulGuess || guessIndex < lowestSuccessfulGuess) { + lowestSuccessfulGuess = guessIndex; + } + end = guessIndex; + guessIndex = Math.max(Math.floor((end + start) / 2) - 1, highestFailedGuess || 0); + } else if (!gasEstimationSucceded) { + if (!highestFailedGuess || guessIndex > highestFailedGuess) { + highestFailedGuess = guessIndex; + } + if (!lowestFailureGuess || guessIndex < lowestFailureGuess) { + lowestFailureGuess = guessIndex; + } + start = guessIndex; + guessIndex = Math.ceil((end + start) / 2); + } + + if ( + (highestFailedGuess !== null && highestFailedGuess + 1 === lowestSuccessfulGuess) || + lowestSuccessfulGuess === 0 || + (lowestSuccessfulGuess !== null && lowestFailureGuess === lowestSuccessfulGuess - 1) + ) { + return String(gasEstimates[lowestSuccessfulGuess]); + } + + if (highestFailedGuess === gasEstimates.length - 1) { + return '-1'; + } + } + return '-1'; +}; + +export const getDefaultGasLimitForTrade = (quote: Quote, chainId: Chain['id']): string => { + const allowsPermit = chainId === mainnet.id && ALLOWS_PERMIT[quote?.sellTokenAddress?.toLowerCase()]; + + let defaultGasLimit = quote?.defaultGasLimit; + + if (allowsPermit) { + defaultGasLimit = Math.max(Number(defaultGasLimit), Number(multiply(gasUnits.basic_swap_permit, EXTRA_GAS_PADDING))).toString(); + } + return defaultGasLimit || multiply(gasUnits.basic_swap[chainId], EXTRA_GAS_PADDING); +}; + +export const estimateSwapGasLimitWithFakeApproval = async ( + chainId: number, + provider: Provider, + quote: Quote | CrosschainQuote +): Promise => { + let stateDiff: unknown; + + try { + stateDiff = await getStateDiff(provider, quote); + const { router, methodName, params, methodArgs } = getQuoteExecutionDetails( + quote, + { from: quote.from }, + provider as StaticJsonRpcProvider + ); + + const { data } = await router.populateTransaction[methodName](...(methodArgs ?? []), params); + + const gasLimit = await getClosestGasEstimate(async (gas: number) => { + const callParams = [ + { + data, + from: quote.from, + gas: toHexNoLeadingZeros(String(gas)), + gasPrice: toHexNoLeadingZeros(`100000000000`), + to: quote.swapType === 'normal' ? getRainbowRouterContractAddress : (quote as CrosschainQuote).allowanceTarget, + value: '0x0', // 100 gwei + }, + 'latest', + ]; + + try { + await (provider as StaticJsonRpcProvider).send('eth_call', [...callParams, stateDiff]); + return true; + } catch (e) { + return false; + } + }); + if (gasLimit && greaterThan(gasLimit, gasUnits.basic_swap[ChainId.mainnet])) { + return gasLimit; + } + } catch (e) { + // + } + return getDefaultGasLimitForTrade(quote, chainId); +}; + +export const populateSwap = async ({ + provider, + quote, +}: { + provider: Provider; + quote: Quote | CrosschainQuote; +}): Promise => { + try { + const { router, methodName, params, methodArgs } = getQuoteExecutionDetails( + quote, + { from: quote.from }, + provider as StaticJsonRpcProvider + ); + const swapTransaction = await router.populateTransaction[methodName](...(methodArgs ?? []), params); + return swapTransaction; + } catch (e) { + return null; + } +}; diff --git a/src/redux/gas.ts b/src/redux/gas.ts index ed30995bfdb..1423c0fe436 100644 --- a/src/redux/gas.ts +++ b/src/redux/gas.ts @@ -67,7 +67,7 @@ const getDefaultGasLimit = (network: Network, defaultGasLimit: number): number = let gasPricesHandle: NodeJS.Timeout | null = null; -interface GasState { +export interface GasState { defaultGasLimit: number; gasLimit: number | null; gasFeeParamsBySpeed: GasFeeParamsBySpeed | LegacyGasFeeParamsBySpeed; diff --git a/src/redux/swap.ts b/src/redux/swap.ts index 8f194ad25b8..10bf5a81080 100644 --- a/src/redux/swap.ts +++ b/src/redux/swap.ts @@ -3,6 +3,7 @@ import { AnyAction } from 'redux'; import { SwappableAsset } from '@/entities'; import { ExchangeModalTypes } from '@/helpers'; import { AppDispatch, AppGetState } from '@/redux/store'; +import { Source } from '@/raps/references'; export interface SwapAmount { display: string | null; @@ -15,12 +16,6 @@ export enum SwapModalField { output = 'outputAmount', } -export enum Source { - AggregatorRainbow = 'rainbow', - Aggregator0x = '0x', - Aggregator1inch = '1inch', -} - export interface TypeSpecificParameters { cTokenBalance: string; supplyBalanceUnderlying: string; diff --git a/src/screens/ExchangeModal.tsx b/src/screens/ExchangeModal.tsx index fe893f7b921..4a879b71b8c 100644 --- a/src/screens/ExchangeModal.tsx +++ b/src/screens/ExchangeModal.tsx @@ -48,14 +48,13 @@ import { } from '@/hooks'; import { loadWallet } from '@/model/wallet'; import { useNavigation } from '@/navigation'; -import { executeRap, getSwapRapEstimationByType, getSwapRapTypeByExchangeType } from '@/raps'; +import { walletExecuteRap } from '@/raps/execute'; import { swapClearState, SwapModalField, TypeSpecificParameters, updateSwapSlippage, updateSwapTypeDetails } from '@/redux/swap'; import { ethUnits } from '@/references'; import Routes from '@/navigation/routesNames'; import { ethereumUtils, gasUtils } from '@/utils'; import { IS_ANDROID, IS_IOS, IS_TEST } from '@/env'; import logger from '@/utils/logger'; -import { CrosschainSwapActionParameters, SwapActionParameters } from '@/raps/common'; import { CROSSCHAIN_SWAPS, useExperimentalFlag } from '@/config'; import { CrosschainQuote, Quote } from '@rainbow-me/swaps'; import store from '@/redux/store'; @@ -71,6 +70,12 @@ import { handleReviewPromptAction } from '@/utils/reviewAlert'; import { ReviewPromptAction } from '@/storage/schema'; import { SwapPriceImpactType } from '@/hooks/usePriceImpactDetails'; import { getNextNonce } from '@/state/nonces'; +import { getChainName } from '@/__swaps__/utils/chains'; +import { ChainName } from '@/__swaps__/types/chains'; +import { AddressOrEth, ParsedAsset } from '@/__swaps__/types/assets'; +import { TokenColors } from '@/graphql/__generated__/metadata'; +import { estimateSwapGasLimit } from '@/raps/actions'; +import { estimateCrosschainSwapGasLimit } from '@/raps/actions/crosschainSwap'; export const DEFAULT_SLIPPAGE_BIPS = { [Network.mainnet]: 100, @@ -290,16 +295,12 @@ export default function ExchangeModal({ fromDiscover, ignoreInitialTypeCheck, te const updateGasLimit = useCallback(async () => { try { const provider = await getProviderForNetwork(currentNetwork); - const swapParams: SwapActionParameters | CrosschainSwapActionParameters = { - chainId, - inputAmount: inputAmount!, - outputAmount: outputAmount!, - provider, - tradeDetails: tradeDetails!, - }; - const rapType = getSwapRapTypeByExchangeType(isCrosschainSwap); - const gasLimit = await getSwapRapEstimationByType(rapType, swapParams); + const quote = isCrosschainSwap ? (tradeDetails as CrosschainQuote) : (tradeDetails as Quote); + const gasLimit = await (isCrosschainSwap ? estimateCrosschainSwapGasLimit : estimateSwapGasLimit)({ + chainId, + quote: quote as any, // this is a temporary fix until we have the correct type coersion here + }); if (gasLimit) { if (getNetworkObj(currentNetwork).gas?.OptimismTxFee) { if (tradeDetails) { @@ -324,7 +325,7 @@ export default function ExchangeModal({ fromDiscover, ignoreInitialTypeCheck, te } catch (error) { updateTxFee(defaultGasLimit, null); } - }, [chainId, currentNetwork, defaultGasLimit, inputAmount, isCrosschainSwap, outputAmount, tradeDetails, type, updateTxFee]); + }, [chainId, currentNetwork, defaultGasLimit, isCrosschainSwap, tradeDetails, updateTxFee]); useEffect(() => { if (tradeDetails && !equal(tradeDetails, lastTradeDetails)) { @@ -394,7 +395,7 @@ export default function ExchangeModal({ fromDiscover, ignoreInitialTypeCheck, te }); const submit = useCallback( - async (amountInUSD: any) => { + async (amountInUSD: any): Promise => { setIsAuthorizing(true); const NotificationManager = ios ? NativeModules.NotificationManager : null; try { @@ -404,6 +405,7 @@ export default function ExchangeModal({ fromDiscover, ignoreInitialTypeCheck, te if (!wallet) { setIsAuthorizing(false); logger.sentry(`aborting ${type} due to missing wallet`); + Alert.alert('Unable to determine wallet address'); return false; } @@ -415,52 +417,70 @@ export default function ExchangeModal({ fromDiscover, ignoreInitialTypeCheck, te wallet = new Wallet(wallet.privateKey, flashbotsProvider); } - let isSucessful = false; - const callback = (success = false, errorMessage: string | null = null) => { - isSucessful = success; - setIsAuthorizing(false); - if (success) { - setParams({ focused: false }); - navigate(Routes.PROFILE_SCREEN); - } else if (errorMessage) { - if (wallet instanceof Wallet) { - Alert.alert(errorMessage); - } else { - setHardwareTXError(true); - } - } - }; + if (!inputAmount || !outputAmount) { + logger.log('[exchange - handle submit] inputAmount or outputAmount is missing'); + Alert.alert('Input amount or output amount is missing'); + return false; + } + + if (!tradeDetails) { + logger.log('[exchange - handle submit] tradeDetails is missing'); + Alert.alert('Missing trade details for swap'); + return false; + } + logger.log('[exchange - handle submit] rap'); - const nonce = await getNextNonce({ address: accountAddress, network: currentNetwork }); + const currentNonce = await getNextNonce({ address: accountAddress, network: currentNetwork }); const { independentField, independentValue, slippageInBips, source } = store.getState().swap; - const swapParameters = { + + const transformedAssetToSell = { + ...inputCurrency, + chainName: getChainName({ chainId: inputCurrency.chainId! }) as ChainName, + address: inputCurrency.address as AddressOrEth, + chainId: inputCurrency.chainId!, + colors: inputCurrency.colors as TokenColors, + } as ParsedAsset; + + const transformedAssetToBuy = { + ...outputCurrency, + chainName: getChainName({ chainId: outputCurrency.chainId! }) as ChainName, + address: outputCurrency.address as AddressOrEth, + chainId: outputCurrency.chainId!, + colors: outputCurrency.colors as TokenColors, + } as ParsedAsset; + + const { nonce, errorMessage } = await walletExecuteRap(wallet, isCrosschainSwap ? 'crosschainSwap' : 'swap', { chainId, flashbots, - inputAmount: inputAmount!, - outputAmount: outputAmount!, - nonce, - tradeDetails: { - ...tradeDetails, - fromChainId: ethereumUtils.getChainIdFromNetwork(inputCurrency?.network), - toChainId: ethereumUtils.getChainIdFromNetwork(outputCurrency?.network), - } as Quote | CrosschainQuote, + nonce: currentNonce, + assetToSell: transformedAssetToSell, + assetToBuy: transformedAssetToBuy, + sellAmount: inputAmount, + quote: tradeDetails, + amount: inputAmount, meta: { - flashbots, - inputAsset: inputCurrency, - outputAsset: outputCurrency, + inputAsset: transformedAssetToSell, + outputAsset: transformedAssetToBuy, independentField: independentField as SwapModalField, independentValue: independentValue as string, slippage: slippageInBips, route: source, }, - }; - - const rapType = getSwapRapTypeByExchangeType(isCrosschainSwap); - await executeRap(wallet, rapType, swapParameters, callback); + selectedGasFee, + gasFeeParamsBySpeed, + }); + setIsAuthorizing(false); // if the transaction was not successful, we need to bubble that up to the caller - if (!isSucessful) { - loggr.debug('[ExchangeModal] transaction was not successful'); + if (errorMessage) { + loggr.debug('[ExchangeModal] transaction was not successful', { + errorMessage, + }); + if (wallet instanceof Wallet) { + Alert.alert(errorMessage); + } else { + setHardwareTXError(true); + } return false; } @@ -498,6 +518,9 @@ export default function ExchangeModal({ fromDiscover, ignoreInitialTypeCheck, te } }, 500); + setParams({ focused: false }); + navigate(Routes.PROFILE_SCREEN); + return true; } catch (error) { setIsAuthorizing(false); diff --git a/src/screens/transaction-details/components/TransactionDetailsHashAndActionsSection.tsx b/src/screens/transaction-details/components/TransactionDetailsHashAndActionsSection.tsx index dad23a4b74c..18ba9b751d8 100644 --- a/src/screens/transaction-details/components/TransactionDetailsHashAndActionsSection.tsx +++ b/src/screens/transaction-details/components/TransactionDetailsHashAndActionsSection.tsx @@ -11,8 +11,8 @@ import * as i18n from '@/languages'; import { ButtonPressAnimation } from '@/components/animations'; import Clipboard from '@react-native-clipboard/clipboard'; import { RainbowTransaction, TransactionStatus } from '@/entities'; -import { swapMetadataStorage } from '@/raps/actions/swap'; -import { SwapMetadata } from '@/raps/common'; +import { swapMetadataStorage } from '@/raps/common'; +import { SwapMetadata } from '@/raps/references'; import { Navigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; import { useSelector } from 'react-redux';