From 8e976a5d70cfa2fead6bb39817574e1ad760b418 Mon Sep 17 00:00:00 2001 From: Linkie Link Date: Mon, 13 Nov 2023 17:55:03 +0100 Subject: [PATCH] Auto repay on trade (#631) --- .../AccountBalancesTable/Columns/Size.tsx | 6 ++-- .../AccountBalancesTable/Columns/Value.tsx | 7 +++- .../TradeModule/SwapForm/AutoRepayToggle.tsx | 32 +++++++++++++++++ .../Trade/TradeModule/SwapForm/index.tsx | 34 +++++++++++++++++-- src/hooks/useUpdatedAccount/index.ts | 28 +++++++++++++-- src/store/slices/broadcast.ts | 12 +++++++ src/store/slices/common.ts | 1 + src/types/interfaces/store/broadcast.d.ts | 1 + src/types/interfaces/store/common.d.ts | 1 + 9 files changed, 113 insertions(+), 9 deletions(-) create mode 100644 src/components/Trade/TradeModule/SwapForm/AutoRepayToggle.tsx diff --git a/src/components/Account/AccountBalancesTable/Columns/Size.tsx b/src/components/Account/AccountBalancesTable/Columns/Size.tsx index f40f43f5..ccb8fc24 100644 --- a/src/components/Account/AccountBalancesTable/Columns/Size.tsx +++ b/src/components/Account/AccountBalancesTable/Columns/Size.tsx @@ -32,6 +32,7 @@ export default function Size(props: Props) { const color = getAmountChangeColor(type, amountChange) const className = classNames('text-xs text-right', color) + const allowZero = !amountChange.isZero() if (size >= 1) return ( @@ -44,11 +45,12 @@ export default function Size(props: Props) { ) const formattedAmount = formatAmountToPrecision(size, MAX_AMOUNT_DECIMALS) - const lowAmount = formattedAmount === 0 ? MIN_AMOUNT : Math.max(formattedAmount, MIN_AMOUNT) + const minimumAmount = allowZero ? 0 : MIN_AMOUNT + const lowAmount = formattedAmount === 0 ? minimumAmount : Math.max(formattedAmount, MIN_AMOUNT) return ( , b: Row + ) } diff --git a/src/components/Trade/TradeModule/SwapForm/AutoRepayToggle.tsx b/src/components/Trade/TradeModule/SwapForm/AutoRepayToggle.tsx new file mode 100644 index 00000000..399ab758 --- /dev/null +++ b/src/components/Trade/TradeModule/SwapForm/AutoRepayToggle.tsx @@ -0,0 +1,32 @@ +import Switch from 'components/Switch' +import Text from 'components/Text' +import { Tooltip } from 'components/Tooltip' + +interface Props { + checked: boolean + onChange: (value: boolean) => void + buyAssetSymbol: string +} + +export default function AutoRepayToggle(props: Props) { + return ( +
+
+ Auto Repay Debt + + Use the bought {props.buyAssetSymbol} directly to repay your {props.buyAssetSymbol}{' '} + debt. + + } + /> +
+ +
+ +
+
+ ) +} diff --git a/src/components/Trade/TradeModule/SwapForm/index.tsx b/src/components/Trade/TradeModule/SwapForm/index.tsx index 40c8ddb3..253ca7e8 100644 --- a/src/components/Trade/TradeModule/SwapForm/index.tsx +++ b/src/components/Trade/TradeModule/SwapForm/index.tsx @@ -7,6 +7,7 @@ import DepositCapMessage from 'components/DepositCapMessage' import Divider from 'components/Divider' import RangeInput from 'components/RangeInput' import AssetAmountInput from 'components/Trade/TradeModule/SwapForm/AssetAmountInput' +import AutoRepayToggle from 'components/Trade/TradeModule/SwapForm/AutoRepayToggle' import MarginToggle from 'components/Trade/TradeModule/SwapForm/MarginToggle' import OrderTypeSelector from 'components/Trade/TradeModule/SwapForm/OrderTypeSelector' import { AvailableOrderType } from 'components/Trade/TradeModule/SwapForm/OrderTypeSelector/types' @@ -38,6 +39,7 @@ interface Props { export default function SwapForm(props: Props) { const { buyAsset, sellAsset } = props const useMargin = useStore((s) => s.useMargin) + const useAutoRepay = useStore((s) => s.useAutoRepay) const account = useCurrentAccount() const swap = useStore((s) => s.swap) const [slippage] = useLocalStorage(LocalStorageKeys.SLIPPAGE, DEFAULT_SETTINGS.slippage) @@ -46,7 +48,9 @@ export default function SwapForm(props: Props) { const { data: marketAssets } = useMarketAssets() const { data: route, isLoading: isRouteLoading } = useSwapRoute(sellAsset.denom, buyAsset.denom) const isBorrowEnabled = !!marketAssets.find(byDenom(sellAsset.denom))?.borrowEnabled + const isRepayable = account?.debts.find(byDenom(buyAsset.denom)) const [isMarginChecked, setMarginChecked] = useToggle(isBorrowEnabled ? useMargin : false) + const [isAutoRepayChecked, setAutoRepayChecked] = useToggle(isRepayable ? useAutoRepay : false) const [buyAssetAmount, setBuyAssetAmount] = useState(BN_ZERO) const [sellAssetAmount, setSellAssetAmount] = useState(BN_ZERO) const [maxBuyableAmountEstimation, setMaxBuyableAmountEstimation] = useState(BN_ZERO) @@ -157,6 +161,7 @@ export default function SwapForm(props: Props) { denomOut: buyAsset.denom, slippage, isMax: sellAssetAmount.isEqualTo(maxSellAmount), + repay: isAutoRepayChecked, }) }, [ removedLends, @@ -173,9 +178,15 @@ export default function SwapForm(props: Props) { const debouncedUpdateAccount = useMemo( () => debounce((removeCoin: BNCoin, addCoin: BNCoin, debtCoin: BNCoin) => { - simulateTrade(removeCoin, addCoin, debtCoin, isAutoLendEnabled ? 'lend' : 'deposit') + simulateTrade( + removeCoin, + addCoin, + debtCoin, + isAutoLendEnabled ? 'lend' : 'deposit', + isAutoRepayChecked, + ) }, 100), - [simulateTrade, isAutoLendEnabled], + [simulateTrade, isAutoLendEnabled, isAutoRepayChecked], ) const handleMarginToggleChange = useCallback( @@ -185,6 +196,13 @@ export default function SwapForm(props: Props) { }, [isBorrowEnabled, setMarginChecked], ) + const handleAutoRepayToggleChange = useCallback( + (isAutoRepay: boolean) => { + useStore.setState({ useAutoRepay: isAutoRepay }) + setAutoRepayChecked(isAutoRepay) + }, + [setAutoRepayChecked], + ) useEffect(() => { setBuyAssetAmount(BN_ZERO) @@ -195,6 +213,7 @@ export default function SwapForm(props: Props) { BNCoin.fromDenomAndBigNumber(sellAsset.denom, BN_ZERO), BNCoin.fromDenomAndBigNumber(sellAsset.denom, BN_ZERO), isAutoLendEnabled ? 'lend' : 'deposit', + isAutoRepayChecked, ) }, [ isBorrowEnabled, @@ -202,6 +221,7 @@ export default function SwapForm(props: Props) { buyAsset.denom, sellAsset.denom, isAutoLendEnabled, + isAutoRepayChecked, simulateTrade, setMarginChecked, ]) @@ -286,6 +306,15 @@ export default function SwapForm(props: Props) { borrowAssetSymbol={sellAsset.symbol} /> + + {isRepayable && ( + + )} +
)} - { + ( + removeCoin: BNCoin, + addCoin: BNCoin, + debtCoin: BNCoin, + target: 'deposit' | 'lend', + repay: boolean, + ) => { removeDeposits([]) removeLends([]) addDebts([]) @@ -143,13 +149,29 @@ export function useUpdatedAccount(account?: Account) { addLends([]) const { deposit, lend } = getDepositAndLendCoinsToSpend(removeCoin, account) + const currentDebtCoin = account?.debts.find(byDenom(addCoin.denom)) + let usedAmountForDebt = BN_ZERO if (!deposit.amount.isZero()) removeDeposits([deposit]) if (!lend.amount.isZero()) removeLends([lend]) - if (target === 'deposit') addDeposits([addCoin]) - if (target === 'lend') addLends([addCoin]) + if (repay && currentDebtCoin) { + if (currentDebtCoin.amount.isGreaterThanOrEqualTo(addCoin.amount)) { + removeDebts([addCoin]) + usedAmountForDebt = addCoin.amount + } else { + removeDebts([currentDebtCoin]) + usedAmountForDebt = currentDebtCoin.amount + } + } + const remainingAddCoin = BNCoin.fromDenomAndBigNumber( + addCoin.denom, + addCoin.amount.minus(usedAmountForDebt), + ) + + if (target === 'deposit') addDeposits(repay ? [remainingAddCoin] : [addCoin]) + if (target === 'lend') addLends(repay ? [remainingAddCoin] : [addCoin]) if (debtCoin.amount.isGreaterThan(BN_ZERO)) addDebts([debtCoin]) }, [account, addDebts, addDeposits, addLends, removeDeposits, removeLends], diff --git a/src/store/slices/broadcast.ts b/src/store/slices/broadcast.ts index bd7f0fea..25032624 100644 --- a/src/store/slices/broadcast.ts +++ b/src/store/slices/broadcast.ts @@ -689,6 +689,7 @@ export default function createBroadcastSlice( denomOut: string slippage: number isMax?: boolean + repay: boolean }) => { const msg: CreditManagerExecuteMsg = { update_credit_account: { @@ -703,6 +704,17 @@ export default function createBroadcastSlice( slippage: options.slippage.toString(), }, }, + ...(options.repay + ? [ + { + repay: { + coin: BNCoin.fromDenomAndBigNumber(options.denomOut, BN_ZERO).toActionCoin( + true, + ), + }, + }, + ] + : []), ], }, } diff --git a/src/store/slices/common.ts b/src/store/slices/common.ts index 8dd59717..1f714cb1 100644 --- a/src/store/slices/common.ts +++ b/src/store/slices/common.ts @@ -13,6 +13,7 @@ export default function createCommonSlice(set: SetState, get: GetSt migrationBanner: true, tutorial: true, useMargin: true, + useAutoRepay: true, isOracleStale: false, } } diff --git a/src/types/interfaces/store/broadcast.d.ts b/src/types/interfaces/store/broadcast.d.ts index e3ced21e..4b8bfb05 100644 --- a/src/types/interfaces/store/broadcast.d.ts +++ b/src/types/interfaces/store/broadcast.d.ts @@ -126,6 +126,7 @@ interface BroadcastSlice { denomOut: string slippage: number isMax?: boolean + repay: boolean }) => ExecutableTx toast: ToastResponse | ToastPending | null unlock: (options: { diff --git a/src/types/interfaces/store/common.d.ts b/src/types/interfaces/store/common.d.ts index c93cf458..0be59f6f 100644 --- a/src/types/interfaces/store/common.d.ts +++ b/src/types/interfaces/store/common.d.ts @@ -12,6 +12,7 @@ interface CommonSlice { migrationBanner: boolean tutorial: boolean useMargin: boolean + useAutoRepay: boolean isOracleStale: boolean }