Skip to content

Commit

Permalink
Merge pull request #3 from wakeuplabs-io/SAF-10_borrow-components
Browse files Browse the repository at this point in the history
SAF-10 Implement borrow components
  • Loading branch information
matzapata authored Aug 12, 2024
2 parents babaf69 + fe53d9f commit 5fa3b69
Show file tree
Hide file tree
Showing 6 changed files with 296 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { FC } from 'react';
import React, { FC, useCallback, useState } from 'react';

import { t } from 'i18next';

import { Button, ButtonStyle } from '@sovryn/ui';

import { translations } from '../../../../../../../locales/i18n';
import { BorrowPoolDetails } from '../../BorrowAssetsList.types';
import { BorrowModalContainer } from '../BorrowModal/BorrowModalContainer';

const pageTranslations = translations.aavePage;

Expand All @@ -14,15 +15,35 @@ type BorrowAssetActionProps = {
};

export const BorrowAssetAction: FC<BorrowAssetActionProps> = () => {
const [isBorrowModalOpen, setIsBorrowModalOpen] = useState<boolean>(false);

const handleBorrowClick = useCallback(() => {
setIsBorrowModalOpen(true);
}, []);

const handleBorrowClose = useCallback(() => {
setIsBorrowModalOpen(false);
}, []);

return (
<div className="flex items-center justify-center lg:justify-end space-x-2">
{/* TODO: these should be modal triggers */}
<Button className="flex-grow" text={t(pageTranslations.common.borrow)} />
<Button
className="flex-grow"
text={t(pageTranslations.common.borrow)}
onClick={handleBorrowClick}
/>

{/* TODO: details link */}
<Button
className="flex-grow"
text={t(pageTranslations.common.details)}
style={ButtonStyle.secondary}
/>

<BorrowModalContainer
handleCloseModal={handleBorrowClose}
isOpen={isBorrowModalOpen}
/>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import React, { FC, useCallback, useMemo, useState } from 'react';

import { t } from 'i18next';

import {
AmountInput,
Button,
Checkbox,
ErrorBadge,
ErrorLevel,
HealthBar,
Link,
Paragraph,
ParagraphSize,
Select,
SimpleTable,
SimpleTableRow,
} from '@sovryn/ui';
import { Decimal } from '@sovryn/utils';

import { AmountRenderer } from '../../../../../../2_molecules/AmountRenderer/AmountRenderer';
import { AssetRenderer } from '../../../../../../2_molecules/AssetRenderer/AssetRenderer';
import { useDecimalAmountInput } from '../../../../../../../hooks/useDecimalAmountInput';
import { translations } from '../../../../../../../locales/i18n';
import { getCollateralRatioThresholds } from './BorrowForm.utils';

const pageTranslations = translations.aavePage;

type BorrowFormProps = {
onSuccess: () => unknown;
};

export const BorrowForm: FC<BorrowFormProps> = () => {
const assetPrice = 3258.47; // TODO: this is mocked data. Replace with proper hook
const totalBorrow = Decimal.from(10); // TODO: this is mocked data. Replace with proper hook
const collateralToLoanRate = Decimal.from(10); // TODO: this is mocked data. Replace with proper hook
const collateralSize = Decimal.from(10); // TODO: this is mockd data. Replace with proper hook
const borrowableAssets = useMemo(() => ['BTC', 'SOV'], []); // TODO: this is mocked data. Replace with proper hook
const [maximumBorrowAmount] = useState<Decimal>(Decimal.from(10)); // TODO: this is mocked data. Replace with proper hook
const [borrowAsset, setBorrowAsset] = useState<string>(borrowableAssets[0]);
const [borrowAmount, setBorrowAmount, borrowSize] = useDecimalAmountInput('');
const [acknowledge, setAcknowledge] = useState<boolean>(false);

const onBorrowAssetChange = useCallback(v => {
setBorrowAsset(v);
}, []);

const borrowableAssetsOptions = useMemo(
() =>
borrowableAssets.map(token => ({
value: token,
label: (
<AssetRenderer
showAssetLogo
asset={token}
assetClassName="font-medium"
/>
),
})),
[borrowableAssets],
);

const isValidBorrowAmount = useMemo(
() => (borrowSize.gt(0) ? borrowSize.lte(maximumBorrowAmount) : true),
[borrowSize, maximumBorrowAmount],
);

const remainingSupply = useMemo(
() => maximumBorrowAmount.sub(borrowSize),
[borrowSize, maximumBorrowAmount],
);

const collateralRatioThresholds = useMemo(
() => getCollateralRatioThresholds(),
[],
);

const collateralRatio = useMemo(() => {
if ([collateralSize, totalBorrow, borrowSize].some(v => v.isZero())) {
return Decimal.ZERO;
}

return collateralSize.mul(collateralToLoanRate).div(totalBorrow).mul(100);
}, [collateralSize, totalBorrow, borrowSize, collateralToLoanRate]);

const submitButtonDisabled = useMemo(
() => !isValidBorrowAmount || borrowSize.lte(0) || !acknowledge,
[isValidBorrowAmount, borrowSize, acknowledge],
);

return (
<form className="flex flex-col gap-6">
<div className="space-y-3">
<div className="flex justify-between items-end">
<Paragraph size={ParagraphSize.base} className="font-medium">
{t(translations.aavePage.common.borrow)}
</Paragraph>

<span className="text-xs underline">
(Max{' '}
<AmountRenderer
value={maximumBorrowAmount}
suffix={borrowAsset}
prefix="~"
/>
)
</span>
</div>

<div>
<div className="flex space-x-3">
<div className="text-right flex-grow space-y-1">
<AmountInput
label={t(translations.common.amount)}
value={borrowAmount}
onChangeText={setBorrowAmount}
placeholder="0"
invalid={!isValidBorrowAmount}
/>
<div className=" pr-4">
<AmountRenderer
className="text-gray-40"
value={0} // TODO: usd equivalent
prefix="$"
/>
</div>
</div>

<Select
value={borrowAsset}
onChange={onBorrowAssetChange}
options={borrowableAssetsOptions}
labelRenderer={({ value }) => (
<AssetRenderer
dataAttribute="borrow-asset-asset"
showAssetLogo
asset={value}
/>
)}
className="min-w-[6.7rem]"
menuClassName="max-h-[10rem] sm:max-h-[20rem]"
dataAttribute="borrow-asset-select"
/>
</div>
</div>
<div>
{!isValidBorrowAmount && (
<ErrorBadge
level={ErrorLevel.Critical}
message={t(pageTranslations.borrowForm.invalidAmountError)}
dataAttribute="borrow-amount-error"
/>
)}
</div>
</div>

<SimpleTable>
<SimpleTableRow
label={t(translations.aavePage.borrowForm.borrowApr)}
value={
<AmountRenderer
value={remainingSupply.toNumber()}
suffix={borrowAsset}
/>
}
/>
</SimpleTable>

<div>
<div className="flex flex-row justify-between items-center mt-6 mb-3">
<div className="flex flex-row justify-start items-center gap-2">
<span>{t(translations.aavePage.borrowForm.collateralRatio)}</span>
</div>
<div className="">
<AmountRenderer value={collateralRatio.toString()} suffix="%" />
</div>
</div>

<HealthBar
start={collateralRatioThresholds.START}
middleStart={collateralRatioThresholds.MIDDLE_START}
middleEnd={collateralRatioThresholds.MIDDLE_END}
end={collateralRatioThresholds.END}
value={collateralRatio.toNumber()}
/>
</div>

<SimpleTable>
<SimpleTableRow
label={t(translations.aavePage.borrowForm.liquidationPrice)}
value={t(translations.aavePage.common['n/a'])}
/>
<SimpleTableRow
label={t(translations.aavePage.borrowForm.tokenPrice, {
token: borrowAsset,
})}
value={<AmountRenderer value={assetPrice} prefix={'$'} />}
/>
</SimpleTable>

<Checkbox
checked={acknowledge}
onChangeValue={setAcknowledge}
label={
<span>
{t(translations.aavePage.borrowForm.acknowledge)}{' '}
<Link text="Learn more" href="#learn-more" />{' '}
{/* TODO: Add proper learn more href */}
</span>
}
/>

<Button
disabled={submitButtonDisabled}
text={t(translations.common.buttons.confirm)}
/>
</form>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { MINIMUM_COLLATERAL_RATIO_LENDING_POOLS } from '../../../../../../../constants/lending';

export const getCollateralRatioThresholds = () => {
// TODO: recheck this and adjust based on aave
const minimumCollateralRatio =
MINIMUM_COLLATERAL_RATIO_LENDING_POOLS.mul(100);

return {
START: minimumCollateralRatio.mul(0.9).toNumber(),
MIDDLE_START: minimumCollateralRatio.toNumber() - 0.1,
MIDDLE_END: minimumCollateralRatio.mul(1.2).toNumber(),
END: minimumCollateralRatio.mul(1.6).toNumber(),
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { FC } from 'react';

import { t } from 'i18next';

import { Dialog, DialogBody, DialogHeader } from '@sovryn/ui';

import { translations } from '../../../../../../../locales/i18n';
import { BorrowForm } from './BorrowForm';

type BorrowModalContainerProps = {
isOpen: boolean;
handleCloseModal: () => unknown;
};

export const BorrowModalContainer: FC<BorrowModalContainerProps> = ({
isOpen,
handleCloseModal,
}) => {
return (
<Dialog disableFocusTrap isOpen={isOpen}>
<DialogHeader
title={t(translations.aavePage.common.borrow)}
onClose={handleCloseModal}
/>
<DialogBody className="flex flex-col gap-6">
<BorrowForm onSuccess={handleCloseModal} />
</DialogBody>
</Dialog>
);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FC, useCallback, useState } from 'react';
import React, { FC, useState } from 'react';

import { t } from 'i18next';

Expand Down
8 changes: 8 additions & 0 deletions apps/frontend/src/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,14 @@
"walletBalance": "Wallet balance",
"canBeCollateral": "Can be collateral"
},
"borrowForm": {
"borrowApr": "Borrow APR",
"collateralRatio": "Collateral ratio",
"liquidationPrice":"Liquidation price",
"acknowledge": "I understand that my collateral may be liquidated or used to pay rollover fees if applicable.",
"invalidAmountError": "Invalid amount",
"tokenPrice": "{{token}} price"
},
"withdrawForm": {
"remainingSupply": "Remaining supply",
"invalidAmountError": "Invalid amount"
Expand Down

0 comments on commit 5fa3b69

Please sign in to comment.