Skip to content

Commit c4123d6

Browse files
creed-victorgrinry
andauthored
SOV-5269: repay loans (#21)
* feat: repay loans + revalidate active position list * feat: allow changing borrow rate mode * wip: emode changing * feat: add efficiency mode * fix: review comments * fix: review comments --------- Co-authored-by: Rytis Grincevicius <rytis.grincevicius@gmail.com>
1 parent 34c351b commit c4123d6

File tree

38 files changed

+1572
-198
lines changed

38 files changed

+1572
-198
lines changed

apps/indexer/src/app/plugins/cache.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ const redisCachePlugin: FastifyPluginAsync<RedisCachePluginOptions> = async (
115115
) => {
116116
const redis = opts.redisClient ?? cacheRedisConnection;
117117

118-
const defaultTtl = opts.defaultTtlSeconds ?? 60;
119-
const defaultStaleTtl = opts.defaultStaleTtlSeconds ?? 600; // 10 minutes
118+
const defaultTtl = opts.defaultTtlSeconds ?? 10; // 10 seconds
119+
const defaultStaleTtl = opts.defaultStaleTtlSeconds ?? 60; // 1 minute
120120
const keyPrefix = opts.keyPrefix ?? 'route-cache';
121121

122122
// @ts-expect-error declare decorator
@@ -305,7 +305,7 @@ export const maybeCache = async <T = unknown>(
305305

306306
const redis = cacheRedisConnection;
307307

308-
const ttl = opts.ttlSeconds ?? 30; // 30 seconds
308+
const ttl = opts.ttlSeconds ?? 10; // 10 seconds
309309

310310
const cacheKey = 'maybe-cache:fn:' + encode.sha256(key);
311311

apps/indexer/src/app/routes/_chain/routes.ts

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { client } from '../../../database/client';
1111
import { tTokens } from '../../../database/schema';
1212
import { tTokensSelectors } from '../../../database/selectors';
1313
import {
14+
fetchEmodeCategoryData,
1415
fetchPoolList,
1516
fetchPoolReserves,
1617
fetchUserReserves,
@@ -112,6 +113,12 @@ export default async function (fastify: ZodFastifyInstance) {
112113
pool,
113114
);
114115

116+
const categories = await fetchEmodeCategoryData(
117+
req.chain.chainId,
118+
pool,
119+
reservesData,
120+
);
121+
115122
const tokens = await client.query.tTokens.findMany({
116123
columns: tTokensSelectors.columns,
117124
where: and(
@@ -157,13 +164,29 @@ export default async function (fastify: ZodFastifyInstance) {
157164
.mul(100)
158165
.toFixed(USD_DECIMALS),
159166
canBeCollateral: item.usageAsCollateralEnabled,
167+
stableBorrowRateEnabled: item.stableBorrowRateEnabled,
160168
isActive: item.isActive,
161169
isFroze: item.isFrozen,
162-
// eModes: item.eModes,
170+
171+
eModeCategoryId: item.eModeCategoryId,
172+
borrowCap: item.borrowCap.toString(),
173+
supplyCap: item.supplyCap.toString(),
174+
eModeLtv: item.eModeLtv,
175+
eModeLiquidationThreshold: item.eModeLiquidationThreshold,
176+
eModeLiquidationBonus: item.eModeLiquidationBonus,
177+
eModePriceSource: item.eModePriceSource.toString(),
178+
eModeLabel: item.eModeLabel.toString(),
163179
};
164180
});
165181

166-
return { data: { reservesData: items, baseCurrencyData } };
182+
const eModes = categories.map((category) => ({
183+
...category,
184+
assets: category.assets.map((asset) => {
185+
return tokens.find((t) => areAddressesEqual(t.address, asset));
186+
}),
187+
}));
188+
189+
return { data: { reservesData: items, baseCurrencyData, eModes } };
167190
},
168191
);
169192

@@ -376,18 +399,48 @@ export default async function (fastify: ZodFastifyInstance) {
376399
.mul(100)
377400
.toFixed(USD_DECIMALS),
378401
canBeCollateral: item.reserve.usageAsCollateralEnabled,
402+
stableBorrowRateEnabled: item.reserve.stableBorrowRateEnabled,
379403
isActive: item.reserve.isActive,
380404
isFroze: item.reserve.isFrozen,
381-
// eModes: item.reserve.eModes,
405+
406+
eModeCategoryId: item.reserve.eModeCategoryId,
407+
borrowCap: item.reserve.borrowCap.toString(),
408+
supplyCap: item.reserve.supplyCap.toString(),
409+
eModeLtv: item.reserve.eModeLtv,
410+
eModeLiquidationThreshold: item.reserve.eModeLiquidationThreshold,
411+
eModeLiquidationBonus: item.reserve.eModeLiquidationBonus,
412+
eModePriceSource: item.reserve.eModePriceSource.toString(),
413+
eModeLabel: item.reserve.eModeLabel.toString(),
382414
},
383415
supplied: item.underlyingBalance,
384416
suppliedUsd: item.underlyingBalanceUSD,
417+
suppliedBalanceMarketReferenceCurrency: Decimal.from(
418+
item.underlyingBalanceMarketReferenceCurrency,
419+
baseCurrencyData.marketReferenceCurrencyDecimals,
420+
).toFixed(USD_DECIMALS),
385421

386422
supplyApy: Decimal.from(item.reserve.supplyAPY).mul(100).toString(),
387423
canToggleCollateral,
388424

389-
borrowed: item.variableBorrows,
390-
borrowedUsd: item.variableBorrowsUSD,
425+
borrowed: Decimal.from(item.totalBorrows).toString(),
426+
borrowedUsd: Decimal.from(item.totalBorrowsUSD).toString(),
427+
borrowedBalanceMarketReferenceCurrency: Decimal.from(
428+
item.totalBorrowsMarketReferenceCurrency,
429+
baseCurrencyData.marketReferenceCurrencyDecimals,
430+
).toFixed(USD_DECIMALS),
431+
432+
borrowedStable: Decimal.from(item.stableBorrows).toString(),
433+
borrowedStableUsd: Decimal.from(item.stableBorrowsUSD).toString(),
434+
borrowedBalanceStableMarketReferenceCurrency: Decimal.from(
435+
item.stableBorrowsMarketReferenceCurrency,
436+
baseCurrencyData.marketReferenceCurrencyDecimals,
437+
).toFixed(USD_DECIMALS),
438+
borrowedVariable: Decimal.from(item.variableBorrows).toString(),
439+
borrowedVariableUsd: Decimal.from(item.variableBorrowsUSD).toString(),
440+
borrowedBalanceVariableMarketReferenceCurrency: Decimal.from(
441+
item.variableBorrowsMarketReferenceCurrency,
442+
baseCurrencyData.marketReferenceCurrencyDecimals,
443+
).toFixed(USD_DECIMALS),
391444

392445
collateral: item.usageAsCollateralEnabledOnUser,
393446

@@ -441,6 +494,10 @@ export default async function (fastify: ZodFastifyInstance) {
441494
netWorthUsd: summary.netWorthUSD,
442495
userEmodeCategoryId: summary.userEmodeCategoryId,
443496
isInIsolationMode: summary.isInIsolationMode,
497+
498+
underlyingBalanceMarketReferenceCurrency: Decimal.from(
499+
summary.totalBorrowsUSD,
500+
),
444501
},
445502
},
446503
};

apps/indexer/src/libs/loaders/money-market.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Decimal } from '@sovryn/slayer-shared';
12
import { Address } from 'viem';
23
import { maybeCache } from '../../app/plugins/cache';
34
import { ChainId, chains, ChainSelector } from '../../configs/chains';
@@ -411,6 +412,55 @@ const uiPoolDataProviderAbi = [
411412
},
412413
] as const;
413414

