Skip to content

Commit

Permalink
feat: Add a trustline step to the borrow flow.
Browse files Browse the repository at this point in the history
  • Loading branch information
kovipu committed Oct 27, 2024
1 parent c8d2bfb commit e2d3e70
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 105 deletions.
58 changes: 58 additions & 0 deletions src/pages/_borrow/BorrowModal/BorrowModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { CurrencyBinding } from 'src/currency-bindings';
import { useWallet } from 'src/stellar-wallet';
import { BorrowStep } from './BorrowStep';
import { TrustLineStep } from './TrustlineStep';

export interface BorrowModalProps {
modalId: string;
onClose: () => void;
currency: CurrencyBinding;
totalSupplied: bigint;
}

export const BorrowModal = ({ modalId, onClose, currency, totalSupplied }: BorrowModalProps) => {
const { name, ticker } = currency;
const { wallet, walletBalances, signTransaction, refetchBalances, prices } = useWallet();

const closeModal = () => {
refetchBalances();
onClose();
};

// Modal is impossible to open before the wallet is loaded.
if (!wallet || !walletBalances || !prices) return null;

const isTrustline = walletBalances[ticker].trustLine;

return (
<dialog id={modalId} className="modal">
<div className="modal-box w-full max-w-full md:w-[700px] p-10">
<h3 className="font-bold text-xl mb-4">Borrow {name}</h3>
{!isTrustline ? (
<TrustLineStep
onClose={closeModal}
currency={currency}
wallet={wallet}
signTransaction={signTransaction}
refetchBalances={refetchBalances}
/>
) : (
<BorrowStep
onClose={closeModal}
currency={currency}
totalSupplied={totalSupplied}
wallet={wallet}
walletBalances={walletBalances}
prices={prices}
/>
)}
</div>
{/* Invisible backdrop that closes the modal on click */}
<form method="dialog" className="modal-backdrop">
<button onClick={closeModal} type="button">
close
</button>
</form>
</dialog>
);
};
Original file line number Diff line number Diff line change
@@ -1,32 +1,31 @@
import { Warning } from '@components/Alert';
import { Button } from '@components/Button';
import { CryptoAmountSelector } from '@components/CryptoAmountSelector';
import { Loading } from '@components/Loading';
import { contractClient as loanManagerClient } from '@contracts/loan_manager';
import { getIntegerPart, to7decimals } from '@lib/converters';
import { SCALAR_7, fromCents, toCents } from '@lib/formatting';
import { createAddTrustlineTransaction, sendTransaction } from '@lib/horizon';
import type { SupportedCurrency } from 'currencies';
import { type ChangeEvent, useState } from 'react';
import { CURRENCY_BINDINGS, CURRENCY_BINDINGS_ARR, type CurrencyBinding } from 'src/currency-bindings';
import { useWallet } from 'src/stellar-wallet';
import { type BalanceRecord, type PriceRecord, type Wallet, useWallet } from 'src/stellar-wallet';

const HEALTH_FACTOR_AUTO_THRESHOLD = 1.65;
const HEALTH_FACTOR_MIN_THRESHOLD = 1.2;
const HEALTH_FACTOR_GOOD_THRESHOLD = 1.6;
const HEALTH_FACTOR_EXCELLENT_THRESHOLD = 2.0;

const HEALTH_FACTOR_AUTO_THRESHOLD = 1.65;

