Skip to content

Commit 97745cc

Browse files
creed-victorgrinry
andauthored
SOV-5258 money market calculate health factor and APY (#19)
* feat: calculate borrowing apy * feat: borrow validation * fix: lend modal --------- Co-authored-by: Rytis Grincevicius <rytis.grincevicius@gmail.com>
1 parent 4f9aca8 commit 97745cc

File tree

19 files changed

+975
-241
lines changed

19 files changed

+975
-241
lines changed

apps/web-app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"@tailwindcss/vite": "4.1.13",
2424
"@tanstack/devtools-vite": "0.3.3",
2525
"@tanstack/react-devtools": "0.7.0",
26-
"@tanstack/react-form": "1.23.0",
26+
"@tanstack/react-form": "1.27.2",
2727
"@tanstack/react-query": "5.90.2",
2828
"@tanstack/react-query-devtools": "5.90.2",
2929
"@tanstack/react-router": "1.131.50",

apps/web-app/src/components/FormComponents.tsx

Lines changed: 113 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ import * as ShadcnSelect from '@/components/ui/select';
88
import { Slider as ShadcnSlider } from '@/components/ui/slider';
99
import { Switch as ShadcnSwitch } from '@/components/ui/switch';
1010
import { Textarea as ShadcnTextarea } from '@/components/ui/textarea';
11+
import type { CheckedState } from '@radix-ui/react-checkbox';
1112
import { Decimal } from '@sovryn/slayer-shared';
1213
import { Loader2Icon } from 'lucide-react';
13-
import { useState } from 'react';
14+
import { useEffect, useState, type ReactNode } from 'react';
1415
import type { GetBalanceData } from 'wagmi/query';
16+
import { AmountRenderer } from './ui/amount-renderer';
17+
import { Checkbox } from './ui/checkbox';
1518
import { Field, FieldDescription, FieldError, FieldLabel } from './ui/field';
19+
import { InputGroup, InputGroupAddon, InputGroupInput } from './ui/input-group';
1620

1721
export function SubscribeButton({ label }: { label: string }) {
1822
const form = useFormContext();
@@ -24,7 +28,7 @@ export function SubscribeButton({ label }: { label: string }) {
2428
<Button
2529
type="submit"
2630
disabled={isSubmitting || !isFormValid}
27-
form={form.formId()}
31+
form={form.formId}
2832
>
2933
<Loader2Icon
3034
className={`mr-2 h-4 w-4 animate-spin ${isSubmitting ? '' : 'hidden'}`}
@@ -60,7 +64,7 @@ export function TextField({
6064
placeholder,
6165
description,
6266
}: {
63-
label: string;
67+
label: ReactNode;
6468
placeholder?: string;
6569
description?: string;
6670
}) {
@@ -69,8 +73,9 @@ export function TextField({
6973

7074
return (
7175
<Field>
72-
<FieldLabel htmlFor={label}>{label}</FieldLabel>
76+
<FieldLabel htmlFor={field.name}>{label}</FieldLabel>
7377
<Input
78+
id={field.name}
7479
value={field.state.value}
7580
placeholder={placeholder}
7681
onBlur={field.handleBlur}
@@ -87,7 +92,7 @@ export function TextArea({
8792
rows = 3,
8893
description,
8994
}: {
90-
label: string;
95+
label: ReactNode;
9196
rows?: number;
9297
description?: string;
9398
}) {
@@ -96,9 +101,9 @@ export function TextArea({
96101

97102
return (
98103
<Field>
99-
<FieldLabel htmlFor={label}>{label}</FieldLabel>
104+
<FieldLabel htmlFor={field.name}>{label}</FieldLabel>
100105
<ShadcnTextarea
101-
id={label}
106+
id={field.name}
102107
value={field.state.value}
103108
onBlur={field.handleBlur}
104109
rows={rows}
@@ -116,7 +121,7 @@ export function Select({
116121
placeholder,
117122
description,
118123
}: {
119-
label: string;
124+
label: ReactNode;
120125
values: Array<{ label: string; value: string }>;
121126
placeholder?: string;
122127
description?: string;
@@ -126,7 +131,7 @@ export function Select({
126131

127132
return (
128133
<Field>
129-
<FieldLabel htmlFor={label}>{label}</FieldLabel>
134+
<FieldLabel htmlFor={field.name}>{label}</FieldLabel>
130135
<ShadcnSelect.Select
131136
name={field.name}
132137
value={field.state.value}
@@ -156,17 +161,17 @@ export function Slider({
156161
label,
157162
description,
158163
}: {
159-
label: string;
164+
label: ReactNode;
160165
description?: string;
161166
}) {
162167
const field = useFieldContext<number>();
163168
const errors = useStore(field.store, (state) => state.meta.errors);
164169

165170
return (
166171
<Field>
167-
<FieldLabel htmlFor={label}>{label}</FieldLabel>
172+
<FieldLabel htmlFor={field.name}>{label}</FieldLabel>
168173
<ShadcnSlider
169-
id={label}
174+
id={field.name}
170175
onBlur={field.handleBlur}
171176
value={[field.state.value]}
172177
onValueChange={(value) => field.handleChange(value[0])}
@@ -181,7 +186,7 @@ export function Switch({
181186
label,
182187
description,
183188
}: {
184-
label: string;
189+
label: ReactNode;
185190
description?: string;
186191
}) {
187192
const field = useFieldContext<boolean>();
@@ -191,19 +196,49 @@ export function Switch({
191196
<Field>
192197
<div className="flex items-center gap-2">
193198
<ShadcnSwitch
194-
id={label}
199+
id={field.name}
195200
onBlur={field.handleBlur}
196201
checked={field.state.value}
197202
onCheckedChange={(checked) => field.handleChange(checked)}
198203
/>
199-
<FieldLabel htmlFor={label}>{label}</FieldLabel>
204+
<FieldLabel htmlFor={field.name}>{label}</FieldLabel>
200205
</div>
201206
{description && <FieldDescription>{description}</FieldDescription>}
202207
{field.state.meta.isTouched && <ErrorMessages errors={errors} />}
203208
</Field>
204209
);
205210
}
206211

212+
export function CheckBox({
213+
label,
214+
description,
215+
}: {
216+
label: ReactNode;
217+
description?: string;
218+
}) {
219+
const field = useFieldContext<CheckedState>();
220+
const errors = useStore(field.store, (state) => state.meta.errors);
221+
222+
return (
223+
<Field>
224+
<div className="flex items-start gap-3">
225+
<Checkbox
226+
id={field.name}
227+
checked={field.state.value}
228+
onCheckedChange={(checked) => field.handleChange(checked)}
229+
onBlur={field.handleBlur}
230+
className="mt-1"
231+
/>
232+
<div className="grid gap-2">
233+
<FieldLabel htmlFor={field.name}>{label}</FieldLabel>
234+
{description && <FieldDescription>{description}</FieldDescription>}
235+
</div>
236+
</div>
237+
{field.state.meta.isTouched && <ErrorMessages errors={errors} />}
238+
</Field>
239+
);
240+
}
241+
207242
const tryDecimalValue = (input: string): string => {
208243
try {
209244
if (input) {
@@ -221,11 +256,13 @@ export function AmountField({
221256
placeholder,
222257
description,
223258
balance,
259+
addonRight,
224260
}: {
225-
label: string;
261+
label: ReactNode;
226262
placeholder?: string;
227263
description?: string;
228-
balance?: GetBalanceData;
264+
balance?: Omit<GetBalanceData, 'formatted'>;
265+
addonRight?: ReactNode;
229266
}) {
230267
const field = useFieldContext<string>();
231268
const errors = useStore(field.store, (state) => state.meta.errors);
@@ -236,28 +273,71 @@ export function AmountField({
236273

237274
const handleChange = (input: string) => {
238275
setRenderedValue(input);
239-
field.handleChange(tryDecimalValue(input));
276+
field.setValue(tryDecimalValue(input) as never, {
277+
dontRunListeners: true,
278+
});
240279
};
241280

281+
useEffect(() => {
282+
const unsub = field.store.subscribe(({ prevVal, currentVal }) => {
283+
if (prevVal.value !== currentVal.value) {
284+
setRenderedValue(tryDecimalValue(currentVal.value));
285+
}
286+
});
287+
288+
return unsub;
289+
}, []);
290+
242291
return (
243292
<Field>
244-
<FieldLabel htmlFor={label}>
245-
{label}
246-
247-
{balance && (
248-
<span className="ml-2 text-sm font-normal text-gray-400">
249-
(Balance:{' '}
250-
{Decimal.from(balance.value, balance.decimals).toFormatted()}{' '}
251-
{balance.symbol})
252-
</span>
293+
<FieldLabel htmlFor={field.name}>
294+
{balance ? (
295+
<>
296+
<div className="w-full flex flex-row gap-4 justify-between items-center">
297+
<span>{label}</span>
298+
<Button
299+
variant="link"
300+
size="sm"
301+
className="p-0"
302+
onClick={() => {
303+
field.setValue(
304+
Decimal.from(balance.value).toString(balance.decimals),
305+
);
306+
}}
307+
>
308+
<span>
309+
(max:&nbsp;
310+
<AmountRenderer
311+
value={Decimal.from(
312+
balance.value,
313+
balance.decimals,
314+
).toString()}
315+
suffix={balance.symbol}
316+
showApproxSign
317+
/>
318+
)
319+
</span>
320+
</Button>
321+
</div>
322+
</>
323+
) : (
324+
<>{label}</>
253325
)}
254326
</FieldLabel>
255-
<Input
256-
value={renderedValue}
257-
placeholder={placeholder}
258-
onBlur={field.handleBlur}
259-
onChange={(e) => handleChange(e.target.value)}
260-
/>
327+
<InputGroup>
328+
<InputGroupInput
329+
id={field.name}
330+
value={renderedValue}
331+
placeholder={placeholder}
332+
onBlur={field.handleBlur}
333+
onChange={(e) => handleChange(e.target.value)}
334+
type="number"
335+
step="0.00001"
336+
/>
337+
{addonRight && (
338+
<InputGroupAddon align="inline-end">{addonRight}</InputGroupAddon>
339+
)}
340+
</InputGroup>
261341
{description && <FieldDescription>{description}</FieldDescription>}
262342
{field.state.meta.isTouched && <ErrorMessages errors={errors} />}
263343
</Field>

apps/web-app/src/components/MoneyMarket/components/BorrowAssetsList/components/AssetsTable/AssetsTable.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export const AssetsTable: FC<AssetsTableProps> = ({ assets }) => {
7070
</div>
7171
</TableCell>
7272
<TableCell className="border-neutral-800 border-y">
73-
<div className="flex flex-col gap-1">
73+
<div className="flex flex-col gap-1 items-start">
7474
<AmountRenderer
7575
value={asset.liquidity}
7676
suffix={asset.token.symbol}
@@ -99,12 +99,6 @@ export const AssetsTable: FC<AssetsTableProps> = ({ assets }) => {
9999
>
100100
Borrow
101101
</Button>
102-
<Button
103-
className="rounded-full min-w-24 h-10 hover:cursor-pointer"
104-
variant="secondary"
105-
>
106-
Details
107-
</Button>
108102
</div>
109103
</TableCell>
110104
</TableRow>

0 commit comments

Comments
 (0)