415+
const poolAbi = [
416+
{
417+
inputs: [
418+
{
419+
internalType: 'uint8',
420+
name: 'id',
421+
type: 'uint8',
422+
},
423+
],
424+
name: 'getEModeCategoryData',
425+
outputs: [
426+
{
427+
components: [
428+
{
429+
internalType: 'uint16',
430+
name: 'ltv',
431+
type: 'uint16',
432+
},
433+
{
434+
internalType: 'uint16',
435+
name: 'liquidationThreshold',
436+
type: 'uint16',
437+
},
438+
{
439+
internalType: 'uint16',
440+
name: 'liquidationBonus',
441+
type: 'uint16',
442+
},
443+
{
444+
internalType: 'address',
445+
name: 'priceSource',
446+
type: 'address',
447+
},
448+
{
449+
internalType: 'string',
450+
name: 'label',
451+
type: 'string',
452+
},
453+
],
454+
internalType: 'struct DataTypes.EModeCategory',
455+
name: '',
456+
type: 'tuple',
457+
},
458+
],
459+
stateMutability: 'view',
460+
type: 'function',
461+
},
462+
] as const;
463+
414464
export type PoolDefinition = {
415465
id: string | 'default';
416466
name: string;
@@ -700,3 +750,51 @@ export async function fetchUserReserves(
700750
userEmodeCategoryId,
701751
};
702752
}
753+
754+
export async function fetchEmodeCategoryData(
755+
chainId: ChainSelector,
756+
pool: PoolDefinition,
757+
reserves: Awaited<ReturnType<typeof fetchPoolReserves>>['reservesData'],
758+
) {
759+
const chain = chains.get(chainId);
760+
if (!chain) {
761+
throw new Error(`Unsupported chain: ${chainId}`);
762+
}
763+
764+
const categoryIds = Array.from(
765+
new Set(reserves.map((reserve) => reserve.eModeCategoryId)),
766+
).filter((id) => id !== 0);
767+
768+
const results = await chain.rpc.multicall({
769+
contracts: categoryIds.map((id) => ({
770+
address: pool.address,
771+
abi: poolAbi,
772+
functionName: 'getEModeCategoryData',
773+
args: [id],
774+
})),
775+
});
776+
777+
return results
778+
.map(({ result }, index) => {
779+
if (!result) {
780+
return null;
781+
}
782+
783+
const { ltv, liquidationThreshold, liquidationBonus, label } = result;
784+
const categoryId = categoryIds[index];
785+
786+
return {
787+
id: categoryId,
788+
ltv: Decimal.from(ltv).div(100).toString(),
789+
liquidationThreshold: Decimal.from(liquidationThreshold)
790+
.div(100)
791+
.toString(),
792+
liquidationBonus: Decimal.from(liquidationBonus).div(100).toString(),
793+
label,
794+
assets: reserves
795+
.filter((reserve) => reserve.eModeCategoryId === categoryId)
796+
.map((reserve) => reserve.underlyingAsset.toLowerCase()),
797+
};
798+
})
799+
.filter((item) => item != null);
800+
}

