From ae4580ce77e52e429ee95473f77892bfdabab9dd Mon Sep 17 00:00:00 2001 From: Konsta Purtsi Date: Wed, 2 Oct 2024 14:26:27 +0300 Subject: [PATCH 1/8] refactor: Use 1 cent accuracy when formatting to dollars. --- currencies.ts | 2 +- src/components/WalletCard/WalletCard.tsx | 22 +++++++++---------- src/lib/formatting.ts | 27 ++++++++++++++++-------- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/currencies.ts b/currencies.ts index 56038183..255b3c8f 100644 --- a/currencies.ts +++ b/currencies.ts @@ -11,7 +11,7 @@ export type Currency = { // TODO: use environment variables for the addresses. export const CURRENCY_XLM: Currency = { - name: 'Stellar Lumen', + name: 'Stellar Lumens', ticker: 'XLM', tokenContractAddress: 'CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC', loanPoolName: 'pool_xlm', diff --git a/src/components/WalletCard/WalletCard.tsx b/src/components/WalletCard/WalletCard.tsx index 71099bef..6a98aa19 100644 --- a/src/components/WalletCard/WalletCard.tsx +++ b/src/components/WalletCard/WalletCard.tsx @@ -1,7 +1,7 @@ import { Loading } from '@components/Loading'; import type { SupportedCurrency } from 'currencies'; import { isNil } from 'ramda'; -import { formatDollarAmount, toDollars } from 'src/lib/formatting'; +import { formatCentAmount, toCents } from 'src/lib/formatting'; import { type PositionsRecord, type PriceRecord, useWallet } from 'src/stellar-wallet'; import { Button } from '../Button'; import { Card } from '../Card'; @@ -48,8 +48,8 @@ const WalletCard = () => { ); } - const hasReceivables = values.receivablesDollars > 0n; - const hasLiabilities = values.liabilitiesDollars > 0n; + const hasReceivables = values.receivablesCents > 0n; + const hasLiabilities = values.liabilitiesCents > 0n; const openAssetModal = () => { const modalEl = document.getElementById(ASSET_MODAL_ID) as HTMLDialogElement; @@ -90,7 +90,7 @@ const WalletCard = () => {

Total deposited

-

{formatDollarAmount(values.receivablesDollars)}

+

{formatCentAmount(values.receivablesCents)}

); diff --git a/src/components/CryptoAmountSelector.tsx b/src/components/CryptoAmountSelector.tsx new file mode 100644 index 00000000..deadcd65 --- /dev/null +++ b/src/components/CryptoAmountSelector.tsx @@ -0,0 +1,47 @@ +import { SCALAR_7, toDollarsFormatted } from '@lib/formatting'; +import type { SupportedCurrency } from 'currencies'; +import type { ChangeEvent } from 'react'; +import { Button } from './Button'; + +export interface CryptoAmountSelectorProps { + max: string; + value: string; + ticker: SupportedCurrency; + price: bigint | undefined; + onChange: (ev: ChangeEvent) => void; + onSelectMaximum: () => void; +} + +export const CryptoAmountSelector = ({ + max, + value, + ticker, + price, + onChange, + onSelectMaximum, +}: CryptoAmountSelectorProps) => { + const dollarValue = price ? toDollarsFormatted(price, BigInt(value) * SCALAR_7) : undefined; + + return ( + <> + +
+ | + | + | + | + | +
+
+ + {dollarValue && `≈ ${dollarValue}`} + +
+ + ); +}; diff --git a/src/components/WalletCard/AssetsModal.tsx b/src/components/WalletCard/AssetsModal.tsx index cfa0bc9d..6e5a6302 100644 --- a/src/components/WalletCard/AssetsModal.tsx +++ b/src/components/WalletCard/AssetsModal.tsx @@ -1,10 +1,10 @@ import { Button } from '@components/Button'; import { Loading } from '@components/Loading'; +import { formatAmount, toDollarsFormatted } from '@lib/formatting'; import type { SupportedCurrency } from 'currencies'; import { isNil } from 'ramda'; import { useState } from 'react'; import { CURRENCY_BINDINGS, type CurrencyBinding } from 'src/currency-bindings'; -import { formatAmount, toDollarsFormatted } from 'src/lib/formatting'; import { useWallet } from 'src/stellar-wallet'; export interface AssetsModalProps { @@ -35,7 +35,7 @@ const AssetsModal = ({ modalId, onClose }: AssetsModalProps) => {
-
diff --git a/src/components/WalletCard/LoansModal.tsx b/src/components/WalletCard/LoansModal.tsx index f6abbfe6..2de75415 100644 --- a/src/components/WalletCard/LoansModal.tsx +++ b/src/components/WalletCard/LoansModal.tsx @@ -4,9 +4,9 @@ import { useState } from 'react'; import { Button } from '@components/Button'; import { Loading } from '@components/Loading'; import { contractClient as loanManagerClient } from '@contracts/loan_manager'; +import { formatAmount, toDollarsFormatted } from '@lib/formatting'; import type { SupportedCurrency } from 'currencies'; import { CURRENCY_BINDINGS, type CurrencyBinding } from 'src/currency-bindings'; -import { formatAmount, toDollarsFormatted } from 'src/lib/formatting'; import { useWallet } from 'src/stellar-wallet'; export interface AssetsModalProps { @@ -37,7 +37,7 @@ const LoansModal = ({ modalId, onClose }: AssetsModalProps) => {
-
diff --git a/src/components/WalletCard/WalletCard.tsx b/src/components/WalletCard/WalletCard.tsx index 6a98aa19..4b28a4de 100644 --- a/src/components/WalletCard/WalletCard.tsx +++ b/src/components/WalletCard/WalletCard.tsx @@ -1,7 +1,7 @@ import { Loading } from '@components/Loading'; +import { formatCentAmount, toCents } from '@lib/formatting'; import type { SupportedCurrency } from 'currencies'; import { isNil } from 'ramda'; -import { formatCentAmount, toCents } from 'src/lib/formatting'; import { type PositionsRecord, type PriceRecord, useWallet } from 'src/stellar-wallet'; import { Button } from '../Button'; import { Card } from '../Card'; @@ -92,7 +92,7 @@ const WalletCard = () => {

Total deposited

{formatCentAmount(values.receivablesCents)}

- @@ -105,7 +105,7 @@ const WalletCard = () => {

Total borrowed

{formatCentAmount(values.liabilitiesCents)}

- diff --git a/src/lib/converters.ts b/src/lib/converters.ts index 7d41a0c7..ba817a96 100644 --- a/src/lib/converters.ts +++ b/src/lib/converters.ts @@ -1,2 +1,4 @@ // 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); + +export const getIntegerPart = (decimal: string): string => Number.parseInt(decimal).toString(); diff --git a/src/lib/formatting.ts b/src/lib/formatting.ts index ac65767f..b42db23c 100644 --- a/src/lib/formatting.ts +++ b/src/lib/formatting.ts @@ -1,6 +1,6 @@ // 7 decimal numbers is the smallest unit of XLM, stroop. -const SCALAR_7 = 10_000_000n; -const CENTS_SCALAR = SCALAR_7 * 100_000n; +export const SCALAR_7 = 10_000_000n; +export const CENTS_SCALAR = SCALAR_7 * SCALAR_7 * 100_000n; const TEN_K = 10_000n * SCALAR_7; const ONE_M = 1_000_000n * SCALAR_7; @@ -22,16 +22,16 @@ export const formatAmount = (amount: bigint): string => { return `${(Number(amount) / 10_000_000).toFixed(1)}`; }; -export const toDollarsFormatted = (price: bigint, amount: bigint) => { +export const toDollarsFormatted = (price: bigint, amount: bigint): string => { if (amount === 0n) return '$0'; return formatCentAmount(toCents(price, amount)); }; -export const toCents = (price: bigint, amount: bigint) => { +export const toCents = (price: bigint, amount: bigint): bigint => { return (price * amount) / CENTS_SCALAR; }; -export const formatCentAmount = (cents: bigint) => { +export const formatCentAmount = (cents: bigint): string => { if (cents === 0n) return '$0'; if (cents > ONE_M_CENTS) { diff --git a/src/pages/_borrow/BorrowModal.tsx b/src/pages/_borrow/BorrowModal.tsx index 1387bcc4..7e8ef6bd 100644 --- a/src/pages/_borrow/BorrowModal.tsx +++ b/src/pages/_borrow/BorrowModal.tsx @@ -1,9 +1,10 @@ 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 { type ChangeEvent, useState } from 'react'; import type { CurrencyBinding } from 'src/currency-bindings'; -import { to7decimals } from 'src/lib/converters'; import { useWallet } from 'src/stellar-wallet'; export interface BorrowModalProps { @@ -16,14 +17,17 @@ export interface BorrowModalProps { export const BorrowModal = ({ modalId, onClose, currency, collateral, totalSupplied }: BorrowModalProps) => { const { name, ticker, contractId: loanCurrencyId } = currency; - const { wallet, walletBalances, signTransaction, refetchBalances } = useWallet(); + const { wallet, walletBalances, signTransaction, refetchBalances, prices } = useWallet(); const [isBorrowing, setIsBorrowing] = useState(false); - const [loanAmount, setLoanAmount] = useState('0'); - const [collateralAmount, setCollateralAmount] = useState('0'); + const [loanAmount, setLoanAmount] = useState('0'); + const [collateralAmount, setCollateralAmount] = useState('0'); const collateralBalance = walletBalances[collateral.ticker]; + const loanPrice = prices?.[ticker]; + const collateralPrice = prices?.[collateral.ticker]; + // The modal is impossible to open without collateral balance. if (!collateralBalance) return null; @@ -39,6 +43,10 @@ export const BorrowModal = ({ modalId, onClose, currency, collateral, totalSuppl alert('Please connect your wallet first!'); return; } + if (!loanAmount || !collateralAmount) { + alert('Empty loan amount or collateral!'); + return; + } setIsBorrowing(true); @@ -75,55 +83,56 @@ export const BorrowModal = ({ modalId, onClose, currency, collateral, totalSuppl const maxLoan = (totalSupplied / 10_000_000n).toString(); - const maxCollateral = collateralBalance.balance.split('.')[0]; + const maxCollateral = getIntegerPart(collateralBalance.balance); + + const handleSelectMaxLoan = () => setLoanAmount(maxLoan); + + const handleSelectMaxCollateral = () => setCollateralAmount(maxCollateral); + + // TODO: get this from the contract. + const interestRate = '7.5%'; return ( -
-

Borrow {name}

+
+

Borrow {name}

+

+ Borrow {name} using another asset as a collateral. The value of the collateral must exceed the value of the + borrowed asset. +

+

+ 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. +

+

+ 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. +

+

The interest rate changes as the amount of assets borrowed from the pools changes.

+

The annual interest rate is currently {interestRate}.

-

Amount to borrow

- Amount to borrow

+ -
- | - | - | - | - | -
-

- {loanAmount} {ticker} out of {maxLoan} {ticker} -

-

Amount of collateral

- Amount of collateral

+ -
- | - | - | - | - | -
-

- {collateralAmount} {collateral.ticker} out of {maxCollateral} {collateral.ticker} -

- {!isBorrowing ? ( diff --git a/src/pages/_lend/DepositModal.tsx b/src/pages/_lend/DepositModal.tsx index ee37a8d4..f4029914 100644 --- a/src/pages/_lend/DepositModal.tsx +++ b/src/pages/_lend/DepositModal.tsx @@ -1,8 +1,9 @@ import { Button } from '@components/Button'; +import { CryptoAmountSelector } from '@components/CryptoAmountSelector'; import { Loading } from '@components/Loading'; +import { getIntegerPart, to7decimals } from '@lib/converters'; import { type ChangeEvent, useState } from 'react'; import type { CurrencyBinding } from 'src/currency-bindings'; -import { to7decimals } from 'src/lib/converters'; import { useWallet } from 'src/stellar-wallet'; export interface DepositModalProps { @@ -14,14 +15,17 @@ export interface DepositModalProps { export const DepositModal = ({ modalId, onClose, currency }: DepositModalProps) => { const { contractClient, name, ticker } = currency; - const { wallet, walletBalances, signTransaction, refetchBalances } = useWallet(); + const { wallet, walletBalances, prices, signTransaction, refetchBalances } = useWallet(); const [isDepositing, setIsDepositing] = useState(false); const [amount, setAmount] = useState('0'); const balance = walletBalances[ticker]; + const price = prices?.[ticker]; if (!balance) return null; + const max = getIntegerPart(balance.balance); + const closeModal = () => { refetchBalances(); setAmount('0'); @@ -58,38 +62,27 @@ export const DepositModal = ({ modalId, onClose, currency }: DepositModalProps) setAmount(ev.target.value); }; + const handleSelectMaxLoan = () => { + setAmount(max); + }; + return (

Deposit {name}

-
-
-

Amount to deposit

- -
- | - | - | - | - | -
-
-
- -

- {amount} {ticker} out of {balance.balance} {ticker} -

+

Amount to deposit

+
- {!isDepositing ? ( diff --git a/src/pages/_lend/LendableAsset.tsx b/src/pages/_lend/LendableAsset.tsx index f144dbf8..f2f546ae 100644 --- a/src/pages/_lend/LendableAsset.tsx +++ b/src/pages/_lend/LendableAsset.tsx @@ -1,9 +1,9 @@ import { Button } from '@components/Button'; import { Loading } from '@components/Loading'; +import { formatAmount, toDollarsFormatted } from '@lib/formatting'; import { isNil } from 'ramda'; import { useCallback, useEffect, useState } from 'react'; import type { CurrencyBinding } from 'src/currency-bindings'; -import { formatAmount, toDollarsFormatted } from 'src/lib/formatting'; import { type Balance, useWallet } from 'src/stellar-wallet'; import { DepositModal } from './DepositModal'; diff --git a/tsconfig.json b/tsconfig.json index 9e94d3f5..d6e5b62b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,8 @@ "@images/*": ["src/images/*"], "@components/*": ["src/components/*"], "@pages/*": ["src/pages/*"], - "@contracts/*": ["src/contracts/*"] + "@contracts/*": ["src/contracts/*"], + "@lib/*": ["src/lib/*"] } } } From 172886df11f16a779f3dc7de3dd9d7366e0de18e Mon Sep 17 00:00:00 2001 From: Konsta Purtsi Date: Wed, 2 Oct 2024 20:15:21 +0300 Subject: [PATCH 3/8] refactor: Rename get_user_balance to get_user_positions --- contracts/loan_pool/src/contract.rs | 2 +- src/stellar-wallet.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/loan_pool/src/contract.rs b/contracts/loan_pool/src/contract.rs index 4d750356..1d9fcba3 100644 --- a/contracts/loan_pool/src/contract.rs +++ b/contracts/loan_pool/src/contract.rs @@ -165,7 +165,7 @@ impl LoanPoolContract { } /// Get user's positions in the pool - pub fn get_user_balance(e: Env, user: Address) -> Positions { + pub fn get_user_positions(e: Env, user: Address) -> Positions { positions::read_positions(&e, &user) } diff --git a/src/stellar-wallet.tsx b/src/stellar-wallet.tsx index 89797479..9e822503 100644 --- a/src/stellar-wallet.tsx +++ b/src/stellar-wallet.tsx @@ -79,7 +79,7 @@ const fetchAllPositions = async (user: string): Promise => { const positionsArr = await Promise.all( CURRENCY_BINDINGS.map(async ({ contractClient, ticker }) => [ ticker, - (await contractClient.get_user_balance({ user })).result, + (await contractClient.get_user_positions({ user })).result, ]), ); return Object.fromEntries(positionsArr); From d3c12a6d63d1aa9b8debc679d7966c7e140b5e76 Mon Sep 17 00:00:00 2001 From: Konsta Purtsi Date: Thu, 3 Oct 2024 12:42:42 +0300 Subject: [PATCH 4/8] feat: Add live health factor. --- src/components/CryptoAmountSelector.tsx | 10 ++--- src/pages/_borrow/BorrowModal.tsx | 54 +++++++++++++++++++++++-- src/pages/_lend/DepositModal.tsx | 5 ++- tailwind.config.mjs | 4 ++ 4 files changed, 63 insertions(+), 10 deletions(-) diff --git a/src/components/CryptoAmountSelector.tsx b/src/components/CryptoAmountSelector.tsx index deadcd65..ffa62bf5 100644 --- a/src/components/CryptoAmountSelector.tsx +++ b/src/components/CryptoAmountSelector.tsx @@ -1,4 +1,4 @@ -import { SCALAR_7, toDollarsFormatted } from '@lib/formatting'; +import { formatCentAmount } from '@lib/formatting'; import type { SupportedCurrency } from 'currencies'; import type { ChangeEvent } from 'react'; import { Button } from './Button'; @@ -6,8 +6,8 @@ import { Button } from './Button'; export interface CryptoAmountSelectorProps { max: string; value: string; + valueCents: bigint | undefined; ticker: SupportedCurrency; - price: bigint | undefined; onChange: (ev: ChangeEvent) => void; onSelectMaximum: () => void; } @@ -15,13 +15,11 @@ export interface CryptoAmountSelectorProps { export const CryptoAmountSelector = ({ max, value, + valueCents, ticker, - price, onChange, onSelectMaximum, }: CryptoAmountSelectorProps) => { - const dollarValue = price ? toDollarsFormatted(price, BigInt(value) * SCALAR_7) : undefined; - return ( <> @@ -37,7 +35,7 @@ export const CryptoAmountSelector = ({ {ticker} - {dollarValue && `≈ ${dollarValue}`} + {valueCents ? `≈ ${formatCentAmount(valueCents)}` : null} diff --git a/src/pages/_borrow/BorrowModal.tsx b/src/pages/_borrow/BorrowModal.tsx index 7e8ef6bd..512f6053 100644 --- a/src/pages/_borrow/BorrowModal.tsx +++ b/src/pages/_borrow/BorrowModal.tsx @@ -3,10 +3,15 @@ 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, toCents } from '@lib/formatting'; import { type ChangeEvent, useState } from 'react'; import type { CurrencyBinding } from 'src/currency-bindings'; import { useWallet } from 'src/stellar-wallet'; +const HEALTH_FACTOR_MIN_THRESHOLD = 1.2; +const HEALTH_FACTOR_GOOD_THRESHOLD = 1.6; +const HEALTH_FACTOR_EXCELLENT_THRESHOLD = 2.0; + export interface BorrowModalProps { modalId: string; onClose: () => void; @@ -28,6 +33,14 @@ export const BorrowModal = ({ modalId, onClose, currency, collateral, totalSuppl const loanPrice = prices?.[ticker]; const collateralPrice = prices?.[collateral.ticker]; + const loanAmountCents = loanPrice ? toCents(loanPrice, BigInt(loanAmount) * SCALAR_7) : undefined; + const collateralAmountCents = collateralPrice + ? toCents(collateralPrice, BigInt(collateralAmount) * SCALAR_7) + : undefined; + + const healthFactor = + loanAmountCents && loanAmountCents > 0n ? Number(collateralAmountCents) / Number(loanAmountCents) : 0; + // The modal is impossible to open without collateral balance. if (!collateralBalance) return null; @@ -79,7 +92,7 @@ export const BorrowModal = ({ modalId, onClose, currency, collateral, totalSuppl setCollateralAmount(ev.target.value); }; - const isBorrowDisabled = loanAmount === '0' || collateralAmount === '0'; + const isBorrowDisabled = loanAmount === '0' || collateralAmount === '0' || healthFactor < HEALTH_FACTOR_MIN_THRESHOLD; const maxLoan = (totalSupplied / 10_000_000n).toString(); @@ -115,8 +128,8 @@ export const BorrowModal = ({ modalId, onClose, currency, collateral, totalSuppl @@ -125,12 +138,15 @@ export const BorrowModal = ({ modalId, onClose, currency, collateral, totalSuppl +

Health Factor

+ +
); }; + +const HealthFactor = ({ value }: { value: number }) => { + if (value < HEALTH_FACTOR_MIN_THRESHOLD) { + return ; + } + if (value < HEALTH_FACTOR_GOOD_THRESHOLD) { + return ; + } + if (value < HEALTH_FACTOR_EXCELLENT_THRESHOLD) { + return ; + } + return ; +}; + +interface HealthBarProps { + text: string; + textColor: string; + bgColor: string; + bars: number; +} + +const HealthBar = ({ text, textColor, bgColor, bars }: HealthBarProps) => ( + <> +

{text}

+
+
+
1 ? bgColor : 'bg-grey'}`} /> +
2 ? bgColor : 'bg-grey'}`} /> +
3 ? bgColor : 'bg-grey'}`} /> +
+ +); diff --git a/src/pages/_lend/DepositModal.tsx b/src/pages/_lend/DepositModal.tsx index f4029914..e25238bd 100644 --- a/src/pages/_lend/DepositModal.tsx +++ b/src/pages/_lend/DepositModal.tsx @@ -2,6 +2,7 @@ import { Button } from '@components/Button'; import { CryptoAmountSelector } from '@components/CryptoAmountSelector'; import { Loading } from '@components/Loading'; import { getIntegerPart, to7decimals } from '@lib/converters'; +import { SCALAR_7, toCents } from '@lib/formatting'; import { type ChangeEvent, useState } from 'react'; import type { CurrencyBinding } from 'src/currency-bindings'; import { useWallet } from 'src/stellar-wallet'; @@ -22,6 +23,8 @@ export const DepositModal = ({ modalId, onClose, currency }: DepositModalProps) const balance = walletBalances[ticker]; const price = prices?.[ticker]; + const amountCents = price ? toCents(price, BigInt(amount) * SCALAR_7) : undefined; + if (!balance) return null; const max = getIntegerPart(balance.balance); @@ -75,8 +78,8 @@ export const DepositModal = ({ modalId, onClose, currency }: DepositModalProps) diff --git a/tailwind.config.mjs b/tailwind.config.mjs index aa03e072..e0cf3afb 100644 --- a/tailwind.config.mjs +++ b/tailwind.config.mjs @@ -15,6 +15,10 @@ export default { }, black: '#000', primary: '#000', + red: '#e51414', + yellow: '#e18c02', + blue: '#0048ff', + green: '#0fdd13', }, fontSize: { sm: ['14px', '20px'], From 628265030cad7204115ec98223351f95d998a64a Mon Sep 17 00:00:00 2001 From: Konsta Purtsi Date: Thu, 3 Oct 2024 13:33:18 +0300 Subject: [PATCH 5/8] feat: Make collateral follow the good health threshold automatically. --- src/lib/formatting.ts | 4 ++++ src/pages/_borrow/BorrowModal.tsx | 18 ++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/lib/formatting.ts b/src/lib/formatting.ts index b42db23c..ae588ba3 100644 --- a/src/lib/formatting.ts +++ b/src/lib/formatting.ts @@ -31,6 +31,10 @@ export const toCents = (price: bigint, amount: bigint): bigint => { return (price * amount) / CENTS_SCALAR; }; +export const fromCents = (price: bigint, cents: bigint): bigint => { + return (cents * CENTS_SCALAR) / price; +} + export const formatCentAmount = (cents: bigint): string => { if (cents === 0n) return '$0'; diff --git a/src/pages/_borrow/BorrowModal.tsx b/src/pages/_borrow/BorrowModal.tsx index 512f6053..b5fcf021 100644 --- a/src/pages/_borrow/BorrowModal.tsx +++ b/src/pages/_borrow/BorrowModal.tsx @@ -3,7 +3,7 @@ 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, toCents } from '@lib/formatting'; +import { fromCents, SCALAR_7, toCents } from '@lib/formatting'; import { type ChangeEvent, useState } from 'react'; import type { CurrencyBinding } from 'src/currency-bindings'; import { useWallet } from 'src/stellar-wallet'; @@ -12,6 +12,8 @@ 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; onClose: () => void; @@ -86,6 +88,18 @@ export const BorrowModal = ({ modalId, onClose, currency, collateral, totalSuppl const handleLoanAmountChange = (ev: ChangeEvent) => { setLoanAmount(ev.target.value); + + if (!loanPrice || !collateralPrice) return; + + // Move the collateral to reach the good health threshold + const loanAmountCents = toCents(loanPrice, BigInt(ev.target.value) * SCALAR_7); + const minHealthyCollateralCents = BigInt(Math.ceil(HEALTH_FACTOR_AUTO_THRESHOLD * Number(loanAmountCents) + 100)); + const minHealthyCollateral = (fromCents(collateralPrice, minHealthyCollateralCents)) / SCALAR_7 + if (minHealthyCollateral <= BigInt(maxCollateral)) { + setCollateralAmount(minHealthyCollateral.toString()); + } else { + setCollateralAmount(maxCollateral); + } }; const handleCollateralAmountChange = (ev: ChangeEvent) => { @@ -126,7 +140,7 @@ export const BorrowModal = ({ modalId, onClose, currency, collateral, totalSuppl

Amount to borrow

Date: Fri, 4 Oct 2024 13:25:27 +0300 Subject: [PATCH 6/8] feat: Allow changing collateral ticker --- src/components/CryptoAmountSelector.tsx | 80 +++++++++++++++++------ src/components/WalletCard/AssetsModal.tsx | 4 +- src/components/WalletCard/LoansModal.tsx | 4 +- src/currency-bindings.ts | 10 ++- src/lib/formatting.ts | 2 +- src/pages/_borrow/BorrowModal.tsx | 39 ++++++----- src/pages/_borrow/BorrowPage.tsx | 4 +- src/pages/_lend/LendPage.tsx | 4 +- src/stellar-wallet.tsx | 4 +- 9 files changed, 103 insertions(+), 48 deletions(-) diff --git a/src/components/CryptoAmountSelector.tsx b/src/components/CryptoAmountSelector.tsx index ffa62bf5..0fe09004 100644 --- a/src/components/CryptoAmountSelector.tsx +++ b/src/components/CryptoAmountSelector.tsx @@ -1,6 +1,9 @@ import { formatCentAmount } from '@lib/formatting'; import type { SupportedCurrency } from 'currencies'; +import { isNil, useWith } from 'ramda'; import type { ChangeEvent } from 'react'; +import { CURRENCY_BINDINGS_ARR } from 'src/currency-bindings'; +import { useWallet } from 'src/stellar-wallet'; import { Button } from './Button'; export interface CryptoAmountSelectorProps { @@ -10,6 +13,7 @@ export interface CryptoAmountSelectorProps { ticker: SupportedCurrency; onChange: (ev: ChangeEvent) => void; onSelectMaximum: () => void; + onSelectTicker?: (ticker: SupportedCurrency) => void; } export const CryptoAmountSelector = ({ @@ -19,27 +23,61 @@ export const CryptoAmountSelector = ({ ticker, onChange, onSelectMaximum, -}: CryptoAmountSelectorProps) => { - return ( - <> - -
- | - | - | - | - | -
-
-
+ +); + +const TickerOption = ({ ticker }: { ticker: SupportedCurrency }) => { + const { walletBalances } = useWallet(); + const balance = walletBalances[ticker]; + const disabled = isNil(balance) || balance.balance === '0'; + + return ( + ); }; diff --git a/src/components/WalletCard/AssetsModal.tsx b/src/components/WalletCard/AssetsModal.tsx index 6e5a6302..9e5e4806 100644 --- a/src/components/WalletCard/AssetsModal.tsx +++ b/src/components/WalletCard/AssetsModal.tsx @@ -4,7 +4,7 @@ import { formatAmount, toDollarsFormatted } from '@lib/formatting'; import type { SupportedCurrency } from 'currencies'; import { isNil } from 'ramda'; import { useState } from 'react'; -import { CURRENCY_BINDINGS, type CurrencyBinding } from 'src/currency-bindings'; +import { CURRENCY_BINDINGS } from 'src/currency-bindings'; import { useWallet } from 'src/stellar-wallet'; export interface AssetsModalProps { @@ -60,7 +60,7 @@ const TableRow = ({ receivables, ticker }: TableRowProps) => { if (receivables === 0n) return null; - const { icon, name, contractClient } = CURRENCY_BINDINGS.find((b) => b.ticker === ticker) as CurrencyBinding; + const { icon, name, contractClient } = CURRENCY_BINDINGS[ticker]; const price = prices?.[ticker]; const handleWithdrawClick = async () => { diff --git a/src/components/WalletCard/LoansModal.tsx b/src/components/WalletCard/LoansModal.tsx index 2de75415..149d3e99 100644 --- a/src/components/WalletCard/LoansModal.tsx +++ b/src/components/WalletCard/LoansModal.tsx @@ -6,7 +6,7 @@ import { Loading } from '@components/Loading'; import { contractClient as loanManagerClient } from '@contracts/loan_manager'; import { formatAmount, toDollarsFormatted } from '@lib/formatting'; import type { SupportedCurrency } from 'currencies'; -import { CURRENCY_BINDINGS, type CurrencyBinding } from 'src/currency-bindings'; +import { CURRENCY_BINDINGS } from 'src/currency-bindings'; import { useWallet } from 'src/stellar-wallet'; export interface AssetsModalProps { @@ -62,7 +62,7 @@ const TableRow = ({ liabilities, ticker }: TableRowProps) => { if (liabilities === 0n) return null; - const { icon, name } = CURRENCY_BINDINGS.find((b) => b.ticker === ticker) as CurrencyBinding; + const { icon, name } = CURRENCY_BINDINGS[ticker]; const price = prices?.[ticker]; const handleWithdrawClick = async () => { diff --git a/src/currency-bindings.ts b/src/currency-bindings.ts index ae504cfe..0cb9b66a 100644 --- a/src/currency-bindings.ts +++ b/src/currency-bindings.ts @@ -51,4 +51,12 @@ export const BINDING_EURC: CurrencyBinding = { icon: EURCIcon.src, }; -export const CURRENCY_BINDINGS = [BINDING_XLM, BINDING_WBTC, BINDING_WETH, BINDING_USDC, BINDING_EURC]; +export const CURRENCY_BINDINGS = { + XLM: BINDING_XLM, + wBTC: BINDING_WBTC, + wETH: BINDING_WETH, + USDC: BINDING_USDC, + EURC: BINDING_EURC, +} as const; + +export const CURRENCY_BINDINGS_ARR = Object.values(CURRENCY_BINDINGS); diff --git a/src/lib/formatting.ts b/src/lib/formatting.ts index ae588ba3..6920432f 100644 --- a/src/lib/formatting.ts +++ b/src/lib/formatting.ts @@ -33,7 +33,7 @@ export const toCents = (price: bigint, amount: bigint): bigint => { export const fromCents = (price: bigint, cents: bigint): bigint => { return (cents * CENTS_SCALAR) / price; -} +}; export const formatCentAmount = (cents: bigint): string => { if (cents === 0n) return '$0'; diff --git a/src/pages/_borrow/BorrowModal.tsx b/src/pages/_borrow/BorrowModal.tsx index b5fcf021..4c7397fe 100644 --- a/src/pages/_borrow/BorrowModal.tsx +++ b/src/pages/_borrow/BorrowModal.tsx @@ -3,9 +3,10 @@ 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 { fromCents, SCALAR_7, toCents } from '@lib/formatting'; +import { SCALAR_7, fromCents, toCents } from '@lib/formatting'; +import type { SupportedCurrency } from 'currencies'; import { type ChangeEvent, useState } from 'react'; -import type { CurrencyBinding } from 'src/currency-bindings'; +import { CURRENCY_BINDINGS, type CurrencyBinding } from 'src/currency-bindings'; import { useWallet } from 'src/stellar-wallet'; const HEALTH_FACTOR_MIN_THRESHOLD = 1.2; @@ -18,22 +19,22 @@ export interface BorrowModalProps { modalId: string; onClose: () => void; currency: CurrencyBinding; - collateral: CurrencyBinding; totalSupplied: bigint; } -export const BorrowModal = ({ modalId, onClose, currency, collateral, totalSupplied }: BorrowModalProps) => { +export const BorrowModal = ({ modalId, onClose, currency, totalSupplied }: BorrowModalProps) => { const { name, ticker, contractId: loanCurrencyId } = currency; const { wallet, walletBalances, signTransaction, refetchBalances, prices } = useWallet(); const [isBorrowing, setIsBorrowing] = useState(false); const [loanAmount, setLoanAmount] = useState('0'); + const [collateralTicker, setCollateralTicker] = useState('XLM'); const [collateralAmount, setCollateralAmount] = useState('0'); - const collateralBalance = walletBalances[collateral.ticker]; + const collateralBalance = walletBalances[collateralTicker]; const loanPrice = prices?.[ticker]; - const collateralPrice = prices?.[collateral.ticker]; + const collateralPrice = prices?.[collateralTicker]; const loanAmountCents = loanPrice ? toCents(loanPrice, BigInt(loanAmount) * SCALAR_7) : undefined; const collateralAmountCents = collateralPrice @@ -68,12 +69,14 @@ export const BorrowModal = ({ modalId, onClose, currency, collateral, totalSuppl try { loanManagerClient.options.publicKey = wallet.address; + const { contractId: collateralCurrencyId } = CURRENCY_BINDINGS[collateralTicker]; + const tx = await loanManagerClient.create_loan({ user: wallet.address, borrowed: to7decimals(loanAmount), borrowed_from: loanCurrencyId, collateral: to7decimals(collateralAmount), - collateral_from: collateral.contractId, + collateral_from: collateralCurrencyId, }); await tx.signAndSend({ signTransaction }); alert('Loan created succesfully!'); @@ -94,7 +97,7 @@ export const BorrowModal = ({ modalId, onClose, currency, collateral, totalSuppl // Move the collateral to reach the good health threshold const loanAmountCents = toCents(loanPrice, BigInt(ev.target.value) * SCALAR_7); const minHealthyCollateralCents = BigInt(Math.ceil(HEALTH_FACTOR_AUTO_THRESHOLD * Number(loanAmountCents) + 100)); - const minHealthyCollateral = (fromCents(collateralPrice, minHealthyCollateralCents)) / SCALAR_7 + const minHealthyCollateral = fromCents(collateralPrice, minHealthyCollateralCents) / SCALAR_7; if (minHealthyCollateral <= BigInt(maxCollateral)) { setCollateralAmount(minHealthyCollateral.toString()); } else { @@ -106,6 +109,11 @@ export const BorrowModal = ({ modalId, onClose, currency, collateral, totalSuppl setCollateralAmount(ev.target.value); }; + const handleCollateralTickerChange = (ticker: SupportedCurrency) => { + setCollateralTicker(ticker); + setCollateralAmount('0'); + }; + const isBorrowDisabled = loanAmount === '0' || collateralAmount === '0' || healthFactor < HEALTH_FACTOR_MIN_THRESHOLD; const maxLoan = (totalSupplied / 10_000_000n).toString(); @@ -140,7 +148,7 @@ export const BorrowModal = ({ modalId, onClose, currency, collateral, totalSuppl

Amount to borrow

Health Factor

@@ -209,12 +218,12 @@ interface HealthBarProps { const HealthBar = ({ text, textColor, bgColor, bars }: HealthBarProps) => ( <> -

{text}

+

{text}

-
-
1 ? bgColor : 'bg-grey'}`} /> -
2 ? bgColor : 'bg-grey'}`} /> -
3 ? bgColor : 'bg-grey'}`} /> +
+
1 ? bgColor : 'bg-grey'}`} /> +
2 ? bgColor : 'bg-grey'}`} /> +
3 ? bgColor : 'bg-grey'}`} />
); diff --git a/src/pages/_borrow/BorrowPage.tsx b/src/pages/_borrow/BorrowPage.tsx index 717303fc..a7f575bd 100644 --- a/src/pages/_borrow/BorrowPage.tsx +++ b/src/pages/_borrow/BorrowPage.tsx @@ -1,7 +1,7 @@ import { Card } from '@components/Card'; import { Table } from '@components/Table'; import WalletCard from '@components/WalletCard/WalletCard'; -import { CURRENCY_BINDINGS } from 'src/currency-bindings'; +import { CURRENCY_BINDINGS_ARR } from 'src/currency-bindings'; import { BorrowableAsset } from './BorrowableAsset'; const links = [ @@ -17,7 +17,7 @@ const BorrowPage = () => (

Borrow Assets

- {CURRENCY_BINDINGS.map((currency) => ( + {CURRENCY_BINDINGS_ARR.map((currency) => ( ))}
diff --git a/src/pages/_lend/LendPage.tsx b/src/pages/_lend/LendPage.tsx index 1b7effe2..00b9c59d 100644 --- a/src/pages/_lend/LendPage.tsx +++ b/src/pages/_lend/LendPage.tsx @@ -1,7 +1,7 @@ import { Card } from '@components/Card'; import { Table } from '@components/Table'; import WalletCard from '@components/WalletCard/WalletCard'; -import { CURRENCY_BINDINGS } from 'src/currency-bindings'; +import { CURRENCY_BINDINGS_ARR } from 'src/currency-bindings'; import { LendableAsset } from './LendableAsset'; const links = [ @@ -18,7 +18,7 @@ const LendPage = () => {

Lend Assets

- {CURRENCY_BINDINGS.map((currency) => ( + {CURRENCY_BINDINGS_ARR.map((currency) => ( ))}
diff --git a/src/stellar-wallet.tsx b/src/stellar-wallet.tsx index 9e822503..1566a117 100644 --- a/src/stellar-wallet.tsx +++ b/src/stellar-wallet.tsx @@ -4,7 +4,7 @@ import { type PropsWithChildren, createContext, useContext, useEffect, useState import { contractClient as loanManagerClient } from '@contracts/loan_manager'; import type { SupportedCurrency } from 'currencies'; -import { CURRENCY_BINDINGS } from './currency-bindings'; +import { CURRENCY_BINDINGS_ARR } from './currency-bindings'; const HorizonServer = new StellarSdk.Horizon.Server('https://horizon-testnet.stellar.org/'); @@ -77,7 +77,7 @@ const createWalletObj = (address: string): Wallet => ({ const fetchAllPositions = async (user: string): Promise => { const positionsArr = await Promise.all( - CURRENCY_BINDINGS.map(async ({ contractClient, ticker }) => [ + CURRENCY_BINDINGS_ARR.map(async ({ contractClient, ticker }) => [ ticker, (await contractClient.get_user_positions({ user })).result, ]), From c1f9fc24958650bd3c9f9d066ebe04c6cb3a5036 Mon Sep 17 00:00:00 2001 From: Konsta Purtsi Date: Thu, 17 Oct 2024 21:51:02 +0300 Subject: [PATCH 7/8] feat: Collateral must have balance and cannot be the same token as the borrowed currency --- src/components/CryptoAmountSelector.tsx | 21 +++++++++++++-------- src/lib/converters.ts | 2 ++ src/pages/_borrow/BorrowModal.tsx | 13 ++++++++++--- src/pages/_borrow/BorrowableAsset.tsx | 25 ++++++++++--------------- 4 files changed, 35 insertions(+), 26 deletions(-) diff --git a/src/components/CryptoAmountSelector.tsx b/src/components/CryptoAmountSelector.tsx index 0fe09004..2c4512f4 100644 --- a/src/components/CryptoAmountSelector.tsx +++ b/src/components/CryptoAmountSelector.tsx @@ -1,8 +1,8 @@ +import { isBalanceZero } from '@lib/converters'; import { formatCentAmount } from '@lib/formatting'; import type { SupportedCurrency } from 'currencies'; -import { isNil, useWith } from 'ramda'; +import { isNil } from 'ramda'; import type { ChangeEvent } from 'react'; -import { CURRENCY_BINDINGS_ARR } from 'src/currency-bindings'; import { useWallet } from 'src/stellar-wallet'; import { Button } from './Button'; @@ -13,7 +13,12 @@ export interface CryptoAmountSelectorProps { ticker: SupportedCurrency; onChange: (ev: ChangeEvent) => void; onSelectMaximum: () => void; - onSelectTicker?: (ticker: SupportedCurrency) => void; + tickerChangeOptions?: TickerChangeOptions; +} + +export interface TickerChangeOptions { + options: SupportedCurrency[]; + onSelectTicker: (ticker: SupportedCurrency) => void; } export const CryptoAmountSelector = ({ @@ -23,7 +28,7 @@ export const CryptoAmountSelector = ({ ticker, onChange, onSelectMaximum, - onSelectTicker, + tickerChangeOptions, }: CryptoAmountSelectorProps) => ( <> @@ -35,7 +40,7 @@ export const CryptoAmountSelector = ({ |
- {isNil(onSelectTicker) ? ( + {isNil(tickerChangeOptions) ? (