Skip to content

Commit

Permalink
Merge pull request #37 from Laina-Protocol/feat/borrow-modal
Browse files Browse the repository at this point in the history
Feat: Borrow modal, refetch balances on deposit/borrow
  • Loading branch information
kovipu authored Sep 17, 2024
2 parents 21d786f + 6b8348e commit 12c5507
Show file tree
Hide file tree
Showing 9 changed files with 247 additions and 66 deletions.
3 changes: 2 additions & 1 deletion initialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ function importContract(contract) {
const importContent =
`import * as Client from '${filenameNoExt}';\n` +
`import { rpcUrl } from './util';\n\n` +
`export default new Client.Client({\n` +
`export const contractId = Client.networks.${process.env.SOROBAN_NETWORK}.contractId;\n\n` +
`export const contractClient = new Client.Client({\n` +
` ...Client.networks.${process.env.SOROBAN_NETWORK},\n` +
` rpcUrl,\n` +
`${process.env.SOROBAN_NETWORK === 'local' || 'standalone' ? ` allowHttp: true,\n` : null}` +
Expand Down
7 changes: 7 additions & 0 deletions src/components/Loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface LoadingProps {
size?: 'xs' | 'sm' | 'md' | 'lg';
}

export const Loading = ({ size = 'md' }: LoadingProps) => (
<span className={`loading loading-${size} loading-spinner`} />
);
38 changes: 21 additions & 17 deletions src/currencies.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import XLMPoolContract from '@contracts/loan_pool';
import USDCPoolContract from '@contracts/usdc_pool';
import * as XLMPoolContract from '@contracts/loan_pool';
import * as USDCPoolContract from '@contracts/usdc_pool';
import StellarIcon from '@images/Stellar_Symbol.png';
import USDCIcon from '@images/usdc.svg';
import type { SupportedCurrency } from './stellar-wallet';
Expand All @@ -8,20 +8,24 @@ export type Currency = {
name: string;
symbol: SupportedCurrency;
icon: string;
loanPoolContract: typeof XLMPoolContract;
loanPoolContract: typeof XLMPoolContract.contractClient;
contractId: string;
};

export const CURRENCIES: Currency[] = [
{
name: 'Stellar Lumen',
symbol: 'XLM',
icon: StellarIcon.src,
loanPoolContract: XLMPoolContract,
},
{
name: 'USD Coin',
symbol: 'USDC',
icon: USDCIcon.src,
loanPoolContract: USDCPoolContract,
},
] as const;
export const CURRENCY_XLM: Currency = {
name: 'Stellar Lumen',
symbol: 'XLM',
icon: StellarIcon.src,
loanPoolContract: XLMPoolContract.contractClient,
contractId: XLMPoolContract.contractId,
} as const;

export const CURRENCY_USDC: Currency = {
name: 'USD Coin',
symbol: 'USDC',
icon: USDCIcon.src,
loanPoolContract: USDCPoolContract.contractClient,
contractId: USDCPoolContract.contractId,
} as const;

export const CURRENCIES: Currency[] = [CURRENCY_XLM, CURRENCY_USDC] as const;
2 changes: 2 additions & 0 deletions src/lib/converters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Stellar operates with a precision of 7 decimal places for assets like XLM & USDC.
export const to7decimals = (amount: string): bigint => BigInt(amount) * BigInt(10_000_000);
147 changes: 147 additions & 0 deletions src/pages/_borrow/BorrowModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { Button } from '@components/Button';
import { Loading } from '@components/Loading';
import { contractClient as loanManagerClient } from '@contracts/loan_manager';
import { type ChangeEvent, useState } from 'react';
import type { Currency } from 'src/currencies';
import { to7decimals } from 'src/lib/converters';
import { useWallet } from 'src/stellar-wallet';

export interface BorrowModalProps {
modalId: string;
onClose: () => void;
currency: Currency;
collateral: Currency;
}

// TODO: calculate this from the amount of funds in the pool.
const MAX_LOAN = 10000;

export const BorrowModal = ({ modalId, onClose, currency, collateral }: BorrowModalProps) => {
const { name, symbol, contractId: loanCurrencyId } = currency;
const { wallet, balances, signTransaction, refetchBalances } = useWallet();

const [isBorrowing, setIsBorrowing] = useState(false);
const [loanAmount, setLoanAmount] = useState('0');
const [collateralAmount, setCollateralAmount] = useState('0');

const collateralBalance = balances[collateral.symbol];

// The modal is impossible to open without collateral balance.
if (!collateralBalance) return null;

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

const handleBorrowClick = async () => {
if (!wallet) {
alert('Please connect your wallet first!');
return;
}

setIsBorrowing(true);

try {
loanManagerClient.options.publicKey = wallet.address;

const tx = await loanManagerClient.initialize({
user: wallet.address,
borrowed: to7decimals(loanAmount),
borrowed_from: loanCurrencyId,
collateral: to7decimals(collateralAmount),
collateral_from: collateral.contractId,
});
await tx.signAndSend({ signTransaction });
alert('Loan created succesfully!');
closeModal();
} catch (err) {
console.error('Error borrowing', err);
alert('Error borrowing');
}

setIsBorrowing(false);
};

const handleLoanAmountChange = (ev: ChangeEvent<HTMLInputElement>) => {
setLoanAmount(ev.target.value);
};

const handleCollateralAmountChange = (ev: ChangeEvent<HTMLInputElement>) => {
setCollateralAmount(ev.target.value);
};

const isBorrowDisabled = loanAmount === '0' || collateralAmount === '0';

return (
<dialog id={modalId} className="modal">
<div className="modal-box">
<h3 className="font-bold text-lg mb-8">Borrow {name}</h3>

<p className="text-lg mb-2">Amount to borrow</p>
<input
type="range"
min={0}
max={MAX_LOAN}
value={loanAmount}
className="range"
onChange={handleLoanAmountChange}
/>
<div className="flex w-full justify-between px-2 text-xs">
<span>|</span>
<span>|</span>
<span>|</span>
<span>|</span>
<span>|</span>
</div>
<p>
{loanAmount} {symbol} out of {MAX_LOAN} {symbol}
</p>

<p className="text-lg mb-2 mt-4">Amount of collateral</p>
<input
type="range"
min={0}
max={collateralBalance.balance}
value={collateralAmount}
className="range"
onChange={handleCollateralAmountChange}
/>
<div className="flex w-full justify-between px-2 text-xs">
<span>|</span>
<span>|</span>
<span>|</span>
<span>|</span>
<span>|</span>
</div>
<p>
{collateralAmount} {collateral.symbol} out of {collateralBalance.balance} {collateral.symbol}
</p>

<div className="flex flex-row justify-end mt-8">
<Button onClick={closeModal} className="btn-ghost mr-4">
Cancel
</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>
);
};
55 changes: 28 additions & 27 deletions src/pages/_borrow/BorrowableAssetCard.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { Button } from '@components/Button';
import { Card } from '@components/Card';
import loanManagerContract from '@contracts/loan_manager';
import type { Currency } from 'src/currencies';
import { CURRENCY_USDC, CURRENCY_XLM, type Currency } from 'src/currencies';
import { useWallet } from 'src/stellar-wallet';

// Temporary hack to use XLM pool for all loans and collaterals.
const XLM_LOAN_POOL_ID = 'CCRWVENKGBUX7EP273GOUMY542VWRFEEIHUICTTDDG5ESLSDNENCH6AT';
import { BorrowModal } from './BorrowModal';

interface BorrowableAssetCardProps {
currency: Currency;
Expand All @@ -14,30 +11,25 @@ interface BorrowableAssetCardProps {
export const BorrowableAssetCard = ({ currency }: BorrowableAssetCardProps) => {
const { icon, name, symbol } = currency;

const { wallet, signTransaction } = useWallet();
const modalId = `borrow-modal-${symbol}`;

const { wallet, balances } = useWallet();

// Collateral is the other supported currency for now.
const collateral = symbol === 'XLM' ? CURRENCY_USDC : CURRENCY_XLM;

const handleBorrowClick = async () => {
if (!wallet) {
alert('Please connect your wallet first!');
return;
}
const collateralBalance = balances[collateral.symbol];

try {
loanManagerContract.options.publicKey = wallet.address;
const borrowDisabled = !wallet || !collateralBalance;

const openModal = () => {
const modalEl = document.getElementById(modalId) as HTMLDialogElement;
modalEl.showModal();
};

const tx = await loanManagerContract.initialize({
user: wallet.address,
borrowed: BigInt(10),
borrowed_from: XLM_LOAN_POOL_ID,
collateral: BigInt(1000),
collateral_from: XLM_LOAN_POOL_ID,
});
await tx.signAndSend({ signTransaction });
alert('Loan created succesfully!');
} catch (err) {
console.error('Error borrowing', err);
alert('Error borrowing');
}
const closeModal = () => {
const modalEl = document.getElementById(modalId) as HTMLDialogElement;
modalEl.close();
};

return (
Expand All @@ -60,7 +52,16 @@ export const BorrowableAssetCard = ({ currency }: BorrowableAssetCardProps) => {
<p className="text-xl font-semibold leading-6">1.61%</p>
</div>

{wallet && <Button onClick={handleBorrowClick}>Borrow</Button>}
{borrowDisabled ? (
<div className="tooltip" data-tip={!wallet ? 'Connect a wallet first' : 'Not enough funds for collateral'}>
<Button disabled={true} onClick={() => {}}>
Borrow
</Button>
</div>
) : (
<Button onClick={openModal}>Borrow</Button>
)}
<BorrowModal modalId={modalId} onClose={closeModal} currency={currency} collateral={collateral} />
</Card>
);
};
28 changes: 18 additions & 10 deletions src/pages/_lend/DepositModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Button } from '@components/Button';
import { Loading } from '@components/Loading';
import { type ChangeEvent, useState } from 'react';
import type { Currency } from 'src/currencies';
import { to7decimals } from 'src/lib/converters';
import { useWallet } from 'src/stellar-wallet';

export interface DepositModalProps {
Expand All @@ -12,14 +14,20 @@ export interface DepositModalProps {
export const DepositModal = ({ modalId, onClose, currency }: DepositModalProps) => {
const { loanPoolContract, name, symbol } = currency;

const { wallet, balances, signTransaction } = useWallet();
const { wallet, balances, signTransaction, refetchBalances } = useWallet();
const [isDepositing, setIsDepositing] = useState(false);
const [amount, setAmount] = useState('0');

const balance = balances[symbol];

if (!balance) return null;

const closeModal = () => {
refetchBalances();
setAmount('0');
onClose();
};

const handleDepositClick = async () => {
if (!wallet) {
alert('Please connect your wallet first!');
Expand All @@ -30,20 +38,18 @@ export const DepositModal = ({ modalId, onClose, currency }: DepositModalProps)

loanPoolContract.options.publicKey = wallet.address;

// Multiply by ten million by adding zeroes.
const stroops = BigInt(amount) * BigInt(10_000_000);

const tx = await loanPoolContract.deposit({
user: wallet.address,
amount: stroops,
amount: to7decimals(amount),
});

try {
const { result } = await tx.signAndSend({ signTransaction });
alert(`Deposit successful, result: ${result}`);
onClose();
closeModal();
} catch (err) {
alert(`Error depositing: ${JSON.stringify(err)}`);
console.error('Error depositing', err);
alert('Error depositing');
}
setIsDepositing(false);
};
Expand Down Expand Up @@ -83,7 +89,7 @@ export const DepositModal = ({ modalId, onClose, currency }: DepositModalProps)
</p>

<div className="flex flex-row justify-end mt-8">
<Button onClick={onClose} className="btn-ghost mr-4">
<Button onClick={closeModal} className="btn-ghost mr-4">
Cancel
</Button>
{!isDepositing ? (
Expand All @@ -92,15 +98,17 @@ export const DepositModal = ({ modalId, onClose, currency }: DepositModalProps)
</Button>
) : (
<Button disabled>
<span className="loading loading-spinner" />
<Loading />
Depositing
</Button>
)}
</div>
</div>
{/* Invisible backdrop that closes the modal on click */}
<form method="dialog" className="modal-backdrop">
<button type="button">close</button>
<button onClick={closeModal} type="button">
close
</button>
</form>
</dialog>
);
Expand Down
Loading

0 comments on commit 12c5507

Please sign in to comment.