export interface BorrowModalProps {
modalId: string;
export interface BorrowStepProps {
onClose: () => void;
currency: CurrencyBinding;
totalSupplied: bigint;
wallet: Wallet;
walletBalances: BalanceRecord;
prices: PriceRecord;
}

export const BorrowModal = ({ modalId, onClose, currency, totalSupplied }: BorrowModalProps) => {
export const BorrowStep = ({ onClose, currency, totalSupplied, wallet, walletBalances, prices }: BorrowStepProps) => {
const { name, ticker, contractId: loanCurrencyId } = currency;
const { wallet, walletBalances, signTransaction, refetchBalances, prices } = useWallet();
const { signTransaction } = useWallet();

const [isBorrowing, setIsBorrowing] = useState(false);
const [loanAmount, setLoanAmount] = useState<string>('0');
Expand All @@ -37,14 +36,11 @@ export const BorrowModal = ({ modalId, onClose, currency, totalSupplied }: Borro
({ ticker }) => ticker,
);

// The modal is impossible to open without a wallet or before the balances load.
if (!wallet || !walletBalances) return null;

const loanBalance = walletBalances[ticker];
const collateralBalance = walletBalances[collateralTicker];

const loanPrice = prices?.[ticker];
const collateralPrice = prices?.[collateralTicker];
const loanPrice = prices[ticker];
const collateralPrice = prices[collateralTicker];

const loanAmountCents = loanPrice ? toCents(loanPrice, BigInt(loanAmount) * SCALAR_7) : undefined;
const collateralAmountCents = collateralPrice
Expand All @@ -54,22 +50,15 @@ export const BorrowModal = ({ modalId, onClose, currency, totalSupplied }: Borro
const healthFactor =
loanAmountCents && loanAmountCents > 0n ? Number(collateralAmountCents) / Number(loanAmountCents) : 0;

// The modal is impossible to open without collateral balance.
if (!collateralBalance.trustLine) return null;
// TODO: get this from the contract.
const interestRate = '7.5%';

const closeModal = () => {
refetchBalances();
const handleCancel = () => {
setLoanAmount('0');
setCollateralAmount('0');
onClose();
};

const handleAddTrustlineClick = async () => {
const tx = await createAddTrustlineTransaction(wallet.address, currency);
const signedTx = await signTransaction(tx.toXDR());
sendTransaction(signedTx);
};

const handleBorrowClick = async () => {
if (!wallet) {
alert('Please connect your wallet first!');
Expand All @@ -96,7 +85,7 @@ export const BorrowModal = ({ modalId, onClose, currency, totalSupplied }: Borro
});
await tx.signAndSend({ signTransaction });
alert('Loan created succesfully!');
closeModal();
onClose();
} catch (err) {
console.error('Error borrowing', err);
alert('Error borrowing');
Expand Down Expand Up @@ -137,90 +126,72 @@ export const BorrowModal = ({ modalId, onClose, currency, totalSupplied }: Borro

const maxLoan = (totalSupplied / 10_000_000n).toString();

const maxCollateral = getIntegerPart(collateralBalance.balanceLine.balance);
const maxCollateral = getIntegerPart(collateralBalance.trustLine ? collateralBalance.balanceLine.balance : '0');

const handleSelectMaxLoan = () => setLoanAmount(maxLoan);

const handleSelectMaxCollateral = () => setCollateralAmount(maxCollateral);

// TODO: get this from the contract.
const interestRate = '7.5%';

return (
<dialog id={modalId} className="modal">
<div className="modal-box w-full max-w-full md:w-[700px] p-10">
<h3 className="font-bold text-xl mb-4">Borrow {name}</h3>
{!isTrustline ? (
<Warning>
<span>Create a trustline for {ticker} in your wallet first!</span>
<Button onClick={handleAddTrustlineClick}>Add trustline</Button>
</Warning>
) : null}
<p className="my-4">
Borrow {name} using another asset as a collateral. The value of the collateral must exceed the value of the
borrowed asset. You will receive the collateral back to your wallet after repaying the loan in full.
</p>
<p className="my-4">
The higher the value of the collateral is to the value of the borrowed asset, the safer this loan is. This is
visualised by the health factor.
</p>
<p className="my-4">
The loan will be available for liquidation if the value of the borrowed asset raises to the value of the
collateral, causing you to lose some of your collateral.
</p>
<p className="my-4">The interest rate changes as the amount of assets borrowed from the pools changes.</p>
<p className="my-4">The annual interest rate is currently {interestRate}.</p>

<p className="font-bold mb-2 mt-6">Amount to borrow</p>
<CryptoAmountSelector
max={maxLoan}
value={loanAmount}
valueCents={loanAmountCents}
ticker={ticker}
onChange={handleLoanAmountChange}
onSelectMaximum={handleSelectMaxLoan}
/>

<p className="font-bold mb-2 mt-4">Amount of collateral</p>
<CryptoAmountSelector
max={maxCollateral}
value={collateralAmount}
valueCents={collateralAmountCents}
ticker={collateralTicker}
onChange={handleCollateralAmountChange}
onSelectMaximum={handleSelectMaxCollateral}
tickerChangeOptions={{
onSelectTicker: handleCollateralTickerChange,
options: collateralOptions,
}}
/>

<p className="font-bold mt-6 mb-2">Health Factor</p>
<HealthFactor value={healthFactor} />

<div className="flex flex-row justify-end mt-8">
<Button onClick={closeModal} variant="ghost" className="mr-4">
Cancel
<>
<p className="my-4">
Borrow {name} using another asset as a collateral. The value of the collateral must exceed the value of the
borrowed asset. You will receive the collateral back to your wallet after repaying the loan in full.
</p>
<p className="my-4">
The higher the value of the collateral is to the value of the borrowed asset, the safer this loan is. This is
visualised by the health factor.
</p>
<p className="my-4">
The loan will be available for liquidation if the value of the borrowed asset raises to the value of the
collateral, causing you to lose some of your collateral.
</p>
<p className="my-4">The interest rate changes as the amount of assets borrowed from the pools changes.</p>
<p className="my-4">The annual interest rate is currently {interestRate}.</p>

<p className="font-bold mb-2 mt-6">Amount to borrow</p>
<CryptoAmountSelector
max={maxLoan}
value={loanAmount}
valueCents={loanAmountCents}
ticker={ticker}
onChange={handleLoanAmountChange}
onSelectMaximum={handleSelectMaxLoan}
/>

<p className="font-bold mb-2 mt-4">Amount of collateral</p>
<CryptoAmountSelector
max={maxCollateral}
value={collateralAmount}
valueCents={collateralAmountCents}
ticker={collateralTicker}
onChange={handleCollateralAmountChange}
onSelectMaximum={handleSelectMaxCollateral}
tickerChangeOptions={{
onSelectTicker: handleCollateralTickerChange,
options: collateralOptions,
}}
/>

<p className="font-bold mt-6 mb-2">Health Factor</p>
<HealthFactor value={healthFactor} />

<div className="flex flex-row justify-end mt-8">
<Button onClick={handleCancel} variant="ghost" className="mr-4">
Cancel
</Button>
{!isBorrowing ? (
<Button disabled={isBorrowDisabled} onClick={handleBorrowClick}>
Borrow
</Button>
) : (
<Button disabled>
<Loading />
Borrowing
</Button>
{!isBorrowing ? (
<Button disabled={isBorrowDisabled} onClick={handleBorrowClick}>
Borrow
</Button>
) : (
<Button disabled>
<Loading />
Borrowing
</Button>
)}
</div>
)}
</div>
{/* Invisible backdrop that closes the modal on click */}
<form method="dialog" className="modal-backdrop">
<button onClick={closeModal} type="button">
close
</button>
</form>
</dialog>
</>
);
};

Expand Down
56 changes: 56 additions & 0 deletions src/pages/_borrow/BorrowModal/TrustlineStep.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Button } from '@components/Button';
import { Loading } from '@components/Loading';
import { createAddTrustlineTransaction, sendTransaction } from '@lib/horizon';
import { useState } from 'react';
import type { CurrencyBinding } from 'src/currency-bindings';
import type { SignTransaction, Wallet } from 'src/stellar-wallet';

export interface TrustLineStepProps {
onClose: () => void;
currency: CurrencyBinding;
wallet: Wallet;
signTransaction: SignTransaction;
refetchBalances: () => void;
}

export const TrustLineStep = ({ onClose, currency, wallet, signTransaction, refetchBalances }: TrustLineStepProps) => {
const { name, ticker } = currency;

const [isCreating, setIsCreating] = useState(false);

const handleAddTrustlineClick = async () => {
try {
setIsCreating(true);
const tx = await createAddTrustlineTransaction(wallet.address, currency);
const signedTx = await signTransaction(tx.toXDR());
await sendTransaction(signedTx);
alert(`Succesfully created a trustline for ${ticker}!`);
} catch (err) {
alert('Error creating a trustline :(');
console.error('Error creating trustline:', err);
}
refetchBalances();
setIsCreating(false);
};

return (
<>
<p>
You don't have a trustline for {name} in your wallet. Laina cannot transfer you the tokens without a trustline.
</p>
<div className="flex flex-row justify-end mt-8">
<Button onClick={onClose} variant="ghost" className="mr-4">
Cancel
</Button>
{!isCreating ? (
<Button onClick={handleAddTrustlineClick}>Create Trustline</Button>
) : (
<Button disabled>
<Loading />
Creating
</Button>
)}
</div>
</>
);
};
2 changes: 1 addition & 1 deletion src/pages/_borrow/BorrowableAsset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { contractClient as loanManagerClient } from '@contracts/loan_manager';
import { isBalanceZero } from '@lib/converters';
import type { CurrencyBinding } from 'src/currency-bindings';
import { useWallet } from 'src/stellar-wallet';
import { BorrowModal } from './BorrowModal';
import { BorrowModal } from './BorrowModal/BorrowModal';

interface BorrowableAssetCardProps {
currency: CurrencyBinding;
Expand Down
2 changes: 1 addition & 1 deletion src/pages/_lend/DepositModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const DepositModal = ({ modalId, onClose, currency }: DepositModalProps)

const amountCents = price ? toCents(price, BigInt(amount) * SCALAR_7) : undefined;

if (!balance || !balance.trustline) return null;
if (!balance || !balance.trustLine) return null;

const max = getIntegerPart(balance.balanceLine.balance);

Expand Down
2 changes: 1 addition & 1 deletion src/pages/_lend/LendableAsset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const LendableAsset = ({ currency }: LendableAssetProps) => {

const balance: Balance | undefined = walletBalances?.[ticker];

const isPoor = !balance?.trustline || isBalanceZero(balance.balanceLine.balance);
const isPoor = !balance?.trustLine || isBalanceZero(balance.balanceLine.balance);

const fetchAvailableContractBalance = useCallback(async () => {
if (!contractClient) return;
Expand Down
2 changes: 1 addition & 1 deletion src/stellar-wallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export type WalletContext = {
signTransaction: SignTransaction;
};

type SignTransaction = (
export type SignTransaction = (
tx: XDR_BASE64,
opts?: {
network?: string;
Expand Down

0 comments on commit e2d3e70

Please sign in to comment.