From a7cd75337464cc48816ea7d0969a2d6ca83ffad6 Mon Sep 17 00:00:00 2001 From: veado <61792675+veado@users.noreply.github.com> Date: Mon, 10 May 2021 12:04:28 +0200 Subject: [PATCH] [Deposit] Refactor fee + min. value handling (#1404) - [x] Refactor `symDeposit$` - [x] Update types for sym deposit fees - [x] Update `SymDeposit` - [x] Helpers in `Deposit.helper`: `priceFeeAmountForAsset`, `minAssetAmountToDepositMax1e8`, `sumFees`, `minRuneAmountToDeposit` (tests will be added with next PR - see #1409) - [x] Add `poolsData: PoolsDataMap` to `poolsState$` - [x] Update `AssetCard` to show deposit `minValue` - [x] Improvements for `Swap` --- src/renderer/components/deposit/Deposit.tsx | 2 +- .../deposit/add/Deposit.helper.test.ts | 36 +-- .../components/deposit/add/Deposit.helper.ts | 123 +++++++- .../components/deposit/add/Deposit.style.ts | 4 - .../deposit/add/SymDeposit.stories.tsx | 36 ++- .../components/deposit/add/SymDeposit.tsx | 277 ++++++++---------- src/renderer/components/swap/Swap.stories.tsx | 41 ++- src/renderer/components/swap/Swap.tsx | 136 ++++----- src/renderer/components/swap/Swap.utils.ts | 5 +- .../assets/assetCard/AssetCard.style.ts | 11 +- .../uielements/assets/assetCard/AssetCard.tsx | 11 +- src/renderer/helpers/fp/eq.ts | 19 +- src/renderer/services/chain/const.ts | 7 +- src/renderer/services/chain/fees/common.ts | 4 +- src/renderer/services/chain/fees/deposit.ts | 201 ++++--------- src/renderer/services/chain/fees/swap.ts | 2 +- src/renderer/services/chain/fees/utils.ts | 10 +- src/renderer/services/chain/types.ts | 42 +-- src/renderer/services/midgard/pools.ts | 42 +-- src/renderer/services/midgard/types.ts | 3 + src/renderer/services/midgard/utils.test.ts | 11 +- src/renderer/services/midgard/utils.ts | 21 +- .../views/deposit/add/SymDepositView.tsx | 27 +- src/renderer/views/swap/SwapView.tsx | 6 +- 24 files changed, 560 insertions(+), 517 deletions(-) diff --git a/src/renderer/components/deposit/Deposit.tsx b/src/renderer/components/deposit/Deposit.tsx index 8ddcf290a..e098ebd4d 100644 --- a/src/renderer/components/deposit/Deposit.tsx +++ b/src/renderer/components/deposit/Deposit.tsx @@ -127,7 +127,7 @@ export const Deposit: React.FC = (props) => { // content: // } ], - [intl, assetWD, SymDepositContent, hasSymPoolShare, WidthdrawContent, combinedPoolShare, poolDetailRD] + [intl, SymDepositContent, poolDetailRD, assetWD, hasSymPoolShare, WidthdrawContent, combinedPoolShare] ) const alignTopShareContent: boolean = useMemo( diff --git a/src/renderer/components/deposit/add/Deposit.helper.test.ts b/src/renderer/components/deposit/add/Deposit.helper.test.ts index 286e90efd..92f4fb6c2 100644 --- a/src/renderer/components/deposit/add/Deposit.helper.test.ts +++ b/src/renderer/components/deposit/add/Deposit.helper.test.ts @@ -1,10 +1,10 @@ import * as RD from '@devexperts/remote-data-ts' import { PoolData } from '@thorchain/asgardex-util' -import { baseAmount } from '@xchainjs/xchain-util' +import { AssetBNB, baseAmount } from '@xchainjs/xchain-util' import * as O from 'fp-ts/Option' -import { eqBaseAmount, eqOptionBaseAmount } from '../../../helpers/fp/eq' -import { DepositFeesRD } from '../../../services/chain/types' +import { eqBaseAmount, eqODepositAssetFees, eqODepositFees } from '../../../helpers/fp/eq' +import { DepositAssetFees, DepositFees, SymDepositFeesRD } from '../../../services/chain/types' import { getAssetAmountToDeposit, getRuneAmountToDeposit, @@ -80,26 +80,28 @@ describe('deposit/Deposit.helper', () => { }) describe('fees getters', () => { - const depositFeesRD: DepositFeesRD = RD.success({ - thor: O.some(baseAmount(100)), - asset: baseAmount(123) + const runeFee: DepositFees = { inFee: baseAmount(10), outFee: baseAmount(11), refundFee: baseAmount(12) } + const assetFee: DepositAssetFees = { + inFee: baseAmount(20), + outFee: baseAmount(21), + refundFee: baseAmount(22), + asset: AssetBNB + } + const feesRD: SymDepositFeesRD = RD.success({ + rune: runeFee, + asset: assetFee }) it('should return chain fees', () => { - expect(eqOptionBaseAmount.equals(getAssetChainFee(depositFeesRD), O.some(baseAmount(123)))).toBeTruthy() + const result = getAssetChainFee(feesRD) + const expected = O.some(assetFee) + expect(eqODepositAssetFees.equals(result, expected)).toBeTruthy() }) it('should return ThorChain fees', () => { - expect(eqOptionBaseAmount.equals(getThorchainFees(depositFeesRD), O.some(baseAmount(100)))).toBeTruthy() - - expect( - getThorchainFees( - RD.success({ - thor: O.none, - asset: baseAmount(123) - }) - ) - ).toBeNone() + const result = getThorchainFees(feesRD) + const expected = O.some(runeFee) + expect(eqODepositFees.equals(result, expected)).toBeTruthy() }) }) }) diff --git a/src/renderer/components/deposit/add/Deposit.helper.ts b/src/renderer/components/deposit/add/Deposit.helper.ts index 98b283348..66f4ef7ed 100644 --- a/src/renderer/components/deposit/add/Deposit.helper.ts +++ b/src/renderer/components/deposit/add/Deposit.helper.ts @@ -1,17 +1,20 @@ import * as RD from '@devexperts/remote-data-ts' -import { PoolData } from '@thorchain/asgardex-util' -import { baseAmount, BaseAmount } from '@xchainjs/xchain-util' +import { getValueOfAsset1InAsset2, PoolData } from '@thorchain/asgardex-util' +import { Asset, assetToString, baseAmount, BaseAmount } from '@xchainjs/xchain-util' import BigNumber from 'bignumber.js' import * as FP from 'fp-ts/function' import * as O from 'fp-ts/Option' import { convertBaseAmountDecimal, + isChainAsset, max1e8BaseAmount, THORCHAIN_DECIMAL, to1e8BaseAmount } from '../../../helpers/assetHelper' -import { DepositFeesRD } from '../../../services/chain/types' +import { sequenceTOption } from '../../../helpers/fpHelpers' +import { DepositAssetFees, DepositFees, SymDepositFeesRD } from '../../../services/chain/types' +import { PoolsDataMap } from '../../../services/midgard/types' /** * Calculates max. value of RUNE to deposit @@ -124,16 +127,122 @@ export const getAssetAmountToDeposit = ({ return max1e8BaseAmount(assetAmountToDeposit) } -export const getAssetChainFee = (feesRD: DepositFeesRD): O.Option => +export const getAssetChainFee = (feesRD: SymDepositFeesRD): O.Option => FP.pipe( feesRD, RD.map(({ asset }) => asset), RD.toOption ) -export const getThorchainFees = (feesRD: DepositFeesRD): O.Option => +export const getThorchainFees = (feesRD: SymDepositFeesRD): O.Option => FP.pipe( feesRD, - RD.map(({ thor }) => thor), - FP.flow(RD.toOption, O.flatten) + RD.map(({ rune }) => rune), + RD.toOption + ) + +export const sumFees = ({ inFee, outFee }: { inFee: BaseAmount; outFee: BaseAmount }) => inFee.plus(outFee) + +export const priceFeeAmountForAsset = ({ + feeAmount, + feeAsset, + asset, + assetDecimal, + poolsData +}: { + feeAmount: BaseAmount + feeAsset: Asset + asset: Asset + assetDecimal: number + poolsData: PoolsDataMap +}): BaseAmount => { + const oFeeAssetPoolData: O.Option = O.fromNullable(poolsData[assetToString(feeAsset)]) + const oAssetPoolData: O.Option = O.fromNullable(poolsData[assetToString(asset)]) + + return FP.pipe( + sequenceTOption(oFeeAssetPoolData, oAssetPoolData), + O.map(([feeAssetPoolData, assetPoolData]) => + // pool data are always 1e8 decimal based + // and we have to convert fees to 1e8, too + getValueOfAsset1InAsset2(to1e8BaseAmount(feeAmount), feeAssetPoolData, assetPoolData) + ), + // convert decimal back to sourceAssetDecimal + O.map((amount) => convertBaseAmountDecimal(amount, feeAmount.decimal)), + O.map((amount) => convertBaseAmountDecimal(amount, assetDecimal)), + O.getOrElse(() => baseAmount(0, assetDecimal)) + ) +} + +export const minAssetAmountToDepositMax1e8 = ({ + fees, + asset, + assetDecimal, + poolsData +}: { + /* fee for deposit */ + fees: DepositAssetFees + /* asset to deposit */ + asset: Asset + assetDecimal: number + poolsData: PoolsDataMap +}): BaseAmount => { + const { asset: feeAsset, inFee, outFee, refundFee } = fees + + const inFeeInAsset = isChainAsset(asset) + ? inFee + : priceFeeAmountForAsset({ + feeAmount: inFee, + feeAsset, + asset, + assetDecimal, + poolsData + }) + + const outFeeInAsset = isChainAsset(asset) + ? outFee + : priceFeeAmountForAsset({ + feeAmount: outFee, + feeAsset, + asset, + assetDecimal, + poolsData + }) + + const refundFeeInAsset = isChainAsset(asset) + ? refundFee + : priceFeeAmountForAsset({ + feeAmount: refundFee, + feeAsset, + asset, + assetDecimal, + poolsData + }) + + const successDepositFee = inFeeInAsset.plus(outFeeInAsset) + const failureDepositFee = inFeeInAsset.plus(refundFeeInAsset) + + const feeToCover: BaseAmount = successDepositFee.gte(failureDepositFee) ? successDepositFee : failureDepositFee + + return FP.pipe( + // Over-estimate fee by 50% + 1.5, + feeToCover.times, + // transform decimal to be `max1e8` + max1e8BaseAmount ) +} + +export const minRuneAmountToDeposit = ({ inFee, outFee, refundFee }: DepositFees): BaseAmount => { + const successDepositFee = inFee.plus(outFee) + const failureDepositFee = inFee.plus(refundFee) + + const feeToCover: BaseAmount = successDepositFee.gte(failureDepositFee) ? successDepositFee : failureDepositFee + + return FP.pipe( + // Over-estimate fee by 50% + 1.5, + feeToCover.times, + // transform decimal to be `max1e8` + max1e8BaseAmount + ) +} diff --git a/src/renderer/components/deposit/add/Deposit.style.ts b/src/renderer/components/deposit/add/Deposit.style.ts index 25dd2afe4..77de0f96f 100644 --- a/src/renderer/components/deposit/add/Deposit.style.ts +++ b/src/renderer/components/deposit/add/Deposit.style.ts @@ -146,7 +146,3 @@ export const SubmitButton = styled(UIButton).attrs({ padding-left: 30px; padding-right: 30px; ` -export const MinAmountLabel = styled(UILabel)` - padding-top: 0; - text-transform: uppercase; -` diff --git a/src/renderer/components/deposit/add/SymDeposit.stories.tsx b/src/renderer/components/deposit/add/SymDeposit.stories.tsx index 69910122f..0aa7895c1 100644 --- a/src/renderer/components/deposit/add/SymDeposit.stories.tsx +++ b/src/renderer/components/deposit/add/SymDeposit.stories.tsx @@ -11,7 +11,8 @@ import { AssetBTC, AssetRuneNative, Asset, - AssetETH + AssetETH, + assetToString } from '@xchainjs/xchain-util' import * as O from 'fp-ts/lib/Option' import * as Rx from 'rxjs' @@ -55,8 +56,17 @@ const defaultProps: SymDepositProps = { fees$: () => Rx.of( RD.success({ - thor: O.some(assetToBase(assetAmount(0.2))), - asset: assetToBase(assetAmount(0.000075)) + rune: { + inFee: assetToBase(assetAmount(0.2)), + outFee: assetToBase(assetAmount(0.6)), + refundFee: assetToBase(assetAmount(0.6)) + }, + asset: { + asset: AssetBNB, + inFee: assetToBase(assetAmount(0.000075)), + outFee: assetToBase(assetAmount(0.000225)), + refundFee: assetToBase(assetAmount(0.000225)) + } }) ), reloadApproveFee: () => console.log('reloadFees'), @@ -98,7 +108,12 @@ const defaultProps: SymDepositProps = { approveERC20Token$: () => Rx.of(RD.success('txHash')), isApprovedERC20Token$: () => Rx.of(RD.success(true)), fundsCap: O.none, - usdPricePool: O.none + poolsData: { + [assetToString(AssetBNB)]: { + assetBalance: baseAmount(1), + runeBalance: baseAmount(20) + } + } } export const Default: Story = () => @@ -122,8 +137,17 @@ export const FeeError: Story = () => { fees$: () => Rx.of( RD.success({ - thor: O.some(assetToBase(assetAmount(2))), - asset: assetToBase(assetAmount(1)) + rune: { + inFee: assetToBase(assetAmount(2)), + outFee: assetToBase(assetAmount(6)), + refundFee: assetToBase(assetAmount(6)) + }, + asset: { + asset: AssetBNB, + inFee: assetToBase(assetAmount(1)), + outFee: assetToBase(assetAmount(3)), + refundFee: assetToBase(assetAmount(3)) + } }) ), assetBalance: O.some(assetToBase(assetAmount(0.5))), diff --git a/src/renderer/components/deposit/add/SymDeposit.tsx b/src/renderer/components/deposit/add/SymDeposit.tsx index bc386670c..e6167e9e8 100644 --- a/src/renderer/components/deposit/add/SymDeposit.tsx +++ b/src/renderer/components/deposit/add/SymDeposit.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import * as RD from '@devexperts/remote-data-ts' -import { getValueOfAsset1InAsset2, PoolData } from '@thorchain/asgardex-util' +import { PoolData } from '@thorchain/asgardex-util' import { Address } from '@xchainjs/xchain-client' import { Asset, @@ -28,34 +28,32 @@ import { isEthAsset, isEthTokenAsset, max1e8BaseAmount, - THORCHAIN_DECIMAL, - to1e8BaseAmount + THORCHAIN_DECIMAL } from '../../../helpers/assetHelper' import { getChainAsset, isEthChain } from '../../../helpers/chainHelper' -import { eqBaseAmount, eqOPoolAddresses } from '../../../helpers/fp/eq' +import { eqBaseAmount, eqOAsset } from '../../../helpers/fp/eq' import { sequenceSOption, sequenceTOption } from '../../../helpers/fpHelpers' import { liveData, LiveData } from '../../../helpers/rx/liveData' import { FundsCap } from '../../../hooks/useFundsCap' import { useSubscriptionState } from '../../../hooks/useSubscriptionState' -import { INITIAL_SYM_DEPOSIT_STATE, ZERO_SYM_DEPOSIT_FEES } from '../../../services/chain/const' +import { INITIAL_SYM_DEPOSIT_STATE } from '../../../services/chain/const' +import { getZeroSymDepositFees } from '../../../services/chain/fees' import { SymDepositMemo, SymDepositState, SymDepositParams, SymDepositStateHandler, - DepositFees, + SymDepositFees, FeeRD, ReloadSymDepositFeesHandler, SymDepositFeesHandler, - DepositFeesRD + SymDepositFeesRD } from '../../../services/chain/types' import { ApproveFeeHandler, ApproveParams, IsApprovedRD, LoadApproveFeeHandler } from '../../../services/ethereum/types' -import { PoolAddress } from '../../../services/midgard/types' +import { PoolAddress, PoolsDataMap } from '../../../services/midgard/types' import { ApiError, TxHashLD, TxHashRD, ValidatePasswordHandler } from '../../../services/wallet/types' import { AssetWithDecimal } from '../../../types/asgardex' import { WalletBalances } from '../../../types/wallet' -import { PricePool } from '../../../views/pools/Pools.types' -import { minPoolTxAmountUSD } from '../../../views/pools/Pools.utils' import { PasswordModal } from '../../modal/password' import { TxModal } from '../../modal/tx' import { DepositAssets } from '../../modal/tx/extra' @@ -72,7 +70,6 @@ export type Props = { runePrice: BigNumber runeBalance: O.Option chainAssetBalance: O.Option - usdPricePool: O.Option poolAddress: O.Option memos: O.Option priceAsset?: Asset @@ -95,6 +92,7 @@ export type Props = { approveERC20Token$: (params: ApproveParams) => TxHashLD isApprovedERC20Token$: (params: ApproveParams) => LiveData fundsCap: O.Option + poolsData: PoolsDataMap } type SelectedInput = 'asset' | 'rune' | 'none' @@ -107,7 +105,6 @@ export const SymDeposit: React.FC = (props) => { runePrice, runeBalance: oRuneBalance, chainAssetBalance: oChainAssetBalance, - usdPricePool: oUsdPricePool, memos: oMemos, poolAddress: oPoolAddress, viewAssetTx = (_) => {}, @@ -129,12 +126,13 @@ export const SymDeposit: React.FC = (props) => { approveERC20Token$, reloadApproveFee, approveFee$, - fundsCap: oFundsCap + fundsCap: oFundsCap, + poolsData } = props const intl = useIntl() - const prevPoolAddresses = useRef>(O.none) + const prevAsset = useRef>(O.none) /** Asset balance based on original decimal */ const assetBalance: BaseAmount = useMemo( @@ -165,51 +163,6 @@ export const SymDeposit: React.FC = (props) => { [assetAmountToDepositMax1e8, runeAmountToDeposit] ) - // TODO (@asgdx-team) Remove min. amount if xchain-* gets fee rates from THORChain - // @see: https://github.com/xchainjs/xchainjs-lib/issues/299 - const minUSDAmount = useMemo(() => minPoolTxAmountUSD(asset), [asset]) - - // Helper to price target fees into target asset - const minAssetAmountToDepositMax1e8: BaseAmount = useMemo(() => { - return FP.pipe( - oUsdPricePool, - O.fold( - () => ZERO_BASE_AMOUNT, - ({ poolData: usdPoolData }) => - // pool data are always 1e8 decimal based - // and we have to convert fees to 1e8, too - getValueOfAsset1InAsset2(to1e8BaseAmount(minUSDAmount), usdPoolData, poolData) - ) - ) - }, [oUsdPricePool, minUSDAmount, poolData]) - - const minAssetAmountError = useMemo(() => { - if (isZeroAmountToDeposit) return false - - return assetAmountToDepositMax1e8.lt(minAssetAmountToDepositMax1e8) - }, [assetAmountToDepositMax1e8, isZeroAmountToDeposit, minAssetAmountToDepositMax1e8]) - - const minAssetAmountLabel = useMemo( - () => ( - - {intl.formatMessage({ id: 'common.min' })} - {': '} - {formatAssetAmountCurrency({ - asset, - amount: baseToAsset(minAssetAmountToDepositMax1e8), - trimZeros: true - })}{' '} - ( - {formatAssetAmountCurrency({ - trimZeros: true, - amount: baseToAsset(minUSDAmount) - })} - ) - - ), - [asset, intl, minAssetAmountError, minAssetAmountToDepositMax1e8, minUSDAmount] - ) - const [percentValueToDeposit, setPercentValueToDeposit] = useState(0) const [selectedInput, setSelectedInput] = useState('none') @@ -251,12 +204,12 @@ export const SymDeposit: React.FC = (props) => { amounts: { rune: runeAmountToDeposit, // Decimal needs to be converted back for using orginal decimal of this asset (provided by `assetBalance`) - asset: convertBaseAmountDecimal(assetAmountToDepositMax1e8, assetBalance.decimal) + asset: convertBaseAmountDecimal(assetAmountToDepositMax1e8, assetDecimal) }, memos })) ), - [oPoolAddress, oMemos, assetAmountToDepositMax1e8, asset, runeAmountToDeposit, assetBalance.decimal] + [oPoolAddress, oMemos, asset, runeAmountToDeposit, assetAmountToDepositMax1e8, assetDecimal] ) const oApproveParams: O.Option = useMemo(() => { @@ -275,25 +228,37 @@ export const SymDeposit: React.FC = (props) => { ) }, [oPoolAddress, asset]) - const prevDepositFees = useRef>(O.none) + const zeroDepositFees: SymDepositFees = useMemo(() => getZeroSymDepositFees(asset), [asset]) - const [depositFeesRD] = useObservableState( + const prevDepositFees = useRef>(O.none) + + const [depositFeesRD] = useObservableState( () => FP.pipe( - oDepositParams, - fees$, + fees$(asset), liveData.map((fees) => { // store every successfully loaded chainFees to the ref value prevDepositFees.current = O.some(fees) return fees }) ), - RD.success(ZERO_SYM_DEPOSIT_FEES) + RD.success(zeroDepositFees) + ) + + const depositFees: SymDepositFees = useMemo( + () => + FP.pipe( + depositFeesRD, + RD.toOption, + O.alt(() => prevDepositFees.current), + O.getOrElse(() => zeroDepositFees) + ), + [depositFeesRD, zeroDepositFees] ) const reloadFeesHandler = useCallback(() => { - reloadFees(oDepositParams) - }, [oDepositParams, reloadFees]) + reloadFees(asset) + }, [asset, reloadFees]) const approveFees$ = useMemo(() => approveFee$, [approveFee$]) @@ -312,10 +277,27 @@ export const SymDeposit: React.FC = (props) => { FP.pipe(oApproveParams, O.map(reloadApproveFee)) }, [oApproveParams, reloadApproveFee]) - const oThorchainFee: O.Option = useMemo(() => FP.pipe(depositFeesRD, Helper.getThorchainFees), [ - depositFeesRD + const minAssetAmountToDepositMax1e8: BaseAmount = useMemo( + () => Helper.minAssetAmountToDepositMax1e8({ fees: depositFees.asset, asset, assetDecimal, poolsData }), + [asset, assetDecimal, depositFees.asset, poolsData] + ) + + const minAssetAmountError = useMemo(() => { + if (isZeroAmountToDeposit) return false + + return assetAmountToDepositMax1e8.lt(minAssetAmountToDepositMax1e8) + }, [assetAmountToDepositMax1e8, isZeroAmountToDeposit, minAssetAmountToDepositMax1e8]) + + const minRuneAmountToDeposit: BaseAmount = useMemo(() => Helper.minRuneAmountToDeposit(depositFees.rune), [ + depositFees.rune ]) + const minRuneAmountError = useMemo(() => { + if (isZeroAmountToDeposit) return false + + return runeAmountToDeposit.lt(minRuneAmountToDeposit) + }, [isZeroAmountToDeposit, minRuneAmountToDeposit, runeAmountToDeposit]) + const maxRuneAmountToDeposit = useMemo( (): BaseAmount => Helper.maxRuneAmountToDeposit({ poolData, runeBalance, assetBalance }), @@ -329,10 +311,6 @@ export const SymDeposit: React.FC = (props) => { } }, [maxRuneAmountToDeposit, runeAmountToDeposit]) - const oAssetChainFee: O.Option = useMemo(() => FP.pipe(depositFeesRD, Helper.getAssetChainFee), [ - depositFeesRD - ]) - /** * Max asset amount to deposit * Note: It's max. 1e8 decimal based @@ -402,7 +380,6 @@ export const SymDeposit: React.FC = (props) => { } ) - // asym error message const msg = // no balance for pool asset and rune !hasAssetBalance && !hasRuneBalance @@ -430,7 +407,7 @@ export const SymDeposit: React.FC = (props) => { const assetAmountMax1e8 = Helper.getAssetAmountToDeposit({ runeAmount: runeAmount, poolData, - assetDecimal: assetBalance.decimal + assetDecimal }) if (assetAmountMax1e8.amount().isGreaterThan(maxAssetAmountToDepositMax1e8.amount())) { @@ -449,7 +426,7 @@ export const SymDeposit: React.FC = (props) => { } }, [ - assetBalance.decimal, + assetDecimal, maxAssetAmountToDepositMax1e8, maxRuneAmountToDeposit, poolData, @@ -475,7 +452,7 @@ export const SymDeposit: React.FC = (props) => { assetAmountMax1e8 = Helper.getAssetAmountToDeposit({ runeAmount, poolData, - assetDecimal: assetBalance.decimal + assetDecimal }) setRuneAmountToDeposit(maxRuneAmountToDeposit) setAssetAmountToDepositMax1e8(assetAmountMax1e8) @@ -491,8 +468,8 @@ export const SymDeposit: React.FC = (props) => { } }, [ - assetBalance.decimal, assetBalanceMax1e8.decimal, + assetDecimal, maxAssetAmountToDepositMax1e8, maxRuneAmountToDeposit, poolData, @@ -560,48 +537,38 @@ export const SymDeposit: React.FC = (props) => { if (isZeroAmountToDeposit) return false return FP.pipe( - sequenceTOption(oThorchainFee, oRuneBalance), + oRuneBalance, O.fold( - // Missing (or loading) fees does not mean we can't sent something. No error then. - () => !O.isNone(oThorchainFee), - ([fee, balance]) => balance.amount().isLessThan(fee.amount()) + () => true, + (balance) => FP.pipe(depositFees.rune, Helper.sumFees, balance.lt) ) ) - }, [oRuneBalance, oThorchainFee, isZeroAmountToDeposit]) + }, [isZeroAmountToDeposit, oRuneBalance, depositFees.rune]) const renderThorchainFeeError = useMemo(() => { if (!isThorchainFeeError || isBalanceError /* Don't render anything in case of balance errors */) return <> - return FP.pipe( - oThorchainFee, - O.map((fee) => renderFeeError(fee, runeBalance, AssetRuneNative)), - O.getOrElse(() => <>) - ) - }, [isBalanceError, isThorchainFeeError, oThorchainFee, renderFeeError, runeBalance]) + return renderFeeError(Helper.sumFees(depositFees.rune), runeBalance, AssetRuneNative) + }, [depositFees.rune, isBalanceError, isThorchainFeeError, renderFeeError, runeBalance]) const isAssetChainFeeError = useMemo(() => { // ignore error check by having zero amounts if (isZeroAmountToDeposit) return false return FP.pipe( - sequenceTOption(oAssetChainFee, oChainAssetBalance), + oChainAssetBalance, O.fold( - // Missing (or loading) fees does not mean we can't sent something. No error then. - () => !O.isNone(oAssetChainFee), - ([fee, balance]) => balance.amount().isLessThan(fee.amount()) + () => true, + (balance) => FP.pipe(depositFees.asset, Helper.sumFees, balance.lt) ) ) - }, [oAssetChainFee, oChainAssetBalance, isZeroAmountToDeposit]) + }, [isZeroAmountToDeposit, oChainAssetBalance, depositFees.asset]) const renderAssetChainFeeError = useMemo(() => { if (!isAssetChainFeeError || isBalanceError /* Don't render anything in case of balance errors */) return <> - return FP.pipe( - oAssetChainFee, - O.map((fee) => renderFeeError(fee, chainAssetBalance, asset)), - O.getOrElse(() => <>) - ) - }, [isAssetChainFeeError, isBalanceError, oAssetChainFee, renderFeeError, chainAssetBalance, asset]) + return renderFeeError(Helper.sumFees(depositFees.asset), chainAssetBalance, asset) + }, [isAssetChainFeeError, isBalanceError, renderFeeError, depositFees.asset, chainAssetBalance, asset]) const txModalExtraContent = useMemo(() => { const stepDescriptions = [ @@ -776,26 +743,27 @@ export const SymDeposit: React.FC = (props) => { isThorchainFeeError || isAssetChainFeeError || isZeroAmountToDeposit || + minRuneAmountError || minAssetAmountError, - [depositFeesRD, disabledForm, isAssetChainFeeError, isThorchainFeeError, isZeroAmountToDeposit, minAssetAmountError] + [ + depositFeesRD, + disabledForm, + isAssetChainFeeError, + isThorchainFeeError, + isZeroAmountToDeposit, + minAssetAmountError, + minRuneAmountError + ] ) const uiFeesRD: UIFeesRD = useMemo( () => FP.pipe( depositFeesRD, - RD.map(({ asset: assetFeeAmount, thor }) => - FP.pipe( - thor, - O.fold( - () => [{ asset, amount: assetFeeAmount }], - (thorAmount) => [ - { asset: getChainAsset(asset.chain), amount: assetFeeAmount }, - { asset: AssetRuneNative, amount: thorAmount } - ] - ) - ) - ) + RD.map(({ asset: assetFee, rune }) => [ + { asset: getChainAsset(asset.chain), amount: assetFee.inFee.plus(assetFee.outFee) }, + { asset: AssetRuneNative, amount: rune.inFee.plus(rune.outFee) } + ]) ), [depositFeesRD, asset] ) @@ -904,8 +872,9 @@ export const SymDeposit: React.FC = (props) => { }, [asset, isApprovedERC20Token$, needApprovement, oPoolAddress, subscribeIsApprovedState]) useEffect(() => { - if (!eqOPoolAddresses.equals(prevPoolAddresses.current, oPoolAddress)) { - prevPoolAddresses.current = oPoolAddress + if (!eqOAsset.equals(prevAsset.current, O.some(asset))) { + prevAsset.current = O.some(asset) + // reset deposit state resetDepositState() // set values to zero @@ -916,6 +885,8 @@ export const SymDeposit: React.FC = (props) => { resetIsApprovedState() // check approved status checkApprovedStatus() + // reset fees + prevDepositFees.current = O.none // reload fees reloadFeesHandler() } @@ -929,7 +900,8 @@ export const SymDeposit: React.FC = (props) => { resetIsApprovedState, reloadSelectedPoolDetail, resetDepositState, - changePercentHandler + changePercentHandler, + minRuneAmountToDeposit ]) return ( @@ -940,44 +912,55 @@ export const SymDeposit: React.FC = (props) => { )} - -
- setSelectedInput('asset')} - inputOnBlurHandler={inputOnBlur} - price={assetPrice} - balances={balances} - percentValue={percentValueToDeposit} - onChangePercent={changePercentHandler} - onChangeAsset={onChangeAssetHandler} - priceAsset={priceAsset} - network={network} - onAfterSliderChange={onAfterSliderChangeHandler} - /> - {minAssetAmountLabel} -
- - setSelectedInput('rune')} + asset={asset} + selectedAmount={assetAmountToDepositMax1e8} + maxAmount={maxAssetAmountToDepositMax1e8} + onChangeAssetAmount={assetAmountChangeHandler} + inputOnFocusHandler={() => setSelectedInput('asset')} inputOnBlurHandler={inputOnBlur} - price={runePrice} + price={assetPrice} + balances={balances} + percentValue={percentValueToDeposit} + onChangePercent={changePercentHandler} + onChangeAsset={onChangeAssetHandler} priceAsset={priceAsset} network={network} - balances={[]} + onAfterSliderChange={onAfterSliderChangeHandler} + minAmountError={minAssetAmountError} + minAmountLabel={`${intl.formatMessage({ id: 'common.min' })}: ${formatAssetAmountCurrency({ + asset, + amount: baseToAsset(minAssetAmountToDepositMax1e8), + trimZeros: true + })}`} /> + + + <> + setSelectedInput('rune')} + inputOnBlurHandler={inputOnBlur} + price={runePrice} + priceAsset={priceAsset} + network={network} + balances={[]} + minAmountError={minRuneAmountError} + minAmountLabel={`${intl.formatMessage({ id: 'common.min' })}: ${formatAssetAmountCurrency({ + asset: AssetRuneNative, + amount: baseToAsset(minRuneAmountToDeposit), + trimZeros: true + })}`} + /> + +
{isApproved ? ( diff --git a/src/renderer/components/swap/Swap.stories.tsx b/src/renderer/components/swap/Swap.stories.tsx index 69f8cdca8..a193a5813 100644 --- a/src/renderer/components/swap/Swap.stories.tsx +++ b/src/renderer/components/swap/Swap.stories.tsx @@ -3,7 +3,16 @@ import React from 'react' import * as RD from '@devexperts/remote-data-ts' import { Story, Meta } from '@storybook/react' import { BTC_DECIMAL } from '@xchainjs/xchain-bitcoin' -import { assetAmount, AssetBTC, AssetRuneNative, assetToBase, baseAmount, bn } from '@xchainjs/xchain-util' +import { + assetAmount, + AssetBNB, + AssetBTC, + AssetRuneNative, + assetToBase, + assetToString, + baseAmount, + bn +} from '@xchainjs/xchain-util' import * as O from 'fp-ts/lib/Option' import * as Rx from 'rxjs' import * as RxOp from 'rxjs/operators' @@ -36,30 +45,16 @@ const defaultProps: SwapProps = { Rx.of({ ...INITIAL_SWAP_STATE, step: 3, swapTx: RD.success('tx-hash'), swap: RD.success(true) }) ) ), - poolDetails: [ - { - asset: 'BNB.BNB', - assetDepth: '3403524249', - assetPrice: '25.249547241877167', - assetPriceUSD: '272.13', - poolAPY: '0', - runeDepth: '85937446314', - status: 'available', - units: '200000000000', - volume24h: '0' + poolsData: { + [assetToString(AssetBNB)]: { + assetBalance: baseAmount(1), + runeBalance: baseAmount(20) }, - { - asset: 'BTC.BTC', - assetDepth: '17970413', - assetPrice: '56851.67420275761', - assetPriceUSD: '59543.12', - poolAPY: '0', - runeDepth: '1021648065165', - status: 'available', - units: '700389963172', - volume24h: '0' + [assetToString(AssetBTC)]: { + assetBalance: baseAmount(1), + runeBalance: baseAmount(3000) } - ], + }, walletBalances: O.some([ { asset: AssetRuneNative, diff --git a/src/renderer/components/swap/Swap.tsx b/src/renderer/components/swap/Swap.tsx index d22c678fd..e792a40a6 100644 --- a/src/renderer/components/swap/Swap.tsx +++ b/src/renderer/components/swap/Swap.tsx @@ -8,7 +8,6 @@ import { assetToString, baseToAsset, BaseAmount, - AssetRuneNative, baseAmount, formatAssetAmount, formatAssetAmountCurrency, @@ -52,14 +51,11 @@ import { SwapFeesHandler, ReloadSwapFeesHandler, SwapFeesRD, - SwapFeesParams, SwapFees, FeeRD } from '../../services/chain/types' import { ApproveFeeHandler, ApproveParams, IsApprovedRD, LoadApproveFeeHandler } from '../../services/ethereum/types' import { PoolAssetDetail, PoolAssetDetails, PoolAddress, PoolsDataMap } from '../../services/midgard/types' -import { PoolDetails } from '../../services/midgard/types' -import { getPoolDetailsHashMap } from '../../services/midgard/utils' import { ApiError, KeystoreState, @@ -90,7 +86,7 @@ export type SwapProps = { assets: { inAsset: AssetWithDecimal; outAsset: AssetWithDecimal } poolAddress: O.Option swap$: SwapStateHandler - poolDetails: PoolDetails + poolsData: PoolsDataMap walletBalances: O.Option goToTransaction: (txHash: string) => void validatePassword$: ValidatePasswordHandler @@ -113,7 +109,7 @@ export const Swap = ({ assets: { inAsset: sourceAssetWD, outAsset: targetAssetWD }, poolAddress: oPoolAddress, swap$, - poolDetails, + poolsData, walletBalances, goToTransaction = (_) => {}, validatePassword$, @@ -139,9 +135,6 @@ export const Swap = ({ const prevSourceAsset = useRef>(O.none) const prevTargetAsset = useRef>(O.none) - // convert to hash map here instead of using getPoolDetail - const poolsData: PoolsDataMap = useMemo(() => getPoolDetailsHashMap(poolDetails, AssetRuneNative), [poolDetails]) - const oSourcePoolAsset: O.Option = useMemo( () => Utils.pickPoolAsset(availableAssets, sourceAssetProp), [availableAssets, sourceAssetProp] @@ -253,15 +246,6 @@ export const Swap = ({ return max1e8BaseAmount(swapResultAmount) }, [swapData.swapResult, targetAssetDecimal]) - const swapFeesParams: SwapFeesParams = useMemo( - () => ({ - inAsset: sourceAssetProp, - outAsset: targetAssetProp, - inAmount: amountToSwapMax1e8 - }), - [amountToSwapMax1e8, sourceAssetProp, targetAssetProp] - ) - const oApproveParams: O.Option = useMemo(() => { return FP.pipe( sequenceTOption( @@ -288,8 +272,10 @@ export const Swap = ({ const [swapFeesRD] = useObservableState(() => { return FP.pipe( - swapFeesParams, - fees$, + fees$({ + inAsset: sourceAssetProp, + outAsset: targetAssetProp + }), liveData.map((chainFees) => { // store every successfully loaded chainFees to the ref value prevChainFees.current = O.some(chainFees) @@ -298,9 +284,23 @@ export const Swap = ({ ) }, RD.success(zeroSwapFees)) + const swapFees: SwapFees = useMemo( + () => + FP.pipe( + swapFeesRD, + RD.toOption, + O.alt(() => prevChainFees.current), + O.getOrElse(() => zeroSwapFees) + ), + [swapFeesRD, zeroSwapFees] + ) + const reloadFeesHandler = useCallback(() => { - reloadFees(swapFeesParams) - }, [swapFeesParams, reloadFees]) + reloadFees({ + inAsset: sourceAssetProp, + outAsset: targetAssetProp + }) + }, [reloadFees, sourceAssetProp, targetAssetProp]) const [approveFeesRD, approveFeesParamsUpdated] = useObservableState>( (oApproveFeeParam$) => { @@ -362,29 +362,16 @@ export const Swap = ({ [onChangePath, oSourceAsset] ) - const prevMinAmountToSwapMax1e8 = useRef>(O.none) - const minAmountToSwapMax1e8: BaseAmount = useMemo( () => - FP.pipe( - RD.toOption(swapFeesRD), - O.map((swapFees) => - Utils.minAmountToSwapMax1e8({ - swapFees, - inAsset: sourceAssetProp, - inAssetDecimal: sourceAssetDecimal, - outAsset: targetAssetProp, - poolsData - }) - ), - O.map((minAmount) => { - prevMinAmountToSwapMax1e8.current = O.some(minAmount) - return minAmount - }), - - O.getOrElse(() => ZERO_BASE_AMOUNT) - ), - [poolsData, sourceAssetDecimal, sourceAssetProp, swapFeesRD, targetAssetProp] + Utils.minAmountToSwapMax1e8({ + swapFees, + inAsset: sourceAssetProp, + inAssetDecimal: sourceAssetDecimal, + outAsset: targetAssetProp, + poolsData + }), + [poolsData, sourceAssetDecimal, sourceAssetProp, swapFees, targetAssetProp] ) const minAmountError = useMemo(() => { @@ -696,51 +683,44 @@ export const Swap = ({ // ignore error check by having zero amounts or min amount errors if (isZeroAmountToSwap || minAmountError) return false - return FP.pipe( - swapFeesRD, - RD.getOrElse(() => zeroSwapFees), - (swapFees) => Utils.minBalanceToSwap(swapFees).gt(sourceChainAssetAmount) - ) - }, [isZeroAmountToSwap, minAmountError, swapFeesRD, zeroSwapFees, sourceChainAssetAmount]) + return Utils.minBalanceToSwap(swapFees).gt(sourceChainAssetAmount) + }, [isZeroAmountToSwap, minAmountError, swapFees, sourceChainAssetAmount]) const sourceChainFeeErrorLabel: JSX.Element = useMemo(() => { if (!sourceChainFeeError) { return <> } - return FP.pipe( - RD.toOption(swapFeesRD), - O.map(({ inFee }) => ( - - {intl.formatMessage( - { id: 'swap.errors.amount.balanceShouldCoverChainFee' }, - { - balance: formatAssetAmountCurrency({ - asset: sourceAssetProp, - amount: baseToAsset(sourceAssetAmount), - trimZeros: true - }), - fee: formatAssetAmountCurrency({ - asset: inFee.asset, - trimZeros: true, - amount: baseToAsset(inFee.amount) - }) - } - )} - - )), - O.getOrElse(() => <>) + const { + inFee: { amount: inFeeAmount, asset: inFeeAsset } + } = swapFees + + return ( + + {intl.formatMessage( + { id: 'swap.errors.amount.balanceShouldCoverChainFee' }, + { + balance: formatAssetAmountCurrency({ + asset: sourceAssetProp, + amount: baseToAsset(sourceAssetAmount), + trimZeros: true + }), + fee: formatAssetAmountCurrency({ + asset: inFeeAsset, + trimZeros: true, + amount: baseToAsset(inFeeAmount) + }) + } + )} + ) - }, [sourceChainFeeError, swapFeesRD, intl, sourceAssetProp, sourceAssetAmount]) + }, [sourceChainFeeError, swapFees, intl, sourceAssetProp, sourceAssetAmount]) // Helper to price target fees into source asset const outFeeInTargetAsset: BaseAmount = useMemo(() => { const { outFee: { amount: outFeeAmount, asset: outFeeAsset } - }: SwapFees = FP.pipe( - swapFeesRD, - RD.getOrElse(() => zeroSwapFees) - ) + } = swapFees // no pricing if target asset === target fee asset if (eqAsset.equals(targetAssetProp, outFeeAsset)) return outFeeAmount @@ -758,7 +738,7 @@ export const Swap = ({ getValueOfAsset1InAsset2(to1e8BaseAmount(outFeeAmount), targetFeeAssetPoolData, targetAssetPoolData) ) ) - }, [swapFeesRD, targetAssetProp, poolsData, zeroSwapFees]) + }, [swapFees, targetAssetProp, poolsData]) const swapResultLabel = useMemo( () => formatAssetAmount({ amount: baseToAsset(swapResultAmountMax1e8), trimZeros: true }), @@ -981,7 +961,7 @@ export const Swap = ({ onBlur={() => reloadFeesHandler()} amount={amountToSwapMax1e8} maxAmount={maxAmountToSwapMax1e8} - hasError={sourceChainFeeError || minAmountError} + hasError={minAmountError} asset={sourceAssetProp} disabled={unlockedWallet} /> diff --git a/src/renderer/components/swap/Swap.utils.ts b/src/renderer/components/swap/Swap.utils.ts index dd32b81e8..0f0434abd 100644 --- a/src/renderer/components/swap/Swap.utils.ts +++ b/src/renderer/components/swap/Swap.utils.ts @@ -22,8 +22,9 @@ import { } from '../../helpers/assetHelper' import { eqAsset } from '../../helpers/fp/eq' import { sequenceTOption } from '../../helpers/fpHelpers' -import { SwapFee, SwapFees } from '../../services/chain/types' +import { SwapFees } from '../../services/chain/types' import { PoolAssetDetail, PoolAssetDetails, PoolsDataMap } from '../../services/midgard/types' +import { AssetWithAmount } from '../../types/asgardex' import { SwapData } from './Swap.types' /** @@ -172,7 +173,7 @@ export const priceFeeAmountForInAsset = ({ inAssetDecimal, poolsData }: { - fee: SwapFee + fee: AssetWithAmount inAsset: Asset inAssetDecimal: number poolsData: PoolsDataMap diff --git a/src/renderer/components/uielements/assets/assetCard/AssetCard.style.ts b/src/renderer/components/uielements/assets/assetCard/AssetCard.style.ts index e0f5df1be..fbe4ebe27 100644 --- a/src/renderer/components/uielements/assets/assetCard/AssetCard.style.ts +++ b/src/renderer/components/uielements/assets/assetCard/AssetCard.style.ts @@ -24,12 +24,12 @@ export const AssetCardWrapper = styled.div` } ` -export const CardBorderWrapper = styled.div` +export const CardBorderWrapper = styled.div<{ error: boolean }>` display: flex; flex-direction: column; margin-bottom: 20px; - - border: 1px solid ${palette('gray', 0)}; + border: 1px solid; + border-color: ${({ error }) => (error ? palette('error', 0) : palette('gray', 0))}; border-radius: 3px; background-color: ${palette('background', 1)}; @@ -69,6 +69,11 @@ export const FooterLabel = styled(Label).attrs({ padding: 0; ` +export const MinAmountLabel = styled(Label)` + padding-top: 0; + text-transform: uppercase; +` + export const AssetSelect = styled(BaseAssetSelect)` width: 100%; diff --git a/src/renderer/components/uielements/assets/assetCard/AssetCard.tsx b/src/renderer/components/uielements/assets/assetCard/AssetCard.tsx index 993e78cb8..2deadedc5 100644 --- a/src/renderer/components/uielements/assets/assetCard/AssetCard.tsx +++ b/src/renderer/components/uielements/assets/assetCard/AssetCard.tsx @@ -49,6 +49,8 @@ export type Props = { disabled?: boolean network: Network onAfterSliderChange?: (value: number) => void + minAmountError?: boolean + minAmountLabel?: string } export const AssetCard: React.FC = (props): JSX.Element => { @@ -73,7 +75,9 @@ export const AssetCard: React.FC = (props): JSX.Element => { maxAmount, disabled, network, - onAfterSliderChange + onAfterSliderChange, + minAmountError = false, + minAmountLabel = '' } = props const [openDropdown, setOpenDropdown] = useState(false) @@ -148,7 +152,7 @@ export const AssetCard: React.FC = (props): JSX.Element => { {!!title && } - + = (props): JSX.Element => { + {minAmountLabel && ( + {minAmountLabel} + )} {withPercentSlider && ( ({ }) export const eqOSwapFeesParams = O.getEq(eqSwapFeesParams) + +export const eqDepositFees = Eq.getStructEq({ + inFee: eqBaseAmount, + outFee: eqBaseAmount, + refundFee: eqBaseAmount +}) + +export const eqODepositFees = O.getEq(eqDepositFees) + +export const eqDepositAssetFees = Eq.getStructEq({ + inFee: eqBaseAmount, + outFee: eqBaseAmount, + refundFee: eqBaseAmount, + asset: eqAsset +}) + +export const eqODepositAssetFees = O.getEq(eqDepositAssetFees) diff --git a/src/renderer/services/chain/const.ts b/src/renderer/services/chain/const.ts index b3b0c55f4..ddab19f87 100644 --- a/src/renderer/services/chain/const.ts +++ b/src/renderer/services/chain/const.ts @@ -1,8 +1,6 @@ import * as RD from '@devexperts/remote-data-ts' import { FeeOptionKey } from '@xchainjs/xchain-client' -import * as O from 'fp-ts/lib/Option' -import { ZERO_BASE_AMOUNT } from '../../const' import { AsymDepositState, SwapState, @@ -10,8 +8,7 @@ import { WithdrawState, UpgradeRuneTxState, SendTxState, - TxTypes, - DepositFees + TxTypes } from './types' export const MAX_SWAP_STEPS = 3 @@ -47,8 +44,6 @@ export const INITIAL_SYM_DEPOSIT_STATE: SymDepositState = { deposit: RD.initial } -export const ZERO_SYM_DEPOSIT_FEES: DepositFees = { thor: O.some(ZERO_BASE_AMOUNT), asset: ZERO_BASE_AMOUNT } - export const INITIAL_WITHDRAW_STATE: WithdrawState = { step: 1, stepsTotal: 3, diff --git a/src/renderer/services/chain/fees/common.ts b/src/renderer/services/chain/fees/common.ts index 8aad8ff0e..86ccd8baa 100644 --- a/src/renderer/services/chain/fees/common.ts +++ b/src/renderer/services/chain/fees/common.ts @@ -6,7 +6,7 @@ import { liveData } from '../../../helpers/rx/liveData' import { service as midgardService } from '../../midgard/service' import * as THOR from '../../thorchain' import { FeeOptionKeys } from '../const' -import { SwapFeeLD } from '../types' +import { PoolFeeLD } from '../types' import { getChainFeeByGasRate } from './utils' const { @@ -16,7 +16,7 @@ const { /** * Fees for swap txs */ -export const poolFee$ = (asset: Asset): SwapFeeLD => { +export const poolFee$ = (asset: Asset): PoolFeeLD => { // special case for RUNE if (isRuneNativeAsset(asset)) { return FP.pipe( diff --git a/src/renderer/services/chain/fees/deposit.ts b/src/renderer/services/chain/fees/deposit.ts index 3b1317e02..5844ce5b3 100644 --- a/src/renderer/services/chain/fees/deposit.ts +++ b/src/renderer/services/chain/fees/deposit.ts @@ -1,172 +1,87 @@ import * as RD from '@devexperts/remote-data-ts' -import { ETHAddress } from '@xchainjs/xchain-ethereum' -import { - Asset, - AssetRuneNative, - BaseAmount, - BCHChain, - BNBChain, - BTCChain, - CosmosChain, - ETHChain, - LTCChain, - PolkadotChain, - THORChain -} from '@xchainjs/xchain-util' -import BigNumber from 'bignumber.js' +import { Asset, AssetRuneNative } from '@xchainjs/xchain-util' import * as FP from 'fp-ts/lib/function' import * as O from 'fp-ts/lib/Option' import * as Rx from 'rxjs' import * as RxOp from 'rxjs/operators' -import { getEthTokenAddress, isEthAsset } from '../../../helpers/assetHelper' -import { sequenceTRDFromArray } from '../../../helpers/fpHelpers' +import { ZERO_BASE_AMOUNT } from '../../../const' +import { getChainAsset } from '../../../helpers/chainHelper' +import { eqOAsset } from '../../../helpers/fp/eq' import { liveData } from '../../../helpers/rx/liveData' import { observableState } from '../../../helpers/stateHelper' -import * as BNB from '../../binance' -import * as BTC from '../../bitcoin' -import * as BCH from '../../bitcoincash' -import { ethRouterABI } from '../../const' -import * as ETH from '../../ethereum' -import * as LTC from '../../litecoin' -import { PoolAddress } from '../../midgard/types' +import { service as midgardService } from '../../midgard/service' import * as THOR from '../../thorchain' -import { FeeOptionKeys, ZERO_SYM_DEPOSIT_FEES } from '../const' -import { FeeLD, DepositFeesLD, Memo, SymDepositParams, AsymDepositParams, SymDepositFeesHandler } from '../types' +import { SymDepositFees, SymDepositFeesHandler } from '../types' +import { poolFee$ } from './common' -const depositFee$ = ({ - asset, - poolAddress: oPoolAddress, - amount, - memo -}: { - readonly poolAddress: O.Option - readonly asset: Asset - readonly amount: BaseAmount - readonly memo: Memo -}): FeeLD => { - switch (asset.chain) { - case BNBChain: - return BNB.fees$().pipe(liveData.map(({ fast }) => fast)) - case BTCChain: { - return BTC.feesWithRates$(memo).pipe(liveData.map(({ fees }) => fees[FeeOptionKeys.DEPOSIT])) - } +const { + pools: { reloadGasRates } +} = midgardService - case THORChain: { - return THOR.fees$().pipe(liveData.map((fees) => fees[FeeOptionKeys.DEPOSIT])) - } - case ETHChain: { - return FP.pipe( - oPoolAddress, - O.chain(({ router }) => router), - O.fold( - () => Rx.of(RD.failure(Error('ETH router address is missing'))), - (router) => { - const routerAddress = router.toLowerCase() - return ETH.poolInTxFees$({ - address: router, - abi: ethRouterABI, - func: 'deposit', - params: isEthAsset(asset) - ? [ - routerAddress, - ETHAddress, - 0, - memo, - { - // Send `BaseAmount` w/o decimal and always round down for currencies - value: amount.amount().toFixed(0, BigNumber.ROUND_DOWN) - } - ] - : [ - routerAddress, - FP.pipe(getEthTokenAddress(asset), O.toUndefined), - // Send `BaseAmount` w/o decimal and always round down for currencies - amount.amount().toFixed(0, BigNumber.ROUND_DOWN), - memo - ] - }) - } - ), - // Actual gas fee changes time to time so in many cases, actual fast gas fee is bigger than estimated fast fee - // To avoid low gas fee error, we apply fastest fee for ETH only - liveData.map((fees) => fees['fastest']) - ) - } - case CosmosChain: - return Rx.of(RD.failure(Error('Deposit fee for Cosmos has not been implemented'))) - case PolkadotChain: - return Rx.of(RD.failure(Error('Deposit fee for Polkadot has not been implemented'))) - case BCHChain: { - return BCH.feesWithRates$(memo).pipe(liveData.map(({ fees }) => fees[FeeOptionKeys.DEPOSIT])) - } - case LTCChain: { - return LTC.feesWithRates$(memo).pipe(liveData.map(({ fees }) => fees[FeeOptionKeys.DEPOSIT])) - } +/** + * Returns zero sym deposit fees + * by given `in` / `out` assets of a swap + */ +export const getZeroSymDepositFees = (asset: Asset): SymDepositFees => ({ + rune: { inFee: ZERO_BASE_AMOUNT, outFee: ZERO_BASE_AMOUNT, refundFee: ZERO_BASE_AMOUNT }, + asset: { + asset: getChainAsset(asset.chain), + inFee: ZERO_BASE_AMOUNT, + outFee: ZERO_BASE_AMOUNT, + refundFee: ZERO_BASE_AMOUNT } -} +}) // State to reload sym deposit fees -const { get$: reloadSymDepositFees$, set: reloadSymDepositFees } = observableState>(O.none) +const { get$: reloadSymDepositFees$, get: reloadSymDepositFeesState, set: _reloadSymDepositFees } = observableState< + O.Option +>(O.none) + +// Triggers reloading of deposit fees +const reloadSymDepositFees = (asset: Asset) => { + // (1) update reload state only, if prev. vs. current assets are different + if (!eqOAsset.equals(O.some(asset), reloadSymDepositFeesState())) { + _reloadSymDepositFees(O.some(asset)) + } + // (2) Reload fees for RUNE + THOR.reloadFees() + // (3) Reload fees for asset + reloadGasRates() +} -const symDepositFees$: SymDepositFeesHandler = (oInitialParams) => { +const symDepositFees$: SymDepositFeesHandler = (initialAsset) => { return FP.pipe( reloadSymDepositFees$, RxOp.debounceTime(300), RxOp.switchMap((oReloadParams) => { - return FP.pipe( - // (1) Always check reload params first + // Since `oReloadParams` is `none` by default, + // `initialAsset` will be used as first value + const asset = FP.pipe( oReloadParams, - // (2) If reload params not set (which is by default), use initial params - O.alt(() => oInitialParams), - O.fold( - // If both (initial + reload params) are not set, return zero fees - () => Rx.of(RD.success(ZERO_SYM_DEPOSIT_FEES)), - ({ amounts, poolAddress, asset, memos }) => { - const { asset: assetAmount, rune: runeAmount } = amounts - - // in case of zero amount, return zero fees (no API request needed) - if (assetAmount.amount().isZero() || runeAmount.amount().isZero()) - return Rx.of(RD.success(ZERO_SYM_DEPOSIT_FEES)) + O.getOrElse(() => initialAsset) + ) - return FP.pipe( - Rx.combineLatest([ - // asset - depositFee$({ - asset, - amount: assetAmount, - poolAddress: O.some(poolAddress), - memo: memos.asset - }), - // rune - depositFee$({ - asset: AssetRuneNative, - amount: runeAmount, - memo: memos.rune, - poolAddress: O.none - }) - ]), - RxOp.map(sequenceTRDFromArray), - liveData.map(([asset, thor]) => ({ - asset, - thor: O.fromNullable(thor) - })) - ) + return FP.pipe( + liveData.sequenceS({ + runeInFee: poolFee$(AssetRuneNative), + assetInFee: poolFee$(asset) + }), + liveData.map(({ runeInFee, assetInFee }) => ({ + rune: { inFee: runeInFee.amount, outFee: runeInFee.amount.times(3), refundFee: runeInFee.amount.times(3) }, + asset: { + asset: getChainAsset(asset.chain), + inFee: assetInFee.amount, + outFee: assetInFee.amount.times(3), + refundFee: assetInFee.amount.times(3) } - ) + })) ) }) ) } // State to reload sym deposit fees -const { get$: reloadAsymDepositFee$, set: reloadAsymDepositFee } = observableState( - undefined -) -const asymDepositFee$ = (_: AsymDepositParams): DepositFeesLD => - FP.pipe( - reloadAsymDepositFee$, - RxOp.debounceTime(300), - RxOp.map((_) => RD.failure(Error('not implemented yet'))) - ) +const { get$: _reloadAsymDepositFee$, set: reloadAsymDepositFee } = observableState>(O.none) +const asymDepositFee$ = Rx.of(RD.failure(Error('asym deposit fees have not implemented yet'))) export { symDepositFees$, asymDepositFee$, reloadSymDepositFees, reloadAsymDepositFee } diff --git a/src/renderer/services/chain/fees/swap.ts b/src/renderer/services/chain/fees/swap.ts index a259bef14..9078f9ece 100644 --- a/src/renderer/services/chain/fees/swap.ts +++ b/src/renderer/services/chain/fees/swap.ts @@ -32,7 +32,7 @@ const { get$: updateSwapFeesParams$, get: updateSwapFeesParamsState, set: update O.Option >(O.none) -// To trigger reloading of swap fees accept `none` option values of `SwapFeesParams` only +// To trigger reload of swap fees const reloadSwapFees = (params: SwapFeesParams) => { const { inAsset, outAsset } = params diff --git a/src/renderer/services/chain/fees/utils.ts b/src/renderer/services/chain/fees/utils.ts index 0392f9ba2..b9b01c970 100644 --- a/src/renderer/services/chain/fees/utils.ts +++ b/src/renderer/services/chain/fees/utils.ts @@ -15,7 +15,7 @@ import { isLtcAsset } from '../../../helpers/assetHelper' import { isBnbChain } from '../../../helpers/chainHelper' -import { SwapFee } from '../types' +import { AssetWithAmount } from '../../../types/asgardex' /** * @@ -24,7 +24,13 @@ import { SwapFee } from '../types' * Formulas based on "Better Fees Handling #1381" * @see https://github.com/thorchain/asgardex-electron/issues/1381#issuecomment-827513798 */ -export const getChainFeeByGasRate = ({ gasRate, asset }: { gasRate: BigNumber; asset: Asset }): O.Option => { +export const getChainFeeByGasRate = ({ + gasRate, + asset +}: { + gasRate: BigNumber + asset: Asset +}): O.Option => { const gasRateGwei = gasRate.multipliedBy(10 ** 9) if (isBnbChain(asset.chain)) { diff --git a/src/renderer/services/chain/types.ts b/src/renderer/services/chain/types.ts index e54f468fb..a7e2626f6 100644 --- a/src/renderer/services/chain/types.ts +++ b/src/renderer/services/chain/types.ts @@ -7,6 +7,7 @@ import * as Rx from 'rxjs' import { Network } from '../../../shared/api/types' import { LiveData } from '../../helpers/rx/liveData' import { AssetWithDecimal } from '../../types/asgardex' +import { AssetWithAmount } from '../../types/asgardex' import { PoolAddress } from '../midgard/types' import { ApiError, TxHashRD } from '../wallet/types' @@ -31,20 +32,28 @@ export type MemoRx = Rx.Observable> export type SymDepositMemo = { rune: Memo; asset: Memo } export type SymDepositMemoRx = Rx.Observable> +export type DepositFees = { inFee: BaseAmount; outFee: BaseAmount; refundFee: BaseAmount } +export type DepositAssetFees = DepositFees & { asset: Asset } /** - * Deposit fees - - * One fee (asymmetrical deposit) or two fees (symmetrical deposit): + * Sym. deposit fees * - * thor: Fee for transaction on Thorchain. Needed for sym deposit txs. It's `O.none` for asym deposit txs - * asset: Fee for transaction on asset chain */ -export type DepositFees = { thor: O.Option; asset: BaseAmount } -export type DepositFeesRD = RD.RemoteData -export type DepositFeesLD = LiveData +export type SymDepositFees = { + /** fee for RUNE txs */ + readonly rune: DepositFees + /** fee for asset txs */ + readonly asset: DepositAssetFees +} + +export type SymDepositFeesRD = RD.RemoteData +export type SymDepositFeesLD = LiveData + +export type SymDepositFeesParams = { + readonly asset: Asset +} -export type SymDepositFeesHandler = (params: O.Option) => DepositFeesLD -export type ReloadSymDepositFeesHandler = (params: O.Option) => void +export type SymDepositFeesHandler = (asset: Asset) => SymDepositFeesLD +export type ReloadSymDepositFeesHandler = (asset: Asset) => void export type AsymDepositParams = { readonly poolAddress: PoolAddress @@ -117,20 +126,13 @@ export type SwapOutTx = { readonly memo: Memo } -export type SwapFee = { - /** fee amount */ - readonly amount: BaseAmount - /** Asset, which fee is related to */ - readonly asset: Asset -} - -export type SwapFeeLD = LiveData +export type PoolFeeLD = LiveData export type SwapFees = { /** Inbound tx fee */ - readonly inFee: SwapFee + readonly inFee: AssetWithAmount /** Outbound tx fee */ - readonly outFee: SwapFee + readonly outFee: AssetWithAmount } export type SwapFeesRD = RD.RemoteData diff --git a/src/renderer/services/midgard/pools.ts b/src/renderer/services/midgard/pools.ts index 0dc442b72..2aa8babb0 100644 --- a/src/renderer/services/midgard/pools.ts +++ b/src/renderer/services/midgard/pools.ts @@ -59,7 +59,8 @@ import { InboundAddressesLD, PoolAddresses, InboundAddresses, - GasRateLD + GasRateLD, + PoolsState } from './types' import { getPoolAddressesByChain, @@ -68,7 +69,8 @@ import { pricePoolSelector, pricePoolSelectorFromRD, inboundToPoolAddresses, - getGasRateByChain + getGasRateByChain, + toPoolsData } from './utils' const PRICE_POOL_KEY = 'asgdx-price-pool' @@ -158,7 +160,7 @@ const createPoolsService = ( * * If status is not set (or undefined), `PoolDetails` of all Pools will be loaded */ - const apiGetPoolsByStatus$ = (status?: GetPoolsStatusEnum) => { + const apiGetPoolsByStatus$ = (status?: GetPoolsStatusEnum): PoolDetailsLD => { switch (status) { case GetPoolsStatusEnum.Available: return apiGetPoolsEnabled$ @@ -202,7 +204,7 @@ const createPoolsService = ( ) /** - * `PoolDetails` data from Midgard + * `PoolDetails` data by given `GetPoolsStatusEnum` */ const apiGetPoolDetails$: (assetOrAssets: string | string[], status?: GetPoolsStatusEnum) => PoolDetailsLD = ( assetOrAssets, @@ -322,20 +324,26 @@ const createPoolsService = ( Rx.combineLatest([poolAssets$, assetDetails$, poolDetails$, pricePools$]), RxOp.map((state) => RD.combine(...state)), RxOp.map( - RD.map(([poolAssets, assetDetails, poolDetails, pricePools]) => { - const prevAsset = getSelectedPricePoolAsset() - const nullablePricePools = O.toNullable(pricePools) - if (nullablePricePools) { - const selectedPricePool = pricePoolSelector(nullablePricePools, prevAsset) - setSelectedPricePoolAsset(selectedPricePool.asset) + RD.map( + ([poolAssets, assetDetails, poolDetails, pricePools]): PoolsState => { + const prevAsset = getSelectedPricePoolAsset() + const nullablePricePools = O.toNullable(pricePools) + if (nullablePricePools) { + const selectedPricePool = pricePoolSelector(nullablePricePools, prevAsset) + setSelectedPricePoolAsset(selectedPricePool.asset) + } + // Provide `PoolData` map (needed for pricing) + const poolsData = toPoolsData(poolDetails) + + return { + poolAssets, + assetDetails, + poolsData, + poolDetails, + pricePools + } } - return { - poolAssets, - assetDetails, - poolDetails, - pricePools - } - }) + ) ), RxOp.startWith(RD.pending), RxOp.catchError((error: Error) => Rx.of(RD.failure(error))) diff --git a/src/renderer/services/midgard/types.ts b/src/renderer/services/midgard/types.ts index 22dc9a3fe..3cfc91ef6 100644 --- a/src/renderer/services/midgard/types.ts +++ b/src/renderer/services/midgard/types.ts @@ -51,12 +51,14 @@ export type PoolDetailRD = RD.RemoteData export type PoolDetailLD = LiveData export type PoolDetails = PoolDetail[] +export type PoolDetailsRD = RD.RemoteData export type PoolDetailsLD = LiveData /** * Hash map for storing `PoolData` (key: string of asset) */ export type PoolsDataMap = Record +export type PoolsDataMapRD = RD.RemoteData export type PriceDataIndex = { [symbol: string]: BigNumber @@ -66,6 +68,7 @@ export type PoolsState = { assetDetails: PoolAssetDetails poolAssets: PoolAssets poolDetails: PoolDetails + poolsData: PoolsDataMap pricePools: O.Option } export type PoolsStateRD = RD.RemoteData diff --git a/src/renderer/services/midgard/utils.test.ts b/src/renderer/services/midgard/utils.test.ts index b11553ed8..81c1451e2 100644 --- a/src/renderer/services/midgard/utils.test.ts +++ b/src/renderer/services/midgard/utils.test.ts @@ -37,7 +37,7 @@ import { getPoolDetail, toPoolData, filterPoolAssets, - getPoolDetailsHashMap, + toPoolsData, getPoolAddressesByChain, combineShares, combineSharesByAsset, @@ -151,12 +151,13 @@ describe('services/midgard/utils/', () => { const btc: PricePool = { asset: AssetBTC, poolData } const rune: PricePool = RUNE_PRICE_POOL const mockPoolsStateSuccess = (pricePools: PricePools): PoolsStateRD => - RD.success({ + RD.success({ assetDetails: [], poolAssets: [], poolDetails: [], - pricePools: O.some(pricePools) - } as PoolsState) + pricePools: O.some(pricePools), + poolsData: {} + }) it('selects ETH pool', () => { const poolsRD = mockPoolsStateSuccess([rune, eth, BUSDBAF, btc]) @@ -213,7 +214,7 @@ describe('services/midgard/utils/', () => { const bnbDetail = { asset: assetToString(AssetBNB) } as PoolDetail it('returns hashMap of pool details', () => { - const result = getPoolDetailsHashMap([runeDetail, bnbDetail], AssetRuneNative) + const result = toPoolsData([runeDetail, bnbDetail]) /** * Compare stringified structures 'cause diff --git a/src/renderer/services/midgard/utils.ts b/src/renderer/services/midgard/utils.ts index c116906a9..9aa299aa8 100644 --- a/src/renderer/services/midgard/utils.ts +++ b/src/renderer/services/midgard/utils.ts @@ -112,26 +112,11 @@ export const getPoolDetail = (details: PoolDetails, asset: Asset): O.Option { - const res = poolDetails.reduce((acc, cur) => { - if (!cur.asset) { - return acc - } - - return { ...acc, [cur.asset]: toPoolData(cur) } - }, {}) - - const runePricePool = RUNE_PRICE_POOL - - res[assetToString(runeAsset)] = { - ...runePricePool.poolData - } - - return res -} +export const toPoolsData = (poolDetails: Array>): PoolsDataMap => + poolDetails.reduce((acc, cur) => ({ ...acc, [cur.asset]: toPoolData(cur) }), {}) /** * Helper to get PoolData of BUSD pool diff --git a/src/renderer/views/deposit/add/SymDepositView.tsx b/src/renderer/views/deposit/add/SymDepositView.tsx index 924668b14..40e4e95f1 100644 --- a/src/renderer/views/deposit/add/SymDepositView.tsx +++ b/src/renderer/views/deposit/add/SymDepositView.tsx @@ -22,9 +22,9 @@ import { useWalletContext } from '../../../contexts/WalletContext' import { getChainAsset } from '../../../helpers/chainHelper' import { sequenceTRD } from '../../../helpers/fpHelpers' import { getAssetPoolPrice } from '../../../helpers/poolHelper' +import { liveData } from '../../../helpers/rx/liveData' import { filterWalletBalancesByAssets } from '../../../helpers/walletHelper' import { FundsCap, useFundsCap } from '../../../hooks/useFundsCap' -import { usePricePools } from '../../../hooks/usePricePools' import * as poolsRoutes from '../../../routes/pools' import { SymDepositMemo } from '../../../services/chain/types' import { DEFAULT_NETWORK } from '../../../services/const' @@ -47,6 +47,7 @@ export const SymDepositView: React.FC = (props) => { const intl = useIntl() const { network$ } = useAppContext() + const network = useObservableState(network$, DEFAULT_NETWORK) const onChangeAsset = useCallback( @@ -64,7 +65,8 @@ export const SymDepositView: React.FC = (props) => { selectedPricePoolAsset$, reloadSelectedPoolDetail, selectedPoolAddress$, - reloadInboundAddresses + reloadInboundAddresses, + poolsState$ }, shares: { reloadShares } } @@ -78,6 +80,15 @@ export const SymDepositView: React.FC = (props) => { getExplorerUrlByAsset$ } = useChainContext() + const [poolsDataRD] = useObservableState( + () => + FP.pipe( + poolsState$, + liveData.map(({ poolsData }) => poolsData) + ), + RD.initial + ) + const oPoolAddress: O.Option = useObservableState(selectedPoolAddress$, O.none) const { @@ -88,11 +99,9 @@ export const SymDepositView: React.FC = (props) => { const { data: fundsCapRD } = useFundsCap() - const { usdPricePool } = usePricePools() - const { approveERC20Token$, isApprovedERC20Token$, approveFee$, reloadApproveFee } = useEthereumContext() - // reload inbound addresses at `onMount` to get always latest `pool address` + // reload inbound addresses at `onMount` to get always latest `pool address` + `feeRates` useEffect(() => { reloadInboundAddresses() }, [reloadInboundAddresses]) @@ -222,7 +231,7 @@ export const SymDepositView: React.FC = (props) => { isApprovedERC20Token$={isApprovedERC20Token$} balances={[]} fundsCap={O.none} - usdPricePool={O.none} + poolsData={{}} /> ), @@ -246,12 +255,12 @@ export const SymDepositView: React.FC = (props) => { ) return FP.pipe( - sequenceTRD(assetPriceRD, poolAssetsRD, poolDetailRD), + sequenceTRD(assetPriceRD, poolAssetsRD, poolDetailRD, poolsDataRD), RD.fold( renderDisabledAddDeposit, (_) => renderDisabledAddDeposit(), (error) => renderDisabledAddDeposit(error), - ([assetPrice, poolAssets, poolDetail]) => { + ([assetPrice, poolAssets, poolDetail, poolsData]) => { const filteredBalances = FP.pipe( walletBalances, O.map((balances) => filterWalletBalancesByAssets(balances, poolAssets)), @@ -288,7 +297,7 @@ export const SymDepositView: React.FC = (props) => { approveERC20Token$={approveERC20Token$} isApprovedERC20Token$={isApprovedERC20Token$} fundsCap={fundsCap} - usdPricePool={usdPricePool} + poolsData={poolsData} /> ) diff --git a/src/renderer/views/swap/SwapView.tsx b/src/renderer/views/swap/SwapView.tsx index c07d3d520..10e380f1d 100644 --- a/src/renderer/views/swap/SwapView.tsx +++ b/src/renderer/views/swap/SwapView.tsx @@ -79,7 +79,7 @@ export const SwapView: React.FC = (_): JSX.Element => { } }, [oRouteSource, setSelectedPoolAsset]) - // reload inbound addresses at `onMount` to get always latest `pool address` + // reload inbound addresses at `onMount` to get always latest `pool address` + `feeRates` useEffect(() => { reloadInboundAddresses() }, [reloadInboundAddresses]) @@ -207,7 +207,7 @@ export const SwapView: React.FC = (_): JSX.Element => { () => <>, () => , renderError, - ([{ assetDetails: availableAssets, poolDetails }, sourceAsset, targetAsset]) => { + ([{ assetDetails: availableAssets, poolsData }, sourceAsset, targetAsset]) => { const hasRuneAsset = Boolean(availableAssets.find(({ asset }) => isRuneNativeAsset(asset))) if (!hasRuneAsset) { @@ -222,7 +222,7 @@ export const SwapView: React.FC = (_): JSX.Element => { assets={{ inAsset: sourceAsset, outAsset: targetAsset }} poolAddress={selectedPoolAddress} availableAssets={availableAssets} - poolDetails={poolDetails} + poolsData={poolsData} walletBalances={balances} reloadFees={reloadSwapFees} fees$={swapFees$}