From 06207557a409ad17747319db600a75c8c2e20754 Mon Sep 17 00:00:00 2001 From: Daniel Simon Date: Mon, 14 Jun 2021 11:20:56 +0700 Subject: [PATCH] feat: dev UI wip --- .../src/components/Trove/Adjusting.tsx | 27 ++++-- .../Trove/ExpensiveTroveChangeWarning.tsx | 86 +++++++++++++++++++ .../src/components/Trove/Opening.tsx | 25 +++++- .../src/components/Trove/TroveAction.tsx | 14 ++- .../src/components/Trove/TroveManager.tsx | 1 + .../dev-frontend/src/components/Warning.tsx | 27 ++++++ .../src/hooks/useStableTroveChange.ts | 32 +++++++ 7 files changed, 200 insertions(+), 12 deletions(-) create mode 100644 packages/dev-frontend/src/components/Trove/ExpensiveTroveChangeWarning.tsx create mode 100644 packages/dev-frontend/src/components/Warning.tsx create mode 100644 packages/dev-frontend/src/hooks/useStableTroveChange.ts diff --git a/packages/dev-frontend/src/components/Trove/Adjusting.tsx b/packages/dev-frontend/src/components/Trove/Adjusting.tsx index c6ac78fb04..b6d0f8bc35 100644 --- a/packages/dev-frontend/src/components/Trove/Adjusting.tsx +++ b/packages/dev-frontend/src/components/Trove/Adjusting.tsx @@ -9,6 +9,8 @@ import { Difference } from "@liquity/lib-base"; import { useLiquitySelector } from "@liquity/lib-react"; + +import { useStableTroveChange } from "../../hooks/useStableTroveChange"; import { ActionDescription } from "../ActionDescription"; import { useMyTransactionState } from "../Transaction"; import { TroveAction } from "./TroveAction"; @@ -19,6 +21,7 @@ import { InfoIcon } from "../InfoIcon"; import { LoadingOverlay } from "../LoadingOverlay"; import { CollateralRatio } from "./CollateralRatio"; import { EditableRow, StaticRow } from "./Editor"; +import { ExpensiveTroveChangeWarning, GasEstimationState } from "./ExpensiveTroveChangeWarning"; import { selectForTroveChangeValidation, validateTroveChange @@ -118,10 +121,6 @@ export const Adjusting: React.FC = () => { setNetDebt(trove.netDebt); }, [trove.collateral, trove.netDebt]); - if (trove.status !== "open") { - return null; - } - const isDirty = !collateral.eq(trove.collateral) || !netDebt.eq(trove.netDebt); const isDebtIncrease = netDebt.gt(trove.netDebt); const debtIncreaseAmount = isDebtIncrease ? netDebt.sub(trove.netDebt) : Decimal.ZERO; @@ -147,10 +146,17 @@ export const Adjusting: React.FC = () => { validationContext ); + const stableTroveChange = useStableTroveChange(troveChange); + const [gasEstimationState, setGasEstimationState] = useState({ type: "idle" }); + const isTransactionPending = transactionState.type === "waitingForApproval" || transactionState.type === "waitingForConfirmation"; + if (trove.status !== "open") { + return null; + } + return ( @@ -252,16 +258,25 @@ export const Adjusting: React.FC = () => { )} + + - {troveChange ? ( + {stableTroveChange ? ( Confirm diff --git a/packages/dev-frontend/src/components/Trove/ExpensiveTroveChangeWarning.tsx b/packages/dev-frontend/src/components/Trove/ExpensiveTroveChangeWarning.tsx new file mode 100644 index 0000000000..0e3a70d1e8 --- /dev/null +++ b/packages/dev-frontend/src/components/Trove/ExpensiveTroveChangeWarning.tsx @@ -0,0 +1,86 @@ +import React, { useEffect } from "react"; + +import { Decimal, TroveChange } from "@liquity/lib-base"; +import { PopulatedEthersLiquityTransaction } from "@liquity/lib-ethers"; + +import { useLiquity } from "../../hooks/LiquityContext"; +import { Warning } from "../Warning"; + +export type GasEstimationState = + | { type: "idle" | "inProgress" } + | { type: "complete"; populatedTx: PopulatedEthersLiquityTransaction }; + +type ExpensiveTroveChangeWarningParams = { + troveChange?: Exclude, { type: "invalidCreation" }>; + maxBorrowingRate: Decimal; + borrowingFeeDecayToleranceMinutes: number; + gasEstimationState: GasEstimationState; + setGasEstimationState: (newState: GasEstimationState) => void; +}; + +export const ExpensiveTroveChangeWarning: React.FC = ({ + troveChange, + maxBorrowingRate, + borrowingFeeDecayToleranceMinutes, + gasEstimationState, + setGasEstimationState +}) => { + const { liquity } = useLiquity(); + + useEffect(() => { + if (troveChange && troveChange.type !== "closure") { + setGasEstimationState({ type: "inProgress" }); + + let cancelled = false; + + const timeoutId = setTimeout(async () => { + const populatedTx = await (troveChange.type === "creation" + ? liquity.populate.openTrove(troveChange.params, { + maxBorrowingRate, + borrowingFeeDecayToleranceMinutes + }) + : liquity.populate.adjustTrove(troveChange.params, { + maxBorrowingRate, + borrowingFeeDecayToleranceMinutes + })); + + if (!cancelled) { + setGasEstimationState({ type: "complete", populatedTx }); + console.log( + "Estimated TX cost: " + + Decimal.from(`${populatedTx.rawPopulatedTransaction.gasLimit}`).prettify(0) + ); + } + }, 333); + + return () => { + clearTimeout(timeoutId); + cancelled = true; + }; + } else { + setGasEstimationState({ type: "idle" }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [troveChange]); + + if ( + troveChange && + gasEstimationState.type === "complete" && + gasEstimationState.populatedTx.gasHeadroom !== undefined && + gasEstimationState.populatedTx.gasHeadroom >= 200000 + ) { + return troveChange.type === "creation" ? ( + + The cost of opening a Trove in this collateral ratio range is high. It is recommended to + choose a slightly different collateral ratio. + + ) : ( + + The cost of adjusting a Trove into this collateral ratio range is high. It is recommended to + choose a slightly different collateral ratio. + + ); + } + + return null; +}; diff --git a/packages/dev-frontend/src/components/Trove/Opening.tsx b/packages/dev-frontend/src/components/Trove/Opening.tsx index b96ebf8111..e05721a2dc 100644 --- a/packages/dev-frontend/src/components/Trove/Opening.tsx +++ b/packages/dev-frontend/src/components/Trove/Opening.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useState } from "react"; -import { Flex, Button, Box, Card, Heading } from "theme-ui"; +import { Flex, Button, Box, Card, Heading, Spinner } from "theme-ui"; import { LiquityStoreState, Decimal, @@ -9,6 +9,8 @@ import { Percent } from "@liquity/lib-base"; import { useLiquitySelector } from "@liquity/lib-react"; + +import { useStableTroveChange } from "../../hooks/useStableTroveChange"; import { ActionDescription } from "../ActionDescription"; import { useMyTransactionState } from "../Transaction"; import { TroveAction } from "./TroveAction"; @@ -19,6 +21,7 @@ import { InfoIcon } from "../InfoIcon"; import { LoadingOverlay } from "../LoadingOverlay"; import { CollateralRatio } from "./CollateralRatio"; import { EditableRow, StaticRow } from "./Editor"; +import { ExpensiveTroveChangeWarning, GasEstimationState } from "./ExpensiveTroveChangeWarning"; import { selectForTroveChangeValidation, validateTroveChange @@ -67,6 +70,9 @@ export const Opening: React.FC = () => { validationContext ); + const stableTroveChange = useStableTroveChange(troveChange); + const [gasEstimationState, setGasEstimationState] = useState({ type: "idle" }); + const transactionState = useMyTransactionState(TRANSACTION_ID); const isTransactionPending = transactionState.type === "waitingForApproval" || @@ -188,16 +194,29 @@ export const Opening: React.FC = () => { )} + + - {troveChange ? ( + {gasEstimationState.type === "inProgress" ? ( + + ) : stableTroveChange ? ( Confirm diff --git a/packages/dev-frontend/src/components/Trove/TroveAction.tsx b/packages/dev-frontend/src/components/Trove/TroveAction.tsx index d8ba7bc5de..2e9567809a 100644 --- a/packages/dev-frontend/src/components/Trove/TroveAction.tsx +++ b/packages/dev-frontend/src/components/Trove/TroveAction.tsx @@ -9,23 +9,31 @@ type TroveActionProps = { transactionId: string; change: Exclude, { type: "invalidCreation" }>; maxBorrowingRate: Decimal; + borrowingFeeDecayToleranceMinutes: number; }; export const TroveAction: React.FC = ({ children, transactionId, change, - maxBorrowingRate + maxBorrowingRate, + borrowingFeeDecayToleranceMinutes }) => { const { liquity } = useLiquity(); const [sendTransaction] = useTransactionFunction( transactionId, change.type === "creation" - ? liquity.send.openTrove.bind(liquity.send, change.params, maxBorrowingRate) + ? liquity.send.openTrove.bind(liquity.send, change.params, { + maxBorrowingRate, + borrowingFeeDecayToleranceMinutes + }) : change.type === "closure" ? liquity.send.closeTrove.bind(liquity.send) - : liquity.send.adjustTrove.bind(liquity.send, change.params, maxBorrowingRate) + : liquity.send.adjustTrove.bind(liquity.send, change.params, { + maxBorrowingRate, + borrowingFeeDecayToleranceMinutes + }) ); return ; diff --git a/packages/dev-frontend/src/components/Trove/TroveManager.tsx b/packages/dev-frontend/src/components/Trove/TroveManager.tsx index 43c91d9fce..3c5d238f8f 100644 --- a/packages/dev-frontend/src/components/Trove/TroveManager.tsx +++ b/packages/dev-frontend/src/components/Trove/TroveManager.tsx @@ -235,6 +235,7 @@ export const TroveManager: React.FC = ({ collateral, debt }) transactionId={`${transactionIdPrefix}${validChange.type}`} change={validChange} maxBorrowingRate={maxBorrowingRate} + borrowingFeeDecayToleranceMinutes={60} > Confirm diff --git a/packages/dev-frontend/src/components/Warning.tsx b/packages/dev-frontend/src/components/Warning.tsx new file mode 100644 index 0000000000..f1f683666d --- /dev/null +++ b/packages/dev-frontend/src/components/Warning.tsx @@ -0,0 +1,27 @@ +import { Box, Flex, Text } from "theme-ui"; + +import { Icon } from "./Icon"; + +export const Warning: React.FC = ({ children }) => ( + + + + {children} + + +); diff --git a/packages/dev-frontend/src/hooks/useStableTroveChange.ts b/packages/dev-frontend/src/hooks/useStableTroveChange.ts new file mode 100644 index 0000000000..17ef8f3c4c --- /dev/null +++ b/packages/dev-frontend/src/hooks/useStableTroveChange.ts @@ -0,0 +1,32 @@ +import { useEffect, useState } from "react"; +import { Decimal, TroveChange } from "@liquity/lib-base"; + +type ValidTroveChange = Exclude, { type: "invalidCreation" }>; + +const paramsEq = (a?: Decimal, b?: Decimal) => (a && b ? a.eq(b) : !a && !b); + +const equals = (a: ValidTroveChange, b: ValidTroveChange): boolean => { + return ( + a.type === b.type && + paramsEq(a.params.borrowLUSD, b.params.borrowLUSD) && + paramsEq(a.params.repayLUSD, b.params.repayLUSD) && + paramsEq(a.params.depositCollateral, b.params.depositCollateral) && + paramsEq(a.params.withdrawCollateral, b.params.withdrawCollateral) + ); +}; + +export const useStableTroveChange = ( + troveChange: ValidTroveChange | undefined +): ValidTroveChange | undefined => { + const [stableTroveChange, setStableTroveChange] = useState(troveChange); + + useEffect(() => { + if (!!stableTroveChange !== !!troveChange) { + setStableTroveChange(troveChange); + } else if (stableTroveChange && troveChange && !equals(stableTroveChange, troveChange)) { + setStableTroveChange(troveChange); + } + }, [stableTroveChange, troveChange]); + + return stableTroveChange; +};