diff --git a/src/components/Modals/Send/utils.ts b/src/components/Modals/Send/utils.ts index ace22d13e18..f2eac1bcb10 100644 --- a/src/components/Modals/Send/utils.ts +++ b/src/components/Modals/Send/utils.ts @@ -1,4 +1,4 @@ -import type { AssetId, ChainId } from '@shapeshiftoss/caip' +import type { AccountId, AssetId, ChainId } from '@shapeshiftoss/caip' import { CHAIN_NAMESPACE, fromAccountId, fromAssetId, fromChainId } from '@shapeshiftoss/caip' import type { CosmosSdkChainId, @@ -36,7 +36,7 @@ export type EstimateFeesInput = { from?: string to: string sendMax: boolean - accountId: string + accountId: AccountId contractAddress: string | undefined } diff --git a/src/components/Sweep.tsx b/src/components/Sweep.tsx index ba985d39790..2fae6ae2e8d 100644 --- a/src/components/Sweep.tsx +++ b/src/components/Sweep.tsx @@ -48,6 +48,7 @@ export const Sweep = ({ isSuccess: isEstimatedFeesDataSuccess, } = useGetEstimatedFeesQuery({ amountCryptoPrecision: '0', + feeAssetId: assetId, assetId, to: fromAddress ?? '', sendMax: true, diff --git a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/ThorchainSaversDeposit.tsx b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/ThorchainSaversDeposit.tsx index bed22d91d29..ced6d8a94c8 100644 --- a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/ThorchainSaversDeposit.tsx +++ b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/ThorchainSaversDeposit.tsx @@ -2,6 +2,7 @@ import { Center } from '@chakra-ui/react' import type { AccountId } from '@shapeshiftoss/caip' import { toAssetId } from '@shapeshiftoss/caip' import type { Asset } from '@shapeshiftoss/types' +import { useQuery } from '@tanstack/react-query' import { DefiModalContent } from 'features/defi/components/DefiModal/DefiModalContent' import { DefiModalHeader } from 'features/defi/components/DefiModal/DefiModalHeader' import type { @@ -10,8 +11,9 @@ import type { } from 'features/defi/contexts/DefiManagerProvider/DefiCommon' import { DefiAction, DefiStep } from 'features/defi/contexts/DefiManagerProvider/DefiCommon' import qs from 'qs' -import { useCallback, useEffect, useMemo, useReducer, useState } from 'react' +import { useCallback, useEffect, useMemo, useReducer } from 'react' import { useTranslate } from 'react-polyglot' +import { reactQueries } from 'react-queries' import { useSelector } from 'react-redux' import type { AccountDropdownProps } from 'components/AccountDropdown/AccountDropdown' import { CircularProgress } from 'components/CircularProgress/CircularProgress' @@ -20,7 +22,6 @@ import { Steps } from 'components/DeFi/components/Steps' import { Sweep } from 'components/Sweep' import { useBrowserRouter } from 'hooks/useBrowserRouter/useBrowserRouter' import { useWallet } from 'hooks/useWallet/useWallet' -import { getThorchainFromAddress } from 'lib/utils/thorchain' import { getThorchainSaversPosition } from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/utils' import type { StakingId } from 'state/slices/opportunitiesSlice/types' import { serializeUserStakingId, toOpportunityId } from 'state/slices/opportunitiesSlice/utils' @@ -51,8 +52,6 @@ export const ThorchainSaversDeposit: React.FC = ({ accountId, onAccountIdChange: handleAccountIdChange, }) => { - const [fromAddress, setFromAddress] = useState(null) - const { state: { wallet }, } = useWallet() @@ -126,21 +125,16 @@ export const ThorchainSaversDeposit: React.FC = ({ selectPortfolioAccountMetadataByAccountId(state, accountFilter), ) - useEffect(() => { - if (!(accountId && wallet && accountMetadata)) return - ;(async () => { - const _fromAddress = await getThorchainFromAddress({ - accountId, - getPosition: getThorchainSaversPosition, - assetId, - wallet, - accountMetadata, - }) - - if (!_fromAddress) return - setFromAddress(_fromAddress) - })() - }, [accountId, accountMetadata, assetId, fromAddress, wallet]) + const { data: fromAddress } = useQuery({ + ...reactQueries.common.thorchainFromAddress({ + accountId: accountId!, + getPosition: getThorchainSaversPosition, + assetId, + wallet: wallet!, + accountMetadata: accountMetadata!, + }), + enabled: Boolean(accountId && wallet && accountMetadata), + }) const makeHandleSweepBack = useCallback( (onNext: StepComponentProps['onNext']) => () => onNext(DefiStep.Info), @@ -162,7 +156,7 @@ export const ThorchainSaversDeposit: React.FC = ({ ), @@ -172,7 +166,7 @@ export const ThorchainSaversDeposit: React.FC = ({ component: ({ onNext }) => ( = ({ accountId, onNext }) => { selectMarketDataByAssetIdUserCurrency(state, feeAsset?.assetId ?? ''), ) - const { routerContractAddress: saversRouterContractAddress } = useRouterContractAddress({ - feeAssetId: feeAsset?.assetId ?? '', - skip: !isTokenDeposit || !feeAsset?.assetId, - excludeHalted: true, + const { data: thorchainSaversDepositQuote } = useGetThorchainSaversDepositQuoteQuery({ + asset, + amountCryptoBaseUnit: toBaseUnit(state?.deposit.cryptoAmount, asset.precision), + }) + + const { inboundAddress } = useSendThorTx({ + assetId, + accountId: accountId ?? null, + amountCryptoBaseUnit: toBaseUnit(state?.deposit.cryptoAmount, asset.precision), + memo: thorchainSaversDepositQuote?.memo ?? null, + fromAddress: '', + action: 'depositSavers', + enableEstimateFees: false, }) const handleApprove = useCallback(async () => { @@ -120,22 +127,14 @@ export const Approve: React.FC = ({ accountId, onNext }) => { !wallet || !accountId || !dispatch || - !saversRouterContractAddress || - !opportunityData + !opportunityData || + !inboundAddress ) return dispatch({ type: ThorchainSaversDepositActionType.SET_LOADING, payload: true }) try { - const daemonUrl = getConfig().REACT_APP_THORCHAIN_NODE_URL - const maybeInboundAddressData = await getInboundAddressDataForChain( - daemonUrl, - feeAsset?.assetId, - ) - if (maybeInboundAddressData.isErr()) - throw new Error(maybeInboundAddressData.unwrapErr().message) - const amountCryptoBaseUnit = toBaseUnit(state.deposit.cryptoAmount, asset.precision) const poolId = assetIdToPoolAssetId({ assetId: asset.assetId }) @@ -153,7 +152,7 @@ export const Approve: React.FC = ({ accountId, onNext }) => { const data = encodeFunctionData({ abi: contract.abi, functionName: 'approve', - args: [getAddress(saversRouterContractAddress), BigInt(amountToApprove)], + args: [getAddress(inboundAddress), BigInt(amountToApprove)], }) const adapter = assertGetEvmChainAdapter(chainId) @@ -175,7 +174,7 @@ export const Approve: React.FC = ({ accountId, onNext }) => { fn: () => getErc20Allowance({ address: fromAssetId(assetId).assetReference, - spender: saversRouterContractAddress, + spender: inboundAddress, from: fromAccountId(accountId).account, chainId: asset.chainId, }), @@ -186,64 +185,6 @@ export const Approve: React.FC = ({ accountId, onNext }) => { maxAttempts: 60, }) - const estimatedDepositGasCryptoPrecision = await (async () => { - const maybeQuote = await getMaybeThorchainSaversDepositQuote({ - asset, - amountCryptoBaseUnit, - }) - if (maybeQuote.isErr()) throw new Error(maybeQuote.unwrapErr()) - const quote = maybeQuote.unwrap() - const thorContract = getOrCreateContractByType({ - address: saversRouterContractAddress!, - type: ContractType.ThorRouter, - chainId: asset.chainId, - }) - - const data = encodeFunctionData({ - abi: thorContract.abi, - functionName: 'depositWithExpiry', - args: [ - getAddress(quote.inbound_address), - getAddress(fromAssetId(assetId).assetReference), - BigInt(amountCryptoBaseUnit), - quote.memo, - BigInt(quote.expiry), - ], - }) - - const adapter = assertGetEvmChainAdapter(chainId) - - const customTxInput = await createBuildCustomTxInput({ - accountNumber, - adapter, - data, - value: '0', // this is not a token send, but a smart contract call so we don't send anything here, THOR router does - to: saversRouterContractAddress!, - wallet, - }) - - const fees = await adapter.getFeeData({ - to: customTxInput.to, - value: customTxInput.value, - chainSpecific: { - from: fromAccountId(accountId).account, - data: customTxInput.data, - }, - }) - - const fastFeeCryptoBaseUnit = fees.fast.txFee - const fastFeeCryptoPrecision = bnOrZero( - bn(fastFeeCryptoBaseUnit).div(bn(10).pow(feeAsset.precision)), - ) - - return fastFeeCryptoPrecision.toString() - })() - - dispatch({ - type: ThorchainSaversDepositActionType.SET_DEPOSIT, - payload: { estimatedGasCryptoPrecision: estimatedDepositGasCryptoPrecision }, - }) - trackOpportunityEvent( MixPanelEvent.DepositApprove, { @@ -268,12 +209,10 @@ export const Approve: React.FC = ({ accountId, onNext }) => { assets, chainId, dispatch, - feeAsset?.assetId, - feeAsset.precision, + inboundAddress, onNext, opportunityData, poll, - saversRouterContractAddress, showErrorToast, state?.deposit.cryptoAmount, state?.deposit.fiatAmount, @@ -316,7 +255,7 @@ export const Approve: React.FC = ({ accountId, onNext }) => { const handleCancel = useCallback(() => history.push('/'), [history]) - if (!saversRouterContractAddress || !state || !dispatch) return null + if (!isTokenDeposit || !inboundAddress || !state || !dispatch) return null return ( = ({ accountId, onNext }) => { isExactAllowance={state.isExactAllowance} onCancel={handleCancel} onConfirm={handleApprove} - spenderContractAddress={saversRouterContractAddress} + spenderContractAddress={inboundAddress} onToggle={onExactAllowanceToggle} /> ) diff --git a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Confirm.tsx b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Confirm.tsx index 331cbbd4796..e79b4417606 100644 --- a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Confirm.tsx +++ b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Confirm.tsx @@ -9,15 +9,11 @@ import { useToast, } from '@chakra-ui/react' import type { AccountId } from '@shapeshiftoss/caip' -import { fromAccountId, fromAssetId, toAssetId } from '@shapeshiftoss/caip' -import { CONTRACT_INTERACTION, FeeDataKey } from '@shapeshiftoss/chain-adapters' -import type { BuildCustomTxInput } from '@shapeshiftoss/chain-adapters/src/evm/types' +import { toAssetId } from '@shapeshiftoss/caip' import { supportsETH } from '@shapeshiftoss/hdwallet-core' import { SwapperName } from '@shapeshiftoss/swapper' -import type { Asset, KnownChainIds } from '@shapeshiftoss/types' -import { getConfig } from 'config' -import { getOrCreateContractByType } from 'contracts/contractManager' -import { ContractType } from 'contracts/types' +import type { Asset } from '@shapeshiftoss/types' +import { useQuery } from '@tanstack/react-query' import { Confirm as ReusableConfirm } from 'features/defi/components/Confirm/Confirm' import { Summary } from 'features/defi/components/Summary' import type { @@ -27,15 +23,12 @@ import type { import { DefiStep } from 'features/defi/contexts/DefiManagerProvider/DefiCommon' import { useCallback, useContext, useEffect, useMemo, useState } from 'react' import { useTranslate } from 'react-polyglot' +import { reactQueries } from 'react-queries' import { useIsTradingActive } from 'react-queries/hooks/useIsTradingActive' -import { encodeFunctionData, getAddress, toHex } from 'viem' import { Amount } from 'components/Amount/Amount' import { AssetIcon } from 'components/AssetIcon' import type { StepComponentProps } from 'components/DeFi/components/Steps' import { HelperTooltip } from 'components/HelperTooltip/HelperTooltip' -import type { SendInput } from 'components/Modals/Send/Form' -import type { EstimateFeesInput } from 'components/Modals/Send/utils' -import { estimateFees, handleSend } from 'components/Modals/Send/utils' import { Row } from 'components/Row/Row' import { RawText, Text } from 'components/Text' import type { TextPropTypes } from 'components/Text/Text' @@ -44,52 +37,37 @@ import { useBrowserRouter } from 'hooks/useBrowserRouter/useBrowserRouter' import { useIsSmartContractAddress } from 'hooks/useIsSmartContractAddress/useIsSmartContractAddress' import { useWallet } from 'hooks/useWallet/useWallet' import { bn, bnOrZero } from 'lib/bignumber/bignumber' -import { toBaseUnit } from 'lib/math' +import { fromBaseUnit, toBaseUnit } from 'lib/math' import { trackOpportunityEvent } from 'lib/mixpanel/helpers' import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton' import { MixPanelEvent } from 'lib/mixpanel/types' -import { isToken, tokenOrUndefined } from 'lib/utils' -import { - assertGetEvmChainAdapter, - buildAndBroadcast, - createBuildCustomTxInput, - getSupportedEvmChainIds, -} from 'lib/utils/evm' -import { fromThorBaseUnit, getThorchainFromAddress, toThorBaseUnit } from 'lib/utils/thorchain' +import { fromThorBaseUnit, toThorBaseUnit } from 'lib/utils/thorchain' import { BASE_BPS_POINTS } from 'lib/utils/thorchain/constants' -import { getInboundAddressDataForChain } from 'lib/utils/thorchain/getInboundAddressDataForChain' -import { isUtxoChainId } from 'lib/utils/utxo' +import { useSendThorTx } from 'lib/utils/thorchain/hooks/useSendThorTx' +import type { ThorchainSaversDepositQuoteResponseSuccess } from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/types' import { getMaybeThorchainSaversDepositQuote, getThorchainSaversPosition, makeDaysToBreakEven, } from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/utils' import { - selectAccountNumberByAccountId, selectAssetById, selectAssets, selectFeeAssetById, selectMarketDataByAssetIdUserCurrency, selectPortfolioAccountMetadataByAccountId, selectPortfolioCryptoBalanceBaseUnitByFilter, - selectSelectedCurrency, } from 'state/slices/selectors' -import { store, useAppSelector } from 'state/store' +import { useAppSelector } from 'state/store' import { ThorchainSaversDepositActionType } from '../DepositCommon' import { DepositContext } from '../DepositContext' type ConfirmProps = { accountId: AccountId | undefined } & StepComponentProps -// Estimated miner fees are approximative since there might be a reconciliation Tx -// and an actual savers Tx with different fees, and we're doubling the fees -// This does NOT ensure dust will be kept for future Txs, but will ensure we are conservative WRT gas used -// so that the final outbound Tx can go through -const TXS_BUFFER = 10 - export const Confirm: React.FC = ({ accountId, onNext }) => { + const [quote, setQuote] = useState(null) const [protocolFeeCryptoBaseUnit, setProtocolFeeCryptoBaseUnit] = useState('') - const [networkFeeCryptoBaseUnit, setNetworkFeeCryptoBaseUnit] = useState('') const { state, dispatch: contextDispatch } = useContext(DepositContext) const [slippageCryptoAmountPrecision, setSlippageCryptoAmountPrecision] = useState( null, @@ -105,22 +83,17 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { const chainAdapter = getChainAdapterManager().get(chainId)! - const supportedEvmChainIds = useMemo(() => getSupportedEvmChainIds(), []) - const assetId = toAssetId({ chainId, assetNamespace, assetReference, }) - const [maybeFromUTXOAccountAddress, setMaybeFromUTXOAccountAddress] = useState('') const asset: Asset | undefined = useAppSelector(state => selectAssetById(state, assetId ?? '')) const feeAsset = useAppSelector(state => selectFeeAssetById(state, assetId)) if (!asset) throw new Error(`Asset not found for AssetId ${assetId}`) if (!feeAsset) throw new Error(`Fee asset not found for AssetId ${assetId}`) - const isTokenDeposit = isToken(fromAssetId(assetId).assetReference) - const marketData = useAppSelector(state => selectMarketDataByAssetIdUserCurrency(state, assetId ?? ''), ) @@ -132,17 +105,7 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { const accountMetadata = useAppSelector(state => selectPortfolioAccountMetadataByAccountId(state, accountFilter), ) - const accountType = accountMetadata?.accountType const bip44Params = accountMetadata?.bip44Params - const userAddress = useMemo( - () => (accountId ? fromAccountId(accountId).account : ''), - [accountId], - ) - const accountNumberFilter = useMemo(() => ({ accountId }), [accountId]) - const accountNumber = useAppSelector(state => - selectAccountNumberByAccountId(state, accountNumberFilter), - ) - // user info const { state: { wallet }, @@ -151,11 +114,6 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { // notify const toast = useToast() - const assetBalanceFilter = useMemo( - () => ({ assetId: asset?.assetId, accountId }), - [accountId, asset?.assetId], - ) - const feeAssetBalanceFilter = useMemo( () => ({ assetId: feeAsset?.assetId, accountId }), [accountId, feeAsset?.assetId], @@ -165,17 +123,13 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { selectPortfolioCryptoBalanceBaseUnitByFilter(s, feeAssetBalanceFilter), ) - const selectedCurrency = useAppSelector(selectSelectedCurrency) - useEffect(() => { ;(async () => { if (!opportunity?.apy) return if (!(accountId && state?.deposit.cryptoAmount && asset)) return if (protocolFeeCryptoBaseUnit) return - const amountCryptoBaseUnit = bnOrZero(state?.deposit.cryptoAmount).times( - bn(10).pow(asset.precision), - ) + const amountCryptoBaseUnit = bn(toBaseUnit(state?.deposit.cryptoAmount, asset.precision)) if (amountCryptoBaseUnit.isZero()) return @@ -188,12 +142,13 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { if (maybeQuote.isErr()) throw new Error(maybeQuote.unwrapErr()) - const quote = maybeQuote.unwrap() + const _quote = maybeQuote.unwrap() + setQuote(_quote) const { expected_amount_deposit: expectedAmountOutThorBaseUnit, fees: { slippage_bps }, - } = quote + } = _quote // Total downside const thorchainFeeCryptoPrecision = fromThorBaseUnit( @@ -219,323 +174,53 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { }) setDaysToBreakEven(daysToBreakEven) })() - }, [accountId, asset, protocolFeeCryptoBaseUnit, opportunity?.apy, state?.deposit.cryptoAmount]) - - const getEstimateFeesArgs: () => Promise = - useCallback(async () => { - if (isTokenDeposit) return - if (!accountId) throw new Error('accountId required') - if (isUtxoChainId(chainId) && !maybeFromUTXOAccountAddress) { - throw new Error('UTXO from address required') - } - - if (!state?.deposit.cryptoAmount) { - throw new Error('Cannot send 0-value THORCHain savers Tx') - } - - const amountCryptoBaseUnit = bnOrZero(state.deposit.cryptoAmount).times( - bn(10).pow(asset.precision), - ) - const maybeQuote = await getMaybeThorchainSaversDepositQuote({ asset, amountCryptoBaseUnit }) - if (maybeQuote.isErr()) throw new Error(maybeQuote.unwrapErr()) - const quote = maybeQuote.unwrap() - - const amountCryptoThorBaseUnit = toThorBaseUnit({ - valueCryptoBaseUnit: amountCryptoBaseUnit, - asset, - }) - - setProtocolFeeCryptoBaseUnit( - toBaseUnit( - fromThorBaseUnit(amountCryptoThorBaseUnit.minus(quote.expected_amount_deposit)), - asset.precision, - ), - ) - - const memoUtf8 = quote.memo - return { - amountCryptoPrecision: state.deposit.cryptoAmount, - assetId, - from: maybeFromUTXOAccountAddress, - to: quote.inbound_address, - memo: supportedEvmChainIds.includes(chainId as KnownChainIds) ? toHex(memoUtf8) : memoUtf8, - sendMax: Boolean(!isUtxoChainId(chainId) && state?.deposit.sendMax), - accountId, - contractAddress: tokenOrUndefined(fromAssetId(asset.assetId).assetReference), - } - }, [ - accountId, - asset, - assetId, - chainId, - isTokenDeposit, - maybeFromUTXOAccountAddress, - state?.deposit.cryptoAmount, - state?.deposit.sendMax, - supportedEvmChainIds, - ]) - - const getEstimatedFees = useCallback(async () => { - if (isUtxoChainId(chainId) && !maybeFromUTXOAccountAddress) { - // UTXO from address not fetched yet - return - } - - const estimateFeesArgs = await getEstimateFeesArgs() - if (!estimateFeesArgs) return - return estimateFees(estimateFeesArgs) - }, [chainId, getEstimateFeesArgs, maybeFromUTXOAccountAddress]) - - const getCustomTxInput: () => Promise = useCallback(async () => { - if (!contextDispatch) return - if (!(accountId && assetId && feeAsset && accountNumber !== undefined && wallet)) return - if (!state?.deposit.cryptoAmount) { - throw new Error('Cannot send 0-value THORCHain savers Tx') - } - - try { - const adapter = assertGetEvmChainAdapter(chainId) - - const amountCryptoBaseUnit = toBaseUnit(state.deposit.cryptoAmount, asset.precision) - const maybeQuote = await getMaybeThorchainSaversDepositQuote({ asset, amountCryptoBaseUnit }) - if (maybeQuote.isErr()) throw new Error(maybeQuote.unwrapErr()) - const quote = maybeQuote.unwrap() - - const daemonUrl = getConfig().REACT_APP_THORCHAIN_NODE_URL - const maybeInboundAddressData = await getInboundAddressDataForChain( - daemonUrl, - feeAsset?.assetId, - ) - if (maybeInboundAddressData.isErr()) - throw new Error(maybeInboundAddressData.unwrapErr().message) - const inboundAddressData = maybeInboundAddressData.unwrap() - // Guaranteed to be defined for EVM chains, and approve are only for EVM chains - const router = inboundAddressData.router! - - const thorContract = getOrCreateContractByType({ - address: router, - type: ContractType.ThorRouter, - chainId: asset.chainId, - }) - - const data = encodeFunctionData({ - abi: thorContract.abi, - functionName: 'depositWithExpiry', - args: [ - getAddress(quote.inbound_address), - getAddress(fromAssetId(assetId).assetReference), - BigInt(amountCryptoBaseUnit.toString()), - quote.memo, - BigInt(quote.expiry), - ], - }) - - const buildCustomTxInput = await createBuildCustomTxInput({ - accountNumber, - adapter, - data, - value: '0', // this is not a token send, but a smart contract call so we don't send anything here, THOR router does - to: router, - wallet, - }) - - return buildCustomTxInput - } catch (e) { - console.error(e) - } }, [ - contextDispatch, accountId, - assetId, - feeAsset, - accountNumber, - wallet, - state?.deposit.cryptoAmount, - chainId, asset, + protocolFeeCryptoBaseUnit, + opportunity?.apy, + state?.deposit.cryptoAmount, + quote, ]) - const getCustomTxFees = useCallback(async () => { - if (!wallet || !accountId) return - - const adapter = assertGetEvmChainAdapter(chainId) - const customTxInput = await getCustomTxInput() - if (!customTxInput) return undefined + const { data: fromAddress } = useQuery({ + ...reactQueries.common.thorchainFromAddress({ + accountId: accountId!, + assetId, + wallet: wallet!, + accountMetadata: accountMetadata!, + getPosition: getThorchainSaversPosition, + }), + enabled: Boolean(accountId && wallet && accountMetadata), + }) - const fees = await adapter.getFeeData({ - to: customTxInput.to, - value: customTxInput.value, - chainSpecific: { - from: fromAccountId(accountId).account, - data: customTxInput.data, - }, - }) + const { executeTransaction, estimatedFeesData } = useSendThorTx({ + accountId: accountId ?? null, + assetId, + amountCryptoBaseUnit: toBaseUnit(state?.deposit.cryptoAmount, asset.precision), + action: 'depositSavers', + memo: quote?.memo ?? null, + fromAddress: fromAddress ?? null, + }) - return fees - }, [accountId, chainId, getCustomTxInput, wallet]) + const estimatedGasCryptoPrecision = useMemo(() => { + if (!estimatedFeesData) return + return fromBaseUnit(estimatedFeesData.txFeeCryptoBaseUnit, feeAsset.precision) + }, [estimatedFeesData, feeAsset.precision]) useEffect(() => { if (!contextDispatch) return - ;(async () => { - // TODO(gomes): use new fees estimation hook here instead once support for non-UTXO chains and EVM assets is handled at consumption level - const estimatedFees = await (isTokenDeposit ? getCustomTxFees() : getEstimatedFees()) - if (!estimatedFees) return - - setNetworkFeeCryptoBaseUnit(estimatedFees.fast.txFee) - contextDispatch({ - type: ThorchainSaversDepositActionType.SET_DEPOSIT, - payload: { - networkFeeCryptoBaseUnit: estimatedFees.fast.txFee, - }, - }) - })() - }, [ - contextDispatch, - getCustomTxFees, - getEstimatedFees, - isTokenDeposit, - state?.deposit.estimatedGasCryptoPrecision, - ]) - - const getSendInput: () => Promise = useCallback(async () => { - if (isTokenDeposit) return - if (!contextDispatch) return - if (!(accountId && assetId && feeAsset)) return - if (!state?.deposit.cryptoAmount) { - throw new Error('Cannot send 0-value THORCHain savers Tx') - } - - try { - const estimatedFees = await getEstimatedFees() - if (!estimatedFees) return - setNetworkFeeCryptoBaseUnit(estimatedFees.fast.txFee) - contextDispatch({ - type: ThorchainSaversDepositActionType.SET_DEPOSIT, - payload: { - networkFeeCryptoBaseUnit: estimatedFees.fast.txFee, - }, - }) - - const amountCryptoBaseUnit = toBaseUnit(state.deposit.cryptoAmount, asset.precision) - - const maybeQuote = await getMaybeThorchainSaversDepositQuote({ asset, amountCryptoBaseUnit }) - if (maybeQuote.isErr()) throw new Error(maybeQuote.unwrapErr()) - const quote = maybeQuote.unwrap() - - let maybeGasDeductedCryptoAmountCryptoPrecision = '' - if (isUtxoChainId(chainId)) { - if (!maybeFromUTXOAccountAddress) { - throw new Error('Account address required to deposit in THORChain savers') - } - - // DO NOT MAKE ME A `useAppSelector()` HOOK - // DO NOT extract store.getState() to a component/module-scope variable - // React reconciliation algorithm makes it so this wouldn't change until the next time this is fired - // But the balance actually changes from the gas fees of the reconciliation Tx if it's fired - // So the next time we fire the actual send Tx, we should deduct from the udpated balance - const assetBalanceCryptoBaseUnit = selectPortfolioCryptoBalanceBaseUnitByFilter( - store.getState(), - assetBalanceFilter, - ) - const fastFeesBaseUnit = estimatedFees.fast.txFee - - setNetworkFeeCryptoBaseUnit(fastFeesBaseUnit) - contextDispatch({ - type: ThorchainSaversDepositActionType.SET_DEPOSIT, - payload: { - networkFeeCryptoBaseUnit: fastFeesBaseUnit, - }, - }) - - const cryptoAmountBaseUnit = toBaseUnit(state.deposit.cryptoAmount, asset.precision) - - const needsFeeDeduction = bn(cryptoAmountBaseUnit) - .plus(fastFeesBaseUnit) - .gte(assetBalanceCryptoBaseUnit) - - if (state?.deposit.sendMax) { - maybeGasDeductedCryptoAmountCryptoPrecision = bnOrZero(assetBalanceCryptoBaseUnit) - .minus(bn(fastFeesBaseUnit).times(TXS_BUFFER)) - .div(bn(10).pow(asset.precision)) - .toFixed() - } else if (needsFeeDeduction) - // We tend to overestimate so that SHOULD be safe but this is both - // a safety factor as well as ensuring we keep a bit of gas away for another Tx - maybeGasDeductedCryptoAmountCryptoPrecision = bnOrZero(state.deposit.cryptoAmount) - .minus(bn(fastFeesBaseUnit).times(TXS_BUFFER).div(bn(10).pow(asset.precision))) - .toFixed() - } - - const memoUtf8 = quote.memo - - const sendInput: SendInput = { - amountCryptoPrecision: - maybeGasDeductedCryptoAmountCryptoPrecision || state.deposit.cryptoAmount, - assetId, - to: quote.inbound_address, - from: maybeFromUTXOAccountAddress, - sendMax: Boolean(state?.deposit.sendMax), - accountId, - memo: supportedEvmChainIds.includes(chainId as KnownChainIds) ? toHex(memoUtf8) : memoUtf8, - amountFieldError: '', - estimatedFees, - feeType: FeeDataKey.Fast, - fiatAmount: '', - fiatSymbol: selectedCurrency, - vanityAddress: '', - input: quote.inbound_address, - } - - return sendInput - } catch (e) { - console.error(e) - } - }, [ - isTokenDeposit, - contextDispatch, - accountId, - assetId, - feeAsset, - state?.deposit.cryptoAmount, - state?.deposit.sendMax, - getEstimatedFees, - asset, - chainId, - maybeFromUTXOAccountAddress, - supportedEvmChainIds, - selectedCurrency, - assetBalanceFilter, - ]) - - const handleCustomTx = useCallback(async (): Promise => { - if (!wallet) return - const buildCustomTxInput = await getCustomTxInput() - if (!buildCustomTxInput) return - - const adapter = assertGetEvmChainAdapter(chainId) - - const txid = await buildAndBroadcast({ - adapter, - buildCustomTxInput, - receiverAddress: CONTRACT_INTERACTION, // no receiver for this contract call + if (!estimatedFeesData) return + if (!estimatedGasCryptoPrecision) return + + contextDispatch({ + type: ThorchainSaversDepositActionType.SET_DEPOSIT, + payload: { + estimatedGasCryptoPrecision, + networkFeeCryptoBaseUnit: estimatedFeesData.txFeeCryptoBaseUnit, + }, }) - return txid - }, [wallet, getCustomTxInput, chainId]) - - useEffect(() => { - if (!(accountId && chainAdapter && wallet && bip44Params && accountType)) return - ;(async () => { - const accountAddress = await getThorchainFromAddress({ - accountId, - assetId, - wallet, - accountMetadata, - getPosition: getThorchainSaversPosition, - }) - - setMaybeFromUTXOAccountAddress(accountAddress) - })() - }, [accountId, accountMetadata, accountType, assetId, bip44Params, chainAdapter, wallet]) + }, [contextDispatch, estimatedFeesData, estimatedGasCryptoPrecision]) const { isTradingActive, refetch: refetchIsTradingActive } = useIsTradingActive({ assetId, @@ -548,7 +233,7 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { try { if ( !( - userAddress && + fromAddress && assetReference && wallet && supportsETH(wallet) && @@ -576,33 +261,17 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { throw new Error(`THORChain pool halted for assetId: ${assetId}`) } - const maybeTxId = await (async () => { - if (isTokenDeposit) { - return handleCustomTx() - } - const sendInput = await getSendInput() - if (!sendInput) throw new Error('Error building send input') - - const txId = await handleSend({ - sendInput, - wallet, - }) - - return txId - })() - - if (!maybeTxId) { - throw new Error('Error sending THORCHain savers Txs') - } + const _txId = await executeTransaction() + if (!_txId) throw new Error('failed to broadcast transaction') contextDispatch({ type: ThorchainSaversDepositActionType.SET_DEPOSIT, payload: { protocolFeeCryptoBaseUnit, - maybeFromUTXOAccountAddress, + maybeFromUTXOAccountAddress: fromAddress, }, }) - contextDispatch({ type: ThorchainSaversDepositActionType.SET_TXID, payload: maybeTxId }) + contextDispatch({ type: ThorchainSaversDepositActionType.SET_TXID, payload: _txId }) onNext(DefiStep.Status) trackOpportunityEvent( MixPanelEvent.DepositConfirm, @@ -629,7 +298,7 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { bip44Params, accountId, assetId, - userAddress, + fromAddress, assetReference, wallet, opportunity, @@ -638,13 +307,10 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { state?.deposit.fiatAmount, isTradingActive, refetchIsTradingActive, + executeTransaction, protocolFeeCryptoBaseUnit, - maybeFromUTXOAccountAddress, onNext, assets, - isTokenDeposit, - getSendInput, - handleCustomTx, toast, translate, ]) @@ -656,9 +322,11 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { const hasEnoughBalanceForGas = useMemo( () => bnOrZero(feeAssetBalanceCryptoBaseUnit) - .minus(state?.deposit.estimatedGasCryptoPrecision ?? 0) + .minus( + toBaseUnit(state?.deposit.estimatedGasCryptoPrecision ?? 0, feeAsset?.precision ?? 0), + ) .gte(0), - [feeAssetBalanceCryptoBaseUnit, state?.deposit.estimatedGasCryptoPrecision], + [feeAssetBalanceCryptoBaseUnit, state?.deposit.estimatedGasCryptoPrecision, feeAsset], ) useEffect(() => { @@ -673,7 +341,7 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { ) const { data: _isSmartContractAddress, isLoading: isAddressByteCodeLoading } = - useIsSmartContractAddress(userAddress) + useIsSmartContractAddress(fromAddress ?? '') const disableSmartContractDeposit = useMemo(() => { // This is either a smart contract address, or the bytecode is still loading - disable confirm @@ -710,11 +378,11 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { preFooter={preFooter} isDisabled={ !hasEnoughBalanceForGas || - !userAddress || + !fromAddress || disableSmartContractDeposit || isTradingActive === false } - loading={state.loading || !userAddress || isAddressByteCodeLoading} + loading={state.loading || !fromAddress || isAddressByteCodeLoading} loadingText={translate('common.confirm')} headerText='modals.confirm.deposit.header' > @@ -762,16 +430,13 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { @@ -787,16 +452,11 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { diff --git a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Deposit.tsx b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Deposit.tsx index cb13badd8fd..c7f4d644c1d 100644 --- a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Deposit.tsx +++ b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Deposit.tsx @@ -1,13 +1,8 @@ import { Skeleton, useToast } from '@chakra-ui/react' import type { AccountId } from '@shapeshiftoss/caip' import { fromAccountId, fromAssetId, toAssetId } from '@shapeshiftoss/caip' -import type { FeeDataEstimate } from '@shapeshiftoss/chain-adapters' -import type { SwapErrorRight } from '@shapeshiftoss/swapper' -import type { Asset, KnownChainIds } from '@shapeshiftoss/types' -import type { Result } from '@sniptt/monads/build' -import { Ok } from '@sniptt/monads/build' +import type { Asset } from '@shapeshiftoss/types' import { useQueryClient } from '@tanstack/react-query' -import { getConfig } from 'config' import { getOrCreateContractByType } from 'contracts/contractManager' import { ContractType } from 'contracts/types' import type { DepositValues } from 'features/defi/components/Deposit/Deposit' @@ -21,40 +16,28 @@ import debounce from 'lodash/debounce' import qs from 'qs' import { useCallback, useContext, useEffect, useMemo, useState } from 'react' import { useTranslate } from 'react-polyglot' -import type { EstimatedFeesQueryKey } from 'react-queries/hooks/useQuoteEstimatedFeesQuery' import { useHistory } from 'react-router-dom' import { encodeFunctionData, getAddress, maxUint256 } from 'viem' import type { AccountDropdownProps } from 'components/AccountDropdown/AccountDropdown' import { Amount } from 'components/Amount/Amount' import type { StepComponentProps } from 'components/DeFi/components/Steps' import { HelperTooltip } from 'components/HelperTooltip/HelperTooltip' -import { estimateFees } from 'components/Modals/Send/utils' import { Row } from 'components/Row/Row' -import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton' import { useBrowserRouter } from 'hooks/useBrowserRouter/useBrowserRouter' import { useWallet } from 'hooks/useWallet/useWallet' import { bn, bnOrZero } from 'lib/bignumber/bignumber' import { fromBaseUnit, toBaseUnit } from 'lib/math' import { trackOpportunityEvent } from 'lib/mixpanel/helpers' import { MixPanelEvent } from 'lib/mixpanel/types' -import { useRouterContractAddress } from 'lib/swapper/swappers/ThorchainSwapper/utils/useRouterContractAddress' import { isToken } from 'lib/utils' -import { - assertGetEvmChainAdapter, - createBuildCustomTxInput, - getErc20Allowance, - getFeesWithWallet, -} from 'lib/utils/evm' -import { fromThorBaseUnit } from 'lib/utils/thorchain' +import { assertGetEvmChainAdapter, getErc20Allowance, getFeesWithWallet } from 'lib/utils/evm' import { fetchHasEnoughBalanceForTxPlusFeesPlusSweep } from 'lib/utils/thorchain/balance' import { BASE_BPS_POINTS } from 'lib/utils/thorchain/constants' -import { getInboundAddressDataForChain } from 'lib/utils/thorchain/getInboundAddressDataForChain' import { useGetThorchainSaversDepositQuoteQuery } from 'lib/utils/thorchain/hooks/useGetThorchainSaversDepositQuoteQuery' +import { useSendThorTx } from 'lib/utils/thorchain/hooks/useSendThorTx' import { isUtxoChainId } from 'lib/utils/utxo' -import { - queryFn as getEstimatedFeesQueryFn, - useGetEstimatedFeesQuery, -} from 'pages/Lending/hooks/useGetEstimatedFeesQuery' +import type { EstimatedFeesQueryKey } from 'pages/Lending/hooks/useGetEstimatedFeesQuery' +import { queryFn as getEstimatedFeesQueryFn } from 'pages/Lending/hooks/useGetEstimatedFeesQuery' import type { IsSweepNeededQueryKey } from 'pages/Lending/hooks/useIsSweepNeededQuery' import { queryFn as isSweepNeededQueryFn, @@ -95,10 +78,10 @@ export const Deposit: React.FC = ({ fromAddress, onNext, }) => { - const [outboundFeeCryptoBaseUnit, setOutboundFeeCryptoBaseUnit] = useState('') const [isApprovalRequired, setIsApprovalRequired] = useState(false) const { state, dispatch: contextDispatch } = useContext(DepositContext) + const toast = useToast() const queryClient = useQueryClient() const history = useHistory() const translate = useTranslate() @@ -160,7 +143,16 @@ export const Deposit: React.FC = ({ const asset: Asset | undefined = useAppSelector(state => selectAssetById(state, assetId)) if (!asset) throw new Error(`Asset not found for AssetId ${assetId}`) - const marketData = useAppSelector(state => selectMarketDataByAssetIdUserCurrency(state, assetId)) + const assetMarketData = useAppSelector(state => + selectMarketDataByAssetIdUserCurrency(state, assetId), + ) + const feeAssetMarketData = useAppSelector(state => + selectMarketDataByAssetIdUserCurrency(state, feeAsset?.assetId ?? ''), + ) + + const assetPriceInFeeAsset = useMemo(() => { + return bn(assetMarketData.price).div(feeAssetMarketData.price) + }, [assetMarketData.price, feeAssetMarketData.price]) const userAddress: string | undefined = accountId && fromAccountId(accountId).account const balanceFilter = useMemo(() => ({ assetId, accountId }), [accountId, assetId]) @@ -169,224 +161,56 @@ export const Deposit: React.FC = ({ selectPortfolioCryptoBalanceBaseUnitByFilter(state, balanceFilter), ) - const getOutboundFeeCryptoBaseUnit = useCallback(async (): Promise => { - if (!assetId) return '0' - - // We only want to display the outbound fee as a minimum for assets which have a zero dust threshold i.e EVM and Cosmos assets - if (!bn(THORCHAIN_SAVERS_DUST_THRESHOLDS_CRYPTO_BASE_UNIT[assetId]).isZero()) return '0' - const daemonUrl = getConfig().REACT_APP_THORCHAIN_NODE_URL - const maybeInboundAddressData = await getInboundAddressDataForChain(daemonUrl, assetId) - - return maybeInboundAddressData - .match>({ - ok: ({ outbound_fee }) => { - const outboundFeeCryptoBaseUnit = toBaseUnit( - fromThorBaseUnit(outbound_fee), - asset.precision, - ) - - return Ok(outboundFeeCryptoBaseUnit) - }, - err: _err => Ok('0'), - }) - .unwrap() - }, [asset.precision, assetId]) - - useEffect(() => { - ;(async () => { - if (outboundFeeCryptoBaseUnit) return - - const outboundFee = await getOutboundFeeCryptoBaseUnit() - if (!outboundFee) return - - setOutboundFeeCryptoBaseUnit(outboundFee) - })() - }, [getOutboundFeeCryptoBaseUnit, outboundFeeCryptoBaseUnit]) - // notify - const toast = useToast() + const { + data: thorchainSaversDepositQuote, + isLoading: isThorchainSaversDepositQuoteLoading, + isSuccess: isThorchainSaversDepositQuoteSuccess, + isError: isThorchainSaversDepositQuoteError, + error: thorchainSaversDepositQuoteError, + } = useGetThorchainSaversDepositQuoteQuery({ + asset, + amountCryptoBaseUnit: toBaseUnit(inputValues?.cryptoAmount, asset.precision), + }) const { - routerContractAddress: saversRouterContractAddress, - isLoading: isSaversRouterContractAddressLoading, - } = useRouterContractAddress({ - feeAssetId: feeAsset?.assetId ?? '', - skip: !isTokenDeposit || !feeAsset?.assetId, - excludeHalted: true, + estimatedFeesData, + isEstimatedFeesDataLoading, + outboundFeeCryptoBaseUnit, + inboundAddress, + } = useSendThorTx({ + assetId, + accountId: accountId ?? null, + amountCryptoBaseUnit: toBaseUnit(inputValues?.cryptoAmount, asset?.precision ?? 0), + memo: thorchainSaversDepositQuote?.memo ?? null, + fromAddress, + action: 'depositSavers', + enableEstimateFees: Boolean(!isApprovalRequired && bnOrZero(inputValues?.cryptoAmount).gt(0)), }) useEffect(() => { - if (!inputValues || !accountId) - return // Router contract address is only set in case we're depositting a token, not a native asset + if (!accountId) return ;(async () => { const isApprovalRequired = await (async () => { - if (!saversRouterContractAddress) return false + // Router contract address is only set in case we're depositting a token, not a native asset + if (!inboundAddress) return false + const allowanceOnChainCryptoBaseUnit = await getErc20Allowance({ address: fromAssetId(assetId).assetReference, - spender: saversRouterContractAddress, + spender: inboundAddress, from: fromAccountId(accountId).account, chainId: asset.chainId, }) - const { cryptoAmount } = inputValues - const cryptoAmountBaseUnit = toBaseUnit(cryptoAmount, asset.precision) + const cryptoAmountBaseUnit = toBaseUnit(inputValues?.cryptoAmount, asset.precision) + if (bnOrZero(allowanceOnChainCryptoBaseUnit).eq(0)) return true if (bn(cryptoAmountBaseUnit).gt(allowanceOnChainCryptoBaseUnit)) return true + return false })() setIsApprovalRequired(isApprovalRequired) })() - }, [accountId, asset.chainId, asset.precision, assetId, inputValues, saversRouterContractAddress]) - - const { - data: thorchainSaversDepositQuote, - isLoading: isThorchainSaversDepositQuoteLoading, - isSuccess: isThorchainSaversDepositQuoteSuccess, - isError: isThorchainSaversDepositQuoteError, - error: thorchainSaversDepositQuoteError, - } = useGetThorchainSaversDepositQuoteQuery({ - asset, - amountCryptoBaseUnit: toBaseUnit(inputValues?.cryptoAmount, asset.precision), - }) - - // TODO(gomes): use useGetEstimatedFeesQuery instead of this. - // The logic of useGetEstimatedFeesQuery and its consumption will need some touching up to work with custom Txs - // since the guts of it are made to accomodate Tx/fees/sweep fees deduction and there are !isUtxoChainId checks in place currently - // The method below is now only used for non-UTXO chains - const getDepositGasEstimateCryptoPrecision = useCallback( - async (deposit: DepositValues): Promise => { - if ( - !( - userAddress && - assetReference && - accountId && - opportunityData && - accountNumber !== undefined && - wallet && - feeAsset && - !isThorchainSaversDepositQuoteLoading - ) - ) - return - try { - const amountCryptoBaseUnit = toBaseUnit(deposit.cryptoAmount, asset.precision) - - if (isThorchainSaversDepositQuoteError) - throw new Error(thorchainSaversDepositQuoteError.message) - const quote = thorchainSaversDepositQuote! - - const chainAdapters = getChainAdapterManager() - - // We can only estimate the gas at this stage if allowance is already granted - // Else, the Tx simulation will fail - if (isTokenDeposit && !isApprovalRequired) { - const thorContract = getOrCreateContractByType({ - address: saversRouterContractAddress!, - type: ContractType.ThorRouter, - chainId: asset.chainId, - }) - - const data = encodeFunctionData({ - abi: thorContract.abi, - functionName: 'depositWithExpiry', - args: [ - getAddress(quote.inbound_address), - getAddress(fromAssetId(assetId).assetReference), - BigInt(amountCryptoBaseUnit.toString()), - quote.memo, - BigInt(quote.expiry), - ], - }) - - const adapter = assertGetEvmChainAdapter(chainId) - - const customTxInput = await createBuildCustomTxInput({ - accountNumber, - adapter, - data, - value: '0', // this is not a token send, but a smart contract call so we don't send anything here, THOR router does - to: saversRouterContractAddress!, - wallet, - }) - - const fees = (await estimateFees({ - accountId, - contractAddress: undefined, - assetId, - sendMax: false, - amountCryptoPrecision: '0', - to: customTxInput.to, - from: fromAccountId(accountId).account, - memo: customTxInput.data, - })) as FeeDataEstimate - - const fastFeeCryptoBaseUnit = fees.fast.txFee - - const fastFeeCryptoPrecision = fromBaseUnit(fastFeeCryptoBaseUnit, feeAsset.precision) - - return fastFeeCryptoPrecision - } - - const adapter = chainAdapters.get(chainId)! - const getFeeDataInput = { - to: quote.inbound_address, - value: amountCryptoBaseUnit, - chainSpecific: { - pubkey: userAddress, - from: fromAccountId(accountId).account, - }, - sendMax: Boolean(state?.deposit.sendMax), - } - const fastFeeCryptoBaseUnit = (await adapter.getFeeData(getFeeDataInput)).fast.txFee - const fastFeeCryptoPrecision = fromBaseUnit(fastFeeCryptoBaseUnit, asset.precision) - - return fastFeeCryptoPrecision - } catch (error) { - console.error(error) - toast({ - position: 'top-right', - description: translate('common.somethingWentWrongBody'), - title: translate('common.somethingWentWrong'), - status: 'error', - }) - } - }, - [ - userAddress, - assetReference, - accountId, - opportunityData, - accountNumber, - wallet, - feeAsset, - isThorchainSaversDepositQuoteLoading, - asset, - isThorchainSaversDepositQuoteError, - thorchainSaversDepositQuoteError?.message, - thorchainSaversDepositQuote, - isTokenDeposit, - isApprovalRequired, - chainId, - state?.deposit.sendMax, - saversRouterContractAddress, - assetId, - toast, - translate, - ], - ) - - const { - data: estimatedFeesData, - isLoading: isEstimatedFeesDataLoading, - isSuccess: isEstimatedFeesDataSuccess, - } = useGetEstimatedFeesQuery({ - amountCryptoPrecision: inputValues?.cryptoAmount ?? '0', - assetId, - to: thorchainSaversDepositQuote?.inbound_address ?? '', - sendMax: false, - accountId: accountId ?? '', - contractAddress: undefined, - enabled: Boolean(thorchainSaversDepositQuote && accountId && isUtxoChainId(asset.chainId)), - }) + }, [accountId, asset.chainId, asset.precision, assetId, inputValues, inboundAddress]) // TODO(gomes): this will work for UTXO but is invalid for tokens since they use diff. denoms // the current workaround is to not do fee deduction for non-UTXO chains, @@ -424,12 +248,12 @@ export const Deposit: React.FC = ({ // Don't fetch sweep needed if there isn't enough balance for the tx + fees, since adding in a sweep Tx would obviously fail too enabled: Boolean( bnOrZero(inputValues?.cryptoAmount).gt(0) && - isEstimatedFeesDataSuccess && + estimatedFeesData && getHasEnoughBalanceForTxPlusFees({ precision: asset.precision, balanceCryptoBaseUnit, amountCryptoPrecision: inputValues?.cryptoAmount ?? '', - txFeeCryptoBaseUnit: estimatedFeesData?.txFeeCryptoBaseUnit ?? '', + txFeeCryptoBaseUnit: estimatedFeesData.txFeeCryptoBaseUnit, }), ), }), @@ -437,12 +261,11 @@ export const Deposit: React.FC = ({ asset.precision, assetId, balanceCryptoBaseUnit, - estimatedFeesData?.txFeeCryptoBaseUnit, + estimatedFeesData, feeAsset?.precision, fromAddress, getHasEnoughBalanceForTxPlusFees, inputValues?.cryptoAmount, - isEstimatedFeesDataSuccess, ], ) @@ -451,31 +274,34 @@ export const Deposit: React.FC = ({ const handleContinue = useCallback( async (formValues: DepositValues) => { - if ( - !(userAddress && opportunityData && inputValues && accountId && contextDispatch && feeAsset) - ) - return + if (!feeAsset) return + if (!accountId) return + if (!userAddress) return + if (!inputValues) return + if (!opportunityData) return + if (!contextDispatch) return + if (!isApprovalRequired && !estimatedFeesData) return + // set deposit state for future use contextDispatch({ type: ThorchainSaversDepositActionType.SET_DEPOSIT, payload: formValues }) contextDispatch({ type: ThorchainSaversDepositActionType.SET_LOADING, payload: true }) + try { - const estimatedGasCryptoPrecision = isUtxoChainId(chainId) - ? fromBaseUnit(estimatedFeesData?.txFeeCryptoBaseUnit ?? 0, feeAsset.precision) - : await getDepositGasEstimateCryptoPrecision(formValues) - - if (!estimatedGasCryptoPrecision) return - contextDispatch({ - type: ThorchainSaversDepositActionType.SET_DEPOSIT, - payload: { estimatedGasCryptoPrecision }, - }) + if (estimatedFeesData) { + contextDispatch({ + type: ThorchainSaversDepositActionType.SET_DEPOSIT, + payload: { + estimatedGasCryptoPrecision: fromBaseUnit( + estimatedFeesData.txFeeCryptoBaseUnit, + feeAsset.precision, + ), + networkFeeCryptoBaseUnit: estimatedFeesData.txFeeCryptoBaseUnit, + }, + }) + } const approvalFees = await (() => { - if ( - !isApprovalRequired || - !saversRouterContractAddress || - accountNumber === undefined || - !wallet - ) + if (!isApprovalRequired || !inboundAddress || accountNumber === undefined || !wallet) return undefined const contract = getOrCreateContractByType({ @@ -488,7 +314,7 @@ export const Deposit: React.FC = ({ const data = encodeFunctionData({ abi: contract.abi, functionName: 'approve', - args: [getAddress(saversRouterContractAddress), maxUint256], + args: [getAddress(inboundAddress), maxUint256], }) const adapter = assertGetEvmChainAdapter(chainId) @@ -547,14 +373,13 @@ export const Deposit: React.FC = ({ contextDispatch, feeAsset, chainId, - estimatedFeesData?.txFeeCryptoBaseUnit, - getDepositGasEstimateCryptoPrecision, + estimatedFeesData, onNext, isSweepNeeded, assetId, assets, isApprovalRequired, - saversRouterContractAddress, + inboundAddress, accountNumber, wallet, asset.chainId, @@ -567,9 +392,22 @@ export const Deposit: React.FC = ({ browserHistory.goBack() }, [browserHistory]) + const outboundFeeInAssetCryptoBaseUnit = useMemo(() => { + if (!asset) return bn(0) + if (!feeAsset) return bn(0) + if (!outboundFeeCryptoBaseUnit) return bn(0) + + const outboundFeeCryptoPrecision = fromBaseUnit(outboundFeeCryptoBaseUnit, feeAsset.precision) + const outboundFeeInAssetCryptoPrecision = bn(outboundFeeCryptoPrecision).div( + assetPriceInFeeAsset, + ) + + return toBaseUnit(outboundFeeInAssetCryptoPrecision, asset.precision) + }, [outboundFeeCryptoBaseUnit, assetPriceInFeeAsset, asset, feeAsset]) + const validateCryptoAmount = useCallback( async (value: string) => { - if (!accountId) return + if (!accountId || !outboundFeeInAssetCryptoBaseUnit) return const valueCryptoBaseUnit = toBaseUnit(value, asset.precision) const balanceCryptoPrecision = bn(fromBaseUnit(balanceCryptoBaseUnit, asset.precision)) @@ -582,14 +420,17 @@ export const Deposit: React.FC = ({ const isBelowMinSellAmount = !isAboveDepositDustThreshold({ valueCryptoBaseUnit, assetId }) const isBelowOutboundFee = - bn(outboundFeeCryptoBaseUnit).gt(0) && - bnOrZero(valueCryptoBaseUnit).lt(outboundFeeCryptoBaseUnit) + bn(outboundFeeInAssetCryptoBaseUnit).gt(0) && + bnOrZero(valueCryptoBaseUnit).lt(outboundFeeInAssetCryptoBaseUnit) const minLimitCryptoPrecision = fromBaseUnit( THORCHAIN_SAVERS_DUST_THRESHOLDS_CRYPTO_BASE_UNIT[assetId], asset.precision, ) - const outboundFeeCryptoPrecision = fromBaseUnit(outboundFeeCryptoBaseUnit, asset.precision) + const outboundFeeCryptoPrecision = fromBaseUnit( + outboundFeeInAssetCryptoBaseUnit, + asset.precision, + ) const minLimit = `${minLimitCryptoPrecision} ${asset.symbol}` const outboundFeeLimit = `${outboundFeeCryptoPrecision} ${asset.symbol}` @@ -622,7 +463,7 @@ export const Deposit: React.FC = ({ asset, balanceCryptoBaseUnit, assetId, - outboundFeeCryptoBaseUnit, + outboundFeeInAssetCryptoBaseUnit, translate, chainId, fromAddress, @@ -631,25 +472,29 @@ export const Deposit: React.FC = ({ const validateFiatAmount = useCallback( async (value: string) => { - if (!accountId) return - const valueCryptoPrecision = bnOrZero(value).div(marketData.price) + if (!accountId || !outboundFeeInAssetCryptoBaseUnit) return + + const valueCryptoPrecision = bnOrZero(value).div(assetMarketData.price) const balanceCryptoPrecision = bn(fromBaseUnit(balanceCryptoBaseUnit, asset.precision)) - const fiatBalance = balanceCryptoPrecision.times(marketData.price) + const fiatBalance = balanceCryptoPrecision.times(assetMarketData.price) if (fiatBalance.isZero() || fiatBalance.lt(value)) return 'common.insufficientFunds' const valueCryptoBaseUnit = toBaseUnit(valueCryptoPrecision, asset.precision) const isBelowMinSellAmount = !isAboveDepositDustThreshold({ valueCryptoBaseUnit, assetId }) const isBelowOutboundFee = - bn(outboundFeeCryptoBaseUnit).gt(0) && - bnOrZero(valueCryptoBaseUnit).lt(outboundFeeCryptoBaseUnit) + bn(outboundFeeInAssetCryptoBaseUnit).gt(0) && + bnOrZero(valueCryptoBaseUnit).lt(outboundFeeInAssetCryptoBaseUnit) const minLimitCryptoPrecision = fromBaseUnit( THORCHAIN_SAVERS_DUST_THRESHOLDS_CRYPTO_BASE_UNIT[assetId], asset.precision, ) - const outboundFeeCryptoPrecision = fromBaseUnit(outboundFeeCryptoBaseUnit, asset.precision) + const outboundFeeCryptoPrecision = fromBaseUnit( + outboundFeeInAssetCryptoBaseUnit, + asset.precision, + ) const minLimit = `${minLimitCryptoPrecision} ${asset.symbol}` const outboundFeeLimit = `${outboundFeeCryptoPrecision} ${asset.symbol}` @@ -679,11 +524,11 @@ export const Deposit: React.FC = ({ }, [ accountId, - marketData.price, + assetMarketData.price, balanceCryptoBaseUnit, asset, assetId, - outboundFeeCryptoBaseUnit, + outboundFeeInAssetCryptoBaseUnit, translate, chainId, fromAddress, @@ -695,8 +540,8 @@ export const Deposit: React.FC = ({ [balanceCryptoBaseUnit, asset?.precision], ) const fiatAmountAvailable = useMemo( - () => bnOrZero(balanceCryptoPrecision).times(marketData.price), - [balanceCryptoPrecision, marketData?.price], + () => bnOrZero(balanceCryptoPrecision).times(assetMarketData.price), + [balanceCryptoPrecision, assetMarketData?.price], ) const setIsSendMax = useCallback( @@ -729,15 +574,17 @@ export const Deposit: React.FC = ({ const estimatedFeesQueryArgs = { estimateFeesInput: { amountCryptoPrecision: _percentageCryptoAmountPrecisionBeforeTxFees.toFixed(), + // The same as assetId since this only runs for UTXOs + feeAssetId: assetId, assetId, to: fromAddress ?? '', sendMax: false, accountId: accountId ?? '', contractAddress: undefined, }, - asset, - assetMarketData: marketData, - enabled: Boolean(accountId && isUtxoChainId(asset.chainId)), + feeAsset, + feeAssetMarketData, + enabled: Boolean(accountId && feeAsset?.assetId && isUtxoChainId(asset.chainId)), } const estimatedFeesQueryKey: EstimatedFeesQueryKey = ['estimateFees', estimatedFeesQueryArgs] @@ -776,11 +623,13 @@ export const Deposit: React.FC = ({ _isSweepNeeded && accountId && isUtxoChainId(asset.chainId), ) const estimatedSweepFeesQueryArgs = { - asset, - assetMarketData: marketData, + feeAsset, + feeAssetMarketData, estimateFeesInput: { amountCryptoPrecision: '0', assetId, + // The same as assetId since this only runs for UTXOs + feeAssetId: assetId, to: fromAddress ?? '', sendMax: true, accountId: accountId ?? '', @@ -805,7 +654,7 @@ export const Deposit: React.FC = ({ .minus(fromBaseUnit(_estimatedSweepFeesData?.txFeeCryptoBaseUnit ?? 0, asset.precision)) const _percentageFiatAmount = _percentageCryptoAmountPrecisionAfterTxFeesAndSweep.times( - marketData.price, + assetMarketData.price, ) contextDispatch({ type: ThorchainSaversDepositActionType.SET_LOADING, payload: false }) return { @@ -814,15 +663,18 @@ export const Deposit: React.FC = ({ } }, [ - accountId, - asset, - assetId, + asset.chainId, + asset.precision, contextDispatch, + setIsSendMax, balanceCryptoPrecision, fromAddress, - marketData, + accountId, + assetId, + feeAsset, + feeAssetMarketData, queryClient, - setIsSendMax, + assetMarketData.price, ], ) @@ -840,7 +692,9 @@ export const Deposit: React.FC = ({ if (isThorchainSaversDepositQuoteError) throw new Error(thorchainSaversDepositQuoteError.message) - const quote = thorchainSaversDepositQuote! + const quote = thorchainSaversDepositQuote + + if (!quote) return const { fees: { slippage_bps }, @@ -928,7 +782,7 @@ export const Deposit: React.FC = ({ cryptoInputValidation={cryptoInputValidation} fiatAmountAvailable={fiatAmountAvailable.toFixed(2)} fiatInputValidation={fiatInputValidation} - marketData={marketData} + marketData={assetMarketData} onCancel={handleCancel} onPercentClick={handlePercentClick} onContinue={handleContinue} @@ -940,7 +794,6 @@ export const Deposit: React.FC = ({ isEstimatedFeesDataLoading || isSweepNeededLoading || isThorchainSaversDepositQuoteLoading || - isSaversRouterContractAddressLoading || state.loading } > diff --git a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Status.tsx b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Status.tsx index 457a15dd686..4dbde903257 100644 --- a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Status.tsx +++ b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Status.tsx @@ -68,18 +68,13 @@ export const Status: React.FC = ({ accountId }) => { selectMarketDataByAssetIdUserCurrency(state, assetId ?? ''), ) - const accountAddress = useMemo(() => accountId && fromAccountId(accountId).account, [accountId]) - const userAddress = useMemo( - () => state?.deposit.maybeFromUTXOAccountAddress || accountAddress, - [accountAddress, state?.deposit.maybeFromUTXOAccountAddress], - ) - - const opportunity = state?.opportunity + const account = useMemo(() => accountId && fromAccountId(accountId).account, [accountId]) const serializedTxIndex = useMemo(() => { - if (!(state?.txid && userAddress && accountId)) return '' - return serializeTxIndex(accountId, state.txid, userAddress) - }, [state?.txid, userAddress, accountId]) + if (!(state?.txid && accountId && account)) return '' + return serializeTxIndex(accountId, state.txid, account) + }, [state?.txid, accountId, account]) + const confirmedTransaction = useAppSelector(gs => selectTxById(gs, serializedTxIndex)) useEffect(() => { @@ -114,12 +109,12 @@ export const Status: React.FC = ({ accountId }) => { }, [browserHistory]) useEffect(() => { - if (!opportunity || !assetId) return + if (!state?.opportunity || !assetId) return if (state?.deposit.txStatus === 'success') { trackOpportunityEvent( MixPanelEvent.DepositSuccess, { - opportunity, + opportunity: state.opportunity, fiatAmounts: [state.deposit.fiatAmount], cryptoAmounts: [{ assetId, amountCryptoHuman: state.deposit.cryptoAmount }], }, @@ -130,7 +125,7 @@ export const Status: React.FC = ({ accountId }) => { assets, assetId, mixpanel, - opportunity, + state?.opportunity, state?.deposit.cryptoAmount, state?.deposit.fiatAmount, state?.deposit.txStatus, diff --git a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/ThorchainSaversWithdraw.tsx b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/ThorchainSaversWithdraw.tsx index abd485e9448..9025e8d703a 100644 --- a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/ThorchainSaversWithdraw.tsx +++ b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/ThorchainSaversWithdraw.tsx @@ -1,6 +1,7 @@ import { Center } from '@chakra-ui/react' import type { AccountId } from '@shapeshiftoss/caip' import { toAssetId } from '@shapeshiftoss/caip' +import { useQuery } from '@tanstack/react-query' import { DefiModalContent } from 'features/defi/components/DefiModal/DefiModalContent' import { DefiModalHeader } from 'features/defi/components/DefiModal/DefiModalHeader' import type { @@ -9,8 +10,9 @@ import type { } from 'features/defi/contexts/DefiManagerProvider/DefiCommon' import { DefiAction, DefiStep } from 'features/defi/contexts/DefiManagerProvider/DefiCommon' import qs from 'qs' -import { useCallback, useEffect, useMemo, useReducer, useState } from 'react' +import { useCallback, useEffect, useMemo, useReducer } from 'react' import { useTranslate } from 'react-polyglot' +import { reactQueries } from 'react-queries' import type { AccountDropdownProps } from 'components/AccountDropdown/AccountDropdown' import { CircularProgress } from 'components/CircularProgress/CircularProgress' import type { DefiStepProps, StepComponentProps } from 'components/DeFi/components/Steps' @@ -18,7 +20,6 @@ import { Steps } from 'components/DeFi/components/Steps' import { Sweep } from 'components/Sweep' import { useBrowserRouter } from 'hooks/useBrowserRouter/useBrowserRouter' import { useWallet } from 'hooks/useWallet/useWallet' -import { getThorchainFromAddress } from 'lib/utils/thorchain' import { getThorchainSaversPosition } from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/utils' import { serializeUserStakingId, toOpportunityId } from 'state/slices/opportunitiesSlice/utils' import { @@ -43,7 +44,6 @@ type WithdrawProps = { } export const ThorchainSaversWithdraw: React.FC = ({ accountId }) => { - const [fromAddress, setFromAddress] = useState(null) const [state, dispatch] = useReducer(reducer, initialState) const translate = useTranslate() const { query, history, location } = useBrowserRouter() @@ -98,21 +98,16 @@ export const ThorchainSaversWithdraw: React.FC = ({ accountId }) selectPortfolioAccountMetadataByAccountId(state, accountFilter), ) - useEffect(() => { - if (!(accountId && wallet && accountMetadata)) return - ;(async () => { - const _fromAddress = await getThorchainFromAddress({ - accountId, - getPosition: getThorchainSaversPosition, - assetId, - wallet, - accountMetadata, - }) - - if (!_fromAddress) return - setFromAddress(_fromAddress) - })() - }, [accountId, accountMetadata, assetId, fromAddress, wallet]) + const { data: fromAddress } = useQuery({ + ...reactQueries.common.thorchainFromAddress({ + accountId: accountId!, + getPosition: getThorchainSaversPosition, + assetId, + wallet: wallet!, + accountMetadata: accountMetadata!, + }), + enabled: Boolean(accountId && wallet && accountMetadata), + }) useEffect(() => { if (state.opportunity) return @@ -147,7 +142,7 @@ export const ThorchainSaversWithdraw: React.FC = ({ accountId }) asset: asset.symbol, }), component: ownProps => ( - + ), }, [DefiStep.Sweep]: { @@ -155,7 +150,7 @@ export const ThorchainSaversWithdraw: React.FC = ({ accountId }) component: ({ onNext }) => ( = ({ accountId, onNext }) => { const [quoteLoading, setQuoteLoading] = useState(false) + const [quote, setQuote] = useState(null) const [isDangerousWithdraw, setIsDangerousWithdraw] = useState(false) const [expiry, setExpiry] = useState('') - const [maybeFromUTXOAccountAddress, setMaybeFromUTXOAccountAddress] = useState('') + const [fromAddress, setfromAddress] = useState(null) const [protocolFeeCryptoBaseUnit, setProtocolFeeCryptoBaseUnit] = useState('') - const [networkFeeCryptoBaseUnit, setNetworkFeeCryptoBaseUnit] = useState('') const [dustAmountCryptoBaseUnit, setDustAmountCryptoBaseUnit] = useState('') const [slippageCryptoAmountPrecision, setSlippageCryptoAmountPrecision] = useState( null, @@ -158,11 +141,6 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { [accountId], ) - const accountNumberFilter = useMemo(() => ({ accountId }), [accountId]) - const accountNumber = useAppSelector(state => - selectAccountNumberByAccountId(state, accountNumberFilter), - ) - if (!asset) throw new Error(`Asset not found for AssetId ${opportunityData?.assetId}`) if (!feeAsset) throw new Error(`Fee asset not found for AssetId ${assetId}`) @@ -190,8 +168,6 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { selectPortfolioCryptoBalanceBaseUnitByFilter(s, feeAssetBalanceFilter), ) - const selectedCurrency = useAppSelector(selectSelectedCurrency) - // notify const toast = useToast() @@ -226,13 +202,15 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { }) if (maybeQuote.isErr()) throw new Error(maybeQuote.unwrapErr()) + const _quote = maybeQuote.unwrap() + setQuote(_quote) const { expiry: _expiry, dust_amount, expected_amount_out, fees: { slippage_bps }, - } = maybeQuote.unwrap() + } = _quote setExpiry(_expiry) @@ -277,7 +255,7 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { useEffect(() => { ;(async () => { - if (maybeFromUTXOAccountAddress || !isUtxoChainId(chainId) || !accountId) return + if (fromAddress || !accountId) return try { const position = await getThorchainSaversPosition({ accountId, assetId }) @@ -286,315 +264,39 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { const accountAddress = chainId === bchChainId ? `bitcoincash:${asset_address}` : asset_address - setMaybeFromUTXOAccountAddress(accountAddress) + setfromAddress(accountAddress) } catch (_e) { throw new Error(`Cannot get savers position for accountId: ${accountId}`) } })() - }, [accountId, assetId, chainId, maybeFromUTXOAccountAddress]) + }, [accountId, assetId, chainId, fromAddress]) - const getEstimateFeesArgs: () => Promise = - useCallback(async () => { - if (isTokenWithdraw) return - if (!(accountId && opportunityData?.stakedAmountCryptoBaseUnit?.[0])) - throw new Error('accountId is undefined') - - if (bnOrZero(state?.withdraw.cryptoAmount).isZero()) return - - const amountCryptoBaseUnit = toBaseUnit(state?.withdraw.cryptoAmount, asset.precision) - - const withdrawBps = getWithdrawBps({ - withdrawAmountCryptoBaseUnit: amountCryptoBaseUnit, - stakedAmountCryptoBaseUnit: opportunityData?.stakedAmountCryptoBaseUnit, - rewardsAmountCryptoBaseUnit: opportunityData?.rewardsCryptoBaseUnit?.amounts[0] ?? '0', - }) - const maybeQuote = await getThorchainSaversWithdrawQuote({ - asset, - accountId, - bps: withdrawBps, - }) - - if (isUtxoChainId(chainId) && !maybeFromUTXOAccountAddress) { - throw new Error('Account address required to withdraw from THORChain savers') - } - - if (maybeQuote.isErr()) throw new Error(maybeQuote.unwrapErr()) - const quote = maybeQuote.unwrap() - const { expiry, expected_amount_out, dust_amount } = quote - - const amountCryptoThorBaseUnit = toThorBaseUnit({ - valueCryptoBaseUnit: amountCryptoBaseUnit, - asset, - }) - setExpiry(expiry) - - // If there's nothing being withdrawn, then the protocol fee is the entire amount - const _isDangerousWithdraw = bnOrZero(expected_amount_out).isZero() - setIsDangerousWithdraw(_isDangerousWithdraw) - const protocolFeeCryptoThorBaseUnit = _isDangerousWithdraw - ? amountCryptoThorBaseUnit - : amountCryptoThorBaseUnit.minus(expected_amount_out) - setProtocolFeeCryptoBaseUnit( - toBaseUnit(fromThorBaseUnit(protocolFeeCryptoThorBaseUnit), asset.precision), - ) - - if (!maybeQuote) throw new Error('Cannot get THORCHain savers withdraw quote') - - return { - from: maybeFromUTXOAccountAddress, - amountCryptoPrecision: fromThorBaseUnit(dust_amount).toFixed(asset.precision), - assetId, - to: quote.inbound_address, - sendMax: false, - accountId, - contractAddress: '', - } - }, [ - accountId, - asset, - assetId, - chainId, - isTokenWithdraw, - maybeFromUTXOAccountAddress, - opportunityData?.rewardsCryptoBaseUnit?.amounts, - opportunityData?.stakedAmountCryptoBaseUnit, - state?.withdraw.cryptoAmount, - ]) - - const getCustomTxInput: () => Promise = useCallback(async () => { - if (!contextDispatch || !opportunityData?.stakedAmountCryptoBaseUnit) return - if (!(accountId && assetId && feeAsset && accountNumber !== undefined && wallet)) return - if (!state?.withdraw.cryptoAmount) { - throw new Error('Cannot send 0-value THORCHain savers Tx') - } - - try { - const adapter = assertGetEvmChainAdapter(chainId) - - const amountCryptoBaseUnit = toBaseUnit(state?.withdraw.cryptoAmount, asset.precision) - const withdrawBps = getWithdrawBps({ - withdrawAmountCryptoBaseUnit: amountCryptoBaseUnit, - stakedAmountCryptoBaseUnit: opportunityData.stakedAmountCryptoBaseUnit, - rewardsAmountCryptoBaseUnit: opportunityData.rewardsCryptoBaseUnit?.amounts[0] ?? '0', - }) - - if (bn(withdrawBps).isZero()) return - const maybeQuote = await getThorchainSaversWithdrawQuote({ - asset, - accountId, - bps: withdrawBps, - }) - - if (maybeQuote.isErr()) throw new Error(maybeQuote.unwrapErr()) - - const quote = maybeQuote.unwrap() - - const daemonUrl = getConfig().REACT_APP_THORCHAIN_NODE_URL - const maybeInboundAddressData = await getInboundAddressDataForChain( - daemonUrl, - feeAsset?.assetId, - ) - if (maybeInboundAddressData.isErr()) - throw new Error(maybeInboundAddressData.unwrapErr().message) - const inboundAddressData = maybeInboundAddressData.unwrap() - // Guaranteed to be defined for EVM chains, and approve are only for EVM chains - const router = inboundAddressData.router! - - const thorContract = getOrCreateContractByType({ - address: router, - type: ContractType.ThorRouter, - chainId: asset.chainId, - }) - - // i.e 10 Gwei for EVM chains - // This function call is super dumb, and the param we pass as `amount` isn't actually the amount we intend to withdraw - // In addition to being used as the `memo` positional param, it is also the value of ETH to be sent with the Tx to actually trigger a withdraw - const amount = THORCHAIN_SAVERS_DUST_THRESHOLDS_CRYPTO_BASE_UNIT[feeAsset.assetId] - - const data = encodeFunctionData({ - abi: thorContract.abi, - functionName: 'depositWithExpiry', - args: [ - getAddress(quote.inbound_address), - // This looks incorrect according to https://dev.thorchain.org/thorchain-dev/concepts/sending-transactions#evm-chains - // But this is how THORSwap does it, and it actually works - using the actual asset address as "asset" will result in reverts - zeroAddress, - BigInt(amount), - quote.memo, - BigInt(quote.expiry), - ], - }) - - const buildCustomTxInput = await createBuildCustomTxInput({ - accountNumber, - adapter, - data, - value: amount, - to: router, - wallet, - }) - - return buildCustomTxInput - } catch (e) { - console.error(e) - } - }, [ - contextDispatch, - opportunityData?.stakedAmountCryptoBaseUnit, - opportunityData?.rewardsCryptoBaseUnit?.amounts, - accountId, + const { executeTransaction, estimatedFeesData } = useSendThorTx({ + accountId: accountId ?? null, assetId, - feeAsset, - accountNumber, - wallet, - state?.withdraw.cryptoAmount, - chainId, - asset, - ]) - - const getCustomTxFees = useCallback(async () => { - if (!isTokenWithdraw) return - if (!wallet || !accountId) return - - const adapter = assertGetEvmChainAdapter(chainId) - const customTxInput = await getCustomTxInput() - if (!customTxInput) return undefined - - const fees = await adapter.getFeeData({ - to: customTxInput.to, - value: customTxInput.value, - chainSpecific: { - from: fromAccountId(accountId).account, - data: customTxInput.data, - }, - }) + // withdraw savers will use dust amount + amountCryptoBaseUnit: null, + action: 'withdrawSavers', + memo: quote?.memo ?? null, + fromAddress, + }) - return fees - }, [accountId, chainId, getCustomTxInput, isTokenWithdraw, wallet]) + const estimatedGasCryptoPrecision = useMemo(() => { + if (!estimatedFeesData) return + return fromBaseUnit(estimatedFeesData.txFeeCryptoBaseUnit, feeAsset.precision) + }, [estimatedFeesData, feeAsset.precision]) useEffect(() => { - ;(async () => { - if (!contextDispatch) return - const estimatedFees = await (async () => { - if (isTokenWithdraw) return getCustomTxFees() - const estimateFeeArgs = await getEstimateFeesArgs() - return estimateFees(estimateFeeArgs!) - })() - - if (!estimatedFees) return - - setNetworkFeeCryptoBaseUnit(estimatedFees.fast.txFee) + if (!estimatedFeesData || !contextDispatch) return - contextDispatch({ - type: ThorchainSaversWithdrawActionType.SET_WITHDRAW, - payload: { - networkFeeCryptoBaseUnit: estimatedFees.fast.txFee, - }, - }) - })() - }, [contextDispatch, getCustomTxFees, getEstimateFeesArgs, isTokenWithdraw]) - - const getWithdrawInput: () => Promise = useCallback(async () => { - if (!(accountId && assetId && opportunityData?.stakedAmountCryptoBaseUnit && contextDispatch)) - return - - try { - const estimateFeesArgs = await getEstimateFeesArgs() - if (!estimateFeesArgs) return - const estimatedFees = await estimateFees(estimateFeesArgs) - - contextDispatch({ - type: ThorchainSaversWithdrawActionType.SET_WITHDRAW, - payload: { - networkFeeCryptoBaseUnit: estimatedFees.fast.txFee, - }, - }) - - const amountCryptoBaseUnit = toBaseUnit(state?.withdraw.cryptoAmount, asset.precision) - const withdrawBps = getWithdrawBps({ - withdrawAmountCryptoBaseUnit: amountCryptoBaseUnit, - stakedAmountCryptoBaseUnit: opportunityData?.stakedAmountCryptoBaseUnit, - rewardsAmountCryptoBaseUnit: opportunityData?.rewardsCryptoBaseUnit?.amounts[0] ?? '0', - }) - - if (bn(withdrawBps).isZero()) return - - const maybeQuote = await getThorchainSaversWithdrawQuote({ - asset, - accountId, - bps: withdrawBps, - }) - - if (maybeQuote.isErr()) throw new Error(maybeQuote.unwrapErr()) - const quote = maybeQuote.unwrap() - - const { dust_amount } = quote - - if (isUtxoChainId(chainId) && !maybeFromUTXOAccountAddress) { - throw new Error('Account address required to withdraw from THORChain savers') - } - - const sendInput: SendInput = { - amountCryptoPrecision: fromThorBaseUnit(dust_amount).toFixed(asset.precision), - assetId, - to: quote.inbound_address, - from: maybeFromUTXOAccountAddress, - sendMax: false, - accountId, - amountFieldError: '', - estimatedFees, - feeType: FeeDataKey.Fast, - fiatAmount: '', - fiatSymbol: selectedCurrency, - vanityAddress: '', - input: quote.inbound_address, - } - - return sendInput - } catch (e) { - console.error(e) - } - }, [ - accountId, - assetId, - opportunityData?.stakedAmountCryptoBaseUnit, - opportunityData?.rewardsCryptoBaseUnit?.amounts, - getEstimateFeesArgs, - contextDispatch, - state?.withdraw.cryptoAmount, - asset, - chainId, - maybeFromUTXOAccountAddress, - selectedCurrency, - ]) - - const handleCustomTx = useCallback(async (): Promise => { - if (!wallet || accountNumber === undefined) return - const buildCustomTxInput = await getCustomTxInput() - if (!buildCustomTxInput) return - - const adapter = assertGetEvmChainAdapter(chainId) - - const txid = await buildAndBroadcast({ - adapter, - buildCustomTxInput, - receiverAddress: maybeFromUTXOAccountAddress, - }) - return txid - }, [wallet, accountNumber, getCustomTxInput, chainId, maybeFromUTXOAccountAddress]) - - const handleMultiTxSend = useCallback(async (): Promise => { - if (!wallet) return - - const withdrawInput = await getWithdrawInput() - if (!withdrawInput) throw new Error('Error building send input') - - const txId = await handleSend({ - sendInput: withdrawInput, - wallet, + contextDispatch({ + type: ThorchainSaversWithdrawActionType.SET_WITHDRAW, + payload: { + estimatedGasCryptoBaseUnit: estimatedFeesData.txFeeCryptoBaseUnit, + networkFeeCryptoBaseUnit: estimatedFeesData.txFeeCryptoBaseUnit, + }, }) - return txId - }, [getWithdrawInput, wallet]) + }, [contextDispatch, estimatedFeesData]) const { isTradingActive, refetch: refetchIsTradingActive } = useIsTradingActive({ assetId, @@ -618,8 +320,6 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { ) return - if (isUtxoChainId(chainId) && !maybeFromUTXOAccountAddress) return - contextDispatch({ type: ThorchainSaversWithdrawActionType.SET_LOADING, payload: true }) if (!state?.withdraw.cryptoAmount) return @@ -646,31 +346,16 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { throw new Error(`THORChain pool halted for assetId: ${assetId}`) } - const maybeTxId = await (async () => { - if (isTokenWithdraw) { - return handleCustomTx() - } - - const withdrawInput = await getWithdrawInput() - if (!withdrawInput) throw new Error('Error building send input') - return handleMultiTxSend() - })() - - if (!maybeTxId) { - throw new Error('Error sending THORCHain savers Txs') - } - - if (!maybeTxId) { - throw new Error('Error sending THORCHain savers Txs') - } + const _txId = await executeTransaction() + if (!_txId) throw new Error('No txId returned from onSignTx') - contextDispatch({ type: ThorchainSaversWithdrawActionType.SET_TXID, payload: maybeTxId }) + contextDispatch({ type: ThorchainSaversWithdrawActionType.SET_TXID, payload: _txId }) contextDispatch({ type: ThorchainSaversWithdrawActionType.SET_WITHDRAW, payload: { dustAmountCryptoBaseUnit, protocolFeeCryptoBaseUnit, - maybeFromUTXOAccountAddress, + maybeFromUTXOAccountAddress: fromAddress ?? '', }, }) onNext(DefiStep.Status) @@ -700,28 +385,24 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { accountId, assetId, opportunityData, + expiry, userAddress, assetReference, wallet, opportunity, chainAdapter, - chainId, - maybeFromUTXOAccountAddress, + fromAddress, state?.withdraw.cryptoAmount, state?.withdraw.fiatAmount, - expiry, isTradingActive, refetchIsTradingActive, + executeTransaction, dustAmountCryptoBaseUnit, protocolFeeCryptoBaseUnit, onNext, assets, toast, translate, - isTokenWithdraw, - getWithdrawInput, - handleMultiTxSend, - handleCustomTx, ]) const handleCancel = useCallback(() => { @@ -862,16 +543,13 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { @@ -889,16 +567,11 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { @@ -917,16 +590,13 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { diff --git a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Status.tsx b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Status.tsx index 9610f5f2919..c347f57f03f 100644 --- a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Status.tsx +++ b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Status.tsx @@ -69,16 +69,13 @@ export const Status: React.FC = ({ accountId }) => { selectMarketDataByAssetIdUserCurrency(state, assetId ?? ''), ) - const accountAddress = useMemo(() => accountId && fromAccountId(accountId).account, [accountId]) - const userAddress = useMemo( - () => state?.withdraw.maybeFromUTXOAccountAddress || accountAddress, - [accountAddress, state?.withdraw.maybeFromUTXOAccountAddress], - ) + const account = useMemo(() => accountId && fromAccountId(accountId).account, [accountId]) const serializedTxIndex = useMemo(() => { - if (!(state?.txid && userAddress && accountId)) return '' - return serializeTxIndex(accountId, state.txid, userAddress) - }, [state?.txid, userAddress, accountId]) + if (!(state?.txid && accountId && account)) return '' + return serializeTxIndex(accountId, state.txid, account) + }, [state?.txid, accountId, account]) + const confirmedTransaction = useAppSelector(gs => selectTxById(gs, serializedTxIndex)) useEffect(() => { diff --git a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx index 3a022abc0dd..c12e90c83a8 100644 --- a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx +++ b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx @@ -1,11 +1,9 @@ import { Alert, AlertIcon, Skeleton, useToast } from '@chakra-ui/react' import type { AccountId } from '@shapeshiftoss/caip' -import { fromAccountId, fromAssetId, toAssetId } from '@shapeshiftoss/caip' -import type { Asset, KnownChainIds } from '@shapeshiftoss/types' +import { toAssetId } from '@shapeshiftoss/caip' +import type { Asset } from '@shapeshiftoss/types' import { Err, Ok, type Result } from '@sniptt/monads' import { useQueryClient } from '@tanstack/react-query' -import { getOrCreateContractByType } from 'contracts/contractManager' -import { ContractType } from 'contracts/types' import type { WithdrawValues } from 'features/defi/components/Withdraw/Withdraw' import { Field, Withdraw as ReusableWithdraw } from 'features/defi/components/Withdraw/Withdraw' import type { @@ -16,27 +14,16 @@ import { DefiStep } from 'features/defi/contexts/DefiManagerProvider/DefiCommon' import { useCallback, useContext, useMemo, useState } from 'react' import { FormProvider, useForm } from 'react-hook-form' import { useTranslate } from 'react-polyglot' -import { encodeFunctionData, getAddress, zeroAddress } from 'viem' import { Amount } from 'components/Amount/Amount' import type { StepComponentProps } from 'components/DeFi/components/Steps' -import { getChainShortName } from 'components/MultiHopTrade/components/MultiHopTradeConfirm/utils/getChainShortName' import { Row } from 'components/Row/Row' import { Text } from 'components/Text' import type { TextPropTypes } from 'components/Text/Text' import { useBrowserRouter } from 'hooks/useBrowserRouter/useBrowserRouter' -import { useWallet } from 'hooks/useWallet/useWallet' import { BigNumber, bn, bnOrZero } from 'lib/bignumber/bignumber' import { fromBaseUnit, toBaseUnit } from 'lib/math' import { trackOpportunityEvent } from 'lib/mixpanel/helpers' import { MixPanelEvent } from 'lib/mixpanel/types' -import { fetchRouterContractAddress } from 'lib/swapper/swappers/ThorchainSwapper/utils/useRouterContractAddress' -import { assertGetChainAdapter, isToken } from 'lib/utils' -import { - assertGetEvmChainAdapter, - createBuildCustomTxInput, - getSupportedEvmChainIds, -} from 'lib/utils/evm' -import { fromThorBaseUnit } from 'lib/utils/thorchain' import { fetchHasEnoughBalanceForTxPlusFeesPlusSweep } from 'lib/utils/thorchain/balance' import { BASE_BPS_POINTS } from 'lib/utils/thorchain/constants' import type { GetThorchainSaversWithdrawQuoteQueryKey } from 'lib/utils/thorchain/hooks/useGetThorchainSaversWithdrawQuoteQuery' @@ -44,14 +31,12 @@ import { queryFn as getThorchainSaversWithdrawQuoteQueryFn, useGetThorchainSaversWithdrawQuoteQuery, } from 'lib/utils/thorchain/hooks/useGetThorchainSaversWithdrawQuoteQuery' +import { useSendThorTx } from 'lib/utils/thorchain/hooks/useSendThorTx' import { isUtxoChainId } from 'lib/utils/utxo' -import { useGetEstimatedFeesQuery } from 'pages/Lending/hooks/useGetEstimatedFeesQuery' import { useIsSweepNeededQuery } from 'pages/Lending/hooks/useIsSweepNeededQuery' import type { ThorchainSaversWithdrawQuoteResponseSuccess } from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/types' -import { THORCHAIN_SAVERS_DUST_THRESHOLDS_CRYPTO_BASE_UNIT } from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/utils' import { serializeUserStakingId, toOpportunityId } from 'state/slices/opportunitiesSlice/utils' import { - selectAccountNumberByAccountId, selectAssetById, selectAssets, selectEarnUserStakingOpportunityByUserStakingId, @@ -81,30 +66,16 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe const { state, dispatch } = useContext(WithdrawContext) const translate = useTranslate() const toast = useToast() + const queryClient = useQueryClient() const { query, history: browserHistory } = useBrowserRouter() const { chainId, assetNamespace, assetReference } = query - const queryClient = useQueryClient() const methods = useForm({ mode: 'onChange' }) const { getValues, setValue } = methods - // Asset info - const assets = useAppSelector(selectAssets) - const assetId = toAssetId({ - chainId, - assetNamespace, - assetReference, - }) - - const accountNumberFilter = useMemo(() => ({ accountId }), [accountId]) - const accountNumber = useAppSelector(state => - selectAccountNumberByAccountId(state, accountNumberFilter), - ) + const assetId = toAssetId({ chainId, assetNamespace, assetReference }) - const { - state: { wallet }, - } = useWallet() const feeAsset = useAppSelector(state => selectFeeAssetById(state, assetId)) const opportunityId = useMemo( () => (assetId ? toOpportunityId({ chainId, assetNamespace, assetReference }) : undefined), @@ -137,10 +108,6 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe if (!asset) throw new Error(`Asset not found for AssetId ${assetId}`) if (!feeAsset) throw new Error(`Fee Asset not found for AssetId ${assetId}`) - const isTokenWithdraw = isToken(fromAssetId(assetId).assetReference) - - const userAddress: string | undefined = accountId && fromAccountId(accountId).account - // user info const amountAvailableCryptoPrecision = useMemo(() => { return bnOrZero(opportunityData?.stakedAmountCryptoBaseUnit) @@ -160,201 +127,17 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe const assetMarketData = useAppSelector(state => selectMarketDataByAssetIdUserCurrency(state, assetId), ) - const fiatAmountAvailable = useMemo( - () => bnOrZero(amountAvailableCryptoPrecision).times(assetMarketData.price), - [amountAvailableCryptoPrecision, assetMarketData.price], - ) - - const getOutboundFeeCryptoBaseUnit = useCallback( - async ( - _quote?: Result, - ): Promise | null> => { - if (!accountId) return null - - const maybeQuote = await (async () => { - if ( - _quote && - _quote.isOk() && - // Too small of quotes may not be able to be withdrawn, hence will not include any fees.outbound - bnOrZero(_quote.unwrap().expected_amount_out).gt(0) && - _quote.unwrap().fees.outbound - ) - return _quote - - // Attempt getting a quote with 100000 bps, i.e 100% withdraw - // - If this succeeds, this allows us to know the oubtound fee, which is always the same regarding of the withdraw bps - // and will allow us to gracefully handle amounts that are lower than the outbound fee - // - If this fails, we know that the withdraw amount is too low anyway, regarding of how many bps are withdrawn - setQuoteLoading(true) - const thorchainSaversWithdrawQuoteQueryKey: GetThorchainSaversWithdrawQuoteQueryKey = [ - 'thorchainSaversWithdrawQuote', - { asset, accountId, withdrawBps: '10000' }, - ] - - const _thorchainSaversWithdrawQuote = await queryClient - .fetchQuery({ - queryKey: thorchainSaversWithdrawQuoteQueryKey, - queryFn: getThorchainSaversWithdrawQuoteQueryFn, - staleTime: 5000, - }) - .then(res => Ok(res)) - .catch((err: Error) => Err(err.message)) - setQuoteLoading(false) - return _thorchainSaversWithdrawQuote - })() - - // Neither the passed quote, nor the safer 10,000 bps quote succeeded - // Meaning the amount being withdraw *is* too small - if (maybeQuote.isErr()) { - console.error(maybeQuote.unwrapErr()) - return Err(translate('trade.errors.amountTooSmallUnknownMinimum')) - } - - const quote = maybeQuote.unwrap() - - const outboundFee = bnOrZero( - toBaseUnit(fromThorBaseUnit(quote.fees.outbound), asset.precision), - ) - const safeOutboundFee = bn(outboundFee).times(105).div(100).toFixed(0) - // Add 5% as as a safety factor since the dust threshold fee is not necessarily going to cut it - return Ok(safeOutboundFee) - }, - [accountId, asset, queryClient, translate], + const feeAssetMarketData = useAppSelector(state => + selectMarketDataByAssetIdUserCurrency(state, feeAsset?.assetId ?? ''), ) - const supportedEvmChainIds = useMemo(() => getSupportedEvmChainIds(), []) - - // TODO(gomes): use useGetEstimatedFeesQuery instead of this. - // The logic of useGetEstimatedFeesQuery and its consumption will need some touching up to work with custom Txs - // since the guts of it are made to accomodate Tx/fees/sweep fees deduction and there are !isUtxoChainId checks in place currently - // The method below is now only used for non-UTXO chains - const getWithdrawGasEstimateCryptoBaseUnit = useCallback( - async ( - maybeQuote: Result, - dustAmountCryptoBaseUnit: string, - ): Promise | null> => { - if (!(userAddress && accountId && wallet && accountNumber !== undefined)) return null - const inputValues = getValues() - try { - const maybeOutboundFeeCryptoBaseUnit = await getOutboundFeeCryptoBaseUnit(maybeQuote) - if (!maybeOutboundFeeCryptoBaseUnit) return null - const amountCryptoBaseUnit = toBaseUnit(inputValues.cryptoAmount, asset.precision) - - // re-returning the outbound fee error, which should take precedence over the withdraw gas estimation one - if (maybeOutboundFeeCryptoBaseUnit.isErr()) return maybeOutboundFeeCryptoBaseUnit - - if (isTokenWithdraw) { - const saversRouterContractAddress = await queryClient.fetchQuery({ - queryKey: ['routerContractAddress', feeAsset.assetId, false], - queryFn: () => fetchRouterContractAddress(assetId, false), - staleTime: 120_000, // 2mn arbitrary staleTime to avoid refetching for the same args (assetId, excludeHalted) - }) - - if (!saversRouterContractAddress) - return Err(`No router contract address found for feeAsset: ${feeAsset.assetId}`) - - const adapter = assertGetEvmChainAdapter(chainId) - const thorContract = getOrCreateContractByType({ - address: saversRouterContractAddress, - type: ContractType.ThorRouter, - chainId: asset.chainId, - }) - - if (maybeQuote.isErr()) - return Err( - translate('trade.errors.amountTooSmallUnknownMinimum', { - assetSymbol: feeAsset.symbol, - }), - ) - const quote = maybeQuote.unwrap() - - const data = encodeFunctionData({ - abi: thorContract.abi, - functionName: 'depositWithExpiry', - args: [ - getAddress(quote.inbound_address), - // This looks incorrect according to https://dev.thorchain.org/thorchain-dev/concepts/sending-transactions#evm-chains - // But this is how THORSwap does it, and it actually works - using the actual asset address as "asset" will result in reverts - zeroAddress, - BigInt(amountCryptoBaseUnit), - quote.memo, - BigInt(quote.expiry), - ], - }) - - const amount = THORCHAIN_SAVERS_DUST_THRESHOLDS_CRYPTO_BASE_UNIT[feeAsset.assetId] - - const customTxInput = await createBuildCustomTxInput({ - accountNumber, - adapter, - data, - value: amount, - to: saversRouterContractAddress, - wallet, - }) - - const fees = await adapter.getFeeData({ - to: customTxInput.to, - value: customTxInput.value, - chainSpecific: { - from: fromAccountId(accountId).account, - data: customTxInput.data, - }, - }) - - const fastFeeCryptoBaseUnit = fees.fast.txFee + const assetPriceInFeeAsset = useMemo(() => { + return bn(assetMarketData.price).div(feeAssetMarketData.price) + }, [assetMarketData.price, feeAssetMarketData.price]) - return Ok(bnOrZero(fastFeeCryptoBaseUnit).toString()) - } - - // Quote errors aren't necessarily user-friendly, we don't want to return them - if (maybeQuote.isErr()) throw new Error(maybeQuote.unwrapErr()) - const quote = maybeQuote.unwrap() - const adapter = assertGetChainAdapter(chainId) - - const getFeeDataInput = { - to: quote.inbound_address, - value: dustAmountCryptoBaseUnit, - // EVM chains are the only ones explicitly requiring a `from` param for the gas estimation to work - // UTXOs simply call /api/v1/fees (common for all accounts), and Cosmos assets fees are hardcoded - chainSpecific: { - pubkey: userAddress, - from: supportedEvmChainIds.includes(chainId as KnownChainIds) ? userAddress : '', - }, - sendMax: false, - } - const fastFeeCryptoBaseUnit = (await adapter.getFeeData(getFeeDataInput)).fast.txFee - return Ok(bnOrZero(fastFeeCryptoBaseUnit).toString()) - } catch (error) { - console.error(error) - // Assume insufficient amount for gas if we've thrown on the try block above - return Err( - translate('common.insufficientAmountForGas', { - assetSymbol: feeAsset.symbol, - chainSymbol: getChainShortName(feeAsset.chainId as KnownChainIds), - }), - ) - } - }, - [ - userAddress, - accountId, - wallet, - accountNumber, - getValues, - getOutboundFeeCryptoBaseUnit, - asset.precision, - asset.chainId, - isTokenWithdraw, - chainId, - supportedEvmChainIds, - queryClient, - feeAsset.assetId, - feeAsset.symbol, - feeAsset.chainId, - translate, - assetId, - ], + const fiatAmountAvailable = useMemo( + () => bnOrZero(amountAvailableCryptoPrecision).times(assetMarketData.price), + [amountAvailableCryptoPrecision, assetMarketData.price], ) // TODO(gomes): this will work for UTXO but is invalid for tokens since they use diff. denoms @@ -384,35 +167,26 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe [], ) - const { - data: thorchainSaversWithdrawQuote, - isLoading: isThorchainSaversWithdrawQuoteLoading, - isSuccess: isThorchainSaversWithdrawQuoteSuccess, - } = useGetThorchainSaversWithdrawQuoteQuery({ - asset, - accountId: accountId ?? '', - amountCryptoBaseUnit: toBaseUnit(getValues()?.cryptoAmount, asset.precision), - }) + const { data: thorchainSaversWithdrawQuote, isLoading: isThorchainSaversWithdrawQuoteLoading } = + useGetThorchainSaversWithdrawQuoteQuery({ + asset, + accountId: accountId ?? '', + amountCryptoBaseUnit: toBaseUnit(getValues()?.cryptoAmount, asset.precision), + }) - const dustAmountCryptoBaseUnit = useMemo( - () => - thorchainSaversWithdrawQuote - ? toBaseUnit(fromThorBaseUnit(thorchainSaversWithdrawQuote.dust_amount), feeAsset.precision) - : '0', - [feeAsset.precision, thorchainSaversWithdrawQuote], - ) const { - data: estimatedFeesData, - isLoading: isEstimatedFeesDataLoading, - isSuccess: isEstimatedFeesDataSuccess, - } = useGetEstimatedFeesQuery({ - amountCryptoPrecision: dustAmountCryptoBaseUnit, + estimatedFeesData, + isEstimatedFeesDataLoading, + dustAmountCryptoBaseUnit, + outboundFeeCryptoBaseUnit, + } = useSendThorTx({ assetId, - to: thorchainSaversWithdrawQuote?.inbound_address ?? '', - sendMax: false, accountId: accountId ?? '', - contractAddress: undefined, - enabled: Boolean(thorchainSaversWithdrawQuote && accountId && isUtxoChainId(asset.chainId)), + // withdraw savers will use dust amount + amountCryptoBaseUnit: null, + memo: thorchainSaversWithdrawQuote?.memo ?? null, + fromAddress, + action: 'withdrawSavers', }) const isSweepNeededArgs = useMemo( @@ -423,13 +197,12 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe txFeeCryptoBaseUnit: estimatedFeesData?.txFeeCryptoBaseUnit ?? '0', // Don't fetch sweep needed if there isn't enough balance for the dust amount + fees, since adding in a sweep Tx would obviously fail too enabled: Boolean( - isEstimatedFeesDataSuccess && - isThorchainSaversWithdrawQuoteSuccess && + estimatedFeesData && getHasEnoughBalanceForTxPlusFees({ precision: asset.precision, balanceCryptoBaseUnit, amountCryptoPrecision: fromBaseUnit(dustAmountCryptoBaseUnit, feeAsset.precision), - txFeeCryptoBaseUnit: estimatedFeesData?.txFeeCryptoBaseUnit ?? '', + txFeeCryptoBaseUnit: estimatedFeesData.txFeeCryptoBaseUnit, }), ), }), @@ -438,12 +211,10 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe assetId, balanceCryptoBaseUnit, dustAmountCryptoBaseUnit, - estimatedFeesData?.txFeeCryptoBaseUnit, + estimatedFeesData, feeAsset.precision, fromAddress, getHasEnoughBalanceForTxPlusFees, - isEstimatedFeesDataSuccess, - isThorchainSaversWithdrawQuoteSuccess, ], ) @@ -451,53 +222,20 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe useIsSweepNeededQuery(isSweepNeededArgs) const handleContinue = useCallback( - async (formValues: WithdrawValues) => { - if (!(userAddress && opportunityData && accountId && dispatch)) return - - const inputValues = getValues() + (formValues: WithdrawValues) => { + if (!dispatch || !estimatedFeesData || !opportunityData) return // set withdraw state for future use dispatch({ type: ThorchainSaversWithdrawActionType.SET_WITHDRAW, payload: formValues }) dispatch({ type: ThorchainSaversWithdrawActionType.SET_LOADING, payload: true }) - try { - const estimatedGasCryptoBaseUnit = await (async () => { - if (isUtxoChainId(chainId)) return estimatedFeesData?.txFeeCryptoBaseUnit - - const { cryptoAmount } = inputValues - const amountCryptoBaseUnit = toBaseUnit(cryptoAmount, asset.precision) - setQuoteLoading(true) - const thorchainSaversWithdrawQuoteQueryKey: GetThorchainSaversWithdrawQuoteQueryKey = [ - 'thorchainSaversWithdrawQuote', - { asset, accountId, amountCryptoBaseUnit }, - ] - - const quote = await queryClient.fetchQuery({ - queryKey: thorchainSaversWithdrawQuoteQueryKey, - queryFn: getThorchainSaversWithdrawQuoteQueryFn, - staleTime: 5000, - }) - setQuoteLoading(false) - - const { dust_amount } = quote - const _dustAmountCryptoBaseUnit = toBaseUnit( - fromThorBaseUnit(dust_amount), - asset.precision, - ) - - const maybeWithdrawGasEstimateCryptoBaseUnit = await getWithdrawGasEstimateCryptoBaseUnit( - Ok(quote), - _dustAmountCryptoBaseUnit, - ) - if (!maybeWithdrawGasEstimateCryptoBaseUnit) return - if (maybeWithdrawGasEstimateCryptoBaseUnit.isErr()) return - return maybeWithdrawGasEstimateCryptoBaseUnit.unwrap() - })() - - if (!estimatedGasCryptoBaseUnit) return + try { dispatch({ type: ThorchainSaversWithdrawActionType.SET_WITHDRAW, - payload: { estimatedGasCryptoBaseUnit }, + payload: { + estimatedGasCryptoBaseUnit: estimatedFeesData.txFeeCryptoBaseUnit, + networkFeeCryptoBaseUnit: estimatedFeesData.txFeeCryptoBaseUnit, + }, }) onNext(isSweepNeeded ? DefiStep.Sweep : DefiStep.Confirm) @@ -524,20 +262,13 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe } }, [ - userAddress, opportunityData, - accountId, dispatch, - getValues, onNext, isSweepNeeded, assetId, assets, - chainId, - estimatedFeesData?.txFeeCryptoBaseUnit, - asset, - queryClient, - getWithdrawGasEstimateCryptoBaseUnit, + estimatedFeesData, toast, translate, ], @@ -558,11 +289,31 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe [amountAvailableCryptoPrecision, assetMarketData, setValue], ) + const outboundFeeInAssetCryptoBaseUnit = useMemo(() => { + if (!asset) return bn(0) + if (!feeAsset) return bn(0) + if (!outboundFeeCryptoBaseUnit) return bn(0) + + const outboundFeeCryptoPrecision = fromBaseUnit(outboundFeeCryptoBaseUnit, feeAsset.precision) + const outboundFeeInAssetCryptoPrecision = bn(outboundFeeCryptoPrecision).div( + assetPriceInFeeAsset, + ) + + return toBaseUnit(outboundFeeInAssetCryptoPrecision, asset.precision) + }, [outboundFeeCryptoBaseUnit, assetPriceInFeeAsset, asset, feeAsset]) + + const safeOutboundFeeInAssetCryptoBaseUnit = useMemo(() => { + if (!outboundFeeInAssetCryptoBaseUnit) return + // Add 5% as as a safety factor since the dust threshold fee is not necessarily going to cut it + return bnOrZero(outboundFeeInAssetCryptoBaseUnit).times(1.05).toFixed() + }, [outboundFeeInAssetCryptoBaseUnit]) + const validateCryptoAmount = useCallback( async (value: string) => { - if (!opportunityData) return false - if (!accountId) return false if (!dispatch) return false + if (!accountId) return false + if (!opportunityData) return false + if (!safeOutboundFeeInAssetCryptoBaseUnit) return false try { const withdrawAmountCryptoPrecision = bnOrZero(value) @@ -588,11 +339,9 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe .then(res => Ok(res)) .catch((err: Error) => Err(err.message)) - const maybeOutboundFeeCryptoBaseUnit = await getOutboundFeeCryptoBaseUnit(maybeQuote) const quote = maybeQuote.unwrap() const { fees: { slippage_bps }, - dust_amount, } = quote const percentage = bnOrZero(slippage_bps).div(BASE_BPS_POINTS).times(100) @@ -602,26 +351,6 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe .div(100) setSlippageCryptoAmountPrecision(cryptoSlippageAmountPrecision.toString()) - const _dustAmountCryptoBaseUnit = toBaseUnit(fromThorBaseUnit(dust_amount), asset.precision) - - const maybeWithdrawGasEstimateCryptoBaseUnit = await getWithdrawGasEstimateCryptoBaseUnit( - maybeQuote, - _dustAmountCryptoBaseUnit, - ) - - if (!maybeOutboundFeeCryptoBaseUnit) return false - if (!maybeWithdrawGasEstimateCryptoBaseUnit) return false - - if (maybeWithdrawGasEstimateCryptoBaseUnit?.isErr()) { - return maybeWithdrawGasEstimateCryptoBaseUnit.unwrapErr() - } - - if (maybeOutboundFeeCryptoBaseUnit.isErr()) { - return maybeOutboundFeeCryptoBaseUnit.unwrapErr() - } - - const outboundFeeCryptoBaseUnit = maybeOutboundFeeCryptoBaseUnit.unwrap() - const balanceCryptoPrecision = bnOrZero(amountAvailableCryptoPrecision.toPrecision()) const hasValidBalance = await (async () => { @@ -651,13 +380,15 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe hasEnoughBalanceForTxPlusFeesPlusSweep ) })() + const isBelowWithdrawThreshold = bn(withdrawAmountCryptoBaseUnit) - .minus(outboundFeeCryptoBaseUnit) + .minus(safeOutboundFeeInAssetCryptoBaseUnit) .lt(0) if (isBelowWithdrawThreshold) { - const minLimitCryptoPrecision = bn(outboundFeeCryptoBaseUnit).div( - bn(10).pow(asset.precision), + const minLimitCryptoPrecision = fromBaseUnit( + safeOutboundFeeInAssetCryptoBaseUnit, + asset.precision, ) const minLimit = `${minLimitCryptoPrecision} ${asset.symbol}` return translate('trade.errors.amountTooSmall', { @@ -675,16 +406,15 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe } }, [ - opportunityData, accountId, - dispatch, - asset, - queryClient, - getOutboundFeeCryptoBaseUnit, - getWithdrawGasEstimateCryptoBaseUnit, amountAvailableCryptoPrecision, + asset, chainId, + dispatch, fromAddress, + opportunityData, + queryClient, + safeOutboundFeeInAssetCryptoBaseUnit, translate, ], ) diff --git a/src/lib/utils/thorchain/balance.ts b/src/lib/utils/thorchain/balance.ts index 50bd6a20c6f..c5670d330f6 100644 --- a/src/lib/utils/thorchain/balance.ts +++ b/src/lib/utils/thorchain/balance.ts @@ -1,15 +1,15 @@ import type { AccountId } from '@shapeshiftoss/caip' import type { Asset } from '@shapeshiftoss/types' -import type { EstimatedFeesQueryKey } from 'react-queries/hooks/useQuoteEstimatedFeesQuery' import { queryClient } from 'context/QueryClientProvider/queryClient' import { bnOrZero } from 'lib/bignumber/bignumber' import { fromBaseUnit, toBaseUnit } from 'lib/math' +import type { EstimatedFeesQueryKey } from 'pages/Lending/hooks/useGetEstimatedFeesQuery' import { queryFn as getEstimatedFeesQueryFn } from 'pages/Lending/hooks/useGetEstimatedFeesQuery' import type { IsSweepNeededQueryKey } from 'pages/Lending/hooks/useIsSweepNeededQuery' import { queryFn as isSweepNeededQueryFn } from 'pages/Lending/hooks/useIsSweepNeededQuery' import { selectPortfolioCryptoBalanceBaseUnitByFilter } from 'state/slices/common-selectors' import type { ThorchainSaversWithdrawQuoteResponseSuccess } from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/types' -import { selectMarketDataByAssetIdUserCurrency } from 'state/slices/selectors' +import { selectFeeAssetById, selectMarketDataByAssetIdUserCurrency } from 'state/slices/selectors' import { store } from 'state/store' import { isUtxoChainId } from '../utxo' @@ -95,7 +95,11 @@ export const fetchHasEnoughBalanceForTxPlusFeesPlusSweep = async ({ assetId: asset.assetId, accountId, }) - const assetMarketData = selectMarketDataByAssetIdUserCurrency(store.getState(), asset.assetId) + const feeAsset = selectFeeAssetById(store.getState(), asset.assetId) + const feeAssetMarketData = selectMarketDataByAssetIdUserCurrency( + store.getState(), + feeAsset?.assetId ?? '', + ) const quote = await (async () => { switch (type) { case 'withdraw': { @@ -141,14 +145,15 @@ export const fetchHasEnoughBalanceForTxPlusFeesPlusSweep = async ({ estimateFeesInput: { amountCryptoPrecision, assetId: asset.assetId, + feeAssetId: feeAsset?.assetId ?? '', to: quote?.inbound_address ?? '', sendMax: false, accountId: accountId ?? '', contractAddress: undefined, }, - asset, - assetMarketData, - enabled: estimateFeesQueryEnabled, + feeAsset, + feeAssetMarketData, + enabled: Boolean(feeAsset && estimateFeesQueryEnabled), } const estimatedFeesQueryKey: EstimatedFeesQueryKey = ['estimateFees', estimatedFeesQueryArgs] @@ -190,11 +195,12 @@ export const fetchHasEnoughBalanceForTxPlusFeesPlusSweep = async ({ const isEstimateSweepFeesQueryEnabled = Boolean(_isSweepNeeded && accountId && isUtxoChain) const estimatedSweepFeesQueryArgs = { - asset, - assetMarketData, + feeAsset, + feeAssetMarketData, estimateFeesInput: { amountCryptoPrecision: '0', assetId: asset.assetId, + feeAssetId: feeAsset?.assetId ?? '', to: fromAddress ?? '', sendMax: true, accountId: accountId ?? '', diff --git a/src/lib/utils/thorchain/hooks/useSendThorTx.tsx b/src/lib/utils/thorchain/hooks/useSendThorTx.tsx new file mode 100644 index 00000000000..2e0a9143bdc --- /dev/null +++ b/src/lib/utils/thorchain/hooks/useSendThorTx.tsx @@ -0,0 +1,432 @@ +import { ExternalLinkIcon } from '@chakra-ui/icons' +import { Link, Text, useToast } from '@chakra-ui/react' +import type { AccountId, AssetId } from '@shapeshiftoss/caip' +import { fromAccountId, fromAssetId, thorchainAssetId } from '@shapeshiftoss/caip' +import type { FeeDataEstimate } from '@shapeshiftoss/chain-adapters' +import { CONTRACT_INTERACTION, FeeDataKey } from '@shapeshiftoss/chain-adapters' +import { SwapperName } from '@shapeshiftoss/swapper' +import type { KnownChainIds } from '@shapeshiftoss/types' +import { useQuery } from '@tanstack/react-query' +import dayjs from 'dayjs' +import { useCallback, useMemo, useState } from 'react' +import { useTranslate } from 'react-polyglot' +import { reactQueries } from 'react-queries' +import { selectInboundAddressData } from 'react-queries/selectors' +import { getAddress, zeroAddress } from 'viem' +import type { SendInput } from 'components/Modals/Send/Form' +import { estimateFees, handleSend } from 'components/Modals/Send/utils' +import { useWallet } from 'hooks/useWallet/useWallet' +import { bn, bnOrZero } from 'lib/bignumber/bignumber' +import { getTxLink } from 'lib/getTxLink' +import { fromBaseUnit, toBaseUnit } from 'lib/math' +import { assertUnreachable, isToken } from 'lib/utils' +import { assertGetThorchainChainAdapter } from 'lib/utils/cosmosSdk' +import { + assertGetEvmChainAdapter, + buildAndBroadcast, + createBuildCustomTxInput, +} from 'lib/utils/evm' +import { THORCHAIN_POOL_MODULE_ADDRESS } from 'lib/utils/thorchain/constants' +import { depositWithExpiry } from 'lib/utils/thorchain/routerCalldata' +import { useGetEstimatedFeesQuery } from 'pages/Lending/hooks/useGetEstimatedFeesQuery' +import { THORCHAIN_SAVERS_DUST_THRESHOLDS_CRYPTO_BASE_UNIT } from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/utils' +import { + selectAccountNumberByAccountId, + selectAssetById, + selectFeeAssetByChainId, + selectSelectedCurrency, +} from 'state/slices/selectors' +import { serializeTxIndex } from 'state/slices/txHistorySlice/utils' +import { useAppSelector } from 'state/store' + +import { fromThorBaseUnit, getThorchainTransactionType } from '..' + +type Action = + | 'swap' + | 'addLiquidity' + | 'withdrawLiquidity' + | 'openLoan' + | 'repayLoan' + | 'depositSavers' + | 'withdrawSavers' + +type UseSendThorTxProps = { + accountId: AccountId | null + action: Action + amountCryptoBaseUnit: string | null + assetId: AssetId | undefined + enableEstimateFees?: boolean + disableEstimateFeesRefetch?: boolean + fromAddress: string | null + memo: string | null +} + +export const useSendThorTx = ({ + accountId, + action, + amountCryptoBaseUnit, + assetId, + enableEstimateFees = true, + disableEstimateFeesRefetch, + fromAddress, + memo, +}: UseSendThorTxProps) => { + const [txId, setTxId] = useState(null) + const [serializedTxIndex, setSerializedTxIndex] = useState(null) + + const wallet = useWallet().state.wallet + const toast = useToast() + const translate = useTranslate() + + const selectedCurrency = useAppSelector(selectSelectedCurrency) + const asset = useAppSelector(state => selectAssetById(state, assetId ?? '')) + const feeAsset = useAppSelector(state => + selectFeeAssetByChainId(state, assetId ? fromAssetId(assetId).chainId : ''), + ) + + const accountNumberFilter = useMemo(() => { + return { assetId, accountId: accountId ?? '' } + }, [accountId, assetId]) + const accountNumber = useAppSelector(s => selectAccountNumberByAccountId(s, accountNumberFilter)) + + const shouldUseDustAmount = useMemo(() => { + return ['withdrawLiquidity', 'withdrawSavers'].includes(action) + }, [action]) + + const dustAmountCryptoBaseUnit = useMemo(() => { + return THORCHAIN_SAVERS_DUST_THRESHOLDS_CRYPTO_BASE_UNIT[feeAsset?.assetId ?? ''] ?? '0' + }, [feeAsset]) + + const amountOrDustCryptoBaseUnit = useMemo(() => { + return shouldUseDustAmount ? dustAmountCryptoBaseUnit : bnOrZero(amountCryptoBaseUnit).toFixed() + }, [shouldUseDustAmount, dustAmountCryptoBaseUnit, amountCryptoBaseUnit]) + + const transactionType = useMemo(() => { + return asset ? getThorchainTransactionType(asset.chainId) : undefined + }, [asset]) + + const { data: inboundAddressData } = useQuery({ + ...reactQueries.thornode.inboundAddresses(), + staleTime: 60_000, + select: data => selectInboundAddressData(data, assetId), + enabled: Boolean(assetId && assetId !== thorchainAssetId), + }) + + const inboundAddress = useMemo(() => { + if (!transactionType) return + + switch (transactionType) { + case 'MsgDeposit': + return THORCHAIN_POOL_MODULE_ADDRESS + case 'EvmCustomTx': + return inboundAddressData?.router + case 'Send': + return inboundAddressData?.address + default: + assertUnreachable(transactionType) + } + }, [inboundAddressData, transactionType]) + + const outboundFeeCryptoBaseUnit = useMemo(() => { + if (!feeAsset || !inboundAddressData) return + return toBaseUnit(fromThorBaseUnit(inboundAddressData.outbound_fee), feeAsset.precision) + }, [feeAsset, inboundAddressData]) + + const depositWithExpiryInputData = useMemo(() => { + if (!memo) return + if (!assetId) return + if (!inboundAddressData?.address) return + if (transactionType !== 'EvmCustomTx') return + + /** + * asset address should be [zero address](https://dev.thorchain.org/concepts/sending-transactions.html#admonition-info-1) + * for native asset (including dust) sends, otherwise token address for token sends + * + * _example of a failed tx using the token address instead of zero address when sending dust amount for a withdraw liquidity transactions: + * https://www.tdly.co/shared/simulation/6d23d42a-8dd6-4e3e-88a8-62da779a765d_ + */ + const assetAddress = + !isToken(fromAssetId(assetId).assetReference) || shouldUseDustAmount + ? zeroAddress + : getAddress(fromAssetId(assetId).assetReference) + + return depositWithExpiry({ + vault: getAddress(inboundAddressData.address), + asset: assetAddress, + amount: amountOrDustCryptoBaseUnit, + memo, + expiry: BigInt(dayjs().add(15, 'minute').unix()), + }) + }, [ + amountOrDustCryptoBaseUnit, + assetId, + inboundAddressData, + memo, + shouldUseDustAmount, + transactionType, + ]) + + const estimateFeesArgs = useMemo(() => { + if (!accountId || !asset || !assetId || !feeAsset || !memo || !transactionType || !wallet) + return + + const { account } = fromAccountId(accountId) + + switch (transactionType) { + case 'MsgDeposit': { + return { + amountCryptoPrecision: fromBaseUnit(amountOrDustCryptoBaseUnit, asset.precision), + assetId: asset.assetId, + feeAssetId: feeAsset.assetId, + memo, + to: THORCHAIN_POOL_MODULE_ADDRESS, + sendMax: false, + accountId, + contractAddress: undefined, + } + } + case 'EvmCustomTx': { + if (!inboundAddressData?.router) return + if (!depositWithExpiryInputData) return + + return { + amountCryptoPrecision: + !isToken(fromAssetId(assetId).assetReference) || shouldUseDustAmount + ? fromBaseUnit(amountOrDustCryptoBaseUnit, feeAsset.precision) + : '0', + assetId: shouldUseDustAmount ? feeAsset.assetId : asset.assetId, + feeAssetId: feeAsset.assetId, + to: inboundAddressData.router, + from: account, + sendMax: false, + memo: depositWithExpiryInputData, + accountId, + // contractAddress is only used for erc20 sends to construct the transfer method input data + // we are already providing the necessary input data to perform the depositWithExpiry transaction + contractAddress: undefined, + } + } + case 'Send': { + if (fromAddress === null) return + if (!inboundAddressData?.address) return + + return { + amountCryptoPrecision: fromBaseUnit(amountOrDustCryptoBaseUnit, asset.precision), + assetId, + feeAssetId: feeAsset.assetId, + to: inboundAddressData.address, + from: fromAddress, + sendMax: false, + memo, + accountId, + contractAddress: undefined, + } + } + default: + assertUnreachable(transactionType) + } + }, [ + accountId, + amountOrDustCryptoBaseUnit, + asset, + assetId, + depositWithExpiryInputData, + feeAsset, + fromAddress, + inboundAddressData, + memo, + shouldUseDustAmount, + transactionType, + wallet, + ]) + + const { + data: estimatedFeesData, + isLoading: isEstimatedFeesDataLoading, + isError: isEstimatedFeesDataError, + } = useGetEstimatedFeesQuery({ + amountCryptoPrecision: estimateFeesArgs?.amountCryptoPrecision ?? '0', + assetId: estimateFeesArgs?.assetId ?? '', + feeAssetId: estimateFeesArgs?.feeAssetId ?? '', + to: estimateFeesArgs?.to ?? '', + sendMax: estimateFeesArgs?.sendMax ?? false, + memo: estimateFeesArgs?.memo ?? '', + accountId: estimateFeesArgs?.accountId ?? '', + contractAddress: estimateFeesArgs?.contractAddress ?? '', + enabled: Boolean(estimateFeesArgs && enableEstimateFees), + disableRefetch: Boolean(txId || disableEstimateFeesRefetch), + }) + + const executeTransaction = useCallback(async () => { + if (!memo) return + if (!asset) return + if (!wallet) return + if (!accountId) return + if (!transactionType) return + if (!estimateFeesArgs) return + if (accountNumber === undefined) return + if (isToken(fromAssetId(asset.assetId).assetReference) && !inboundAddressData) return + + if (!shouldUseDustAmount && !bn(amountOrDustCryptoBaseUnit).gt(0)) + throw new Error('invalid amount specified') + + const { account } = fromAccountId(accountId) + + const { _txId, _serializedTxIndex } = await (async () => { + switch (transactionType) { + case 'MsgDeposit': { + const adapter = assertGetThorchainChainAdapter() + + const estimatedFees = await estimateFees(estimateFeesArgs) + const { fast } = estimatedFees as FeeDataEstimate + + const { txToSign } = await adapter.buildDepositTransaction({ + from: account, + accountNumber, + value: amountOrDustCryptoBaseUnit, + memo, + chainSpecific: { + gas: fast.chainSpecific.gasLimit, + fee: fast.txFee, + }, + }) + + const signedTx = await adapter.signTransaction({ txToSign, wallet }) + + const _txId = await adapter.broadcastTransaction({ + senderAddress: account, + receiverAddress: THORCHAIN_POOL_MODULE_ADDRESS, + hex: signedTx, + }) + + return { + _txId, + _serializedTxIndex: serializeTxIndex(accountId, _txId, account, { + memo, + parser: 'thorchain', + }), + } + } + case 'EvmCustomTx': { + if (!inboundAddressData?.address) throw new Error('No vault address found') + if (!inboundAddressData?.router) throw new Error('No router address found') + if (!depositWithExpiryInputData) throw new Error('No depositWithExpiry input data found') + + const adapter = assertGetEvmChainAdapter(asset.chainId) + + const buildCustomTxInput = await createBuildCustomTxInput({ + accountNumber, + adapter, + data: depositWithExpiryInputData, + value: + !isToken(fromAssetId(asset.assetId).assetReference) || shouldUseDustAmount + ? amountOrDustCryptoBaseUnit + : '0', + to: inboundAddressData.router, + wallet, + }) + + const _txId = await buildAndBroadcast({ + adapter, + buildCustomTxInput, + receiverAddress: CONTRACT_INTERACTION, // no receiver for this contract call + }) + + return { + _txId, + _serializedTxIndex: serializeTxIndex(accountId, _txId, account), + } + } + case 'Send': { + if (fromAddress === null) throw new Error('No account address found') + if (!inboundAddressData?.address) throw new Error('No vault address found') + + const estimatedFees = await estimateFees(estimateFeesArgs) + + const sendInput: SendInput = { + amountCryptoPrecision: fromBaseUnit(amountOrDustCryptoBaseUnit, asset.precision), + assetId: asset.assetId, + to: inboundAddressData?.address, + from: fromAddress, + sendMax: false, + accountId, + memo, + amountFieldError: '', + estimatedFees, + feeType: FeeDataKey.Fast, + fiatAmount: '', + fiatSymbol: selectedCurrency, + vanityAddress: '', + input: '', + } + + const _txId = await handleSend({ + sendInput, + wallet, + }) + + return { + _txId, + _serializedTxIndex: serializeTxIndex(accountId, _txId, account), + } + } + default: + assertUnreachable(transactionType) + } + })() + + const _txIdLink = getTxLink({ + defaultExplorerBaseUrl: 'https://viewblock.io/thorchain/tx/', + txId: _txId ?? '', + name: SwapperName.Thorchain, + }) + + toast({ + title: translate('modals.send.transactionSent'), + description: _txId ? ( + + + {translate('modals.status.viewExplorer')} + + + ) : undefined, + status: 'success', + duration: 9000, + isClosable: true, + position: 'top-right', + }) + + setTxId(_txId) + setSerializedTxIndex(_serializedTxIndex) + + return _txId + }, [ + accountId, + accountNumber, + amountOrDustCryptoBaseUnit, + asset, + depositWithExpiryInputData, + estimateFeesArgs, + fromAddress, + inboundAddressData, + memo, + selectedCurrency, + shouldUseDustAmount, + toast, + transactionType, + translate, + wallet, + ]) + + return { + executeTransaction, + estimatedFeesData, + isEstimatedFeesDataLoading, + isEstimatedFeesDataError, + txId, + serializedTxIndex, + dustAmountCryptoBaseUnit, + outboundFeeCryptoBaseUnit, + inboundAddress, + } +} diff --git a/src/lib/utils/thorchain/hooks/useThorchainFromAddress.tsx b/src/lib/utils/thorchain/hooks/useThorchainFromAddress.tsx new file mode 100644 index 00000000000..28f3ce851a4 --- /dev/null +++ b/src/lib/utils/thorchain/hooks/useThorchainFromAddress.tsx @@ -0,0 +1,51 @@ +import type { AccountId, AssetId } from '@shapeshiftoss/caip' +import type { HDWallet } from '@shapeshiftoss/hdwallet-core' +import type { AccountMetadata } from '@shapeshiftoss/types' +import { useQuery } from '@tanstack/react-query' +import { reactQueries } from 'react-queries' +import type { getThorchainLpPosition } from 'pages/ThorChainLP/queries/queries' +import type { getThorchainSaversPosition } from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/utils' + +import type { getThorchainLendingPosition } from '../lending' + +type UseThorchainFromAddressArgs = { + accountId: AccountId | undefined + assetId: AssetId | undefined + opportunityId: string | undefined + wallet: HDWallet | null + accountMetadata: AccountMetadata | undefined + getPosition: + | typeof getThorchainLendingPosition + | typeof getThorchainSaversPosition + | typeof getThorchainLpPosition + select?: (maybeAddress: string) => string | undefined + enabled?: boolean +} + +export const useThorchainFromAddress = ({ + wallet, + accountId, + assetId, + opportunityId, + accountMetadata, + getPosition, + select, + enabled = true, +}: UseThorchainFromAddressArgs) => { + const query = useQuery({ + ...reactQueries.common.thorchainFromAddress({ + accountId: accountId!, + assetId: assetId!, + opportunityId, + wallet: wallet!, + accountMetadata: accountMetadata!, + getPosition, + }), + staleTime: 0, + gcTime: 0, + select, + enabled: Boolean(enabled && wallet && accountId && accountMetadata && assetId), + }) + + return query +} diff --git a/src/lib/utils/thorchain/index.ts b/src/lib/utils/thorchain/index.ts index 2c355a4c92a..49b1b186145 100644 --- a/src/lib/utils/thorchain/index.ts +++ b/src/lib/utils/thorchain/index.ts @@ -1,8 +1,14 @@ -import type { AccountId } from '@shapeshiftoss/caip' -import { type AssetId, bchChainId, fromAccountId, fromAssetId } from '@shapeshiftoss/caip' +import type { AccountId, AssetId, ChainId } from '@shapeshiftoss/caip' +import { + bchChainId, + cosmosChainId, + fromAccountId, + fromAssetId, + thorchainChainId, +} from '@shapeshiftoss/caip' import type { HDWallet } from '@shapeshiftoss/hdwallet-core' import { isLedger } from '@shapeshiftoss/hdwallet-ledger' -import type { AccountMetadata, Asset } from '@shapeshiftoss/types' +import type { AccountMetadata, Asset, KnownChainIds } from '@shapeshiftoss/types' import { TxStatus } from '@shapeshiftoss/unchained-client' import axios from 'axios' import { getConfig } from 'config' @@ -20,6 +26,7 @@ import { thorService } from 'lib/swapper/swappers/ThorchainSwapper/utils/thorSer import type { getThorchainLpPosition } from 'pages/ThorChainLP/queries/queries' import type { getThorchainSaversPosition } from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/utils' +import { getSupportedEvmChainIds } from '../evm' import { assertGetUtxoChainAdapter, isUtxoAccountId, isUtxoChainId } from '../utxo' import { THOR_PRECISION } from './constants' import type { getThorchainLendingPosition } from './lending' @@ -220,3 +227,22 @@ export const getAccountAddresses = memoize( async (accountId: AccountId): Promise => (await getAccountAddressesWithBalances(accountId)).map(({ address }) => address), ) + +// A THOR Tx can either be: +// - a RUNE MsgDeposit message type +// - an EVM custom Tx, i.e., a Tx with calldata +// - a regular send with a memo (for ATOM and UTXOs) +export const getThorchainTransactionType = (chainId: ChainId) => { + const isRuneTx = chainId === thorchainChainId + if (isRuneTx) return 'MsgDeposit' + + const supportedEvmChainIds = getSupportedEvmChainIds() + if (supportedEvmChainIds.includes(chainId as KnownChainIds)) { + return 'EvmCustomTx' + } + if (isUtxoChainId(chainId) || chainId === cosmosChainId) { + return 'Send' + } + + throw new Error(`Unsupported ChainId ${chainId}`) +} diff --git a/src/lib/utils/thorchain/lending.ts b/src/lib/utils/thorchain/lending.ts index 4ac40a5d8cc..91486ea4ef8 100644 --- a/src/lib/utils/thorchain/lending.ts +++ b/src/lib/utils/thorchain/lending.ts @@ -137,9 +137,11 @@ export const getThorchainLendingPosition = async ({ accountId, assetId, }: { - accountId: AccountId + accountId: AccountId | null assetId: AssetId }): Promise => { + if (!accountId) return null + const address = fromAccountId(accountId).account const poolAssetId = assetIdToPoolAssetId({ assetId }) diff --git a/src/lib/utils/thorchain/lp.ts b/src/lib/utils/thorchain/lp.ts index 8adcc21e773..3cbef88e4ba 100644 --- a/src/lib/utils/thorchain/lp.ts +++ b/src/lib/utils/thorchain/lp.ts @@ -1,6 +1,4 @@ -import type { ChainId } from '@shapeshiftoss/caip' -import { type AssetId, cosmosChainId, thorchainChainId } from '@shapeshiftoss/caip' -import type { KnownChainIds } from '@shapeshiftoss/types' +import type { AssetId } from '@shapeshiftoss/caip' import axios from 'axios' import { getConfig } from 'config' import type { BN } from 'lib/bignumber/bignumber' @@ -9,8 +7,6 @@ import type { ThornodePoolResponse } from 'lib/swapper/swappers/ThorchainSwapper import { assetIdToPoolAssetId } from 'lib/swapper/swappers/ThorchainSwapper/utils/poolAssetHelpers/poolAssetHelpers' import { thorService } from 'lib/swapper/swappers/ThorchainSwapper/utils/thorService' -import { getSupportedEvmChainIds } from '../evm' -import { isUtxoChainId } from '../utxo' import { fromThorBaseUnit } from '.' import { THOR_PRECISION } from './constants' import type { @@ -224,22 +220,3 @@ export const calculateEarnings = ( return { totalEarningsFiat, assetEarningsCryptoPrecision, runeEarningsCryptoPrecision } } - -// A THOR LP deposit can either be: -// - a RUNE MsgDeposit message type -// - an EVM custom Tx, i.e., a Tx with calldata -// - a regular send with a memo (for ATOM and UTXOs) -export const getThorchainLpTransactionType = (chainId: ChainId) => { - const isRuneTx = chainId === thorchainChainId - if (isRuneTx) return 'MsgDeposit' - - const supportedEvmChainIds = getSupportedEvmChainIds() - if (supportedEvmChainIds.includes(chainId as KnownChainIds)) { - return 'EvmCustomTx' - } - if (isUtxoChainId(chainId) || chainId === cosmosChainId) { - return 'Send' - } - - throw new Error(`Unsupported ChainId ${chainId}`) -} diff --git a/src/lib/utils/thorchain/lp/types.ts b/src/lib/utils/thorchain/lp/types.ts index d785658d590..7ee9345c327 100644 --- a/src/lib/utils/thorchain/lp/types.ts +++ b/src/lib/utils/thorchain/lp/types.ts @@ -207,7 +207,6 @@ export type LpConfirmedDepositQuote = { feeAmountFiatUserCurrency: string feeAmountUSD: string assetAddress?: string - quoteInboundAddress: string positionStatus?: PositionStatus // For informative purposes only at confirm step - to be recalculated before signing totalGasFeeFiatUserCurrency: string @@ -226,7 +225,6 @@ export type LpConfirmedWithdrawalQuote = { currentAccountIdByChainId: Record feeBps: string assetAddress: string | undefined - quoteInboundAddress: string withdrawalBps: string positionStatus?: PositionStatus // For informative purposes only at confirm step - to be recalculated before signing diff --git a/src/pages/Lending/Pool/Pool.tsx b/src/pages/Lending/Pool/Pool.tsx index c6ffafda8b4..9c749b289ed 100644 --- a/src/pages/Lending/Pool/Pool.tsx +++ b/src/pages/Lending/Pool/Pool.tsx @@ -123,7 +123,9 @@ export const Pool = () => { null, ) const [repayTxid, setRepayTxid] = useState(null) - const [collateralAccountId, setCollateralAccountId] = useState(poolAccountId ?? '') + const [collateralAccountId, setCollateralAccountId] = useState( + poolAccountId ?? null, + ) const [borrowAsset, setBorrowAsset] = useState(null) const [repaymentAsset, setRepaymentAsset] = useState(null) const [repaymentPercent, setRepaymentPercent] = useState(100) @@ -131,7 +133,7 @@ export const Pool = () => { null, ) const [borrowAccountId, setBorrowAccountId] = useState('') - const [repaymentAccountId, setRepaymentAccountId] = useState('') + const [repaymentAccountId, setRepaymentAccountId] = useState(null) const poolAssetId = useRouteAssetId() const asset = useAppSelector(state => selectAssetById(state, poolAssetId)) @@ -387,7 +389,7 @@ export const Pool = () => { isAccountSelectionDisabled={Boolean(poolAccountId)} borrowAsset={borrowAsset} setBorrowAsset={setBorrowAsset} - collateralAccountId={collateralAccountId} + collateralAccountId={collateralAccountId ?? ''} depositAmountCryptoPrecision={depositAmountCryptoPrecision} setCryptoDepositAmount={setDepositAmountCryptoPrecision} borrowAccountId={borrowAccountId} diff --git a/src/pages/Lending/Pool/components/Borrow/BorrowInput.tsx b/src/pages/Lending/Pool/components/Borrow/BorrowInput.tsx index 38233889341..9e80dd5cd28 100644 --- a/src/pages/Lending/Pool/components/Borrow/BorrowInput.tsx +++ b/src/pages/Lending/Pool/components/Borrow/BorrowInput.tsx @@ -11,9 +11,11 @@ import { } from '@chakra-ui/react' import { type AccountId, type AssetId, fromAccountId } from '@shapeshiftoss/caip' import type { Asset } from '@shapeshiftoss/types' +import { useQuery } from '@tanstack/react-query' import prettyMilliseconds from 'pretty-ms' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslate } from 'react-polyglot' +import { reactQueries } from 'react-queries' import { useQuoteEstimatedFeesQuery } from 'react-queries/hooks/useQuoteEstimatedFeesQuery' import { useHistory } from 'react-router' import { Amount } from 'components/Amount/Amount' @@ -34,7 +36,6 @@ import { fromBaseUnit, toBaseUnit } from 'lib/math' import { getMaybeCompositeAssetSymbol } from 'lib/mixpanel/helpers' import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton' import { MixPanelEvent } from 'lib/mixpanel/types' -import { getThorchainFromAddress } from 'lib/utils/thorchain' import { getThorchainLendingPosition } from 'lib/utils/thorchain/lending' import type { LendingQuoteOpen } from 'lib/utils/thorchain/lending/types' import { isUtxoChainId } from 'lib/utils/utxo' @@ -97,7 +98,6 @@ export const BorrowInput = ({ confirmedQuote, setConfirmedQuote, }: BorrowInputProps) => { - const [fromAddress, setFromAddress] = useState(null) const [borrowAssetIsFiat, toggleBorrowAssetIsFiat] = useToggle(false) const [collateralAssetIsFiat, toggleCollateralAssetIsFiat] = useToggle(false) const [shouldShowWarningAcknowledgement, setShouldShowWarningAcknowledgement] = useState(false) @@ -160,25 +160,16 @@ export const BorrowInput = ({ selectPortfolioAccountMetadataByAccountId(state, collateralAccountFilter), ) - const getBorrowFromAddress = useCallback(() => { - if (!(wallet && collateralAccountMetadata)) return null - return getThorchainFromAddress({ + const { data: fromAddress } = useQuery({ + ...reactQueries.common.thorchainFromAddress({ accountId: collateralAccountId, assetId: collateralAssetId, getPosition: getThorchainLendingPosition, - accountMetadata: collateralAccountMetadata, - wallet, - }) - }, [collateralAccountId, collateralAccountMetadata, collateralAssetId, wallet]) - - useEffect(() => { - if (fromAddress) return - ;(async () => { - const _fromAddress = await getBorrowFromAddress() - if (!_fromAddress) return - setFromAddress(_fromAddress) - })() - }, [getBorrowFromAddress, fromAddress]) + accountMetadata: collateralAccountMetadata!, + wallet: wallet!, + }), + enabled: Boolean(collateralAccountMetadata && wallet), + }) const { data: estimatedFeesData, @@ -249,7 +240,7 @@ export const BorrowInput = ({ const isSweepNeededArgs = useMemo( () => ({ assetId: collateralAssetId, - address: fromAddress, + address: fromAddress ?? null, amountCryptoBaseUnit: toBaseUnit( depositAmountCryptoPrecision ?? 0, collateralAsset?.precision ?? 0, @@ -285,11 +276,12 @@ export const BorrowInput = ({ } = useGetEstimatedFeesQuery({ amountCryptoPrecision: '0', assetId: collateralAssetId, + feeAssetId: collateralFeeAsset?.assetId!, to: fromAddress ?? '', sendMax: true, accountId: collateralAccountId, contractAddress: undefined, - enabled: isSweepNeededSuccess, + enabled: Boolean(collateralFeeAsset && isSweepNeededSuccess), }) const hasEnoughBalanceForTxPlusSweep = useMemo(() => { diff --git a/src/pages/Lending/Pool/components/Borrow/BorrowSweep.tsx b/src/pages/Lending/Pool/components/Borrow/BorrowSweep.tsx index 9fdb4d956a3..30f310bcbb6 100644 --- a/src/pages/Lending/Pool/components/Borrow/BorrowSweep.tsx +++ b/src/pages/Lending/Pool/components/Borrow/BorrowSweep.tsx @@ -1,13 +1,14 @@ import { CardHeader, Flex, Heading } from '@chakra-ui/react' import type { AccountId, AssetId } from '@shapeshiftoss/caip' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useQuery } from '@tanstack/react-query' +import { useCallback, useMemo } from 'react' +import { reactQueries } from 'react-queries' import { useHistory } from 'react-router' import { WithBackButton } from 'components/MultiHopTrade/components/WithBackButton' import { SlideTransition } from 'components/SlideTransition' import { Sweep } from 'components/Sweep' import { Text } from 'components/Text' import { useWallet } from 'hooks/useWallet/useWallet' -import { getThorchainFromAddress } from 'lib/utils/thorchain' import { getThorchainLendingPosition } from 'lib/utils/thorchain/lending' import { selectPortfolioAccountMetadataByAccountId } from 'state/slices/selectors' import { useAppSelector } from 'state/store' @@ -24,8 +25,6 @@ export const BorrowSweep = ({ collateralAssetId, collateralAccountId }: BorrowSw state: { wallet }, } = useWallet() - const [fromAddress, setFromAddress] = useState(null) - const history = useHistory() const handleBack = useCallback(() => { @@ -40,25 +39,16 @@ export const BorrowSweep = ({ collateralAssetId, collateralAccountId }: BorrowSw selectPortfolioAccountMetadataByAccountId(state, collateralAccountFilter), ) - const getBorrowFromAddress = useCallback(() => { - if (!(wallet && collateralAccountMetadata)) return null - return getThorchainFromAddress({ + const { data: fromAddress } = useQuery({ + ...reactQueries.common.thorchainFromAddress({ accountId: collateralAccountId, assetId: collateralAssetId, getPosition: getThorchainLendingPosition, - accountMetadata: collateralAccountMetadata, - wallet, - }) - }, [wallet, collateralAccountId, collateralAssetId, collateralAccountMetadata]) - - useEffect(() => { - if (fromAddress) return - ;(async () => { - const _fromAddress = await getBorrowFromAddress() - if (!_fromAddress) return - setFromAddress(_fromAddress) - })() - }, [getBorrowFromAddress, fromAddress]) + accountMetadata: collateralAccountMetadata!, + wallet: wallet!, + }), + enabled: Boolean(collateralAccountMetadata && wallet), + }) const handleSwepSeen = useCallback(() => { history.push(BorrowRoutePaths.Confirm) @@ -77,7 +67,7 @@ export const BorrowSweep = ({ collateralAssetId, collateralAccountId }: BorrowSw diff --git a/src/pages/Lending/Pool/components/LoanSummary.tsx b/src/pages/Lending/Pool/components/LoanSummary.tsx index 15535e2183a..9ebb65c0353 100644 --- a/src/pages/Lending/Pool/components/LoanSummary.tsx +++ b/src/pages/Lending/Pool/components/LoanSummary.tsx @@ -46,7 +46,7 @@ type LoanSummaryProps = { | { borrowAssetId: AssetId borrowAccountId: AccountId - collateralAccountId: AccountId + collateralAccountId: AccountId | null collateralDecreaseAmountCryptoPrecision?: never debtRepaidAmountUserCurrency?: never debtOccuredAmountUserCurrency: string @@ -59,7 +59,7 @@ type LoanSummaryProps = { | { borrowAssetId?: never borrowAccountId?: never - collateralAccountId: AccountId + collateralAccountId: AccountId | null collateralDecreaseAmountCryptoPrecision: string debtRepaidAmountUserCurrency: string debtOccuredAmountUserCurrency?: never diff --git a/src/pages/Lending/Pool/components/Repay/Repay.tsx b/src/pages/Lending/Pool/components/Repay/Repay.tsx index b6043c8b561..7598f21e8c7 100644 --- a/src/pages/Lending/Pool/components/Repay/Repay.tsx +++ b/src/pages/Lending/Pool/components/Repay/Repay.tsx @@ -12,8 +12,8 @@ const RepayEntries = [RepayRoutePaths.Input, RepayRoutePaths.Confirm] type RepayProps = { isAccountSelectionDisabled?: boolean - collateralAccountId: AccountId - repaymentAccountId: AccountId + collateralAccountId: AccountId | null + repaymentAccountId: AccountId | null repaymentAsset: Asset | null setRepaymentAsset: (asset: Asset | null) => void onCollateralAccountIdChange: (accountId: AccountId) => void @@ -72,8 +72,8 @@ type RepayRoutesProps = { setRepaymentAsset: (asset: Asset | null) => void repaymentPercent: number onRepaymentPercentChange: (value: number) => void - collateralAccountId: AccountId - repaymentAccountId: AccountId + collateralAccountId: AccountId | null + repaymentAccountId: AccountId | null onCollateralAccountIdChange: (accountId: AccountId) => void onRepaymentAccountIdChange: (accountId: AccountId) => void txId: string | null diff --git a/src/pages/Lending/Pool/components/Repay/RepayConfirm.tsx b/src/pages/Lending/Pool/components/Repay/RepayConfirm.tsx index cacc09a940d..0addb99bef4 100644 --- a/src/pages/Lending/Pool/components/Repay/RepayConfirm.tsx +++ b/src/pages/Lending/Pool/components/Repay/RepayConfirm.tsx @@ -15,11 +15,10 @@ import { useInterval, } from '@chakra-ui/react' import type { AccountId, AssetId } from '@shapeshiftoss/caip' -import { fromAccountId, fromAssetId, thorchainAssetId } from '@shapeshiftoss/caip' -import type { FeeDataEstimate } from '@shapeshiftoss/chain-adapters' -import { CONTRACT_INTERACTION, FeeDataKey, isEvmChainId } from '@shapeshiftoss/chain-adapters' +import { fromAssetId } from '@shapeshiftoss/caip' +import { isEvmChainId } from '@shapeshiftoss/chain-adapters' import { isLedger } from '@shapeshiftoss/hdwallet-ledger' -import type { Asset, KnownChainIds } from '@shapeshiftoss/types' +import type { Asset } from '@shapeshiftoss/types' import { TxStatus } from '@shapeshiftoss/unchained-client' import { useMutation, useMutationState, useQuery } from '@tanstack/react-query' import dayjs from 'dayjs' @@ -27,15 +26,11 @@ import prettyMilliseconds from 'pretty-ms' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslate } from 'react-polyglot' import { reactQueries } from 'react-queries' -import { useQuoteEstimatedFeesQuery } from 'react-queries/hooks/useQuoteEstimatedFeesQuery' import { selectInboundAddressData } from 'react-queries/selectors' import { useHistory } from 'react-router' -import { getAddress, toHex } from 'viem' import { Amount } from 'components/Amount/Amount' import { AssetToAsset } from 'components/AssetToAsset/AssetToAsset' import { HelperTooltip } from 'components/HelperTooltip/HelperTooltip' -import type { SendInput } from 'components/Modals/Send/Form' -import { estimateFees, handleSend } from 'components/Modals/Send/utils' import { WithBackButton } from 'components/MultiHopTrade/components/WithBackButton' import { Row } from 'components/Row/Row' import { SlideTransition } from 'components/SlideTransition' @@ -49,16 +44,9 @@ import { getMaybeCompositeAssetSymbol } from 'lib/mixpanel/helpers' import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton' import { MixPanelEvent } from 'lib/mixpanel/types' import { isToken } from 'lib/utils' -import { assertGetThorchainChainAdapter } from 'lib/utils/cosmosSdk' -import { - assertGetEvmChainAdapter, - buildAndBroadcast, - createBuildCustomTxInput, - getSupportedEvmChainIds, -} from 'lib/utils/evm' import { waitForThorchainUpdate } from 'lib/utils/thorchain' +import { useSendThorTx } from 'lib/utils/thorchain/hooks/useSendThorTx' import type { LendingQuoteClose } from 'lib/utils/thorchain/lending/types' -import { depositWithExpiry } from 'lib/utils/thorchain/routerCalldata' import { useLendingQuoteCloseQuery } from 'pages/Lending/hooks/useLendingCloseQuery' import { useLendingPositionData } from 'pages/Lending/hooks/useLendingPositionData' import { @@ -66,7 +54,6 @@ import { selectAssetById, selectAssets, selectFeeAssetById, - selectSelectedCurrency, } from 'state/slices/selectors' import { store, useAppSelector } from 'state/store' @@ -77,8 +64,8 @@ type RepayConfirmProps = { collateralAssetId: AssetId repaymentAsset: Asset | null setRepaymentPercent: (percent: number) => void - collateralAccountId: AccountId - repaymentAccountId: AccountId + collateralAccountId: AccountId | null + repaymentAccountId: AccountId | null txId: string | null setTxid: (txId: string | null) => void confirmedQuote: LendingQuoteClose | null @@ -219,10 +206,8 @@ export const RepayConfirm = ({ const chainAdapter = getChainAdapterManager().get( fromAssetId(repaymentAsset?.assetId ?? '').chainId, ) - const selectedCurrency = useAppSelector(selectSelectedCurrency) - const repaymentAccountNumberFilter = useMemo( - () => ({ accountId: repaymentAccountId }), + () => ({ accountId: repaymentAccountId ?? '' }), [repaymentAccountId], ) const repaymentAccountNumber = useAppSelector(state => @@ -260,6 +245,25 @@ export const RepayConfirm = ({ enabled: !!repaymentAsset?.assetId, }) + const { + executeTransaction, + estimatedFeesData, + isEstimatedFeesDataLoading, + isEstimatedFeesDataError, + } = useSendThorTx({ + assetId: repaymentAsset?.assetId ?? '', + accountId: repaymentAccountId, + amountCryptoBaseUnit: toBaseUnit( + confirmedQuote?.repaymentAmountCryptoPrecision ?? 0, + repaymentAsset?.precision ?? 0, + ), + memo: confirmedQuote?.quoteMemo ?? null, + // no explicit from address required for repayments + fromAddress: '', + action: 'repayLoan', + disableEstimateFeesRefetch: isLoanPending, + }) + const handleConfirm = useCallback(async () => { if (isQuoteExpired) { const { data: refetchedQuote } = await refetchQuote() @@ -301,127 +305,13 @@ export const RepayConfirm = ({ mixpanel?.track(MixPanelEvent.RepayConfirm, eventData) - const supportedEvmChainIds = getSupportedEvmChainIds() - - const estimatedFees = await estimateFees({ - amountCryptoPrecision: confirmedQuote.repaymentAmountCryptoPrecision, - assetId: repaymentAsset.assetId, - memo: supportedEvmChainIds.includes( - fromAssetId(repaymentAsset.assetId).chainId as KnownChainIds, - ) - ? toHex(confirmedQuote.quoteMemo) - : confirmedQuote.quoteMemo, - to: confirmedQuote.quoteInboundAddress, - sendMax: false, - accountId: repaymentAccountId, - contractAddress: undefined, - }) - - const maybeTxId = await (async () => { - if (repaymentAsset.assetId === thorchainAssetId) { - return (async () => { - const { account } = fromAccountId(repaymentAccountId) - - const adapter = assertGetThorchainChainAdapter() - - // repayment using THOR is a MsgDeposit tx - const { txToSign } = await adapter.buildDepositTransaction({ - from: account, - accountNumber: repaymentAccountNumber, - value: bnOrZero(confirmedQuote.repaymentAmountCryptoPrecision) - .times(bn(10).pow(repaymentAsset.precision)) - .toFixed(0), - memo: confirmedQuote.quoteMemo, - chainSpecific: { - gas: (estimatedFees as FeeDataEstimate).fast - .chainSpecific.gasLimit, - fee: (estimatedFees as FeeDataEstimate).fast.txFee, - }, - }) - const signedTx = await adapter.signTransaction({ - txToSign, - wallet, - }) - return adapter.broadcastTransaction({ - senderAddress: account, - receiverAddress: confirmedQuote.quoteInboundAddress, - hex: signedTx, - }) - })() - } - - if (isToken(fromAssetId(repaymentAsset.assetId).assetReference)) { - const data = depositWithExpiry({ - vault: getAddress(inboundAddressData!.address), - asset: getAddress(fromAssetId(repaymentAsset.assetId).assetReference), - amount: toBaseUnit( - confirmedQuote.repaymentAmountCryptoPrecision!, - repaymentAsset.precision, - ), - memo: confirmedQuote.quoteMemo, - expiry: confirmedQuote.quoteExpiry, - }) - - const adapter = assertGetEvmChainAdapter(repaymentAsset.chainId) - - const buildCustomTxInput = await createBuildCustomTxInput({ - accountNumber: repaymentAccountNumber, - adapter, - data, - // value is always denominated in fee asset - the only value we can send when calling a contract is native asset value - value: '0', - to: inboundAddressData!.router!, - wallet, - }) - - const _txId = await buildAndBroadcast({ - adapter, - buildCustomTxInput, - receiverAddress: CONTRACT_INTERACTION, // no receiver for this contract call - }) - - return _txId - } - - const sendInput: SendInput = { - amountCryptoPrecision: confirmedQuote.repaymentAmountCryptoPrecision!, - assetId: repaymentAsset.assetId, - from: '', - to: confirmedQuote.quoteInboundAddress, - sendMax: false, - accountId: repaymentAccountId, - memo: supportedEvmChainIds.includes( - fromAssetId(repaymentAsset?.assetId).chainId as KnownChainIds, - ) - ? toHex(confirmedQuote.quoteMemo) - : confirmedQuote.quoteMemo, - amountFieldError: '', - estimatedFees, - feeType: FeeDataKey.Fast, - fiatAmount: '', - fiatSymbol: selectedCurrency, - vanityAddress: '', - input: confirmedQuote.quoteInboundAddress, - } - - if (!sendInput) throw new Error('Error building send input') - - return handleSend({ sendInput, wallet }) - })() - - if (!maybeTxId) { - throw new Error('Error sending THORCHain lending Txs') - } - - setTxid(maybeTxId) + const _txId = await executeTransaction() + if (!_txId) throw new Error('failed to broadcast transaction') - return maybeTxId + setTxid(_txId) }, [ chainAdapter, - confirmedQuote?.quoteExpiry, - confirmedQuote?.quoteInboundAddress, confirmedQuote?.quoteLoanCollateralDecreaseCryptoPrecision, - confirmedQuote?.quoteMemo, confirmedQuote?.repaymentAmountCryptoPrecision, confirmedQuote?.repaymentPercent, eventData, @@ -430,30 +320,16 @@ export const RepayConfirm = ({ isQuoteExpired, loanTxStatus, mixpanel, + executeTransaction, refetchQuote, - repaymentAccountId, repaymentAccountNumber, repaymentAsset, - selectedCurrency, setConfirmedQuote, setRepaymentPercent, setTxid, wallet, ]) - const { - data: estimatedFeesData, - isLoading: isEstimatedFeesDataLoading, - isError: isEstimatedFeesDataError, - isSuccess: isEstimatedFeesDataSuccess, - } = useQuoteEstimatedFeesQuery({ - collateralAssetId, - collateralAccountId, - repaymentAccountId, - repaymentAsset, - confirmedQuote, - }) - const swapStatus = useMemo(() => { if (loanTxStatus === 'success') return TxStatus.Confirmed if (loanTxStatus === 'pending') return TxStatus.Pending @@ -615,7 +491,7 @@ export const RepayConfirm = ({ {translate('common.gasFee')} - + {/* Actually defined at display time, see isLoaded above */} @@ -641,8 +517,8 @@ export const RepayConfirm = ({ collateralDecreaseAmountCryptoPrecision={ confirmedQuote?.quoteLoanCollateralDecreaseCryptoPrecision ?? '0' } - repaymentAccountId={repaymentAccountId} - collateralAccountId={collateralAccountId} + repaymentAccountId={repaymentAccountId ?? ''} + collateralAccountId={collateralAccountId ?? ''} debtRepaidAmountUserCurrency={confirmedQuote?.quoteDebtRepaidAmountUserCurrency ?? '0'} borderTopWidth={0} mt={0} diff --git a/src/pages/Lending/Pool/components/Repay/RepayInput.tsx b/src/pages/Lending/Pool/components/Repay/RepayInput.tsx index a590790d8e7..c85285d4927 100644 --- a/src/pages/Lending/Pool/components/Repay/RepayInput.tsx +++ b/src/pages/Lending/Pool/components/Repay/RepayInput.tsx @@ -23,7 +23,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslate } from 'react-polyglot' import { reactQueries } from 'react-queries' import { useAllowance } from 'react-queries/hooks/useAllowance' -import { useQuoteEstimatedFeesQuery } from 'react-queries/hooks/useQuoteEstimatedFeesQuery' import { selectInboundAddressData } from 'react-queries/selectors' import { useHistory } from 'react-router' import { Amount } from 'components/Amount/Amount' @@ -45,6 +44,7 @@ import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton' import { MixPanelEvent } from 'lib/mixpanel/types' import { isToken } from 'lib/utils' import { getSupportedEvmChainIds } from 'lib/utils/evm' +import { useSendThorTx } from 'lib/utils/thorchain/hooks/useSendThorTx' import type { LendingQuoteClose } from 'lib/utils/thorchain/lending/types' import { useLendingQuoteCloseQuery } from 'pages/Lending/hooks/useLendingCloseQuery' import { useLendingPositionData } from 'pages/Lending/hooks/useLendingPositionData' @@ -74,8 +74,8 @@ type RepayInputProps = { collateralAssetId: AssetId repaymentPercent: number onRepaymentPercentChange: (value: number) => void - collateralAccountId: AccountId - repaymentAccountId: AccountId + collateralAccountId: AccountId | null + repaymentAccountId: AccountId | null onCollateralAccountIdChange: (accountId: AccountId) => void onRepaymentAccountIdChange: (accountId: AccountId) => void repaymentAsset: Asset | null @@ -190,7 +190,7 @@ export const RepayInput = ({ const mixpanel = getMixPanel() const repaymentAccountNumberFilter = useMemo( - () => ({ accountId: repaymentAccountId }), + () => ({ accountId: repaymentAccountId ?? '' }), [repaymentAccountId], ) const repaymentAccountNumber = useAppSelector(state => @@ -372,21 +372,23 @@ export const RepayInput = ({ accountId: collateralAccountId, }) - const { - data: estimatedFeesData, - isLoading: isEstimatedFeesDataLoading, - isError: isEstimatedFeesDataError, - isSuccess: isEstimatedFeesDataSuccess, - } = useQuoteEstimatedFeesQuery({ - collateralAssetId, - collateralAccountId, - repaymentAccountId, - repaymentAsset, - confirmedQuote, - }) + const { estimatedFeesData, isEstimatedFeesDataLoading, isEstimatedFeesDataError } = useSendThorTx( + { + assetId: repaymentAsset?.assetId ?? '', + accountId: repaymentAccountId, + amountCryptoBaseUnit: toBaseUnit( + confirmedQuote?.repaymentAmountCryptoPrecision ?? 0, + repaymentAsset?.precision ?? 0, + ), + memo: confirmedQuote?.quoteMemo ?? null, + // no explicit from address required for repayments + fromAddress: '', + action: 'repayLoan', + }, + ) const balanceFilter = useMemo( - () => ({ assetId: repaymentAsset?.assetId ?? '', accountId: repaymentAccountId }), + () => ({ assetId: repaymentAsset?.assetId ?? '', accountId: repaymentAccountId ?? '' }), [repaymentAsset?.assetId, repaymentAccountId], ) @@ -394,7 +396,7 @@ export const RepayInput = ({ selectPortfolioCryptoBalanceBaseUnitByFilter(state, balanceFilter), ) const feeAssetBalanceFilter = useMemo( - () => ({ assetId: repaymentFeeAsset?.assetId ?? '', accountId: repaymentAccountId }), + () => ({ assetId: repaymentFeeAsset?.assetId ?? '', accountId: repaymentAccountId ?? '' }), [repaymentFeeAsset?.assetId, repaymentAccountId], ) const feeAssetBalanceCryptoBaseUnit = useAppSelector(state => @@ -604,7 +606,7 @@ export const RepayInput = ({ {/* Actually defined at display time, see isLoaded above */} diff --git a/src/pages/Lending/hooks/useGetEstimatedFeesQuery.ts b/src/pages/Lending/hooks/useGetEstimatedFeesQuery.ts index 9737ac874ea..34bf44e11c4 100644 --- a/src/pages/Lending/hooks/useGetEstimatedFeesQuery.ts +++ b/src/pages/Lending/hooks/useGetEstimatedFeesQuery.ts @@ -1,6 +1,7 @@ +import type { AssetId } from '@shapeshiftoss/caip' +import type { Asset, MarketData } from '@shapeshiftoss/types' import { useQuery } from '@tanstack/react-query' import { useMemo } from 'react' -import type { EstimatedFeesQueryKey } from 'react-queries/hooks/useQuoteEstimatedFeesQuery' import type { EstimateFeesInput } from 'components/Modals/Send/utils' import { estimateFees } from 'components/Modals/Send/utils' import { bn } from 'lib/bignumber/bignumber' @@ -8,16 +9,26 @@ import { fromBaseUnit } from 'lib/math' import { selectAssetById, selectMarketDataByAssetIdUserCurrency } from 'state/slices/selectors' import { useAppSelector } from 'state/store' +export type EstimatedFeesQueryKey = [ + 'estimateFees', + { + enabled: boolean + feeAsset: Asset | undefined + feeAssetMarketData: MarketData + estimateFeesInput: EstimateFeesInput | undefined + }, +] + // For use outside of react with queryClient.fetchQuery() export const queryFn = async ({ queryKey }: { queryKey: EstimatedFeesQueryKey }) => { - const { estimateFeesInput, asset, assetMarketData } = queryKey[1] + const { estimateFeesInput, feeAsset, feeAssetMarketData } = queryKey[1] // These should not be undefined when used with react-query, but may be when used outside of it since there's no "enabled" option - if (!asset || !estimateFeesInput?.to || !estimateFeesInput.accountId) return + if (!feeAsset || !estimateFeesInput?.to || !estimateFeesInput.accountId) return const estimatedFees = await estimateFees(estimateFeesInput) - const txFeeFiat = bn(fromBaseUnit(estimatedFees.fast.txFee, asset.precision)) - .times(assetMarketData.price) + const txFeeFiat = bn(fromBaseUnit(estimatedFees.fast.txFee, feeAsset.precision)) + .times(feeAssetMarketData.price) .toString() return { estimatedFees, txFeeFiat, txFeeCryptoBaseUnit: estimatedFees.fast.txFee } } @@ -25,10 +36,10 @@ export const queryFn = async ({ queryKey }: { queryKey: EstimatedFeesQueryKey }) export const useGetEstimatedFeesQuery = ({ enabled, ...estimateFeesInput -}: EstimateFeesInput & { enabled: boolean; disableRefetch?: boolean }) => { - const asset = useAppSelector(state => selectAssetById(state, estimateFeesInput.assetId)) - const assetMarketData = useAppSelector(state => - selectMarketDataByAssetIdUserCurrency(state, estimateFeesInput.assetId), +}: EstimateFeesInput & { enabled: boolean; disableRefetch?: boolean; feeAssetId: AssetId }) => { + const feeAsset = useAppSelector(state => selectAssetById(state, estimateFeesInput.feeAssetId)) + const feeAssetMarketData = useAppSelector(state => + selectMarketDataByAssetIdUserCurrency(state, estimateFeesInput.feeAssetId), ) const estimatedFeesQueryKey: EstimatedFeesQueryKey = useMemo( @@ -36,12 +47,12 @@ export const useGetEstimatedFeesQuery = ({ 'estimateFees', { enabled, - asset, - assetMarketData, + feeAsset, + feeAssetMarketData, estimateFeesInput, }, ], - [asset, assetMarketData, enabled, estimateFeesInput], + [feeAsset, feeAssetMarketData, enabled, estimateFeesInput], ) const getEstimatedFeesQuery = useQuery({ @@ -51,7 +62,7 @@ export const useGetEstimatedFeesQuery = ({ enabled: enabled && Boolean( - asset && + feeAsset && estimateFeesInput.to && estimateFeesInput.accountId && estimateFeesInput.amountCryptoPrecision, diff --git a/src/pages/Lending/hooks/useLendingCloseQuery.ts b/src/pages/Lending/hooks/useLendingCloseQuery.ts index 178f14e3f53..1eac7813713 100644 --- a/src/pages/Lending/hooks/useLendingCloseQuery.ts +++ b/src/pages/Lending/hooks/useLendingCloseQuery.ts @@ -27,8 +27,8 @@ import { useLendingPositionData } from './useLendingPositionData' type UseLendingQuoteCloseQueryProps = { repaymentAssetId: AssetId - repaymentAccountId: AccountId - collateralAccountId: AccountId + repaymentAccountId: AccountId | null + collateralAccountId: AccountId | null collateralAssetId: AssetId repaymentPercent: number enabled?: boolean @@ -190,7 +190,7 @@ export const useLendingQuoteCloseQuery = ({ ) const collateralAccountMetadataFilter = useMemo( - () => ({ accountId: collateralAccountId }), + () => ({ accountId: collateralAccountId ?? '' }), [collateralAccountId], ) const collateralAccountMetadata = useAppSelector(state => diff --git a/src/pages/Lending/hooks/useLendingPositionData.tsx b/src/pages/Lending/hooks/useLendingPositionData.tsx index a78dc742775..d66415b7179 100644 --- a/src/pages/Lending/hooks/useLendingPositionData.tsx +++ b/src/pages/Lending/hooks/useLendingPositionData.tsx @@ -10,7 +10,7 @@ import { import { store, useAppSelector } from 'state/store' type UseLendingPositionDataProps = { - accountId: AccountId + accountId: AccountId | null assetId: AssetId } @@ -25,10 +25,8 @@ export const thorchainLendingPositionQueryFn = async ({ } export const useLendingPositionData = ({ accountId, assetId }: UseLendingPositionDataProps) => { - const lendingPositionQueryKey: [string, { accountId: AccountId; assetId: AssetId }] = useMemo( - () => ['thorchainLendingPosition', { accountId, assetId }], - [accountId, assetId], - ) + const lendingPositionQueryKey: [string, { accountId: AccountId | null; assetId: AssetId }] = + useMemo(() => ['thorchainLendingPosition', { accountId, assetId }], [accountId, assetId]) const poolAssetMarketData = useAppSelector(state => selectMarketDataByAssetIdUserCurrency(state, assetId), ) diff --git a/src/pages/Lending/hooks/useLendingQuoteQuery.ts b/src/pages/Lending/hooks/useLendingQuoteQuery.ts index 085ba0e6e54..53eef32d2a9 100644 --- a/src/pages/Lending/hooks/useLendingQuoteQuery.ts +++ b/src/pages/Lending/hooks/useLendingQuoteQuery.ts @@ -31,7 +31,7 @@ import { store, useAppSelector } from 'state/store' type UseLendingQuoteQueryProps = { collateralAssetId: AssetId borrowAccountId: AccountId - collateralAccountId: AccountId + collateralAccountId: AccountId | null borrowAssetId: AssetId depositAmountCryptoPrecision: string } diff --git a/src/pages/Lending/hooks/useRepaymentLockData.tsx b/src/pages/Lending/hooks/useRepaymentLockData.tsx index 0fc68c425aa..3817a63e6e3 100644 --- a/src/pages/Lending/hooks/useRepaymentLockData.tsx +++ b/src/pages/Lending/hooks/useRepaymentLockData.tsx @@ -9,7 +9,7 @@ import { THORCHAIN_BLOCK_TIME_SECONDS, thorchainBlockTimeMs } from 'lib/utils/th import { thorchainLendingPositionQueryFn } from './useLendingPositionData' type UseLendingPositionDataProps = { - accountId?: AccountId + accountId?: AccountId | null assetId?: AssetId } diff --git a/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx b/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx index a15d3631654..f725fb8841b 100644 --- a/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx +++ b/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx @@ -23,8 +23,7 @@ import { fromAssetId, thorchainAssetId, thorchainChainId } from '@shapeshiftoss/ import { SwapperName } from '@shapeshiftoss/swapper' import type { Asset, KnownChainIds, MarketData } from '@shapeshiftoss/types' import { TxStatus } from '@shapeshiftoss/unchained-client' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import dayjs from 'dayjs' +import { useMutation, useQueryClient } from '@tanstack/react-query' import { useCallback, useEffect, useMemo, useState } from 'react' import { BiErrorCircle, BiSolidBoltCircle } from 'react-icons/bi' import { FaPlus } from 'react-icons/fa' @@ -32,10 +31,7 @@ import { useTranslate } from 'react-polyglot' import { reactQueries } from 'react-queries' import { useAllowance } from 'react-queries/hooks/useAllowance' import { useIsTradingActive } from 'react-queries/hooks/useIsTradingActive' -import { useQuoteEstimatedFeesQuery } from 'react-queries/hooks/useQuoteEstimatedFeesQuery' -import { selectInboundAddressData } from 'react-queries/selectors' import { useHistory } from 'react-router' -import { getAddress, zeroAddress } from 'viem' import { Amount } from 'components/Amount/Amount' import { TradeAssetSelect } from 'components/AssetSelection/AssetSelection' import { FeeModal } from 'components/FeeModal/FeeModal' @@ -66,15 +62,11 @@ import { } from 'lib/swapper/swappers/ThorchainSwapper/utils/poolAssetHelpers/poolAssetHelpers' import { assertUnreachable, isSome, isToken } from 'lib/utils' import { getSupportedEvmChainIds } from 'lib/utils/evm' -import { getThorchainFromAddress } from 'lib/utils/thorchain' -import { THOR_PRECISION, THORCHAIN_POOL_MODULE_ADDRESS } from 'lib/utils/thorchain/constants' -import { - estimateAddThorchainLiquidityPosition, - getThorchainLpTransactionType, -} from 'lib/utils/thorchain/lp' +import { THOR_PRECISION } from 'lib/utils/thorchain/constants' +import { useSendThorTx } from 'lib/utils/thorchain/hooks/useSendThorTx' +import { useThorchainFromAddress } from 'lib/utils/thorchain/hooks/useThorchainFromAddress' +import { estimateAddThorchainLiquidityPosition } from 'lib/utils/thorchain/lp' import { AsymSide, type LpConfirmedDepositQuote } from 'lib/utils/thorchain/lp/types' -import { depositWithExpiry } from 'lib/utils/thorchain/routerCalldata' -import { useGetEstimatedFeesQuery } from 'pages/Lending/hooks/useGetEstimatedFeesQuery' import { useIsSweepNeededQuery } from 'pages/Lending/hooks/useIsSweepNeededQuery' import { usePools } from 'pages/ThorChainLP/queries/hooks/usePools' import { useUserLpData } from 'pages/ThorChainLP/queries/hooks/useUserLpData' @@ -179,10 +171,8 @@ export const AddLiquidityInput: React.FC = ({ const previousOpportunityId = usePrevious(activeOpportunityId) const [approvalTxId, setApprovalTxId] = useState(null) + const [isApprovalRequired, setIsApprovalRequired] = useState(false) const [runeTxFeeCryptoBaseUnit, setRuneTxFeeCryptoBaseUnit] = useState() - const [poolAssetAccountAddress, setPoolAssetAccountAddress] = useState( - undefined, - ) const [poolAssetTxFeeCryptoBaseUnit, setPoolAssetTxFeeCryptoBaseUnit] = useState< string | undefined >() @@ -220,6 +210,30 @@ export const AddLiquidityInput: React.FC = ({ return poolAssets.map(poolAsset => poolAsset.assetId) }, [poolAssets]) + const poolAssetAccountId = useMemo(() => { + return currentAccountIdByChainId[ + poolAsset?.assetId ? fromAssetId(poolAsset.assetId).chainId : '' + ] + }, [currentAccountIdByChainId, poolAsset?.assetId]) + + const poolAssetAccountMetadataFilter = useMemo( + () => ({ accountId: poolAssetAccountId }), + [poolAssetAccountId], + ) + + const poolAssetAccountMetadata = useAppSelector(state => + selectPortfolioAccountMetadataByAccountId(state, poolAssetAccountMetadataFilter), + ) + + const { data: poolAssetAccountAddress } = useThorchainFromAddress({ + accountId: poolAssetAccountId, + assetId: poolAsset?.assetId, + opportunityId: activeOpportunityId, + wallet, + accountMetadata: poolAssetAccountMetadata, + getPosition: getThorchainLpPosition, + }) + const { data: isSmartContractAccountAddress, isLoading: isSmartContractAccountAddressLoading } = useIsSmartContractAddress(poolAssetAccountAddress ?? '') @@ -339,22 +353,12 @@ export const AddLiquidityInput: React.FC = ({ const poolAssetAccountIds = useAppSelector(state => selectAccountIdsByAssetId(state, { assetId: assetId ?? '' }), ) - const poolAssetAccountId = useMemo(() => { - return currentAccountIdByChainId[assetId ? fromAssetId(assetId).chainId : ''] - }, [currentAccountIdByChainId, assetId]) const poolAssetBalanceFilter = useMemo(() => { return { assetId, accountId: poolAssetAccountId } }, [assetId, poolAssetAccountId]) const poolAssetBalanceCryptoBaseUnit = useAppSelector(state => selectPortfolioCryptoBalanceBaseUnitByFilter(state, poolAssetBalanceFilter), ) - const poolAssetAccountMetadataFilter = useMemo( - () => ({ accountId: poolAssetAccountId }), - [poolAssetAccountId], - ) - const poolAssetAccountMetadata = useAppSelector(state => - selectPortfolioAccountMetadataByAccountId(state, poolAssetAccountMetadataFilter), - ) const poolAssetAccountNumberFilter = useMemo(() => { return { assetId: assetId ?? '', accountId: poolAssetAccountId ?? '' } }, [assetId, poolAssetAccountId]) @@ -475,6 +479,46 @@ export const AddLiquidityInput: React.FC = ({ return '0' }, [opportunityType, virtualRuneDepositAmountFiatUserCurrency]) + const thorchainNotationPoolAssetId = useMemo(() => { + if (!poolAsset) return undefined + return assetIdToPoolAssetId({ + assetId: poolAsset.assetId, + }) + }, [poolAsset]) + + // Note, bps is a placeholder and not the actual bps here, as this memo is just used to estimate fees + const feeEstimationMemo = useMemo(() => { + if (thorchainNotationPoolAssetId === undefined) return null + + if (opportunityType === 'sym') { + return `+:${thorchainNotationPoolAssetId}:${poolAssetAccountAddress ?? ''}:ss:50` + } + + return `+:${thorchainNotationPoolAssetId}::ss:50` + }, [opportunityType, poolAssetAccountAddress, thorchainNotationPoolAssetId]) + + const { + estimatedFeesData: estimatedPoolAssetFeesData, + isEstimatedFeesDataLoading: isEstimatedPoolAssetFeesDataLoading, + isEstimatedFeesDataError: isEstimatedPoolAssetFeesDataError, + inboundAddress: poolAssetInboundAddress, + } = useSendThorTx({ + assetId: poolAsset?.assetId, + accountId: poolAssetAccountId, + amountCryptoBaseUnit: toBaseUnit( + actualAssetDepositAmountCryptoPrecision, + poolAsset?.precision ?? 0, + ), + memo: feeEstimationMemo, + fromAddress: poolAssetAccountAddress ?? null, + action: 'addLiquidity', + enableEstimateFees: Boolean( + bnOrZero(actualAssetDepositAmountCryptoPrecision).gt(0) && + !isApprovalRequired && + incompleteSide !== AsymSide.Rune, + ), + }) + const hasEnoughAssetBalance = useMemo(() => { if (incompleteSide === AsymSide.Rune) return true @@ -491,20 +535,6 @@ export const AddLiquidityInput: React.FC = ({ poolAssetBalanceCryptoBaseUnit, ]) - const { data: inboundAddressesData, isLoading: isInboundAddressesDataLoading } = useQuery({ - ...reactQueries.thornode.inboundAddresses(), - enabled: !!poolAsset, - select: data => selectInboundAddressData(data, poolAsset?.assetId), - // @lukemorales/query-key-factory only returns queryFn and queryKey - all others will be ignored in the returned object - // Go stale instantly - staleTime: 0, - // Never store queries in cache since we always want fresh data - gcTime: 0, - refetchOnWindowFocus: true, - refetchOnMount: true, - refetchInterval: 60_000, - }) - const { isTradingActive, isLoading: isTradingActiveLoading } = useIsTradingActive({ assetId: poolAsset?.assetId, enabled: Boolean(poolAsset?.assetId), @@ -525,7 +555,7 @@ export const AddLiquidityInput: React.FC = ({ } = useMutation({ ...reactQueries.mutations.approve({ assetId: poolAsset?.assetId, - spender: inboundAddressesData?.router, + spender: poolAssetInboundAddress, from: poolAssetAccountAddress, amount: toBaseUnit(actualAssetDepositAmountCryptoPrecision, poolAsset?.precision ?? 0), wallet, @@ -551,7 +581,7 @@ export const AddLiquidityInput: React.FC = ({ await queryClient.invalidateQueries( reactQueries.common.allowanceCryptoBaseUnit( poolAsset?.assetId, - inboundAddressesData?.router, + poolAssetInboundAddress, poolAssetAccountAddress, ), ) @@ -559,7 +589,7 @@ export const AddLiquidityInput: React.FC = ({ }, [ approvalTx, poolAsset?.assetId, - inboundAddressesData?.router, + poolAssetInboundAddress, isApprovalTxPending, poolAssetAccountAddress, queryClient, @@ -567,11 +597,11 @@ export const AddLiquidityInput: React.FC = ({ const { data: allowanceData, isLoading: isAllowanceDataLoading } = useAllowance({ assetId: poolAsset?.assetId, - spender: inboundAddressesData?.router, + spender: poolAssetInboundAddress, from: poolAssetAccountAddress, }) - const isApprovalRequired = useMemo(() => { + const _isApprovalRequired = useMemo(() => { if (!confirmedQuote) return false if (!poolAsset) return false if (incompleteSide === AsymSide.Rune) return false @@ -591,150 +621,10 @@ export const AddLiquidityInput: React.FC = ({ poolAsset, ]) - useEffect(() => { - if (!(wallet && poolAsset && activeOpportunityId && poolAssetAccountMetadata)) return - ;(async () => { - const _accountAssetAddress = await getThorchainFromAddress({ - accountId: poolAssetAccountId, - assetId: poolAsset?.assetId, - opportunityId: activeOpportunityId, - wallet, - accountMetadata: poolAssetAccountMetadata, - getPosition: getThorchainLpPosition, - }) - setPoolAssetAccountAddress(_accountAssetAddress) - })() - }, [activeOpportunityId, poolAsset, poolAssetAccountId, poolAssetAccountMetadata, wallet]) + useEffect(() => setIsApprovalRequired(_isApprovalRequired), [_isApprovalRequired]) // Pool asset fee/balance/sweep data and checks - const poolAssetInboundAddress = useMemo(() => { - if (!poolAsset) return - const transactionType = getThorchainLpTransactionType(poolAsset.chainId) - - switch (transactionType) { - case 'MsgDeposit': { - return THORCHAIN_POOL_MODULE_ADDRESS - } - case 'EvmCustomTx': { - // TODO: this should really be inboundAddressData?.router, but useQuoteEstimatedFeesQuery doesn't yet handle contract calls - // for the purpose of naively assuming a send, using the inbound address instead of the router is fine - return inboundAddressesData?.address - } - case 'Send': { - return inboundAddressesData?.address - } - default: { - assertUnreachable(transactionType as never) - } - } - }, [poolAsset, inboundAddressesData?.address]) - - const thorchainNotationPoolAssetId = useMemo(() => { - if (!poolAsset) return undefined - return assetIdToPoolAssetId({ - assetId: poolAsset.assetId, - }) - }, [poolAsset]) - - const memo = useMemo(() => { - if (thorchainNotationPoolAssetId === undefined) return - - if (opportunityType === 'sym') { - return `+:${thorchainNotationPoolAssetId}:${poolAssetAccountAddress ?? ''}:ss:50` - } - - return `+:${thorchainNotationPoolAssetId}::ss:50` - // Note, bps is a placeholder and not the actual bps here, this memo is just used to estimate fees - }, [opportunityType, poolAssetAccountAddress, thorchainNotationPoolAssetId]) - - const estimateFeesArgs = useMemo(() => { - if (!assetId || !wallet || !poolAsset || !memo || !poolAssetAccountAddress) return undefined - - const amountCryptoBaseUnit = toBaseUnit( - actualAssetDepositAmountCryptoPrecision, - poolAsset.precision, - ) - - const transactionType = getThorchainLpTransactionType(poolAsset.chainId) - - switch (transactionType) { - case 'EvmCustomTx': { - if (!inboundAddressesData?.router) return undefined - - const data = depositWithExpiry({ - vault: getAddress(inboundAddressesData.address), - asset: isToken(fromAssetId(assetId).assetReference) - ? getAddress(fromAssetId(assetId).assetReference) - : // Native EVM asset deposits use the 0 address as the asset address - // https://dev.thorchain.org/concepts/sending-transactions.html#admonition-info-1 - zeroAddress, - amount: amountCryptoBaseUnit, - memo, - expiry: BigInt(dayjs().add(15, 'minute').unix()), - }) - - return { - // amountCryptoPrecision is always denominated in fee asset - the only value we can send when calling a contract is native asset value - amountCryptoPrecision: isToken(fromAssetId(assetId).assetReference) - ? '0' - : actualAssetDepositAmountCryptoPrecision, - // It's a regular 0-value contract-call - assetId: poolAsset?.assetId, - to: inboundAddressesData.router, - from: poolAssetAccountAddress, - sendMax: false, - // This is an ERC-20, we abuse the memo field for the actual hex-encoded calldata - memo: data, - accountId: poolAssetAccountId, - // Note, this is NOT a send. - // contractAddress is only needed when doing a send and the account interacts *directly* with the token's contract address. - // Here, the LP contract is approved beforehand to spend the token value, which it will when calling depositWithExpiry() - contractAddress: undefined, - } - } - case 'Send': { - if (!inboundAddressesData) return undefined - return { - amountCryptoPrecision: actualAssetDepositAmountCryptoPrecision, - assetId: poolAsset.assetId, - to: inboundAddressesData.address, - from: poolAssetAccountAddress, - sendMax: false, - memo, - accountId: poolAssetAccountId, - contractAddress: undefined, - } - } - default: - return undefined - } - }, [ - assetId, - wallet, - poolAsset, - memo, - poolAssetAccountAddress, - actualAssetDepositAmountCryptoPrecision, - inboundAddressesData, - poolAssetAccountId, - ]) - - const { - data: estimatedPoolAssetFeesData, - isLoading: isEstimatedPoolAssetFeesDataLoading, - isError: isEstimatedPoolAssetFeesDataError, - } = useGetEstimatedFeesQuery({ - amountCryptoPrecision: estimateFeesArgs?.amountCryptoPrecision ?? '0', - assetId: estimateFeesArgs?.assetId ?? '', - to: estimateFeesArgs?.to ?? '', - sendMax: estimateFeesArgs?.sendMax ?? false, - memo: estimateFeesArgs?.memo ?? '', - accountId: estimateFeesArgs?.accountId ?? '', - contractAddress: estimateFeesArgs?.contractAddress ?? '', - enabled: Boolean(estimateFeesArgs && !isApprovalRequired && incompleteSide !== AsymSide.Rune), - }) - useEffect(() => { if (!estimatedPoolAssetFeesData) return setPoolAssetTxFeeCryptoBaseUnit(estimatedPoolAssetFeesData.txFeeCryptoBaseUnit) @@ -768,7 +658,7 @@ export const AddLiquidityInput: React.FC = ({ if (bnOrZero(actualAssetDepositAmountCryptoPrecision).isZero()) return true if (incompleteSide === AsymSide.Rune) return true - if (!poolAssetTxFeeCryptoBaseUnit || !poolAsset) return false + if ((!isApprovalRequired && !poolAssetTxFeeCryptoBaseUnit) || !poolAsset) return false // If the asset is not a token, assume it's a native asset and fees are taken from the same asset balance if (!isToken(fromAssetId(poolAsset.assetId).assetReference)) { @@ -777,7 +667,7 @@ export const AddLiquidityInput: React.FC = ({ poolAsset?.precision, ) return bnOrZero(assetAmountCryptoBaseUnit) - .plus(poolAssetTxFeeCryptoBaseUnit) + .plus(bnOrZero(poolAssetTxFeeCryptoBaseUnit)) .lte(poolAssetBalanceCryptoBaseUnit) } @@ -787,6 +677,7 @@ export const AddLiquidityInput: React.FC = ({ }, [ actualAssetDepositAmountCryptoPrecision, incompleteSide, + isApprovalRequired, poolAsset, poolAssetBalanceCryptoBaseUnit, poolAssetFeeAssetBalanceCryptoBaseUnit, @@ -848,17 +739,23 @@ export const AddLiquidityInput: React.FC = ({ // Rune balance / gas data and checks - // We reuse lending utils here since all this does is estimating fees for a given deposit amount with a memo const { - data: estimatedRuneFeesData, - isLoading: isEstimatedRuneFeesDataLoading, - isError: isEstimatedRuneFeesDataError, - } = useQuoteEstimatedFeesQuery({ - collateralAssetId: thorchainAssetId, - collateralAccountId: runeAccountId, - depositAmountCryptoPrecision: actualRuneDepositAmountCryptoPrecision ?? '0', - confirmedQuote, - enabled: incompleteSide !== AsymSide.Asset, + estimatedFeesData: estimatedRuneFeesData, + isEstimatedFeesDataLoading: isEstimatedRuneFeesDataLoading, + isEstimatedFeesDataError: isEstimatedRuneFeesDataError, + } = useSendThorTx({ + assetId: thorchainAssetId, + accountId: runeAccountId, + amountCryptoBaseUnit: toBaseUnit( + actualRuneDepositAmountCryptoPrecision, + runeAsset?.precision ?? 0, + ), + memo: feeEstimationMemo, + fromAddress: null, + action: 'addLiquidity', + enableEstimateFees: Boolean( + bnOrZero(actualRuneDepositAmountCryptoPrecision).gt(0) && incompleteSide !== AsymSide.Asset, + ), }) useEffect(() => { @@ -920,10 +817,29 @@ export const AddLiquidityInput: React.FC = ({ [runeMarketData.price, runeTxFeeCryptoPrecision], ) - const totalGasFeeFiatUserCurrency = useMemo( - () => poolAssetGasFeeFiatUserCurrency.plus(runeGasFeeFiatUserCurrency), - [poolAssetGasFeeFiatUserCurrency, runeGasFeeFiatUserCurrency], - ) + const totalGasFeeFiatUserCurrency = useMemo(() => { + if (!opportunityType) return bn(0) + + switch (opportunityType) { + case AsymSide.Rune: + return runeGasFeeFiatUserCurrency + case AsymSide.Asset: + return poolAssetGasFeeFiatUserCurrency + case 'sym': { + if (!poolAssetTxFeeCryptoBaseUnit) return bn(0) + if (!runeTxFeeCryptoBaseUnit) return bn(0) + return poolAssetGasFeeFiatUserCurrency.plus(runeGasFeeFiatUserCurrency) + } + default: + assertUnreachable(opportunityType) + } + }, [ + opportunityType, + poolAssetGasFeeFiatUserCurrency, + poolAssetTxFeeCryptoBaseUnit, + runeGasFeeFiatUserCurrency, + runeTxFeeCryptoBaseUnit, + ]) const handleApprove = useCallback(() => mutate(undefined), [mutate]) @@ -1048,7 +964,6 @@ export const AddLiquidityInput: React.FC = ({ if (!slippageFiatUserCurrency) return if (!activeOpportunityId) return - if (!poolAssetInboundAddress) return if (!actualAssetDepositAmountCryptoPrecision) return if (!actualAssetDepositAmountFiatUserCurrency) return if (!actualRuneDepositAmountCryptoPrecision) return @@ -1084,7 +999,6 @@ export const AddLiquidityInput: React.FC = ({ feeAmountFiatUserCurrency: feeUsd.times(userCurrencyToUsdRate).toFixed(2), feeAmountUSD: feeUsd.toFixed(2), assetAddress: poolAssetAccountAddress, - quoteInboundAddress: poolAssetInboundAddress, runeGasFeeFiatUserCurrency: runeGasFeeFiatUserCurrency.toFixed(2), poolAssetGasFeeFiatUserCurrency: poolAssetGasFeeFiatUserCurrency.toFixed(2), totalGasFeeFiatUserCurrency: totalGasFeeFiatUserCurrency.toFixed(2), @@ -1101,7 +1015,6 @@ export const AddLiquidityInput: React.FC = ({ isVotingPowerLoading, poolAssetAccountAddress, poolAssetGasFeeFiatUserCurrency, - poolAssetInboundAddress, position, runeGasFeeFiatUserCurrency, setConfirmedQuote, @@ -1315,6 +1228,12 @@ export const AddLiquidityInput: React.FC = ({ const handleAssetChange = useCallback( (asset: Asset) => { const type = getDefaultOpportunityType(asset.assetId) + setPoolAssetTxFeeCryptoBaseUnit(undefined) + setRuneTxFeeCryptoBaseUnit(undefined) + setVirtualAssetDepositAmountCryptoPrecision('0') + setVirtualAssetDepositAmountFiatUserCurrency('0') + setVirtualRuneDepositAmountCryptoPrecision('0') + setVirtualRuneDepositAmountFiatUserCurrency('0') setActiveOpportunityId(toOpportunityId({ assetId: asset.assetId, type })) }, [getDefaultOpportunityType], @@ -1635,13 +1554,11 @@ export const AddLiquidityInput: React.FC = ({ isLoading={ (poolAssetTxFeeCryptoBaseUnit === undefined && isEstimatedPoolAssetFeesDataLoading) || isVotingPowerLoading || - isInboundAddressesDataLoading || isTradingActiveLoading || isSmartContractAccountAddressLoading || isAllowanceDataLoading || isApprovalTxPending || (isSweepNeeded === undefined && isSweepNeededLoading) || - isInboundAddressesDataLoading || (runeTxFeeCryptoBaseUnit === undefined && isEstimatedPoolAssetFeesDataLoading) } onClick={handleDepositSubmit} diff --git a/src/pages/ThorChainLP/components/RemoveLiquidity/RemoveLiquidityInput.tsx b/src/pages/ThorChainLP/components/RemoveLiquidity/RemoveLiquidityInput.tsx index fade7ec3d55..17268bed295 100644 --- a/src/pages/ThorChainLP/components/RemoveLiquidity/RemoveLiquidityInput.tsx +++ b/src/pages/ThorChainLP/components/RemoveLiquidity/RemoveLiquidityInput.tsx @@ -31,8 +31,6 @@ import { FaPlus } from 'react-icons/fa6' import { useTranslate } from 'react-polyglot' import { reactQueries } from 'react-queries' import { useIsTradingActive } from 'react-queries/hooks/useIsTradingActive' -import { useQuoteEstimatedFeesQuery } from 'react-queries/hooks/useQuoteEstimatedFeesQuery' -import { selectInboundAddressData } from 'react-queries/selectors' import { useHistory } from 'react-router' import { Amount } from 'components/Amount/Amount' import { AssetInput } from 'components/DeFi/components/AssetInput' @@ -51,12 +49,10 @@ import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton' import { MixPanelEvent } from 'lib/mixpanel/types' import { THORCHAIN_OUTBOUND_FEE_RUNE_THOR_UNIT } from 'lib/swapper/swappers/ThorchainSwapper/constants' import { assertUnreachable } from 'lib/utils' -import { fromThorBaseUnit, getThorchainFromAddress } from 'lib/utils/thorchain' -import { THOR_PRECISION, THORCHAIN_POOL_MODULE_ADDRESS } from 'lib/utils/thorchain/constants' -import { - estimateRemoveThorchainLiquidityPosition, - getThorchainLpTransactionType, -} from 'lib/utils/thorchain/lp' +import { fromThorBaseUnit } from 'lib/utils/thorchain' +import { THOR_PRECISION } from 'lib/utils/thorchain/constants' +import { useSendThorTx } from 'lib/utils/thorchain/hooks/useSendThorTx' +import { estimateRemoveThorchainLiquidityPosition } from 'lib/utils/thorchain/lp' import type { LpConfirmedWithdrawalQuote, UserLpDataPosition } from 'lib/utils/thorchain/lp/types' import { AsymSide } from 'lib/utils/thorchain/lp/types' import { isLpConfirmedDepositQuote } from 'lib/utils/thorchain/lp/utils' @@ -65,7 +61,6 @@ import { usePool } from 'pages/ThorChainLP/queries/hooks/usePool' import { useUserLpData } from 'pages/ThorChainLP/queries/hooks/useUserLpData' import { getThorchainLpPosition } from 'pages/ThorChainLP/queries/queries' import { fromOpportunityId } from 'pages/ThorChainLP/utils' -import { THORCHAIN_SAVERS_DUST_THRESHOLDS_CRYPTO_BASE_UNIT } from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/utils' import { selectAccountIdsByAssetId, selectAssetById, @@ -74,6 +69,7 @@ import { selectPortfolioAccountMetadataByAccountId, selectPortfolioCryptoBalanceBaseUnitByFilter, } from 'state/slices/selectors' +import { convertPercentageToBasisPoints } from 'state/slices/tradeQuoteSlice/utils' import { useAppSelector } from 'state/store' import { RemoveLiquidityRoutePaths } from './types' @@ -126,9 +122,6 @@ export const RemoveLiquidityInput: React.FC = ({ const [percentageSelection, setPercentageSelection] = useState(INITIAL_REMOVAL_PERCENTAGE) const [sliderValue, setSliderValue] = useState(INITIAL_REMOVAL_PERCENTAGE) const [shareOfPoolDecimalPercent, setShareOfPoolDecimalPercent] = useState() - const [poolAssetAccountAddress, setPoolAssetAccountAddress] = useState( - undefined, - ) const [shouldShowWarningAcknowledgement, setShouldShowWarningAcknowledgement] = useState(false) const { assetId, type: opportunityType } = useMemo( @@ -182,19 +175,6 @@ export const RemoveLiquidityInput: React.FC = ({ selectPortfolioCryptoBalanceBaseUnitByFilter(state, runeBalanceFilter), ) - const { data: inboundAddressesData } = useQuery({ - ...reactQueries.thornode.inboundAddresses(), - select: data => selectInboundAddressData(data, assetId), - // @lukemorales/query-key-factory only returns queryFn and queryKey - all others will be ignored in the returned object - // Go stale instantly - staleTime: 0, - // Never store queries in cache since we always want fresh data - gcTime: 0, - refetchOnWindowFocus: true, - refetchOnMount: true, - refetchInterval: 60_000, - }) - const { isTradingActive, isLoading: isTradingActiveLoading } = useIsTradingActive({ assetId: poolAsset?.assetId, enabled: !!poolAsset, @@ -384,55 +364,67 @@ export const RemoveLiquidityInput: React.FC = ({ ) }, [percentageSelection, position]) - const poolAssetFeeAssetDustAmountCryptoPrecision = useMemo(() => { - if (!poolAssetFeeAsset) return '0' - const dustAmountCryptoBaseUnit = - THORCHAIN_SAVERS_DUST_THRESHOLDS_CRYPTO_BASE_UNIT[poolAssetFeeAsset?.assetId] ?? '0' - return fromBaseUnit(dustAmountCryptoBaseUnit, poolAssetFeeAsset?.precision) - }, [poolAssetFeeAsset]) - - const runeDustAmountCryptoPrecision = useMemo(() => { - if (!runeAsset) return '0' - const dustAmountCryptoBaseUnit = - THORCHAIN_SAVERS_DUST_THRESHOLDS_CRYPTO_BASE_UNIT[runeAsset?.assetId] ?? '0' - return fromBaseUnit(dustAmountCryptoBaseUnit, runeAsset?.precision) - }, [runeAsset]) - - // We reuse lending utils here since all this does is estimating fees for a given withdrawal amount with a memo - // It's not going to be 100% accurate for EVM chains as it doesn't calculate the cost of depositWithExpiry, but rather a simple send, - // however that's fine for now until accurate fees estimation is implemented + const memo = useMemo(() => { + const withdrawalBps = convertPercentageToBasisPoints(percentageSelection).toFixed() + return `-:${poolAssetId}:${withdrawalBps}` + }, [poolAssetId, percentageSelection]) + const { - data: estimatedRuneFeesData, - isLoading: isEstimatedRuneFeesDataLoading, - isError: isEstimatedRuneFeesDataError, - } = useQuoteEstimatedFeesQuery({ - collateralAssetId: thorchainAssetId, - collateralAccountId: runeAccountId ?? '', // This will be undefined for asym asset side LPs, and that's ok - repaymentAccountId: runeAccountId ?? '', // This will be undefined for asym asset side LPs, and that's ok - repaymentAsset: runeAsset ?? null, - repaymentAmountCryptoPrecision: runeDustAmountCryptoPrecision, - confirmedQuote, + estimatedFeesData: estimatedRuneFeesData, + isEstimatedFeesDataLoading: isEstimatedRuneFeesDataLoading, + isEstimatedFeesDataError: isEstimatedRuneFeesDataError, + dustAmountCryptoBaseUnit: runeDustAmountCryptoBaseUnit, + } = useSendThorTx({ + assetId: thorchainAssetId, + accountId: runeAccountId ?? null, + // withdraw liquidity will use dust amount + amountCryptoBaseUnit: null, + memo, + fromAddress: null, + action: 'withdrawLiquidity', + enableEstimateFees: Boolean(opportunityType !== AsymSide.Asset), + }) + + const poolAssetAccountMetadataFilter = useMemo(() => ({ accountId }), [accountId]) + const poolAssetAccountMetadata = useAppSelector(state => + selectPortfolioAccountMetadataByAccountId(state, poolAssetAccountMetadataFilter), + ) + + const { data: poolAssetAccountAddress } = useQuery({ + ...reactQueries.common.thorchainFromAddress({ + accountId, + assetId: poolAsset?.assetId!, + opportunityId, + wallet: wallet!, + accountMetadata: poolAssetAccountMetadata!, + getPosition: getThorchainLpPosition, + }), + enabled: Boolean(poolAsset?.assetId && wallet && poolAssetAccountMetadata), }) const { - data: estimatedPoolAssetFeesData, - isLoading: isEstimatedPoolAssetFeesDataLoading, - isError: isEstimatedPoolAssetFeesDataError, - } = useQuoteEstimatedFeesQuery({ - // Sym opportunities do *not* require a pool asset Tx, all we need is a RUNE Tx to trigger the withdraw - enabled: opportunityType !== 'sym', - collateralAssetId: poolAssetFeeAsset?.assetId ?? '', - collateralAccountId: accountId, - repaymentAccountId: accountId, - repaymentAsset: poolAssetFeeAsset ?? null, - confirmedQuote, - repaymentAmountCryptoPrecision: poolAssetFeeAssetDustAmountCryptoPrecision, + estimatedFeesData: estimatedPoolAssetFeesData, + isEstimatedFeesDataLoading: isEstimatedPoolAssetFeesDataLoading, + isEstimatedFeesDataError: isEstimatedPoolAssetFeesDataError, + dustAmountCryptoBaseUnit: poolAssetFeeAssetDustAmountCryptoBaseUnit, + outboundFeeCryptoBaseUnit, + } = useSendThorTx({ + // Asym asset withdraws are the only ones occurring an asset Tx - both sym and asym RUNE side withdraws occur a RUNE Tx instead + enableEstimateFees: Boolean(opportunityType === AsymSide.Asset), + assetId: poolAsset?.assetId, + accountId, + // withdraw liquidity will use dust amount + amountCryptoBaseUnit: null, + memo, + fromAddress: poolAssetAccountAddress ?? null, + action: 'withdrawLiquidity', }) const poolAssetProtocolFeeCryptoPrecision = useMemo(() => { + if (!poolAssetFeeAsset || !outboundFeeCryptoBaseUnit) return bn(0) if (bnOrZero(actualAssetWithdrawAmountCryptoPrecision).eq(0)) return bn(0) - return fromThorBaseUnit(inboundAddressesData?.outbound_fee ?? '0') - }, [inboundAddressesData?.outbound_fee, actualAssetWithdrawAmountCryptoPrecision]) + return bnOrZero(fromBaseUnit(outboundFeeCryptoBaseUnit, poolAssetFeeAsset.precision)) + }, [outboundFeeCryptoBaseUnit, actualAssetWithdrawAmountCryptoPrecision, poolAssetFeeAsset]) const poolAssetProtocolFeeFiatUserCurrency = useMemo(() => { return poolAssetProtocolFeeCryptoPrecision.times(poolAssetFeeAssetMarketData.price) @@ -511,28 +503,6 @@ export const RemoveLiquidityInput: React.FC = ({ ) }, []) - const poolAssetInboundAddress = useMemo(() => { - if (!poolAsset) return - - const transactionType = getThorchainLpTransactionType(poolAsset.chainId) - - switch (transactionType) { - case 'MsgDeposit': - return THORCHAIN_POOL_MODULE_ADDRESS - - case 'EvmCustomTx': - // TODO: this should really be inboundAddressData?.router, but useQuoteEstimatedFeesQuery doesn't yet handle contract calls - // for the purpose of naively assuming a send, using the inbound address instead of the router is fine - return inboundAddressesData?.address - - case 'Send': - return inboundAddressesData?.address - - default: - assertUnreachable(transactionType as never) - } - }, [poolAsset, inboundAddressesData?.address]) - const renderHeader = useMemo(() => { if (headerComponent) return headerComponent return ( @@ -613,7 +583,7 @@ export const RemoveLiquidityInput: React.FC = ({ const estimate = await estimateRemoveThorchainLiquidityPosition({ liquidityUnits: position?.liquidityUnits, - bps: bnOrZero(percentageSelection).times(100).toFixed(), + bps: convertPercentageToBasisPoints(percentageSelection).toFixed(), assetId: poolAsset.assetId, runeAmountThorBaseUnit, assetAmountThorBaseUnit, @@ -646,7 +616,6 @@ export const RemoveLiquidityInput: React.FC = ({ if (!actualRuneWithdrawAmountCryptoPrecision) return if (!actualRuneWithdrawAmountFiatUserCurrency) return if (!shareOfPoolDecimalPercent) return - if (!poolAssetInboundAddress) return setConfirmedQuote({ assetWithdrawAmountCryptoPrecision: actualAssetWithdrawAmountCryptoPrecision, @@ -656,12 +625,11 @@ export const RemoveLiquidityInput: React.FC = ({ shareOfPoolDecimalPercent, slippageFiatUserCurrency, opportunityId, - quoteInboundAddress: poolAssetInboundAddress, runeGasFeeFiatUserCurrency: runeGasFeeFiatUserCurrency.toFixed(2), poolAssetGasFeeFiatUserCurrency: poolAssetGasFeeFiatUserCurrency.toFixed(2), totalGasFeeFiatUserCurrency: totalGasFeeFiatUserCurrency.toFixed(2), feeBps: '0', - withdrawalBps: bnOrZero(percentageSelection).times(100).toString(), + withdrawalBps: convertPercentageToBasisPoints(percentageSelection).toString(), currentAccountIdByChainId, assetAddress: poolAssetAccountAddress, positionStatus: position?.status, @@ -676,7 +644,6 @@ export const RemoveLiquidityInput: React.FC = ({ percentageSelection, poolAsset, poolAssetGasFeeFiatUserCurrency, - poolAssetInboundAddress, position, runeAccountId, runeGasFeeFiatUserCurrency, @@ -688,26 +655,6 @@ export const RemoveLiquidityInput: React.FC = ({ poolAssetAccountAddress, ]) - const poolAssetAccountMetadataFilter = useMemo(() => ({ accountId }), [accountId]) - const poolAssetAccountMetadata = useAppSelector(state => - selectPortfolioAccountMetadataByAccountId(state, poolAssetAccountMetadataFilter), - ) - - useEffect(() => { - if (!(wallet && poolAsset && opportunityId && poolAssetAccountMetadata)) return - ;(async () => { - const _accountAssetAddress = await getThorchainFromAddress({ - accountId, - assetId: poolAsset?.assetId, - opportunityId, - wallet, - accountMetadata: poolAssetAccountMetadata, - getPosition: getThorchainLpPosition, - }) - setPoolAssetAccountAddress(_accountAssetAddress) - })() - }, [accountId, opportunityId, poolAsset, poolAssetAccountMetadata, wallet]) - const isDeposit = useMemo(() => isLpConfirmedDepositQuote(confirmedQuote), [confirmedQuote]) const isSymWithdraw = useMemo( () => opportunityType === 'sym' && !isDeposit, @@ -880,6 +827,11 @@ export const RemoveLiquidityInput: React.FC = ({ poolAssetFeeAsset?.precision, ) + const poolAssetFeeAssetDustAmountCryptoPrecision = fromBaseUnit( + poolAssetFeeAssetDustAmountCryptoBaseUnit, + poolAssetFeeAsset?.precision, + ) + return bnOrZero(poolAssetTxFeeCryptoPrecision) .plus(poolAssetFeeAssetDustAmountCryptoPrecision) .lte(poolAssetFeeAssetBalanceCryptoPrecision) @@ -888,7 +840,7 @@ export const RemoveLiquidityInput: React.FC = ({ opportunityType, poolAssetFeeAsset, poolAssetFeeAssetBalanceCryptoBaseUnit, - poolAssetFeeAssetDustAmountCryptoPrecision, + poolAssetFeeAssetDustAmountCryptoBaseUnit, poolAssetTxFeeCryptoPrecision, ]) @@ -899,6 +851,10 @@ export const RemoveLiquidityInput: React.FC = ({ if (!runeAsset) return false const runeBalanceCryptoPrecision = fromBaseUnit(runeBalanceCryptoBaseUnit, runeAsset?.precision) + const runeDustAmountCryptoPrecision = fromBaseUnit( + runeDustAmountCryptoBaseUnit, + runeAsset?.precision, + ) return bnOrZero(runeTxFeeCryptoPrecision) .plus(runeDustAmountCryptoPrecision) @@ -908,7 +864,7 @@ export const RemoveLiquidityInput: React.FC = ({ opportunityType, runeAsset, runeBalanceCryptoBaseUnit, - runeDustAmountCryptoPrecision, + runeDustAmountCryptoBaseUnit, runeTxFeeCryptoPrecision, ]) diff --git a/src/pages/ThorChainLP/components/ReusableLpStatus/TransactionRow.tsx b/src/pages/ThorChainLP/components/ReusableLpStatus/TransactionRow.tsx index 513cb95bf2c..e0b24d46cf5 100644 --- a/src/pages/ThorChainLP/components/ReusableLpStatus/TransactionRow.tsx +++ b/src/pages/ThorChainLP/components/ReusableLpStatus/TransactionRow.tsx @@ -1,4 +1,3 @@ -import { ExternalLinkIcon } from '@chakra-ui/icons' import { Button, Card, @@ -9,26 +8,12 @@ import { Flex, Link, Skeleton, - Text, - useToast, } from '@chakra-ui/react' -import { - type AssetId, - fromAccountId, - fromAssetId, - thorchainAssetId, - thorchainChainId, -} from '@shapeshiftoss/caip' -import { - CONTRACT_INTERACTION, - type FeeDataEstimate, - FeeDataKey, -} from '@shapeshiftoss/chain-adapters' +import type { AssetId } from '@shapeshiftoss/caip' +import { fromAssetId, thorchainAssetId, thorchainChainId } from '@shapeshiftoss/caip' import { SwapperName } from '@shapeshiftoss/swapper' -import type { KnownChainIds } from '@shapeshiftoss/types' import { TxStatus } from '@shapeshiftoss/unchained-client' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import dayjs from 'dayjs' import { useCallback, useEffect, useMemo, useState } from 'react' import { FaCheck } from 'react-icons/fa' import { FaX } from 'react-icons/fa6' @@ -36,12 +21,9 @@ import { useTranslate } from 'react-polyglot' import { reactQueries } from 'react-queries' import { useIsTradingActive } from 'react-queries/hooks/useIsTradingActive' import { selectInboundAddressData } from 'react-queries/selectors' -import { getAddress, zeroAddress } from 'viem' import { Amount } from 'components/Amount/Amount' import { AssetIcon } from 'components/AssetIcon' import { CircularProgress } from 'components/CircularProgress/CircularProgress' -import type { SendInput } from 'components/Modals/Send/Form' -import { estimateFees, handleSend } from 'components/Modals/Send/utils' import { Row } from 'components/Row/Row' import { useWallet } from 'hooks/useWallet/useWallet' import { getTxLink } from 'lib/getTxLink' @@ -50,16 +32,9 @@ import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton' import { MixPanelEvent } from 'lib/mixpanel/types' import { sleep } from 'lib/poll/poll' import { assetIdToPoolAssetId } from 'lib/swapper/swappers/ThorchainSwapper/utils/poolAssetHelpers/poolAssetHelpers' -import { assertUnreachable, isToken } from 'lib/utils' -import { assertGetThorchainChainAdapter } from 'lib/utils/cosmosSdk' -import { - assertGetEvmChainAdapter, - buildAndBroadcast, - createBuildCustomTxInput, -} from 'lib/utils/evm' -import { getThorchainFromAddress, waitForThorchainUpdate } from 'lib/utils/thorchain' -import { THORCHAIN_POOL_MODULE_ADDRESS } from 'lib/utils/thorchain/constants' -import { getThorchainLpTransactionType } from 'lib/utils/thorchain/lp' +import { waitForThorchainUpdate } from 'lib/utils/thorchain' +import { useSendThorTx } from 'lib/utils/thorchain/hooks/useSendThorTx' +import { useThorchainFromAddress } from 'lib/utils/thorchain/hooks/useThorchainFromAddress' import type { LpConfirmedDepositQuote, LpConfirmedWithdrawalQuote, @@ -68,20 +43,14 @@ import { isLpConfirmedDepositQuote, isLpConfirmedWithdrawalQuote, } from 'lib/utils/thorchain/lp/utils' -import { depositWithExpiry } from 'lib/utils/thorchain/routerCalldata' -import { useGetEstimatedFeesQuery } from 'pages/Lending/hooks/useGetEstimatedFeesQuery' import { getThorchainLpPosition } from 'pages/ThorChainLP/queries/queries' import type { OpportunityType } from 'pages/ThorChainLP/utils' -import { THORCHAIN_SAVERS_DUST_THRESHOLDS_CRYPTO_BASE_UNIT } from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/utils' import { - selectAccountNumberByAccountId, selectAssetById, selectFeeAssetByChainId, selectPortfolioAccountMetadataByAccountId, - selectSelectedCurrency, selectTxById, } from 'state/slices/selectors' -import { serializeTxIndex } from 'state/slices/txHistorySlice/utils' import { useAppSelector } from 'state/store' type TransactionRowProps = { @@ -106,19 +75,14 @@ export const TransactionRow: React.FC = ({ confirmedQuote, opportunityType, }) => { - const toast = useToast() const queryClient = useQueryClient() const translate = useTranslate() - const selectedCurrency = useAppSelector(selectSelectedCurrency) const wallet = useWallet().state.wallet const mixpanel = getMixPanel() const [status, setStatus] = useState(TxStatus.Unknown) const [isSubmitting, setIsSubmitting] = useState(false) - const [txId, setTxId] = useState(null) const [txFeeCryptoPrecision, setTxFeeCryptoPrecision] = useState() - const [assetAddress, setAssetAddress] = useState(null) - const [pairAssetAddress, setPairAssetAddress] = useState(null) const { currentAccountIdByChainId, positionStatus } = confirmedQuote @@ -129,13 +93,6 @@ export const TransactionRow: React.FC = ({ const poolAsset = useAppSelector(state => selectAssetById(state, poolAssetId)) const poolAssetAccountId = currentAccountIdByChainId[fromAssetId(poolAssetId).chainId] - const poolAssetAccountNumberFilter = useMemo( - () => ({ assetId: poolAssetId, accountId: poolAssetAccountId }), - [poolAssetAccountId, poolAssetId], - ) - const poolAssetAccountNumber = useAppSelector(s => - selectAccountNumberByAccountId(s, poolAssetAccountNumberFilter), - ) const poolAssetAccountFilter = useMemo( () => ({ accountId: poolAssetAccountId }), [poolAssetAccountId], @@ -145,13 +102,6 @@ export const TransactionRow: React.FC = ({ ) const runeAccountId = currentAccountIdByChainId[thorchainChainId] - const runeAccountNumberFilter = useMemo( - () => ({ assetId: thorchainAssetId, accountId: runeAccountId }), - [runeAccountId], - ) - const runeAccountNumber = useAppSelector(s => - selectAccountNumberByAccountId(s, runeAccountNumberFilter), - ) const runeAccountFilter = useMemo(() => ({ accountId: runeAccountId }), [runeAccountId]) const runeAccountMetadata = useAppSelector(state => selectPortfolioAccountMetadataByAccountId(state, runeAccountFilter), @@ -161,73 +111,75 @@ export const TransactionRow: React.FC = ({ const isDeposit = isLpConfirmedDepositQuote(confirmedQuote) const isSymWithdraw = isLpConfirmedWithdrawalQuote(confirmedQuote) && opportunityType === 'sym' - const { - isTradingActive, - refetch: refetchIsTradingActive, - isLoading: isTradingActiveLoading, - } = useIsTradingActive({ - assetId: poolAssetId, - enabled: !txId, - swapperName: SwapperName.Thorchain, + const fromAccountId = useMemo(() => { + return isRuneTx ? runeAccountId : poolAssetAccountId + }, [isRuneTx, runeAccountId, poolAssetAccountId]) + + const fromAccountMetadata = useMemo(() => { + return isRuneTx ? runeAccountMetadata : poolAssetAccountMetadata + }, [isRuneTx, runeAccountMetadata, poolAssetAccountMetadata]) + + const { data: fromAddress } = useQuery({ + ...reactQueries.common.thorchainFromAddress({ + accountId: fromAccountId, + assetId: isRuneTx ? thorchainAssetId : poolAssetId, + opportunityId: confirmedQuote.opportunityId, + wallet: wallet!, + accountMetadata: fromAccountMetadata!, + getPosition: getThorchainLpPosition, + }), + enabled: Boolean(fromAccountId && fromAccountMetadata && wallet), }) - useEffect(() => { - if (!wallet) return - - const accountId = isRuneTx ? runeAccountId : poolAssetAccountId - const assetId = isRuneTx ? thorchainAssetId : poolAssetId - const accountMetadata = isRuneTx ? runeAccountMetadata : poolAssetAccountMetadata - - const pairAssetAssetId = isRuneTx ? poolAssetId : thorchainAssetId - const pairAssetAccountId = isRuneTx ? poolAssetAccountId : runeAccountId - const pairAssetAccountMetadata = isRuneTx ? poolAssetAccountMetadata : runeAccountMetadata - - if (!accountMetadata) return - ;(async () => { - const _assetAddress = await getThorchainFromAddress({ - accountId, - assetId, - opportunityId: confirmedQuote.opportunityId, - wallet, - accountMetadata, - getPosition: getThorchainLpPosition, - }) + const pairAssetAccountId = useMemo(() => { + return isRuneTx ? poolAssetAccountId : runeAccountId + }, [isRuneTx, runeAccountId, poolAssetAccountId]) - // use address as is for use in constructing the transaction (not related to memo) - setAssetAddress(_assetAddress) + const pairAssetAccountMetadata = useMemo(() => { + return isRuneTx ? poolAssetAccountMetadata : runeAccountMetadata + }, [isRuneTx, runeAccountMetadata, poolAssetAccountMetadata]) - // We don't want to set the other asset's address in the memo when doing asym deposits or we'll have bigly problems + const { data: pairAssetAddress } = useThorchainFromAddress({ + accountId: pairAssetAccountId, + assetId: isRuneTx ? poolAssetId : thorchainAssetId, + opportunityId: confirmedQuote.opportunityId, + wallet, + accountMetadata: pairAssetAccountMetadata, + getPosition: getThorchainLpPosition, + // strip bech32 prefix for use in thorchain memo (bech32 not supported) + select: address => { + // Paranoia against previously cached calls, this should never happen but it could if (opportunityType !== 'sym') return - if (!pairAssetAccountMetadata) return - - const _pairAssetAddress = await getThorchainFromAddress({ - accountId: pairAssetAccountId, - assetId: pairAssetAssetId, - opportunityId: confirmedQuote.opportunityId, - wallet, - accountMetadata: pairAssetAccountMetadata, - getPosition: getThorchainLpPosition, - }) + return address.replace('bitcoincash:', '') + }, + enabled: Boolean(opportunityType === 'sym'), + }) - // strip bech32 prefix for use in thorchain memo (bech32 not supported) - setPairAssetAddress(_pairAssetAddress.replace('bitcoincash:', '')) - })() - }, [ - assetId, - opportunityType, - confirmedQuote.opportunityId, - isRuneTx, - poolAssetAccountId, - poolAssetAccountMetadata, - poolAssetId, - runeAccountId, - runeAccountMetadata, - wallet, - ]) + const thorchainNotationAssetId = useMemo(() => { + // TODO(gomes): rename the utils to use the same terminology as well instead of the current poolAssetId one. + // Left as-is for this PR to avoid a bigly diff + return assetIdToPoolAssetId({ assetId: poolAssetId }) + }, [poolAssetId]) + + const memo = useMemo(() => { + if (thorchainNotationAssetId === undefined) return null + if (opportunityType === 'sym' && !pairAssetAddress) return null + const maybePairAssetAddress = opportunityType === 'sym' ? pairAssetAddress! : '' - const [serializedTxIndex, setSerializedTxIndex] = useState('') + return isDeposit + ? `+:${thorchainNotationAssetId}:${maybePairAssetAddress}:ss:${confirmedQuote.feeBps}` + : `-:${thorchainNotationAssetId}:${confirmedQuote.withdrawalBps}` + }, [isDeposit, thorchainNotationAssetId, pairAssetAddress, confirmedQuote, opportunityType]) - const tx = useAppSelector(state => selectTxById(state, serializedTxIndex)) + const { executeTransaction, estimatedFeesData, txId, serializedTxIndex } = useSendThorTx({ + assetId: isRuneTx ? thorchainAssetId : poolAssetId, + accountId: isRuneTx ? runeAccountId : poolAssetAccountId, + amountCryptoBaseUnit: toBaseUnit(amountCryptoPrecision, asset?.precision ?? 0), + memo, + fromAddress: fromAddress ?? null, + action: isDeposit ? 'addLiquidity' : 'withdrawLiquidity', + disableEstimateFeesRefetch: isSubmitting, + }) const { mutateAsync } = useMutation({ mutationKey: [txId], @@ -257,6 +209,8 @@ export const TransactionRow: React.FC = ({ }, }) + const tx = useAppSelector(state => selectTxById(state, serializedTxIndex ?? '')) + // manages incomplete sym deposits by setting the already confirmed transaction as complete useEffect(() => { if (isLpConfirmedWithdrawalQuote(confirmedQuote)) return @@ -317,131 +271,10 @@ export const TransactionRow: React.FC = ({ enabled: !!assetId, }) - const thorchainNotationAssetId = useMemo(() => { - // TODO(gomes): rename the utils to use the same terminology as well instead of the current poolAssetId one. - // Left as-is for this PR to avoid a bigly diff - return assetIdToPoolAssetId({ assetId: poolAssetId }) - }, [poolAssetId]) - - const memo = useMemo(() => { - if (thorchainNotationAssetId === undefined) return - if (opportunityType === 'sym' && !pairAssetAddress) return - - return isDeposit - ? `+:${thorchainNotationAssetId}:${pairAssetAddress ?? ''}:ss:${confirmedQuote.feeBps}` - : `-:${thorchainNotationAssetId}:${confirmedQuote.withdrawalBps}` - }, [isDeposit, thorchainNotationAssetId, pairAssetAddress, confirmedQuote, opportunityType]) - - const estimateFeesArgs = useMemo(() => { - if (!assetId || !wallet || !asset || !poolAsset || !memo || !feeAsset) return undefined - - const amountCryptoBaseUnit = toBaseUnit(amountCryptoPrecision, asset.precision) - const dustAmountCryptoBaseUnit = - THORCHAIN_SAVERS_DUST_THRESHOLDS_CRYPTO_BASE_UNIT[feeAsset.assetId] ?? '0' - const dustAmountCryptoPrecision = fromBaseUnit(dustAmountCryptoBaseUnit, feeAsset.precision) - - const transactionType = getThorchainLpTransactionType(asset.chainId) - - switch (transactionType) { - case 'MsgDeposit': { - return { - amountCryptoPrecision: isDeposit ? amountCryptoPrecision : dustAmountCryptoPrecision, - assetId: asset.assetId, - memo, - to: THORCHAIN_POOL_MODULE_ADDRESS, - sendMax: false, - accountId: isRuneTx ? runeAccountId : poolAssetAccountId, - contractAddress: undefined, - } - } - case 'EvmCustomTx': { - if (!inboundAddressData?.router) return undefined - if (!assetAddress) return undefined - - const amountOrDustCryptoBaseUnit = isDeposit - ? amountCryptoBaseUnit - : dustAmountCryptoBaseUnit - - const data = depositWithExpiry({ - vault: getAddress(inboundAddressData.address), - asset: - // The asset param is a directive to initiate a transfer of said asset from the wallet to the contract - // which is *not* what we want for withdrawals, see - // https://www.tdly.co/shared/simulation/6d23d42a-8dd6-4e3e-88a8-62da779a765d - isToken(fromAssetId(assetId).assetReference) && isDeposit - ? getAddress(fromAssetId(assetId).assetReference) - : // Native EVM asset deposits and withdrawals (tokens/native assets) use the 0 address as the asset address - // https://dev.thorchain.org/concepts/sending-transactions.html#admonition-info-1 - zeroAddress, - amount: amountOrDustCryptoBaseUnit, - memo, - expiry: BigInt(dayjs().add(15, 'minute').unix()), - }) - - return { - // amountCryptoPrecision is always denominated in fee asset - the only value we can send when calling a contract is native asset value - // which happens for deposits (0-value) and withdrawals (dust-value, failure to send it means Txs won't be seen by THOR) - amountCryptoPrecision: - isToken(fromAssetId(assetId).assetReference) && isDeposit - ? '0' - : fromBaseUnit(amountOrDustCryptoBaseUnit, feeAsset.precision), - // Withdrawals do NOT occur a dust send to the contract address. - // It's a regular 0-value contract-call - assetId: isDeposit ? asset.assetId : feeAsset.assetId, - to: inboundAddressData.router, - from: assetAddress, - sendMax: false, - // This is an ERC-20, we abuse the memo field for the actual hex-encoded calldata - memo: data, - accountId: poolAssetAccountId, - // Note, this is NOT a send. - // contractAddress is only needed when doing a send and the account interacts *directly* with the token's contract address. - // Here, the LP contract is approved beforehand to spend the token value, which it will when calling depositWithExpiry() - contractAddress: undefined, - } - } - case 'Send': { - if (!inboundAddressData || !assetAddress) return undefined - return { - amountCryptoPrecision: isDeposit ? amountCryptoPrecision : dustAmountCryptoPrecision, - assetId, - to: inboundAddressData.address, - from: assetAddress, - sendMax: false, - memo, - accountId: poolAssetAccountId, - contractAddress: undefined, - } - } - default: - return undefined - } - }, [ - amountCryptoPrecision, - asset, - assetAddress, - assetId, - feeAsset, - inboundAddressData, - isDeposit, - isRuneTx, - memo, - poolAsset, - poolAssetAccountId, - runeAccountId, - wallet, - ]) - - const { data: estimatedFeesData } = useGetEstimatedFeesQuery({ - amountCryptoPrecision: estimateFeesArgs?.amountCryptoPrecision ?? '0', - assetId: estimateFeesArgs?.assetId ?? '', - to: estimateFeesArgs?.to ?? '', - sendMax: estimateFeesArgs?.sendMax ?? false, - memo: estimateFeesArgs?.memo ?? '', - accountId: estimateFeesArgs?.accountId ?? '', - contractAddress: estimateFeesArgs?.contractAddress ?? '', - enabled: Boolean(estimateFeesArgs), - disableRefetch: Boolean(txId || isSubmitting), + const { isTradingActive, isLoading: isTradingActiveLoading } = useIsTradingActive({ + assetId: poolAssetId, + enabled: !txId, + swapperName: SwapperName.Thorchain, }) useEffect(() => { @@ -463,7 +296,7 @@ export const TransactionRow: React.FC = ({ [txId], ) - const handleSignTx = useCallback(() => { + const handleSignTx = useCallback(async () => { setIsSubmitting(true) mixpanel?.track( isDeposit ? MixPanelEvent.LpDepositInitiated : MixPanelEvent.LpWithdrawInitiated, @@ -485,215 +318,13 @@ export const TransactionRow: React.FC = ({ return } - return (async () => { - // Pool just became halted at signing component mount, it's definitely not going to go back to active in just - // the few seconds it takes to go from mount to sign click - const _isTradingActive = await refetchIsTradingActive() - if (!_isTradingActive) throw new Error('Pool Halted') - - const accountId = isRuneTx ? runeAccountId : poolAssetAccountId - if (!accountId) throw new Error(`No accountId found for asset ${asset.assetId}`) - const { account } = fromAccountId(accountId) - - const amountCryptoBaseUnit = toBaseUnit(amountCryptoPrecision, asset.precision) - const dustAmountCryptoBaseUnit = - THORCHAIN_SAVERS_DUST_THRESHOLDS_CRYPTO_BASE_UNIT[feeAsset.assetId] ?? '0' - const dustAmountCryptoPrecision = fromBaseUnit(dustAmountCryptoBaseUnit, feeAsset.precision) - - const transactionType = getThorchainLpTransactionType(asset.chainId) - - await (async () => { - // We'll probably need to switch on chainNamespace instead here - switch (transactionType) { - case 'MsgDeposit': { - if (!estimateFeesArgs) throw new Error('No estimateFeesArgs found') - if (runeAccountNumber === undefined) throw new Error(`No account number found`) - - const adapter = assertGetThorchainChainAdapter() - const estimatedFees = await estimateFees(estimateFeesArgs) - - // LP deposit using THOR is a MsgDeposit tx - const { txToSign } = await adapter.buildDepositTransaction({ - from: account, - accountNumber: runeAccountNumber, - value: isDeposit ? amountCryptoBaseUnit : dustAmountCryptoBaseUnit, - memo, - chainSpecific: { - gas: (estimatedFees as FeeDataEstimate).fast - .chainSpecific.gasLimit, - fee: (estimatedFees as FeeDataEstimate).fast.txFee, - }, - }) - const signedTx = await adapter.signTransaction({ - txToSign, - wallet, - }) - const _txId = await adapter.broadcastTransaction({ - senderAddress: account, - receiverAddress: THORCHAIN_POOL_MODULE_ADDRESS, - hex: signedTx, - }) - const _txIdLink = getTxLink({ - defaultExplorerBaseUrl: 'https://viewblock.io/thorchain/tx/', - txId: _txId ?? '', - name: SwapperName.Thorchain, - }) - - toast({ - title: translate('modals.send.transactionSent'), - description: _txId ? ( - - - {translate('modals.status.viewExplorer')} - - - ) : undefined, - status: 'success', - duration: 9000, - isClosable: true, - position: 'top-right', - }) - - setTxId(_txId) - setSerializedTxIndex( - serializeTxIndex(runeAccountId, _txId, account, { memo, parser: 'thorchain' }), - ) - - break - } - case 'EvmCustomTx': { - if (!assetAddress) throw new Error('No account address found') - if (!inboundAddressData?.address) throw new Error('No vault address found') - if (!inboundAddressData?.router) throw new Error('No router address found') - if (poolAssetAccountNumber === undefined) throw new Error('No account number found') - - const amountOrDustCryptoBaseUnit = isDeposit - ? amountCryptoBaseUnit - : dustAmountCryptoBaseUnit - - const data = depositWithExpiry({ - vault: getAddress(inboundAddressData.address), - // The asset param is a directive to initiate a transfer of said asset from the wallet to the contract - // which is *not* what we want for withdrawals, see - // https://www.tdly.co/shared/simulation/6d23d42a-8dd6-4e3e-88a8-62da779a765d - asset: - isToken(fromAssetId(assetId).assetReference) && isDeposit - ? getAddress(fromAssetId(assetId).assetReference) - : // Native EVM asset deposits and withdrawals (tokens/native assets) use the 0 address as the asset address - // https://dev.thorchain.org/concepts/sending-transactions.html#admonition-info-1 - zeroAddress, - amount: amountOrDustCryptoBaseUnit, - memo, - expiry: BigInt(dayjs().add(15, 'minute').unix()), - }) - - const adapter = assertGetEvmChainAdapter(asset.chainId) - - const buildCustomTxInput = await createBuildCustomTxInput({ - accountNumber: poolAssetAccountNumber, - adapter, - data, - // value is always denominated in fee asset - the only value we can send when calling a contract is native asset value - // which happens for deposits (0-value) and withdrawals (dust-value, failure to send it means Txs won't be seen by THOR) - value: - isToken(fromAssetId(assetId).assetReference) && isDeposit - ? '0' - : amountOrDustCryptoBaseUnit, - to: inboundAddressData.router, - wallet, - }) - - const _txId = await buildAndBroadcast({ - adapter, - buildCustomTxInput, - receiverAddress: CONTRACT_INTERACTION, // no receiver for this contract call - }) - const _txIdLink = getTxLink({ - defaultExplorerBaseUrl: 'https://viewblock.io/thorchain/tx/', - txId: _txId ?? '', - name: SwapperName.Thorchain, - }) - toast({ - title: translate('modals.send.transactionSent'), - description: _txId ? ( - - - {translate('modals.status.viewExplorer')} - - - ) : undefined, - status: 'success', - duration: 9000, - isClosable: true, - position: 'top-right', - }) - - setTxId(_txId) - setSerializedTxIndex(serializeTxIndex(poolAssetAccountId, _txId, assetAddress!)) - - break - } - case 'Send': { - if (!assetAddress) throw new Error('No account address found') - if (!estimateFeesArgs) throw new Error('No estimateFeesArgs found') - if (!inboundAddressData?.address) throw new Error('No vault address found') - - const estimatedFees = await estimateFees(estimateFeesArgs) - const sendInput: SendInput = { - amountCryptoPrecision: isDeposit ? amountCryptoPrecision : dustAmountCryptoPrecision, - assetId, - to: inboundAddressData?.address, - from: assetAddress, - sendMax: false, - accountId, - memo, - amountFieldError: '', - estimatedFees, - feeType: FeeDataKey.Fast, - fiatAmount: '', - fiatSymbol: selectedCurrency, - vanityAddress: '', - input: '', - } + const _txId = await executeTransaction() + if (!_txId) { + setIsSubmitting(false) + throw new Error('failed to broadcast transaction') + } - const txId = await handleSend({ - sendInput, - wallet, - }) - const _txIdLink = getTxLink({ - defaultExplorerBaseUrl: 'https://viewblock.io/thorchain/tx/', - txId: txId ?? '', - name: SwapperName.Thorchain, - }) - toast({ - title: translate('modals.send.transactionSent'), - description: txId ? ( - - - {translate('modals.status.viewExplorer')} - - - ) : undefined, - status: 'success', - duration: 9000, - isClosable: true, - position: 'top-right', - }) - - setTxId(txId) - setSerializedTxIndex( - serializeTxIndex(poolAssetAccountId, txId, fromAccountId(poolAssetAccountId).account), - ) - - break - } - default: - assertUnreachable(transactionType) - } - })() - })().then(() => { - onStart() - }) + onStart() }, [ mixpanel, isDeposit, @@ -707,18 +338,7 @@ export const TransactionRow: React.FC = ({ memo, isRuneTx, inboundAddressData?.address, - inboundAddressData?.router, - refetchIsTradingActive, - runeAccountId, - poolAssetAccountId, - amountCryptoPrecision, - estimateFeesArgs, - runeAccountNumber, - toast, - translate, - assetAddress, - poolAssetAccountNumber, - selectedCurrency, + executeTransaction, onStart, ]) diff --git a/src/react-queries/queries/common.ts b/src/react-queries/queries/common.ts index 5ff24523e23..16f35e4294c 100644 --- a/src/react-queries/queries/common.ts +++ b/src/react-queries/queries/common.ts @@ -1,11 +1,18 @@ import { createQueryKeys } from '@lukemorales/query-key-factory' +import type { AccountId } from '@shapeshiftoss/caip' import { type AssetId, fromAssetId } from '@shapeshiftoss/caip' import type { EvmChainId } from '@shapeshiftoss/chain-adapters' import { evmChainIds } from '@shapeshiftoss/chain-adapters' +import type { HDWallet } from '@shapeshiftoss/hdwallet-core' +import type { AccountMetadata } from '@shapeshiftoss/types' import type { Result } from '@sniptt/monads' import { Err, Ok } from '@sniptt/monads' import { assertGetChainAdapter } from 'lib/utils' import { getErc20Allowance } from 'lib/utils/evm' +import { getThorchainFromAddress } from 'lib/utils/thorchain' +import type { getThorchainLendingPosition } from 'lib/utils/thorchain/lending' +import type { getThorchainLpPosition } from 'pages/ThorChainLP/queries/queries' +import type { getThorchainSaversPosition } from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/utils' import { GetAllowanceErr } from '../types' @@ -45,4 +52,33 @@ export const common = createQueryKeys('common', { return Ok(allowanceOnChainCryptoBaseUnit) }, }), + thorchainFromAddress: ({ + accountId, + assetId, + opportunityId, + wallet, + accountMetadata, + getPosition, + }: { + accountId: AccountId + assetId: AssetId + opportunityId?: string | undefined + wallet: HDWallet + accountMetadata: AccountMetadata + getPosition: + | typeof getThorchainLendingPosition + | typeof getThorchainSaversPosition + | typeof getThorchainLpPosition + }) => ({ + queryKey: ['thorchainFromAddress', accountId, assetId, opportunityId], + queryFn: async () => + await getThorchainFromAddress({ + accountId, + assetId, + opportunityId, + getPosition, + accountMetadata, + wallet, + }), + }), }) diff --git a/src/state/slices/tradeQuoteSlice/utils.ts b/src/state/slices/tradeQuoteSlice/utils.ts index 5dc2a4c8fa7..b0451a9de62 100644 --- a/src/state/slices/tradeQuoteSlice/utils.ts +++ b/src/state/slices/tradeQuoteSlice/utils.ts @@ -14,6 +14,9 @@ export const convertDecimalPercentageToBasisPoints = (decimalPercentage: BigNumb export const convertBasisPointsToPercentage = (basisPoints: BigNumber.Value) => bnOrZero(basisPoints).div(100) +export const convertPercentageToBasisPoints = (percentage: BigNumber.Value) => + bnOrZero(percentage).times(100) + type SumProtocolFeesToDenomArgs = { marketDataByAssetIdUsd: Partial>> outputAssetPriceUsd: BigNumber.Value