Skip to content

Commit 2fc9418

Browse files
Merge pull request #65 from useLiquidOps/feat/withdraw-base-token
Withdraw fixes
2 parents 4bddb97 + fb1c3ee commit 2fc9418

File tree

3 files changed

+76
-65
lines changed

3 files changed

+76
-65
lines changed

src/app/home/WithdrawRepay/WithdrawRepay.tsx

Lines changed: 61 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"use client";
2-
import React, { useState, useCallback, useEffect } from "react";
2+
import React, { useState, useCallback, useEffect, useMemo } from "react";
33
import styles from "./WithdrawRepay.module.css";
44
import SubmitButton from "@/components/SubmitButton/SubmitButton";
55
import PercentagePicker from "@/components/PercentagePicker/PercentagePicker";
@@ -14,7 +14,7 @@ import { tokenInput } from "liquidops";
1414
import { useGetPosition } from "@/hooks/LiquidOpsData/useGetPosition";
1515
import { useLoadingScreen } from "@/components/LoadingScreen/useLoadingScreen";
1616
import { useValueLimit } from "@/hooks/data/useValueLimit";
17-
import { useProtocolStats } from "@/hooks/LiquidOpsData/useProtocolStats";
17+
import { useInfo, useProtocolStats } from "@/hooks/LiquidOpsData/useProtocolStats";
1818
import { AnimatePresence, motion } from "framer-motion";
1919
import { warningVariants } from "@/components/DropDown/FramerMotion";
2020
import { useCooldown } from "@/hooks/data/useCooldown";
@@ -42,9 +42,14 @@ const WithdrawRepay: React.FC<WithdrawRepayProps> = ({
4242
const { data: oTokenBalance, isLoading: isLoadingOTokenBalance } =
4343
useUserBalance(oTokenAddress);
4444

45-
const currentBalance = mode === "withdraw" ? lentBalance : positionBalance;
46-
const isLoadingCurrentBalance =
47-
mode === "withdraw" ? isLoadingBalance : isLoadingPosition;
45+
const currentBalance = useMemo(
46+
() => mode === "withdraw" ? lentBalance : positionBalance,
47+
[mode, lentBalance, positionBalance]
48+
);
49+
const isLoadingCurrentBalance = useMemo(
50+
() => mode === "withdraw" ? isLoadingBalance : isLoadingPosition,
51+
[mode, isLoadingBalance, isLoadingPosition]
52+
);
4853

4954
const { unlend, isUnlending, unlendError } = useLend({
5055
onSuccess: onClose,
@@ -57,9 +62,6 @@ const WithdrawRepay: React.FC<WithdrawRepayProps> = ({
5762

5863
const [inputValue, setInputValue] = useState("");
5964
const [isFocused, setIsFocused] = useState(false);
60-
const [selectedPercentage, setSelectedPercentage] = useState<number | null>(
61-
null,
62-
);
6365
const [notLoadedBalance, setNotLoadedBalance] = useState<string | boolean>(
6466
false,
6567
);
@@ -87,7 +89,6 @@ const WithdrawRepay: React.FC<WithdrawRepayProps> = ({
8789
// Reset input callback
8890
const resetInput = useCallback(() => {
8991
setInputValue("");
90-
setSelectedPercentage(null);
9192
}, []);
9293

9394
// Initialize loading screen hook
@@ -105,64 +106,57 @@ const WithdrawRepay: React.FC<WithdrawRepayProps> = ({
105106
protocolStats,
106107
);
107108

108-
const calculateMaxAmount = () => {
109-
if (mode === "withdraw") {
110-
return isLoadingOTokenBalance ? null : oTokenBalance;
111-
}
112-
return isLoadingCurrentBalance ? null : currentBalance;
113-
};
114-
115109
const handleMaxClick = () => {
116110
setHasUserInteracted(true);
117-
const maxAmount = calculateMaxAmount();
118-
if (!maxAmount) {
111+
if (!currentBalance) {
119112
// Set loading state only in event handlers, not during render
120113
setNotLoadedBalance(mode === "withdraw" ? "oToken" : "token");
121114
return;
122115
}
123-
setInputValue(maxAmount.toString());
116+
setInputValue(currentBalance.toString());
124117
};
125118

126119
const handlePercentageClick = (percentage: number) => {
127120
setHasUserInteracted(true);
128-
const maxAmount = calculateMaxAmount();
129-
if (!maxAmount) {
121+
if (!currentBalance) {
130122
// Set loading state only in event handlers, not during render
131123
setNotLoadedBalance(mode === "withdraw" ? "oToken" : "token");
132124
return;
133125
}
134126

135127
const amount = Quantity.__div(
136-
Quantity.__mul(maxAmount, new Quantity(0n, 12n).fromNumber(percentage)),
128+
Quantity.__mul(currentBalance, new Quantity(0n, 12n).fromNumber(percentage)),
137129
new Quantity(0n, 12n).fromNumber(100),
138130
);
139131
setInputValue(amount.toString());
140-
setSelectedPercentage(percentage);
141132
};
142133

143-
const getCurrentPercentage = () => {
144-
const maxAmount = calculateMaxAmount();
134+
const currentPercentage = useMemo(
135+
() => {
136+
// no data
137+
if (
138+
!currentBalance ||
139+
!inputValue ||
140+
Quantity.eq(currentBalance, new Quantity(0n, 12n))
141+
) {
142+
return 0;
143+
}
145144

146-
// Return 0 if data is still loading or no input
147-
if (
148-
!maxAmount ||
149-
!inputValue ||
150-
Quantity.eq(maxAmount, new Quantity(0n, 12n))
151-
) {
152-
return 0;
153-
}
145+
if (isNaN(Number(inputValue.replace(/,/g, "")))) return 0;
154146

155-
if (isNaN(Number(inputValue.replace(/,/g, "")))) return 0;
147+
const percentage = Quantity.__div(
148+
Quantity.__mul(
149+
new Quantity(0n, currentBalance.denomination).fromString(inputValue),
150+
new Quantity(0n, currentBalance.denomination).fromNumber(100),
151+
),
152+
currentBalance,
153+
);
154+
return Math.min(100, Math.max(0, percentage.toNumber()));
155+
},
156+
[currentBalance, inputValue]
157+
);
156158

157-
const percentage = Quantity.__div(
158-
Quantity.__mul(
159-
new Quantity(0n, maxAmount.denomination).fromString(inputValue),
160-
new Quantity(0n, maxAmount.denomination).fromNumber(100),
161-
),
162-
maxAmount,
163-
);
164-
return Math.min(100, Math.max(0, percentage.toNumber()));
165-
};
159+
const { data: tokenInfo, isLoading: isLoadingTokenInfo } = useInfo(ticker.toUpperCase());
166160

167161
const handleSubmit = () => {
168162
setHasUserInteracted(true);
@@ -177,16 +171,27 @@ const WithdrawRepay: React.FC<WithdrawRepayProps> = ({
177171

178172
// If unlending (withdrawing), multiply by the oToken rate to send correct oToken amount to protocol
179173
if (mode === "withdraw") {
180-
if (isLoadingOTokenBalance) {
181-
setNotLoadedBalance("oToken");
182-
return;
174+
if (lentBalance && oTokenBalance && Quantity.le(lentBalance, quantity)) {
175+
quantity = oTokenBalance;
176+
} else {
177+
if (isLoadingTokenInfo || !tokenInfo) return;
178+
179+
// x _underlying_ = x * totalSupply / totalPooled _oToken_
180+
const { collateralDenomination, denomination } = tokenInfo;
181+
const totalPooled = new Quantity(
182+
BigInt(tokenInfo.cash) + BigInt(tokenInfo.totalBorrows) - BigInt(tokenInfo.totalReserves),
183+
BigInt(collateralDenomination)
184+
);
185+
const totalSupply = new Quantity(tokenInfo.totalSupply, BigInt(denomination));
186+
quantity = Quantity.__convert(
187+
Quantity.__div(Quantity.__mul(quantity, totalSupply), totalPooled),
188+
BigInt(denomination)
189+
);
190+
191+
if (oTokenBalance) {
192+
quantity = Quantity.min(quantity, oTokenBalance)!;
193+
}
183194
}
184-
185-
const userOTokenRate = Number(oTokenBalance) / Number(lentBalance);
186-
const oTokenAmount = Number(quantity) * userOTokenRate;
187-
quantity = new Quantity(0n, currentBalance?.denomination).fromNumber(
188-
oTokenAmount,
189-
);
190195
}
191196

192197
const params = {
@@ -225,16 +230,14 @@ const WithdrawRepay: React.FC<WithdrawRepayProps> = ({
225230
ticker={ticker}
226231
tokenPrice={tokenPrice}
227232
// @ts-ignore, logic relies on undefined to show skeleton loading
228-
walletBalance={mode === "withdraw" ? oTokenBalance : currentBalance}
233+
walletBalance={currentBalance}
229234
onMaxClick={handleMaxClick}
230235
denomination={currentBalance?.denomination || 12n}
231-
oToken={mode === "withdraw" ? true : false}
232236
/>
233237

234238
<PercentagePicker
235239
mode={mode}
236-
selectedPercentage={selectedPercentage}
237-
currentPercentage={getCurrentPercentage()}
240+
currentPercentage={currentPercentage}
238241
onPercentageClick={handlePercentageClick}
239242
// @ts-ignore, logic relies on undefined to disable percentage picker
240243
walletBalance={currentBalance}
@@ -343,7 +346,8 @@ const WithdrawRepay: React.FC<WithdrawRepayProps> = ({
343346
parseFloat(inputValue) <= 0 ||
344347
loadingScreenState.submitStatus === "loading" ||
345348
(mode === "withdraw" && valueLimitReached) ||
346-
cooldownData?.onCooldown
349+
cooldownData?.onCooldown ||
350+
(!tokenInfo && mode === "withdraw")
347351
}
348352
loading={isRepaying || isUnlending}
349353
submitText={mode === "withdraw" ? "Withdraw" : "Repay"}

src/components/PercentagePicker/PercentagePicker.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,13 @@ import { Quantity } from "ao-tokens-lite";
55

66
interface PercentagePickerProps {
77
mode: "withdraw" | "repay";
8-
selectedPercentage: number | null;
98
currentPercentage: number;
109
onPercentageClick: (percentage: number) => void;
1110
walletBalance: Quantity;
1211
}
1312

1413
const PercentagePicker: React.FC<PercentagePickerProps> = ({
1514
mode,
16-
selectedPercentage,
1715
currentPercentage,
1816
onPercentageClick,
1917
walletBalance,
@@ -47,7 +45,7 @@ const PercentagePicker: React.FC<PercentagePickerProps> = ({
4745
<button
4846
key={percentage}
4947
className={`${styles.percentageButton} ${
50-
selectedPercentage === percentage ? styles.selected : ""
48+
Math.round(currentPercentage * 100) / 100 === percentage ? styles.selected : ""
5149
}`}
5250
onClick={() => onPercentageClick(percentage)}
5351
disabled={!walletBalance}

src/hooks/LiquidOpsData/useProtocolStats.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,29 @@ export interface ProtocolStats {
2323

2424
export type ProtocolStatsCache = GetInfoRes;
2525

26+
export function useInfo(token: string) {
27+
return useQuery({
28+
queryKey: ["token-info", token],
29+
queryFn: async () => {
30+
return await LiquidOpsClient.getInfo({ token });
31+
},
32+
staleTime: 30 * 1000,
33+
gcTime: 5 * 60 * 1000,
34+
});
35+
}
36+
2637
export function useProtocolStats(token: string, overrideCache?: boolean) {
2738
const { data: historicalAPR } = useHistoricalAPR(token);
39+
const { data: info } = useInfo(token);
2840

2941
return useQuery({
3042
queryKey: ["protocol-stats", token],
3143
queryFn: async (): Promise<ProtocolStats> => {
3244
const safeHistoricalAPR = historicalAPR ?? [];
33-
const [getInfoRes] = await Promise.all([
34-
LiquidOpsClient.getInfo({ token }),
35-
]);
3645

37-
return await getProtocolStatsData(getInfoRes, safeHistoricalAPR, token);
46+
return await getProtocolStatsData(info!, safeHistoricalAPR, token);
3847
},
39-
enabled: !!historicalAPR,
48+
enabled: !!historicalAPR && !!info,
4049
staleTime: 30 * 1000,
4150
gcTime: 5 * 60 * 1000,
4251
});

0 commit comments

Comments
 (0)