Skip to content

Commit a53272a

Browse files
authored
Redeem screen (#837)
* Redeem screen * Wording * Wording * Update viem / wagmi and related dependencies * TransactionFlow: remove null from the account union type that is passed to the tx flows * Redeem tx flow: show the balances changes estimate * fix react query type * remove unused imports * Revert react query version * Add loading states * Update lockfile * ETH => WETH
1 parent 80ef22e commit a53272a

16 files changed

+964
-229
lines changed

frontend/app/package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@
2424
"@graphql-typed-document-node/core": "^3.2.0",
2525
"@liquity2/uikit": "workspace:*",
2626
"@next/bundle-analyzer": "^15.1.4",
27-
"@rainbow-me/rainbowkit": "^2.2.2",
27+
"@rainbow-me/rainbowkit": "^2.2.3",
2828
"@react-spring/web": "^9.7.5",
2929
"@tanstack/react-query": "^5.64.1",
3030
"@vercel/analytics": "^1.4.1",
31-
"@wagmi/core": "^2.16.3",
31+
"@wagmi/core": "^2.16.4",
3232
"abitype": "^1.0.8",
3333
"blo": "^1.2.0",
3434
"dnum": "^2.14.0",
@@ -39,8 +39,8 @@
3939
"sharp": "^0.33.5",
4040
"ts-pattern": "^5.6.0",
4141
"valibot": "^0.42.1",
42-
"viem": "^2.22.8",
43-
"wagmi": "^2.14.7"
42+
"viem": "^2.23.2",
43+
"wagmi": "^2.14.11"
4444
},
4545
"devDependencies": {
4646
"@babel/plugin-transform-private-methods": "^7.25.9",

frontend/app/src/app/redeem/page.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { RedeemScreen } from "@/src/screens/RedeemScreen/RedeemScreen";
2+
3+
export default function Page() {
4+
return <RedeemScreen />;
5+
}

frontend/app/src/comps/ProtocolStats/ProtocolStats.tsx

+29
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,35 @@ export function ProtocolStats() {
9898
/>
9999
</Link>
100100
)}
101+
<Link
102+
id="footer-redeem-button"
103+
href="/redeem"
104+
passHref
105+
legacyBehavior
106+
scroll={true}
107+
>
108+
<AnchorTextButton
109+
label={
110+
<HFlex gap={4} alignItems="center">
111+
<TokenIcon
112+
size={16}
113+
symbol="BOLD"
114+
/>
115+
Redeem BOLD
116+
</HFlex>
117+
}
118+
className={css({
119+
color: "content",
120+
borderRadius: 4,
121+
_focusVisible: {
122+
outline: "2px solid token(colors.focused)",
123+
},
124+
_active: {
125+
translate: "0 1px",
126+
},
127+
})}
128+
/>
129+
</Link>
101130
</HFlex>
102131
</div>
103132
</div>

frontend/app/src/liquity-utils.ts

