Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

InterestRateField redesign #852

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
266 changes: 191 additions & 75 deletions frontend/app/src/comps/InterestRateField/InterestRateField.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Address, BranchId, Delegate } from "@/src/types";
import type { Dnum } from "dnum";

import { INTEREST_RATE_DEFAULT, INTEREST_RATE_MAX, INTEREST_RATE_MIN } from "@/src/constants";
import { useAppear } from "@/src/anim-utils";
import { INTEREST_RATE_DEFAULT, INTEREST_RATE_START, REDEMPTION_RISK } from "@/src/constants";
import content from "@/src/content";
import { jsonStringifyWithDnum } from "@/src/dnum-utils";
import { useInputFieldValue } from "@/src/form-utils";
Expand All @@ -10,21 +11,12 @@ import { getBranch, useInterestRateChartData } from "@/src/liquity-utils";
import { infoTooltipProps } from "@/src/uikit-utils";
import { noop } from "@/src/utils";
import { css } from "@/styled-system/css";
import {
Dropdown,
HFlex,
InfoTooltip,
InputField,
lerp,
norm,
shortenAddress,
Slider,
TextButton,
} from "@liquity2/uikit";
import { Dropdown, InfoTooltip, InputField, shortenAddress, Slider, TextButton } from "@liquity2/uikit";
import { a } from "@react-spring/web";
import { blo } from "blo";
import * as dn from "dnum";
import Image from "next/image";
import { memo, useId, useState } from "react";
import { memo, useId, useMemo, useState } from "react";
import { match } from "ts-pattern";
import { DelegateModal } from "./DelegateModal";
import { IcStrategiesModal } from "./IcStrategiesModal";
Expand Down Expand Up @@ -68,25 +60,32 @@ export const InterestRateField = memo(
const fieldValue = useInputFieldValue((value) => `${fmtnum(value)}%`, {
defaultValue: interestRate
? dn.toString(dn.mul(interestRate, 100))
: String(INTEREST_RATE_DEFAULT),
: String(INTEREST_RATE_DEFAULT * 100),
onFocusChange: ({ parsed, focused }) => {
if (!focused && parsed) {
const rounded = dn.div(dn.round(dn.mul(parsed, 10)), 10);
fieldValue.setValue(
rounded[0] === 0n
? String(INTEREST_RATE_START * 100)
: dn.toString(rounded),
);
}
},
onChange: ({ parsed }) => {
if (parsed) {
onChange(dn.div(parsed, 100));
}
},
});

const boldInterestPerYear = interestRate && debt && dn.mul(interestRate, debt);

const interestChartData = useInterestRateChartData(branchId);
const interestChartData = useInterestRateChartData();
const interestRateRounded = interestRate && dn.div(dn.round(dn.mul(interestRate, 1000)), 1000);

const interestRateNumber = interestRate && dn.toNumber(
dn.mul(interestRate, 100),
const bracket = interestRateRounded && interestChartData.data?.find(
({ rate }) => rate[0] === interestRateRounded[0],
);
const chartdataPoint = interestChartData.data?.find(
({ rate }) => rate === interestRateNumber,
);
const boldRedeemableInFront = chartdataPoint?.debtInFront ?? dn.from(0, 18);

const redeemableTransition = useAppear(bracket?.debtInFront !== undefined);

const handleDelegateSelect = (delegate: Delegate) => {
setDelegatePicker(null);
Expand All @@ -98,6 +97,8 @@ export const InterestRateField = memo(
const hasStrategies = branch.strategies.length > 0;
const activeDelegateModes = DELEGATE_MODES.filter((mode) => mode !== "strategy" || hasStrategies);

const boldInterestPerYear = interestRate && debt && dn.mul(interestRate, debt);

return (
<>
<InputField
Expand All @@ -107,38 +108,11 @@ export const InterestRateField = memo(
disabled={mode !== "manual"}
contextual={match(mode)
.with("manual", () => (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 300,
}}
>
<Slider
gradient={[1 / 3, 2 / 3]}
gradientMode="high-to-low"
chart={interestChartData.data?.map(
({ size }) => Math.max(0.1, size),
) ?? []}
onChange={(value) => {
fieldValue.setValue(String(
Math.round(
lerp(
INTEREST_RATE_MIN,
INTEREST_RATE_MAX,
value,
) * 10,
) / 10,
));
}}
value={norm(
interestRate ? dn.toNumber(dn.mul(interestRate, 100)) : 0,
INTEREST_RATE_MIN,
INTEREST_RATE_MAX,
)}
/>
</div>
<ManualInterestRateSlider
interestChartData={interestChartData}
interestRate={interestRate}
fieldValue={fieldValue}
/>
))
.with("strategy", () => (
<TextButton
Expand Down Expand Up @@ -234,34 +208,48 @@ export const InterestRateField = memo(
placeholder="0.00"
secondary={{
start: (
<HFlex gap={4}>
<div
className={css({
display: "flex",
alignItems: "center",
gap: 4,
minWidth: 120,
})}
>
<div>
{boldInterestPerYear && (mode === "manual" || delegate !== null)
? fmtnum(boldInterestPerYear)
: "−"} BOLD / year
</div>
<InfoTooltip
{...infoTooltipProps(
content.generalInfotooltips.interestRateBoldPerYear,
)}
/>
</HFlex>
<InfoTooltip {...infoTooltipProps(content.generalInfotooltips.interestRateBoldPerYear)} />
</div>
),
end: (
<span>
Redeemable before you:{" "}
<span
end: redeemableTransition((style, show) => (
show && (
<a.div
className={css({
fontVariantNumeric: "tabular-nums",
overflow: "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
})}
style={style}
>
{(mode === "manual" || delegate !== null)
? fmtnum(boldRedeemableInFront, "compact")
: "−"}
</span>
<span>{" BOLD"}</span>
</span>
),
<span>
Redeemable before you:{" "}
<span
className={css({
fontVariantNumeric: "tabular-nums",
})}
>
{(mode === "manual" || delegate !== null)
? fmtnum(bracket?.debtInFront, "compact")
: "−"}
</span>
<span>{" BOLD"}</span>
</span>
</a.div>
)
)),
}}
{...fieldValue.inputFieldProps}
value={
Expand All @@ -280,7 +268,7 @@ export const InterestRateField = memo(
style={{
display: "flex",
alignItems: "center",
gap: 10,
gap: 8,
}}
>
{delegate !== null && <MiniChart size="medium" />}
Expand Down Expand Up @@ -330,3 +318,131 @@ export const InterestRateField = memo(
jsonStringifyWithDnum(prev) === jsonStringifyWithDnum(next)
),
);

function ManualInterestRateSlider({
fieldValue,
interestChartData,
interestRate,
}: {
fieldValue: ReturnType<typeof useInputFieldValue>;
interestChartData: ReturnType<typeof useInterestRateChartData>;
interestRate: Dnum | null;
}) {
const value = (() => {
const chartRates = interestChartData.data?.map(({ rate }) => rate[0]);
const rate = interestRate?.[0] ?? 0n;

if (!rate || !chartRates || chartRates.length === 0) {
return 0;
}

const firstRate = chartRates.at(0) ?? 0n;
if (rate <= firstRate) return 0;

const lastRate = chartRates.at(-1) ?? 0n;
if (rate >= lastRate) return 1;

// find the closest rate in the chart data
let index = 0;
let currentDiff = firstRate - rate;
if (currentDiff < 0) currentDiff = -currentDiff; // abs()
while (index < (chartRates.length - 1)) {
const nextRate = chartRates[index + 1] ?? 0n;

let nextDiff = nextRate - rate;
if (nextDiff < 0) nextDiff = -nextDiff;

// diff starts increasing = we passed the closest point
if (nextDiff > currentDiff) {
break;
}

// otherwise, keep going
currentDiff = nextDiff;
index++;
}

return index / chartRates.length;
})();

const gradientStops = useMemo((): [number, number] => {
if (!interestChartData.data || interestChartData.data.length === 0) {
return [0, 0];
}
const rates = interestChartData.data.map((bar) => dn.toNumber(bar.rate));
return [
gradientStop(rates, REDEMPTION_RISK.medium),
gradientStop(rates, REDEMPTION_RISK.low),
];
}, [interestChartData.data]);

const transition = useAppear(value !== -1);

return transition((style, show) =>
show && (
<a.div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 260,
paddingTop: 16,
...style,
}}
>
<Slider
gradient={gradientStops}
gradientMode="high-to-low"
chart={interestChartData.data?.map(({ size }) => size) ?? []}
onChange={(value) => {
if (interestChartData.data) {
const index = Math.min(
interestChartData.data.length - 1,
Math.round(value * (interestChartData.data.length)),
);
fieldValue.setValue(String(dn.toNumber(dn.mul(
interestChartData.data[index]?.rate ?? dn.from(0, 18),
100,
))));
}
}}
value={value}
/>
</a.div>
)
);
}

function gradientStop(chartRates: number[], targetRate: number) {
const firstRate = chartRates.at(0);
const lastRate = chartRates.at(-1);

if (firstRate === undefined || lastRate === undefined) {
return 0;
}

if (targetRate <= firstRate) return 0;
if (targetRate >= lastRate) return 1;

let index = 0;
let currentDiff = Math.abs(firstRate - targetRate);

while (index < chartRates.length) {
const nextRate = chartRates[index + 1];
if (nextRate === undefined) {
break;
}

const nextDiff = Math.abs(nextRate - targetRate);

// diff starts increasing = we passed the closest point
if (nextDiff > currentDiff) {
break;
}

currentDiff = nextDiff;
index++;
}

return index / (chartRates.length);
}
3 changes: 1 addition & 2 deletions frontend/app/src/comps/LeverageField/LeverageField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,7 @@ export function LeverageField({
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 300,
marginRight: -20,
width: 260,
}}
>
<Slider
Expand Down
12 changes: 8 additions & 4 deletions frontend/app/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@ export const ETH_MAX_RESERVE = dn.from(0.1, 18); // leave 0.1 ETH when users cli
export const ETH_GAS_COMPENSATION = dn.from(0.0375, 18); // see contracts/src/Dependencies/Constants.sol

export const INTEREST_RATE_ADJ_COOLDOWN = 7 * 24 * 60 * 60; // 7 days in seconds
export const INTEREST_RATE_MIN = 0.5; // 0.5% annualized
export const INTEREST_RATE_MAX = 25; // 25% annualized
export const INTEREST_RATE_DEFAULT = 10;
export const INTEREST_RATE_INCREMENT = 0.1;

// interest rate field config
export const INTEREST_RATE_START = 0.005; // 0.5%
export const INTEREST_RATE_END = 0.25; // 25%
export const INTEREST_RATE_DEFAULT = 0.1; // 10%
export const INTEREST_RATE_PRECISE_UNTIL = 0.1; // use precise increments until 10%
export const INTEREST_RATE_INCREMENT_PRECISE = 0.001; // 0.1% increments (precise)
export const INTEREST_RATE_INCREMENT_NORMAL = 0.005; // 0.5% increments (normal)

export const SP_YIELD_SPLIT = 75n * 10n ** 16n; // 75%

Expand Down
Loading