apps/web-app/public/locales/en/common.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,15 @@
55
"confirm": "Confirm",
66
"continue": "Continue",
77
"abort": "Abort",
8-
"loading": "Loading..."
8+
"loading": "Loading...",
9+
"tx": {
10+
"title": "Transaction Confirmation",
11+
"description": "Please review and confirm transactions in your wallet",
12+
"preparing": "Preparing transaction...",
13+
"connectWallet": "Connect your wallet to proceed.",
14+
"switchNetwork": "Switch to {{name}} network",
15+
"signMessage": "Sign Message",
16+
"signTypedData": "Sign Typed Data",
17+
"sendTransaction": "Send Transaction"
18+
}
919
}

apps/web-app/public/locales/en/tx.json

Lines changed: 0 additions & 10 deletions
This file was deleted.

apps/web-app/src/@types/i18next.d.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { resources as common } from 'public/locales/en/common';
22
import type { resources as glossary } from 'public/locales/en/glossary';
3-
import type { resources as tx } from 'public/locales/en/tx';
43
import type { resources as validation } from 'public/locales/en/validation';
54
import { defaultNS } from '../i18n';
65

@@ -12,7 +11,6 @@ declare module 'i18next' {
1211
common: typeof common;
1312
glossary: typeof glossary;
1413
validation: typeof validation;
15-
tx: typeof tx;
1614
};
1715
}
1816
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import '@tanstack/react-query';
2+
3+
interface GlobalQueryMeta extends Record<string, unknown> {
4+
revalidateCache?: boolean;
5+
}
6+
7+
interface GlobalMutationMeta extends Record<string, unknown> {
8+
invalidates?: Array<QueryKey>;
9+
}
10+
11+
declare module '@tanstack/react-query' {
12+
interface Register {
13+
queryMeta: GlobalQueryMeta;
14+
mutationMeta: GlobalMutationMeta;
15+
}
16+
}

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ import { Checkbox } from './ui/checkbox';
1818
import { Field, FieldDescription, FieldError, FieldLabel } from './ui/field';
1919
import { InputGroup, InputGroupAddon, InputGroupInput } from './ui/input-group';
2020