+1
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,7 @@ export function useLoan(branchId: BranchId, troveId: TroveId): UseQueryResult<Po
659659
isFetching: true,
660660
isLoading: true,
661661
isLoadingError: false,
662+
isPlaceholderData: false,
662663
isPending: true,
663664
isRefetchError: false,
664665
isSuccess: false,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
"use client";
2+
3+
import { Amount } from "@/src/comps/Amount/Amount";
4+
import { ConnectWarningBox } from "@/src/comps/ConnectWarningBox/ConnectWarningBox";
5+
import { Field } from "@/src/comps/Field/Field";
6+
import { Screen } from "@/src/comps/Screen/Screen";
7+
import content from "@/src/content";
8+
import { getProtocolContract } from "@/src/contracts";
9+
import { dnum18 } from "@/src/dnum-utils";
10+
import { parseInputPercentage, useInputFieldValue } from "@/src/form-utils";
11+
import { fmtnum } from "@/src/formatting";
12+
import { getBranches, getCollToken } from "@/src/liquity-utils";
13+
import { useAccount, useBalance } from "@/src/services/Ethereum";
14+
import { useTransactionFlow } from "@/src/services/TransactionFlow";
15+
import { css } from "@/styled-system/css";
16+
import { Button, HFlex, InfoTooltip, InputField, TextButton, TokenIcon } from "@liquity2/uikit";
17+
import * as dn from "dnum";
18+
import Link from "next/link";
19+
import { useRef } from "react";
20+
import { useReadContract } from "wagmi";
21+
22+
export function RedeemScreen() {
23+
const account = useAccount();
24+
const txFlow = useTransactionFlow();
25+
26+
const boldBalance = useBalance(account.address, "BOLD");
27+
28+
const CollateralRegistry = getProtocolContract("CollateralRegistry");
29+
const redemptionRate = useReadContract({
30+
...CollateralRegistry,
31+
functionName: "getRedemptionRateWithDecay",
32+
});
33+
34+
const amount = useInputFieldValue(fmtnum);
35+
const maxFee = useInputFieldValue((value) => `${fmtnum(value, "pct2z")}%`, {
36+
parse: parseInputPercentage,
37+
});
38+
39+
const hasUpdatedRedemptionRate = useRef(false);
40+
if (!hasUpdatedRedemptionRate.current && redemptionRate.data) {
41+
if (maxFee.isEmpty) {
42+
maxFee.setValue(
43+
fmtnum(
44+
dn.mul(dnum18(redemptionRate.data), 1.1),
45+
"pct2z",
46+
),
47+
);
48+
}
49+
hasUpdatedRedemptionRate.current = true;
50+
}
51+
52+
const branches = getBranches();
53+
54+
const allowSubmit = account.isConnected
55+
&& amount.parsed
56+
&& maxFee.parsed
57+
&& boldBalance.data
58+
&& dn.gte(boldBalance.data, amount.parsed);
59+
60+
return (
61+
<Screen
62+
heading={{
63+
title: (
64+
<HFlex>
65+
Redeem <TokenIcon symbol="BOLD" /> BOLD for
66+
<TokenIcon.Group>
67+
{branches.map((b) => getCollToken(b.branchId)).map(({ symbol }) => (
68+
<TokenIcon
69+
key={symbol}
70+
symbol={symbol}
71+
/>
72+
))}
73+
</TokenIcon.Group>{" "}
74+
ETH
75+
</HFlex>
76+
),
77+
}}
78+
>
79+
<div
80+
className={css({
81+
display: "flex",
82+
flexDirection: "column",
83+
gap: 48,
84+
width: 534,
85+
})}
86+
>
87+
<Field
88+
field={
89+
<InputField
90+
id="input-redeem-amount"
91+
contextual={
92+
<InputField.Badge
93+
icon={<TokenIcon symbol="BOLD" />}
94+
label="BOLD"
95+
/>
96+
}
97+
drawer={amount.isFocused
98+
? null
99+
: boldBalance.data
100+
&& amount.parsed
101+
&& dn.gt(amount.parsed, boldBalance.data)
102+
? {
103+
mode: "error",
104+
message: `Insufficient BOLD balance. You have ${fmtnum(boldBalance.data)} BOLD.`,
105+
}
106+
: null}
107+
label="Redeeming"
108+
placeholder="0.00"
109+
secondary={{
110+
start: `$${
111+
amount.parsed
112+
? fmtnum(amount.parsed)
113+
: "0.00"
114+
}`,
115+
end: (
116+
boldBalance.data && dn.gt(boldBalance.data, 0) && (
117+
<TextButton
118+
label={`Max ${fmtnum(boldBalance.data)} BOLD`}
119+
onClick={() => {
120+
amount.setValue(dn.toString(boldBalance.data));
121+
}}
122+
/>
123+
)
124+
),
125+
}}
126+
{...amount.inputFieldProps}
127+
/>
128+
}
129+
/>
130+
131+
<Field
132+
field={
133+
<InputField
134+
id="input-max-fee"
135+
drawer={maxFee.isFocused
136+
? null
137+
: maxFee.parsed && dn.gt(maxFee.parsed, 0.01)
138+
? {
139+
mode: "warning",
140+
message: `A high percentage will result in a higher fee.`,
141+
}
142+
: null}
143+
label="Max redemption fee"
144+
placeholder="0.00"
145+
{...maxFee.inputFieldProps}
146+
/>
147+
}
148+
footer={[
149+
{
150+
end: (
151+
<span
152+
className={css({
153+
display: "flex",
154+
alignItems: "center",
155+
gap: 4,
156+
fontSize: 14,
157+
})}
158+
>
159+
<>
160+
Current redemption rate:
161+
<Amount
162+
percentage
163+
suffix="%"
164+
value={redemptionRate.data ? dnum18(redemptionRate.data) : null}
165+
format="pct1z"
166+
/>
167+
</>
168+
<InfoTooltip
169+
content={{
170+
heading: "Maximum redemption fee",
171+
body: (
172+
<>
173+
This is the maximum redemption fee you are willing to pay. The redemption fee is a percentage
174+
of the redeemed amount that is paid to the protocol. The redemption fee must be higher than
175+
the current fee.
176+
</>
177+
),
178+
footerLink: {
179+
href: "https://dune.com/queries/4641717/7730245",
180+
label: "Redemption fee on Dune",
181+
},
182+
}}
183+
/>
184+
</span>
185+
),
186+
},
187+
]}
188+
/>
189+
190+
<section
191+
className={css({
192+
display: "flex",
193+
flexDirection: "column",
194+
gap: 8,
195+
padding: 16,
196+
color: "infoSurfaceContent",
197+
background: "infoSurface",
198+
border: "1px solid token(colors.infoSurfaceBorder)",
199+
borderRadius: 8,
200+
})}
201+
>
202+
<header
203+
className={css({
204+
display: "flex",
205+
flexDirection: "column",
206+
fontSize: 16,
207+
})}
208+
>
209+
<h1
210+
className={css({
211+
fontWeight: 600,
212+
})}
213+
>
214+
Important note
215+
</h1>
216+
</header>
217+
<p
218+
className={css({
219+
fontSize: 15,
220+
"& a": {
221+
color: "accent",
222+
textDecoration: "underline",
223+
},
224+
})}
225+
>
226+
You will be charged a dynamic redemption fee (the more redemptions, the higher the fee). Trading BOLD on an
227+
exchange could be more favorable.{" "}
228+
<Link
229+
href="https://docs.liquity.org/v2-faq/redemptions-and-delegation"
230+
target="_blank"
231+
rel="noopener noreferrer"
232+
>
233+
Learn more about redemptions.
234+
</Link>
235+
</p>
236+
</section>
237+
238+
<div
239+
style={{
240+
display: "flex",
241+
flexDirection: "column",
242+
justifyContent: "center",
243+
gap: 32,
244+
width: "100%",
245+
}}
246+
>
247+
<ConnectWarningBox />
248+
<Button
249+
disabled={!allowSubmit}
250+
label={content.borrowScreen.action}
251+
mode="primary"
252+
size="large"
253+
wide
254+
onClick={() => {
255+
if (
256+
amount.parsed
257+
&& maxFee.parsed
258+
) {
259+
txFlow.start({
260+
flowId: "redeemCollateral",
261+
backLink: ["/redeem", "Back"],
262+
successLink: ["/", "Go to the Dashboard"],
263+
successMessage: "The redemption was successful.",
264+
265+
time: Date.now(),
266+
amount: amount.parsed,
267+
maxFee: maxFee.parsed,
268+
});
269+
}
270+
}}
271+
/>
272+
</div>
273+
</div>
274+
</Screen>
275+
);
276+
}

0 commit comments

Comments
 (0)