diff --git a/components/OperationsModal/BorrowDateSelector/index.tsx b/components/OperationsModal/BorrowDateSelector/index.tsx
index 23593a004..6e95f25a2 100644
--- a/components/OperationsModal/BorrowDateSelector/index.tsx
+++ b/components/OperationsModal/BorrowDateSelector/index.tsx
@@ -1,15 +1,13 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, useMemo } from 'react';
import { Box, Typography } from '@mui/material';
-
-import DropdownMenu from 'components/DropdownMenu';
-import parseTimestamp from 'utils/parseTimestamp';
-
import { useTranslation } from 'react-i18next';
+import parseTimestamp from 'utils/parseTimestamp';
import { useOperationContext } from 'contexts/OperationContext';
+import ModalAlert from 'components/common/modal/ModalAlert';
+import DropdownMenu from 'components/DropdownMenu';
import getHourUTC2Local from 'utils/getHourUTC2Local';
import { track } from 'utils/mixpanel';
-
-import ModalAlert from '../../common/modal/ModalAlert';
+import { DAY } from 'utils/utils';
type DateOptionProps = {
label: string;
@@ -32,6 +30,11 @@ export function DateOption({ label, option = false }: DateOptionProps) {
function BorrowDateSelector() {
const { t } = useTranslation();
const { date, dates, setDate } = useOperationContext();
+ const daysToMaturity = useMemo(() => {
+ if (!date) return;
+ const now = BigInt(Math.round(Date.now() / 1000));
+ return Number((date - now) / DAY);
+ }, [date]);
const handleChange = useCallback(
(maturity: bigint) => {
@@ -46,8 +49,6 @@ function BorrowDateSelector() {
[date, setDate],
);
- const daysToMaturity = Math.floor((Number(date) - Date.now() / 1000) / 60 / 60 / 24);
-
return (
@@ -62,17 +63,23 @@ function BorrowDateSelector() {
renderValue={date ? : null}
renderOption={(o: bigint) => }
/>
-
- {t('at {{hour}}', { hour: getHourUTC2Local() })}
+
+ {t('at {{hour}}', { hour: getHourUTC2Local(undefined, 'h a [UTC] Z') })}
- {daysToMaturity <= 7 && (
+ {daysToMaturity && daysToMaturity <= 7 && (
)}
diff --git a/components/OperationsModal/ModalGif/index.tsx b/components/OperationsModal/ModalGif/index.tsx
index e1ed6a6f5..c98b69707 100644
--- a/components/OperationsModal/ModalGif/index.tsx
+++ b/components/OperationsModal/ModalGif/index.tsx
@@ -20,7 +20,7 @@ type Props = {
function ModalGif({ tx, tryAgain }: Props) {
const { t } = useTranslation();
const translateOperation = useTranslateOperation();
- const { operation, symbol, qty, date } = useOperationContext();
+ const { operation, symbol, qty, date, installments } = useOperationContext();
const isLoading = useMemo(() => tx.status === 'processing' || tx.status === 'loading', [tx]);
const isSuccess = useMemo(() => tx.status === 'success', [tx]);
@@ -78,7 +78,14 @@ function ModalGif({ tx, tryAgain }: Props) {
pastAction: translateOperation(operation, { variant: 'past' }),
qty,
symbol: formatSymbol(symbol),
- }) + (reminder && date ? t(' until {{daysLeft}}', { daysLeft: parseTimestamp(date) }) : '')}
+ }) +
+ (reminder && date
+ ? installments > 1
+ ? t(' In {{installments}} installments', {
+ installments,
+ })
+ : t(' until {{daysLeft}}', { daysLeft: parseTimestamp(date) })
+ : '')}
{isError && t('Something went wrong')}
diff --git a/components/Reminder/index.tsx b/components/Reminder/index.tsx
index cc9087d56..4e94acb22 100644
--- a/components/Reminder/index.tsx
+++ b/components/Reminder/index.tsx
@@ -7,6 +7,8 @@ import parseTimestamp from 'utils/parseTimestamp';
import useTranslateOperation from 'hooks/useTranslateOperation';
import type { Operation } from 'types/Operation';
import { track } from 'utils/mixpanel';
+import { MATURITY_DAYS } from 'utils/utils';
+import { useOperationContext } from 'contexts/OperationContext';
type Props = {
operation: Operation;
@@ -18,6 +20,7 @@ const Reminder: FC = ({ operation, maturity }) => {
const translateOperation = useTranslateOperation();
const { palette } = useTheme();
const buttonRef = useRef(null);
+ const { installments } = useOperationContext();
const isBorrow = useMemo(() => operation?.startsWith('borrow'), [operation]);
@@ -33,6 +36,7 @@ const Reminder: FC = ({ operation, maturity }) => {
options: ['Google', 'Apple', 'iCal', 'Microsoft365', 'MicrosoftTeams', 'Outlook.com', 'Yahoo'],
timeZone: 'UTC',
lightMode: palette.mode,
+ recurrence: `RRULE:FREQ=DAILY;INTERVAL=${MATURITY_DAYS.toString()};COUNT=${installments}`,
};
track('Button Clicked', {
location: 'Reminder',
@@ -42,7 +46,7 @@ const Reminder: FC = ({ operation, maturity }) => {
});
if (buttonRef.current) atcb_action(config, buttonRef.current);
- }, [maturity, operation, palette.mode, t, translateOperation]);
+ }, [installments, maturity, operation, palette.mode, t, translateOperation]);
return (
{
bestFixedBorrow.rate < Number(formatEther(floatingBorrowRate))
? { maturity: bestFixedBorrow.maturity, rate: bestFixedBorrow.rate }
: { maturity: 0n, rate: Number(formatEther(floatingBorrowRate)) };
-
const [first, second] = [...fixedPools].sort((a, b) => Number(a.maturity) - Number(b.maturity));
const now = BigInt(Math.round(Date.now() / 1000));
const upcomingMaturity = first.maturity - now < WEEK ? second.maturity : first.maturity;
+
tempRows.push({
symbol,
totalDeposited: formatNumber(formatUnits((totalDeposited * usdPrice) / WEI_PER_ETHER, decimals)),
diff --git a/components/operations/BorrowAtMaturity/Installments.tsx b/components/operations/BorrowAtMaturity/Installments.tsx
new file mode 100644
index 000000000..2c708475c
--- /dev/null
+++ b/components/operations/BorrowAtMaturity/Installments.tsx
@@ -0,0 +1,57 @@
+import React, { useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useOperationContext } from 'contexts/OperationContext';
+import { Box, Typography } from '@mui/material';
+import { MATURITY_DAYS } from 'utils/utils';
+import ModalAlert from 'components/common/modal/ModalAlert';
+import InstallmentsOptions from './InstallmentsOptions';
+
+export default function Installments({ setBreakdownSheetOpen }: { setBreakdownSheetOpen: (open: boolean) => void }) {
+ const { t } = useTranslation();
+ const { installments } = useOperationContext();
+
+ const viewBreakdown = useCallback(() => {
+ setBreakdownSheetOpen(true);
+ }, [setBreakdownSheetOpen]);
+
+ return (
+
+
+
+ palette.green,
+ borderRadius: '4px',
+ px: 0.5,
+ textTransform: 'uppercase',
+ }}
+ >
+ {t('New!')}
+
+
+ {t('Installments')}
+
+
+
+
+ {installments >= 2 && (
+
+ {t('Each installment is due every {{interval}} days.', {
+ interval: MATURITY_DAYS.toString(),
+ })}{' '}
+
+ {t('Payment schedule')}
+
+ >
+ }
+ />
+ )}
+
+ );
+}
diff --git a/components/operations/BorrowAtMaturity/InstallmentsBreakdown.tsx b/components/operations/BorrowAtMaturity/InstallmentsBreakdown.tsx
new file mode 100644
index 000000000..2ecd2a931
--- /dev/null
+++ b/components/operations/BorrowAtMaturity/InstallmentsBreakdown.tsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import { Box, Button, Skeleton, Table, TableBody, TableCell, TableHead, TableRow, Typography } from '@mui/material';
+import { useTranslation } from 'react-i18next';
+import { useOperationContext } from 'contexts/OperationContext';
+import TableHeadCell from 'components/common/TableHeadCell';
+import parseTimestamp from 'utils/parseTimestamp';
+import useAccountData from 'hooks/useAccountData';
+
+export default function InstallmentsBreakdown({ onClose }: { onClose: () => void }) {
+ const { t } = useTranslation();
+ const { installmentsDetails, symbol } = useOperationContext();
+ const { marketAccount } = useAccountData(symbol);
+ if (!installmentsDetails || !marketAccount) return ;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {installmentsDetails.installmentsRepayAmount.map((option, index) => (
+
+
+
+ {index + 1}
+
+
+
+ {parseTimestamp(installmentsDetails.maturities[index])}
+
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/components/operations/BorrowAtMaturity/InstallmentsOptions.tsx b/components/operations/BorrowAtMaturity/InstallmentsOptions.tsx
new file mode 100644
index 000000000..5f6b9e2a3
--- /dev/null
+++ b/components/operations/BorrowAtMaturity/InstallmentsOptions.tsx
@@ -0,0 +1,88 @@
+import React, { useCallback, useMemo } from 'react';
+import DropdownMenu from 'components/DropdownMenu';
+import formatNumber from 'utils/formatNumber';
+import { useOperationContext } from 'contexts/OperationContext';
+import useAccountData from 'hooks/useAccountData';
+import { formatUnits } from 'viem';
+import { Box, Skeleton, Typography } from '@mui/material';
+import { useTranslation } from 'react-i18next';
+
+type OptionProps = {
+ installments: number;
+ repayAmount: bigint;
+ option?: boolean;
+};
+
+export function Option({ installments, repayAmount, option = false }: OptionProps) {
+ const { symbol } = useOperationContext();
+ const { marketAccount } = useAccountData(symbol);
+
+ return (
+
+ {installments} x{' '}
+
+ {repayAmount === 0n ? '' : '~'}
+ {marketAccount ? formatNumber(formatUnits(repayAmount, marketAccount.decimals)) : }
+
+
+ );
+}
+
+export default function InstallmentsOptions() {
+ const { t } = useTranslation();
+ const { installments, onInstallmentsChange, installmentsOptions, symbol } = useOperationContext();
+ const { marketAccount } = useAccountData(symbol);
+
+ const handleChange = useCallback(
+ (option: { installments: number }) => {
+ onInstallmentsChange(option.installments);
+ },
+ [onInstallmentsChange],
+ );
+
+ const option = useMemo(() => {
+ if (!installmentsOptions) return;
+ return installmentsOptions.find((o) => Number(o.installments) === installments);
+ }, [installments, installmentsOptions]);
+
+ if (!installmentsOptions || !option) return ;
+
+ return (
+
+ : null}
+ renderOption={(o: OptionProps) => }
+ />
+ {option ? (
+
+ {t('Total')}{' '}
+ {option && marketAccount ? (
+ formatNumber(formatUnits(option.repayAmount * BigInt(option.installments), marketAccount.decimals))
+ ) : (
+
+ )}
+
+ ) : null}
+
+ );
+}
diff --git a/components/operations/BorrowAtMaturity/index.tsx b/components/operations/BorrowAtMaturity/index.tsx
index 61c347cd5..3ba67eb3b 100644
--- a/components/operations/BorrowAtMaturity/index.tsx
+++ b/components/operations/BorrowAtMaturity/index.tsx
@@ -1,23 +1,9 @@
import React, { FC, PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react';
-
import ModalTxCost from 'components/OperationsModal/ModalTxCost';
import ModalGif from 'components/OperationsModal/ModalGif';
-
-import { WEEK, toPercentage } from 'utils/utils';
-
+import { toPercentage } from 'utils/utils';
import { useOperationContext, usePreviewTx } from 'contexts/OperationContext';
-import {
- Box,
- Button,
- Grid,
- Skeleton,
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableRow,
- Typography,
-} from '@mui/material';
+import { Grid } from '@mui/material';
import { ModalBox, ModalBoxCell, ModalBoxRow } from 'components/common/modal/ModalBox';
import AssetInput from 'components/OperationsModal/AssetInput';
import BorrowDateSelector from 'components/OperationsModal/BorrowDateSelector';
@@ -34,216 +20,20 @@ import ModalRewards from 'components/OperationsModal/ModalRewards';
import ModalPenaltyRate from 'components/OperationsModal/ModalPenaltyRate';
import { useTranslation } from 'react-i18next';
import useTranslateOperation from 'hooks/useTranslateOperation';
-import DropdownMenu from '../../DropdownMenu';
-import ModalSheet from '../../common/modal/ModalSheet';
-import TableHeadCell from '../../common/TableHeadCell';
-import { formatUnits } from 'viem';
-import parseTimestamp from '../../../utils/parseTimestamp';
-import usePreviewFixedOperation from '../../../hooks/usePreviewFixedOperation';
-import formatNumber from '../../../utils/formatNumber';
-import Image from 'next/image';
-import formatSymbol from '../../../utils/formatSymbol';
-import { WEI_PER_ETHER } from '../../../utils/const';
-
-type OptionProps = {
- installments: number;
- repayAmount: bigint;
- option?: boolean;
-};
-
-const InstallmentsBreakdown = ({ onClose }: { onClose: () => void }) => {
- const { t } = useTranslation();
- const { installmentsDetails, symbol } = useOperationContext();
- const { marketAccount } = useAccountData(symbol);
- const { options: fixedOptions } = usePreviewFixedOperation('borrow');
-
- if (!installmentsDetails || !marketAccount) return ;
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- {installmentsDetails.installmentsRepayAmount.map((option, index) => {
- const maturity = installmentsDetails.maturities[index];
- const apr = fixedOptions.find((o) => o.maturity === maturity)?.borrowAPR;
- return (
-
- {index + 1}
-
- {parseTimestamp(maturity)}
-
-
-
- {formatNumber(formatUnits(option, marketAccount.decimals))}{' '}
- {symbol && (
-
- )}
-
-
-
- {apr ? toPercentage(apr) : }
-
-
- );
- })}
-
-
-
-
-
- );
-};
-
-export function Option({ installments, repayAmount, option = false }: OptionProps) {
- const { t } = useTranslation();
- const { symbol } = useOperationContext();
- const { marketAccount } = useAccountData(symbol);
-
- return (
-
- {installments} x{' '}
-
- {repayAmount === 0n ? '' : '~'}
- {marketAccount ? formatNumber(formatUnits(repayAmount, marketAccount.decimals)) : }
- {installments > 1 ? t('/mo') : ''}
-
-
- );
-}
-
-const MaturitiesDropdown = () => {
- const { t } = useTranslation();
- const { installments, onInstallmentsChange, installmentsOptions, symbol } = useOperationContext();
- const { marketAccount } = useAccountData(symbol);
-
- const handleChange = useCallback(
- (option: { installments: number }) => {
- onInstallmentsChange(option.installments);
- },
- [onInstallmentsChange],
- );
- if (!installmentsOptions) return ;
-
- const option = installmentsOptions.find((o) => Number(o.installments) === installments);
-
- const usd =
- option && marketAccount
- ? (option?.repayAmount * marketAccount.usdPrice) / (WEI_PER_ETHER * 10n ** BigInt(marketAccount.decimals))
- : 0n;
-
- return (
-
- : null}
- renderOption={(o: OptionProps) => }
- />
- {option ? (
-
- ~${formatNumber(usd || '0', 'USD')}
- {t('/mo')}
-
- ) : null}
-
- );
-};
-
-const Installments = ({ setBreakdownSheetOpen }: { setBreakdownSheetOpen: (open: boolean) => void }) => {
- const { t } = useTranslation();
-
- const { installments } = useOperationContext();
-
- const viewBreakdown = useCallback(() => {
- setBreakdownSheetOpen(true);
- }, [setBreakdownSheetOpen]);
-
- return (
-
-
-
- palette.green,
- borderRadius: '4px',
- px: 0.5,
- textTransform: 'uppercase',
- }}
- >
- {t('New!')}
-
-
- {t('Installments')}
-
-
-
-
-
- {installments >= 2 && (
-
- {t('Each installment is due every {{interval}} days.', {
- interval: ((4n * WEEK) / (60n * 60n * 24n)).toString(),
- })}{' '}
-
- {t('View breakdown')}
-
- >
- }
- />
- )}
-
- );
-};
+import ModalSheet from 'components/common/modal/ModalSheet';
+import useBorrowInInstallments from 'hooks/useBorrowInInstallments';
+import InstallmentsBreakdown from './InstallmentsBreakdown';
+import Installments from './Installments';
const BorrowAtMaturity: FC = ({ children }) => {
const { t } = useTranslation();
const translateOperation = useTranslateOperation();
- const { symbol, errorData, setErrorData, qty, gasCost, tx } = useOperationContext();
+ const { symbol, errorData, setErrorData, qty, gasCost, tx, installments } = useOperationContext();
const {
- isLoading,
+ isLoading: borrowAtMaturityLoading,
onMax,
handleInputChange,
- handleSubmitAction,
+ handleSubmitAction: borrowAtMaturity,
borrow,
updateAPR,
rawSlippage,
@@ -254,6 +44,10 @@ const BorrowAtMaturity: FC = ({ children }) => {
needsApproval,
previewGasCost,
} = useBorrowAtMaturity();
+
+ const { handleSubmitAction: borrowInInstallments, isLoading: borrowInInstallmentsLoading } =
+ useBorrowInInstallments();
+
const { marketAccount } = useAccountData(symbol);
const container = useRef(null);
const breakdownSheetRef = useRef(null);
@@ -276,11 +70,17 @@ const BorrowAtMaturity: FC = ({ children }) => {
const handleBreakdownSheetClose = useCallback(() => {
setBreakdownSheetOpen(false);
}, []);
+ const loading = installments > 1 ? borrowInInstallmentsLoading : borrowAtMaturityLoading || previewIsLoading;
if (tx) return ;
return (
-
+
@@ -333,16 +133,16 @@ const BorrowAtMaturity: FC = ({ children }) => {
1 ? borrowInInstallments : borrowAtMaturity}
+ isLoading={loading}
+ disabled={!qty || parseFloat(qty) <= 0 || loading || previewIsLoading || errorData?.status}
/>
diff --git a/components/operations/DepositAtMaturity/index.tsx b/components/operations/DepositAtMaturity/index.tsx
index 547da11a1..f541f2f8c 100644
--- a/components/operations/DepositAtMaturity/index.tsx
+++ b/components/operations/DepositAtMaturity/index.tsx
@@ -15,7 +15,7 @@ import ModalAdvancedSettings from 'components/common/modal/ModalAdvancedSettings
import ModalInfoTotalDeposits from 'components/OperationsModal/Info/ModalInfoTotalDeposits';
import ModalAlert from 'components/common/modal/ModalAlert';
import ModalSubmit from 'components/OperationsModal/ModalSubmit';
-import BorrowDateSelector from 'components/OperationsModal/DateSelector';
+import DateSelector from 'components/OperationsModal/DateSelector';
import ModalInfoAPR from 'components/OperationsModal/Info/ModalInfoAPR';
import ModalInfoFixedUtilizationRate from 'components/OperationsModal/Info/ModalInfoFixedUtilizationRate';
import ModalInfo from 'components/common/modal/ModalInfo';
@@ -73,7 +73,7 @@ const DepositAtMaturity: FC = () => {
-
+
diff --git a/components/operations/RepayAtMaturity/index.tsx b/components/operations/RepayAtMaturity/index.tsx
index 70eb0cc51..c6359d48e 100644
--- a/components/operations/RepayAtMaturity/index.tsx
+++ b/components/operations/RepayAtMaturity/index.tsx
@@ -14,7 +14,7 @@ import useAccountData from 'hooks/useAccountData';
import { Grid } from '@mui/material';
import { ModalBox, ModalBoxCell, ModalBoxRow } from 'components/common/modal/ModalBox';
import AssetInput from 'components/OperationsModal/AssetInput';
-import BorrowDateSelector from 'components/OperationsModal/DateSelector';
+import DateSelector from 'components/OperationsModal/DateSelector';
import ModalInfoMaturityStatus from 'components/OperationsModal/Info/ModalInfoMaturityStatus';
import ModalInfoAmount from 'components/OperationsModal/Info/ModalInfoAmount';
import ModalInfoHealthFactor from 'components/OperationsModal/Info/ModalInfoHealthFactor';
@@ -338,7 +338,7 @@ const RepayAtMaturity: FC = () => {
-
+
{date !== undefined && }
diff --git a/components/operations/WithdrawAtMaturity/index.tsx b/components/operations/WithdrawAtMaturity/index.tsx
index 0acdf4b06..33236c28c 100644
--- a/components/operations/WithdrawAtMaturity/index.tsx
+++ b/components/operations/WithdrawAtMaturity/index.tsx
@@ -12,7 +12,7 @@ import useAccountData from 'hooks/useAccountData';
import { Grid } from '@mui/material';
import { ModalBox, ModalBoxCell, ModalBoxRow } from 'components/common/modal/ModalBox';
import AssetInput from 'components/OperationsModal/AssetInput';
-import BorrowDateSelector from 'components/OperationsModal/DateSelector';
+import DateSelector from 'components/OperationsModal/DateSelector';
import ModalInfoHealthFactor from 'components/OperationsModal/Info/ModalInfoHealthFactor';
import ModalInfoFixedUtilizationRate from 'components/OperationsModal/Info/ModalInfoFixedUtilizationRate';
import ModalAdvancedSettings from 'components/common/modal/ModalAdvancedSettings';
@@ -274,7 +274,7 @@ const WithdrawAtMaturity: FC = () => {
-
+
{date !== undefined && }
diff --git a/contexts/OperationContext.tsx b/contexts/OperationContext.tsx
index 1f4e8f3ea..d390683fb 100644
--- a/contexts/OperationContext.tsx
+++ b/contexts/OperationContext.tsx
@@ -7,6 +7,7 @@ import React, {
useCallback,
useEffect,
useMemo,
+ SetStateAction,
} from 'react';
import { Address, parseUnits } from 'viem';
@@ -23,7 +24,7 @@ import { Transaction } from 'types/Transaction';
import numbers from 'config/numbers.json';
import type { Operation } from 'types/Operation';
import { Args } from './ModalContext';
-import useInstallments from '../hooks/useInstallments';
+import useInstallmentsData from '../hooks/useInstallmentsData';
type LoadingButton = { withCircularProgress?: boolean; label?: string };
@@ -50,7 +51,7 @@ type ContextValues = {
date?: bigint;
dates: bigint[];
- setDate: (date?: bigint) => void;
+ setDate: React.Dispatch>;
marketContract?: Market;
assetContract?: ERC20;
@@ -71,8 +72,8 @@ type ContextValues = {
installments: number;
onInstallmentsChange: (installments: number) => void;
- installmentsOptions: ReturnType['installmentsOptions'];
- installmentsDetails: ReturnType['installmentsDetails'];
+ installmentsOptions: ReturnType['installmentsOptions'];
+ installmentsDetails: ReturnType['installmentsDetails'];
};
const OperationContext = createContext(null);
@@ -120,12 +121,25 @@ export const OperationContextProvider: FC> = ({ args, c
const assetContract = useERC20(marketAccount?.asset);
const marketContract = useMarket(marketAccount?.market);
const ETHRouterContract = useETHRouter();
- const { installmentsOptions, installmentsDetails } = useInstallments({
+ const { installmentsOptions, installmentsDetails } = useInstallmentsData({
qty,
date,
symbol: marketSymbol,
installments,
});
+ const handleInstallmentsChange = useCallback(
+ (installments_: number) => {
+ setInstallments(installments_);
+ if (!installmentsOptions) return;
+ setDate(installmentsOptions[installments_ - 1].startingDate);
+ },
+ [installmentsOptions],
+ );
+
+ const handleDateChange = useCallback((date_: SetStateAction) => {
+ setDate(date_);
+ setInstallments(1);
+ }, []);
const value: ContextValues = {
operation,
@@ -148,10 +162,7 @@ export const OperationContextProvider: FC> = ({ args, c
date,
dates,
- setDate: (date_?: bigint) => {
- setDate(date_);
- setInstallments(1);
- },
+ setDate: handleDateChange,
assetContract,
marketContract,
@@ -168,11 +179,7 @@ export const OperationContextProvider: FC> = ({ args, c
receiver,
setReceiver,
installments,
- onInstallmentsChange: (installments_: number) => {
- setInstallments(installments_);
- if (!installmentsOptions) return;
- setDate(installmentsOptions[installments_ - 1].startingDate);
- },
+ onInstallmentsChange: handleInstallmentsChange,
installmentsOptions,
installmentsDetails,
};
diff --git a/hooks/useBorrowInInstallments.ts b/hooks/useBorrowInInstallments.ts
new file mode 100644
index 000000000..e0e579776
--- /dev/null
+++ b/hooks/useBorrowInInstallments.ts
@@ -0,0 +1,132 @@
+import { useCallback, useEffect, useState } from 'react';
+import { usePublicClient } from 'wagmi';
+import { Address, Hex, pad, trim, zeroAddress } from 'viem';
+import { useOperationContext } from 'contexts/OperationContext';
+import handleOperationError from 'utils/handleOperationError';
+import { installmentsRouterABI, useInstallmentsRouterBorrow, usePrepareInstallmentsRouterBorrow } from 'types/abi';
+import { Transaction } from 'types/Transaction';
+import { WEI_PER_ETHER } from 'utils/const';
+import useWaitForTransaction from 'hooks/useWaitForTransaction';
+import useAccountData from 'hooks/useAccountData';
+import useSignPermit from 'hooks/useSignPermit';
+import useContract from 'hooks/useContract';
+import { useWeb3 } from 'hooks/useWeb3';
+import useMarket from 'hooks/useMarket';
+
+type Permit = {
+ value: bigint;
+ deadline: bigint;
+ v: number;
+ r: Hex;
+ s: Hex;
+};
+
+function useMarketPermit(marketSymbol: string) {
+ const { walletAddress } = useWeb3();
+ const publicClient = usePublicClient();
+ const { marketAccount } = useAccountData(marketSymbol);
+ const market = useMarket(marketAccount?.market);
+ const signPermit = useSignPermit();
+
+ return useCallback(
+ async (params: { spender: Address; value: bigint; duration: number }) => {
+ if (!market || !walletAddress) return;
+ const implementation = await publicClient.getStorageAt({
+ address: market.address,
+ slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc',
+ });
+ if (!implementation) return;
+
+ return signPermit({
+ ...params,
+ verifyingContract: {
+ ...market,
+ address: pad(trim(implementation), { size: 20 }),
+ },
+ });
+ },
+ [market, publicClient, signPermit, walletAddress],
+ );
+}
+
+export default function useBorrowInInstallments() {
+ const { installmentsDetails, marketContract, date, symbol, slippage, setTx, setErrorData } = useOperationContext();
+ const { walletAddress, opts, chain } = useWeb3();
+ const [permit, setPermit] = useState();
+ const [signingPermit, setSigningPermit] = useState(false);
+ const installmentsRouter = useContract('InstallmentsRouter', installmentsRouterABI);
+ const marketPermit = useMarketPermit(symbol);
+ const maxRepay = installmentsDetails ? (installmentsDetails.maxRepay * slippage) / WEI_PER_ETHER : 0n;
+
+ const { config: installmentsBorrowConfig } = usePrepareInstallmentsRouterBorrow(
+ marketContract && installmentsDetails && date && permit && installmentsRouter
+ ? ({
+ ...opts,
+ chainId: chain.id,
+ enabled: true,
+ address: installmentsRouter.address,
+ account: walletAddress ?? zeroAddress,
+ args: [marketContract.address, date, installmentsDetails.installmentsPrincipal, maxRepay, permit],
+ } as const)
+ : undefined,
+ );
+
+ const {
+ writeAsync: installmentsBorrowWrite,
+ data: installmentsBorrowData,
+ isLoading: installmentsBorrowLoading,
+ } = useInstallmentsRouterBorrow(installmentsBorrowConfig);
+
+ const { isLoading: waitingInstallmentsBorrow } = useWaitForTransaction({
+ hash: installmentsBorrowData?.hash,
+ onSettled: (tx) => setTx(tx as Transaction),
+ });
+
+ const signInstallmentsPermit = useCallback(async () => {
+ if (!installmentsRouter) return;
+ setSigningPermit(true);
+ try {
+ setPermit(
+ await marketPermit({
+ spender: installmentsRouter.address,
+ value: maxRepay,
+ duration: 3_600, // TODO check if correct
+ }),
+ );
+ } catch (error) {
+ setErrorData({
+ status: true,
+ message: handleOperationError(error),
+ });
+ } finally {
+ setSigningPermit(false);
+ }
+ }, [installmentsRouter, marketPermit, maxRepay, setErrorData]);
+
+ const borrow = useCallback(async () => {
+ if (!installmentsBorrowWrite) return;
+ setPermit(undefined);
+ try {
+ const { hash } = await installmentsBorrowWrite();
+ if (hash)
+ setTx({
+ status: 'processing',
+ hash,
+ });
+ } catch (error) {
+ setErrorData({
+ status: true,
+ message: handleOperationError(error),
+ });
+ }
+ }, [installmentsBorrowWrite, setErrorData, setTx]);
+
+ useEffect(() => {
+ if (permit) borrow();
+ }, [borrow, installmentsBorrowWrite, permit]);
+
+ return {
+ handleSubmitAction: signInstallmentsPermit,
+ isLoading: installmentsBorrowLoading || waitingInstallmentsBorrow || signingPermit,
+ };
+}
diff --git a/hooks/useInstallments.ts b/hooks/useInstallmentsData.ts
similarity index 93%
rename from hooks/useInstallments.ts
rename to hooks/useInstallmentsData.ts
index 38bbf3ec5..b375eb2e9 100644
--- a/hooks/useInstallments.ts
+++ b/hooks/useInstallmentsData.ts
@@ -1,14 +1,13 @@
import { useCallback, useMemo } from 'react';
-
-import useAccountData from './useAccountData';
-import useIRM from './useIRM';
-import { fixedUtilization, globalUtilization } from 'utils/interestRateCurve';
+import { parseUnits } from 'viem';
import split from '@exactly/lib/esm/installments/split';
import fromAmounts from '@exactly/lib/esm/installments/fromAmounts';
-import { parseUnits } from 'viem';
-import { INTERVAL } from '../utils/utils';
+import { fixedUtilization, globalUtilization } from 'utils/interestRateCurve';
+import useAccountData from 'hooks/useAccountData';
+import { INTERVAL } from 'utils/utils';
+import useIRM from 'hooks/useIRM';
-export default function useInstallments({
+export default function useInstallmentsData({
qty,
date,
symbol,
@@ -26,13 +25,10 @@ export default function useInstallments({
(amount: bigint, installments_: bigint, firstMaturity: bigint) => {
if (amount === 0n || irmParameters === undefined || marketAccount === undefined) return;
const { floatingUtilization, floatingAssets, floatingDebt, floatingBackupBorrowed, fixedPools } = marketAccount;
-
const fixedPoolsUtilizations = fixedPools
.filter(({ maturity }) => maturity >= firstMaturity && maturity < firstMaturity + installments_ * INTERVAL)
.map(({ supplied, borrowed }) => fixedUtilization(supplied, borrowed, floatingAssets));
-
const timestamp = Math.round(Date.now() / 1000);
-
const parameters = [
floatingAssets,
Number(firstMaturity),
@@ -49,12 +45,6 @@ export default function useInstallments({
const maxRepay = installmentsRepayAmount.reduce((acc, val) => acc + val, 0n);
const averageRepay = maxRepay / installments_;
const maturities = installmentsPrincipal.map((_, index) => firstMaturity + BigInt(index) * INTERVAL);
-
- console.log({
- installments_,
- installmentsRepayAmount,
- });
-
return {
installmentsPrincipal,
maturities,
@@ -71,17 +61,13 @@ export default function useInstallments({
if (!marketAccount || !date) return undefined;
const { decimals, maxFuturePools, fixedPools } = marketAccount;
const amount = parseUnits(qty, decimals);
-
const lastPool = fixedPools.map(({ maturity }) => maturity).sort((a, b) => (a > b ? -1 : 1))[0];
-
return new Array(maxFuturePools).fill(0).map((_, index) => {
const installmentsCount = BigInt(index + 1);
-
const endDate = date + (installmentsCount - 1n) * INTERVAL;
const last = endDate > lastPool ? lastPool : endDate;
const startingDate = last - (installmentsCount - 1n) * INTERVAL;
const details = getDetails(amount, installmentsCount, startingDate);
-
return {
installments: Number(installmentsCount),
repayAmount: amount === 0n || details === undefined ? 0n : details.averageRepay,
@@ -94,7 +80,6 @@ export default function useInstallments({
if (!marketAccount || !date) return undefined;
const { decimals } = marketAccount;
const amount = parseUnits(qty, decimals);
-
return getDetails(amount, BigInt(installments), date);
}, [date, getDetails, installments, marketAccount, qty]);
diff --git a/hooks/useSignPermit.ts b/hooks/useSignPermit.ts
new file mode 100644
index 000000000..3359796b9
--- /dev/null
+++ b/hooks/useSignPermit.ts
@@ -0,0 +1,68 @@
+import { useCallback } from 'react';
+import { Address, Hex } from 'viem';
+import { useSignTypedData } from 'wagmi';
+import dayjs from 'dayjs';
+import useContractVersion from 'hooks/useContractVersion';
+import { splitSignature } from '@ethersproject/bytes';
+import { Market } from 'types/contracts';
+import { useWeb3 } from 'hooks/useWeb3';
+
+export default function useSignPermit() {
+ const { signTypedDataAsync } = useSignTypedData();
+ const { chain, walletAddress, opts } = useWeb3();
+ const contractVersion = useContractVersion();
+
+ return useCallback(
+ async ({
+ spender,
+ value,
+ duration = 3600, // TODO why 1 hour?
+ verifyingContract,
+ }: {
+ spender: Address;
+ value: bigint;
+ duration: number;
+ verifyingContract: Market;
+ }) => {
+ if (!verifyingContract || !walletAddress) return;
+ const nonce = await verifyingContract.read.nonces([walletAddress], opts);
+ const version = await contractVersion(verifyingContract.address);
+ const deadline = BigInt(dayjs().unix() + duration);
+ const signatureHex = await signTypedDataAsync({
+ primaryType: 'Permit',
+ domain: {
+ name: '',
+ version,
+ chainId: chain.id,
+ verifyingContract: verifyingContract.address,
+ },
+ types: {
+ Permit: [
+ { name: 'owner', type: 'address' },
+ { name: 'spender', type: 'address' },
+ { name: 'value', type: 'uint256' },
+ { name: 'nonce', type: 'uint256' },
+ { name: 'deadline', type: 'uint256' },
+ ],
+ },
+ message: {
+ owner: walletAddress,
+ spender,
+ value,
+ nonce,
+ deadline,
+ },
+ });
+ const { v, r, s } = splitSignature(signatureHex);
+
+ return {
+ value,
+ deadline,
+ v,
+ r: r as Hex,
+ s: s as Hex,
+ };
+ },
+ [chain.id, contractVersion, opts, signTypedDataAsync, walletAddress],
+ );
+}
diff --git a/i18n/es/translation.json b/i18n/es/translation.json
index 77c91c31a..0cad21b9e 100644
--- a/i18n/es/translation.json
+++ b/i18n/es/translation.json
@@ -26,17 +26,8 @@
"3M": "3M",
"Historical Variable Rates": "Tasas Variables Históricas",
"Show utilization": "Ver utilización",
- "Compare with other yield curves": "Comparar con otras curvas de rendimiento",
"Loading data...": "Cargando...",
- "ZOOM IN": "AMPLIAR",
- "ZOOM OUT": "REDUCIR",
- "Utilization Rate (Variable Rate Pool)": "Tasa de Utilización (Pool de Tasa Variable)",
"Utilization Rates (Fixed Rate Pools)": "Tasas de Utilización (Pools de Tasa Fija)",
- "Utilization": "Utilización",
- "DEPOSIT APR": "TNA DEPÓSITO",
- "BORROW APR": "TNA PRÉSTAMO",
- "Current Yield Curves": "Curvas de Rendimiento Actuales",
- "Current Utilization": "Utilización Actual",
"Late repayment will result in a penalty daily rate of {{penaltyRate}}": "El retraso en el pago resultará en una tasa de penalización diaria de {{penaltyRate}}",
"Completed": "Completado",
"{{daysLeft}} left": "{{daysLeft}} restantes",
@@ -622,5 +613,20 @@
"Select a network -> check \"Optimism\"": "Selecciona una red -> tocá \"Optimism\"",
"Transaction pending, in short you will receive the funds. Remember to have USDT set as payment currency. Do this by going to the \"Card\" tab -> \"Pay with\" and select Tether": "Transacción pendiente, en breve recibirás los fondos. Recordá tener USDT configurado como moneda de pago. Haz esto yendo a la pestaña \"Tarjeta\" -> \"Pagar con\" y selecciona Tether",
"The Total Utilization is above 90%, and the remaining liquidity is established as a Liquidity Reserve that can't be borrowed and is only available for withdrawals.": "La Utilización Total está por encima del 90%, y la liquidez restante se establece como una Reserva de Liquidez que no se puede pedir prestada y solo está disponible para retiros.",
- "The upgrade to the new Interest Rate Model (EXAIP-08) is scheduled for execution today at 8:30 PM UTC": "La actualización al nuevo Modelo de Tasa de Interés (EXAIP-08) está programada para ejecutarse hoy a las 8:30 PM UTC"
+ "The upgrade to the new Interest Rate Model (EXAIP-08) is scheduled for execution today at 8:30 PM UTC": "La actualización al nuevo Modelo de Tasa de Interés (EXAIP-08) está programada para ejecutarse hoy a las 8:30 PM UTC",
+ "Term Structure of Interest Rates": "Estructura de Plazos de Tasas de Interés",
+ "Rate": "Tasa",
+ "Min": "Mín",
+ "Variable APR, Utilization and Global Utilization": "TNA Variable, Utilización y Utilización Global",
+ "New!": "Nuevo!",
+ "Installments": "Cuotas",
+ "Each installment is due every {{interval}} days.": "Cada cuota vence cada {{interval}} días.",
+ "Payment schedule": "Calendario de pagos",
+ "First Payment Due Date": "Fecha de Vencimiento del Primer Pago",
+ "at {{hour}}": "a las {{hour}}",
+ "Dues in 1 day.": "Vence en 1 día.",
+ "Dues in {{daysToMaturity}} days.": "Vence en {{daysToMaturity}} días.",
+ "For optimal benefits, consider selecting a pool with a longer remaining duration.": "Para obtener beneficios óptimos, considera seleccionar un pool con una duración restante más larga.",
+ "Total": "Total",
+ " In {{installments}} installments": "En {{installments}} cuotas"
}
diff --git a/utils/getHourUTC2Local.ts b/utils/getHourUTC2Local.ts
index 8eebec2af..af61069aa 100644
--- a/utils/getHourUTC2Local.ts
+++ b/utils/getHourUTC2Local.ts
@@ -1,6 +1,6 @@
import dayjs from 'dayjs';
-export default function getHourUTC2Local(utcDateTime = '1970-01-01T00:00:00Z'): string {
+export default function getHourUTC2Local(utcDateTime = '1970-01-01T00:00:00Z', template = 'HH:mm'): string {
const utcDate = dayjs(utcDateTime);
- return utcDate.format('HH:mm');
+ return utcDate.format(template);
}
diff --git a/utils/utils.tsx b/utils/utils.tsx
index af73d1c39..d44bdc6d8 100644
--- a/utils/utils.tsx
+++ b/utils/utils.tsx
@@ -1,8 +1,10 @@
import { WAD } from './queryRates';
const YEAR_IN_SECONDS = 60n * 60n * 24n * 365n;
-export const WEEK = 60n * 60n * 24n * 7n;
-export const INTERVAL = 4n * WEEK;
+export const DAY = 60n * 60n * 24n;
+export const WEEK = DAY * 7n;
+export const MATURITY_DAYS = 28n;
+export const INTERVAL = MATURITY_DAYS * DAY;
import { Hex } from 'viem';
diff --git a/wagmi.config.ts b/wagmi.config.ts
index ea89d4107..1372433b3 100644
--- a/wagmi.config.ts
+++ b/wagmi.config.ts
@@ -20,10 +20,10 @@ import InterestRateModel from '@exactly/protocol/deployments/op-sepolia/Interest
import ExtraFinanceLendingABI from './abi/extraFinanceLending.json' assert { type: 'json' };
import DelegateRegistryABI from './abi/DelegateRegistry.json' assert { type: 'json' };
import GasPriceOracle from './abi/GasPriceOracle.json' assert { type: 'json' };
-
import EscrowedEXA from '@exactly/protocol/deployments/optimism/esEXA.json' assert { type: 'json' };
import SablierV2LockupLinear from '@exactly/protocol/deployments/optimism/SablierV2LockupLinear.json' assert { type: 'json' };
import SablierV2NFTDescriptor from '@exactly/protocol/deployments/optimism/SablierV2NFTDescriptor.json' assert { type: 'json' };
+import InstallmentsRouter from '@exactly/protocol/deployments/op-sepolia/InstallmentsRouter.json' assert { type: 'json' };
import { Abi } from 'viem';
@@ -52,6 +52,7 @@ export default defineConfig({
{ name: 'DelegateRegistry', abi: DelegateRegistryABI as Abi },
{ name: 'EscrowedEXA', abi: EscrowedEXA.abi as Abi },
{ name: 'L1GasPriceOracle', abi: GasPriceOracle as Abi },
+ { name: 'InstallmentsRouter', abi: InstallmentsRouter.abi as Abi },
],
plugins: [
react({