21-
export function SubscribeButton({ label }: { label: string }) {
21+
export function SubscribeButton({
22+
label,
23+
disabled,
24+
}: {
25+
label: string;
26+
disabled?: boolean;
27+
}) {
2228
const form = useFormContext();
2329
return (
2430
<form.Subscribe
@@ -27,7 +33,7 @@ export function SubscribeButton({ label }: { label: string }) {
2733
{([isSubmitting, isFormValid]) => (
2834
<Button
2935
type="submit"
30-
disabled={isSubmitting || !isFormValid}
36+
disabled={isSubmitting || !isFormValid || disabled}
3137
form={form.formId}
3238
>
3339
<Loader2Icon
@@ -272,8 +278,9 @@ export function AmountField({
272278
);
273279

274280
const handleChange = (input: string) => {
281+
const value = input.replace(/,/g, '.');
275282
setRenderedValue(input);
276-
field.setValue(tryDecimalValue(input) as never, {
283+
field.setValue(tryDecimalValue(value) as never, {
277284
dontRunListeners: true,
278285
});
279286
};
@@ -331,8 +338,6 @@ export function AmountField({
331338
placeholder={placeholder}
332339
onBlur={field.handleBlur}
333340
onChange={(e) => handleChange(e.target.value)}
334-
type="number"
335-
step="0.00001"
336341
/>
337342
{addonRight && (
338343
<InputGroupAddon align="inline-end">{addonRight}</InputGroupAddon>

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import { Accordion } from '@/components/ui/accordion';
2+
import { Alert, AlertDescription } from '@/components/ui/alert';
23
import type { MoneyMarketPoolReserve } from '@sovryn/slayer-sdk';
4+
import { CircleAlert } from 'lucide-react';
35
import { useState, type FC } from 'react';
46
import { AssetsTable } from './components/AssetsTable/AssetsTable';
57

68
type BorrowAssetsListProps = {
79
borrowAssets: MoneyMarketPoolReserve[];
10+
eModesCategoryId?: number;
811
loading?: boolean;
912
};
1013

1114
export const BorrowAssetsList: FC<BorrowAssetsListProps> = ({
1215
borrowAssets,
16+
eModesCategoryId,
1317
}) => {
1418
const [open, setOpen] = useState(true);
15-
1619
return (
1720
<Accordion
1821
label={<span className="text-[1rem] font-medium">Assets to borrow</span>}
@@ -21,6 +24,16 @@ export const BorrowAssetsList: FC<BorrowAssetsListProps> = ({
2124
open={open}
2225
onClick={setOpen}
2326
>
27+
{!!eModesCategoryId && (
28+
<Alert>
29+
<CircleAlert />
30+
<AlertDescription>
31+
In E-Mode some assets are not borrowable. Exit E-Mode to get access
32+
to all assets.
33+
</AlertDescription>
34+
</Alert>
35+
)}
36+
2437
<AssetsTable assets={borrowAssets} />
2538
</Accordion>
2639
);

0 commit comments

Comments
 (0)