From d6d04cd5fe65594b7345530438c8843e5faee5c2 Mon Sep 17 00:00:00 2001 From: Konsta Purtsi Date: Sat, 7 Dec 2024 12:16:47 +0200 Subject: [PATCH 1/3] refactor: Create @contexts folder. --- src/App.tsx | 2 +- src/components/CryptoAmountSelector.tsx | 2 +- src/components/Nav.tsx | 2 +- src/components/WalletCard/AssetsModal.tsx | 2 +- src/components/WalletCard/LoansModal.tsx | 2 +- src/components/WalletCard/WalletCard.tsx | 2 +- src/{stellar-wallet.tsx => contexts/wallet-context.tsx} | 2 +- src/pages/_borrow/BorrowModal/BorrowModal.tsx | 2 +- src/pages/_borrow/BorrowModal/BorrowStep.tsx | 2 +- src/pages/_borrow/BorrowModal/TrustlineStep.tsx | 5 +++-- src/pages/_borrow/BorrowableAsset.tsx | 2 +- src/pages/_lend/DepositModal.tsx | 2 +- src/pages/_lend/LendableAsset.tsx | 2 +- tsconfig.json | 3 ++- 14 files changed, 17 insertions(+), 15 deletions(-) rename src/{stellar-wallet.tsx => contexts/wallet-context.tsx} (99%) diff --git a/src/App.tsx b/src/App.tsx index 35c804d..97cfb0d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,12 +3,12 @@ import { Outlet, RouterProvider, createBrowserRouter } from 'react-router-dom'; import Footer from '@components/Footer'; import Nav from '@components/Nav'; +import { WalletProvider } from '@contexts/wallet-context'; import BorrowPage from '@pages/_borrow/BorrowPage'; import LandingPage from '@pages/_landing/LandingPage'; import LendPage from '@pages/_lend/LendPage'; import LiquidatePage from '@pages/_liquidate/LiquidatePage'; import WelcomePage from '@pages/_welcome/WelcomePage'; -import { WalletProvider } from './stellar-wallet'; const PageWrapper = () => { return ( diff --git a/src/components/CryptoAmountSelector.tsx b/src/components/CryptoAmountSelector.tsx index 57f34ee..b323e49 100644 --- a/src/components/CryptoAmountSelector.tsx +++ b/src/components/CryptoAmountSelector.tsx @@ -1,9 +1,9 @@ +import { useWallet } from '@contexts/wallet-context'; import { isBalanceZero } from '@lib/converters'; import { formatCentAmount } from '@lib/formatting'; import type { SupportedCurrency } from 'currencies'; import { isNil } from 'ramda'; import type { ChangeEvent } from 'react'; -import { useWallet } from 'src/stellar-wallet'; import { Button } from './Button'; export interface CryptoAmountSelectorProps { diff --git a/src/components/Nav.tsx b/src/components/Nav.tsx index 5d35db5..1cb9957 100644 --- a/src/components/Nav.tsx +++ b/src/components/Nav.tsx @@ -1,6 +1,6 @@ +import { useWallet } from '@contexts/wallet-context'; import type { PropsWithChildren } from 'react'; import { Link } from 'react-router-dom'; -import { useWallet } from 'src/stellar-wallet'; import logo from '/public/laina_v3_shrinked.png'; import { Button } from './Button'; import Identicon from './Identicon'; diff --git a/src/components/WalletCard/AssetsModal.tsx b/src/components/WalletCard/AssetsModal.tsx index 9e5e480..2738dfb 100644 --- a/src/components/WalletCard/AssetsModal.tsx +++ b/src/components/WalletCard/AssetsModal.tsx @@ -1,11 +1,11 @@ import { Button } from '@components/Button'; import { Loading } from '@components/Loading'; +import { useWallet } from '@contexts/wallet-context'; import { formatAmount, toDollarsFormatted } from '@lib/formatting'; import type { SupportedCurrency } from 'currencies'; import { isNil } from 'ramda'; import { useState } from 'react'; import { CURRENCY_BINDINGS } from 'src/currency-bindings'; -import { useWallet } from 'src/stellar-wallet'; export interface AssetsModalProps { modalId: string; diff --git a/src/components/WalletCard/LoansModal.tsx b/src/components/WalletCard/LoansModal.tsx index 149d3e9..1795300 100644 --- a/src/components/WalletCard/LoansModal.tsx +++ b/src/components/WalletCard/LoansModal.tsx @@ -3,11 +3,11 @@ import { useState } from 'react'; import { Button } from '@components/Button'; import { Loading } from '@components/Loading'; +import { useWallet } from '@contexts/wallet-context'; import { contractClient as loanManagerClient } from '@contracts/loan_manager'; import { formatAmount, toDollarsFormatted } from '@lib/formatting'; import type { SupportedCurrency } from 'currencies'; import { CURRENCY_BINDINGS } from 'src/currency-bindings'; -import { useWallet } from 'src/stellar-wallet'; export interface AssetsModalProps { modalId: string; diff --git a/src/components/WalletCard/WalletCard.tsx b/src/components/WalletCard/WalletCard.tsx index 5239939..2761df7 100644 --- a/src/components/WalletCard/WalletCard.tsx +++ b/src/components/WalletCard/WalletCard.tsx @@ -1,8 +1,8 @@ import { Loading } from '@components/Loading'; +import { type PositionsRecord, type PriceRecord, useWallet } from '@contexts/wallet-context'; import { formatCentAmount, toCents } from '@lib/formatting'; import type { SupportedCurrency } from 'currencies'; import { isNil } from 'ramda'; -import { type PositionsRecord, type PriceRecord, useWallet } from 'src/stellar-wallet'; import { Button } from '../Button'; import { Card } from '../Card'; import Identicon from '../Identicon'; diff --git a/src/stellar-wallet.tsx b/src/contexts/wallet-context.tsx similarity index 99% rename from src/stellar-wallet.tsx rename to src/contexts/wallet-context.tsx index 07231ce..06a2b2f 100644 --- a/src/stellar-wallet.tsx +++ b/src/contexts/wallet-context.tsx @@ -5,7 +5,7 @@ import { type PropsWithChildren, createContext, useContext, useEffect, useState import { contractClient as loanManagerClient } from '@contracts/loan_manager'; import { getBalances } from '@lib/horizon'; import { type SupportedCurrency, isSupportedCurrency } from 'currencies'; -import { CURRENCY_BINDINGS_ARR } from './currency-bindings'; +import { CURRENCY_BINDINGS_ARR } from '../currency-bindings'; const WALLET_TIMEOUT_DAYS = 3; diff --git a/src/pages/_borrow/BorrowModal/BorrowModal.tsx b/src/pages/_borrow/BorrowModal/BorrowModal.tsx index 604dc48..419b2cd 100644 --- a/src/pages/_borrow/BorrowModal/BorrowModal.tsx +++ b/src/pages/_borrow/BorrowModal/BorrowModal.tsx @@ -1,5 +1,5 @@ +import { useWallet } from '@contexts/wallet-context'; import type { CurrencyBinding } from 'src/currency-bindings'; -import { useWallet } from 'src/stellar-wallet'; import { BorrowStep } from './BorrowStep'; import { TrustLineStep } from './TrustlineStep'; diff --git a/src/pages/_borrow/BorrowModal/BorrowStep.tsx b/src/pages/_borrow/BorrowModal/BorrowStep.tsx index 07b74ce..9e47072 100644 --- a/src/pages/_borrow/BorrowModal/BorrowStep.tsx +++ b/src/pages/_borrow/BorrowModal/BorrowStep.tsx @@ -1,13 +1,13 @@ import { Button } from '@components/Button'; import { CryptoAmountSelector } from '@components/CryptoAmountSelector'; import { Loading } from '@components/Loading'; +import { type BalanceRecord, type PriceRecord, type Wallet, useWallet } from '@contexts/wallet-context'; import { contractClient as loanManagerClient } from '@contracts/loan_manager'; import { getIntegerPart, to7decimals } from '@lib/converters'; import { SCALAR_7, fromCents, toCents } from '@lib/formatting'; import type { SupportedCurrency } from 'currencies'; import { type ChangeEvent, useState } from 'react'; import { CURRENCY_BINDINGS, CURRENCY_BINDINGS_ARR, type CurrencyBinding } from 'src/currency-bindings'; -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; diff --git a/src/pages/_borrow/BorrowModal/TrustlineStep.tsx b/src/pages/_borrow/BorrowModal/TrustlineStep.tsx index 3a48049..8b7138d 100644 --- a/src/pages/_borrow/BorrowModal/TrustlineStep.tsx +++ b/src/pages/_borrow/BorrowModal/TrustlineStep.tsx @@ -1,9 +1,10 @@ +import { useState } from 'react'; + import { Button } from '@components/Button'; import { Loading } from '@components/Loading'; +import type { SignTransaction, Wallet } from '@contexts/wallet-context'; 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; diff --git a/src/pages/_borrow/BorrowableAsset.tsx b/src/pages/_borrow/BorrowableAsset.tsx index ea11e14..100bfbe 100644 --- a/src/pages/_borrow/BorrowableAsset.tsx +++ b/src/pages/_borrow/BorrowableAsset.tsx @@ -3,11 +3,11 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { Button } from '@components/Button'; import { Loading } from '@components/Loading'; +import { useWallet } from '@contexts/wallet-context'; import { contractClient as loanManagerClient } from '@contracts/loan_manager'; import { isBalanceZero } from '@lib/converters'; import { formatAPR } from '@lib/formatting'; import type { CurrencyBinding } from 'src/currency-bindings'; -import { useWallet } from 'src/stellar-wallet'; import { BorrowModal } from './BorrowModal/BorrowModal'; interface BorrowableAssetCardProps { diff --git a/src/pages/_lend/DepositModal.tsx b/src/pages/_lend/DepositModal.tsx index 19bb3bd..93f41ee 100644 --- a/src/pages/_lend/DepositModal.tsx +++ b/src/pages/_lend/DepositModal.tsx @@ -1,11 +1,11 @@ import { Button } from '@components/Button'; import { CryptoAmountSelector } from '@components/CryptoAmountSelector'; import { Loading } from '@components/Loading'; +import { useWallet } from '@contexts/wallet-context'; 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'; export interface DepositModalProps { modalId: string; diff --git a/src/pages/_lend/LendableAsset.tsx b/src/pages/_lend/LendableAsset.tsx index 6425a79..c033df1 100644 --- a/src/pages/_lend/LendableAsset.tsx +++ b/src/pages/_lend/LendableAsset.tsx @@ -1,11 +1,11 @@ import { Button } from '@components/Button'; import { Loading } from '@components/Loading'; +import { type Balance, useWallet } from '@contexts/wallet-context'; import { isBalanceZero } from '@lib/converters'; import { formatAPY, formatAmount, toDollarsFormatted } from '@lib/formatting'; import { isNil } from 'ramda'; import { useCallback, useEffect, useState } from 'react'; import type { CurrencyBinding } from 'src/currency-bindings'; -import { type Balance, useWallet } from 'src/stellar-wallet'; import { DepositModal } from './DepositModal'; export interface LendableAssetProps { diff --git a/tsconfig.json b/tsconfig.json index d6e5b62..b755c04 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,8 @@ "@components/*": ["src/components/*"], "@pages/*": ["src/pages/*"], "@contracts/*": ["src/contracts/*"], - "@lib/*": ["src/lib/*"] + "@lib/*": ["src/lib/*"], + "@contexts/*": ["src/contexts/*"] } } } From f86f49105841f0e96a9daf48e3fb15d0b8b42477 Mon Sep 17 00:00:00 2001 From: Konsta Purtsi Date: Sat, 7 Dec 2024 12:34:23 +0200 Subject: [PATCH 2/3] refactor: Create a new PoolContext for holding Pool related data. --- src/App.tsx | 5 +- src/components/WalletCard/AssetsModal.tsx | 4 +- src/components/WalletCard/LoansModal.tsx | 4 +- src/components/WalletCard/WalletCard.tsx | 4 +- src/contexts/pool-context.tsx | 97 ++++++++++++++ src/contexts/wallet-context.tsx | 39 +----- src/pages/_borrow/BorrowModal/BorrowModal.tsx | 4 +- src/pages/_borrow/BorrowableAsset.tsx | 119 +++--------------- src/pages/_lend/DepositModal.tsx | 4 +- src/pages/_lend/LendableAsset.tsx | 67 ++-------- 10 files changed, 149 insertions(+), 198 deletions(-) create mode 100644 src/contexts/pool-context.tsx diff --git a/src/App.tsx b/src/App.tsx index 97cfb0d..26620b2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import { Outlet, RouterProvider, createBrowserRouter } from 'react-router-dom'; import Footer from '@components/Footer'; import Nav from '@components/Nav'; +import { PoolProvider } from '@contexts/pool-context'; import { WalletProvider } from '@contexts/wallet-context'; import BorrowPage from '@pages/_borrow/BorrowPage'; import LandingPage from '@pages/_landing/LandingPage'; @@ -39,7 +40,9 @@ const App = () => { return ( - + + + ); diff --git a/src/components/WalletCard/AssetsModal.tsx b/src/components/WalletCard/AssetsModal.tsx index 2738dfb..6e6d0e9 100644 --- a/src/components/WalletCard/AssetsModal.tsx +++ b/src/components/WalletCard/AssetsModal.tsx @@ -1,5 +1,6 @@ import { Button } from '@components/Button'; import { Loading } from '@components/Loading'; +import { usePools } from '@contexts/pool-context'; import { useWallet } from '@contexts/wallet-context'; import { formatAmount, toDollarsFormatted } from '@lib/formatting'; import type { SupportedCurrency } from 'currencies'; @@ -55,7 +56,8 @@ interface TableRowProps { } const TableRow = ({ receivables, ticker }: TableRowProps) => { - const { wallet, prices, signTransaction, refetchBalances } = useWallet(); + const { wallet, signTransaction, refetchBalances } = useWallet(); + const { prices } = usePools(); const [isWithdrawing, setIsWithdrawing] = useState(false); if (receivables === 0n) return null; diff --git a/src/components/WalletCard/LoansModal.tsx b/src/components/WalletCard/LoansModal.tsx index 1795300..2cf705b 100644 --- a/src/components/WalletCard/LoansModal.tsx +++ b/src/components/WalletCard/LoansModal.tsx @@ -3,6 +3,7 @@ import { useState } from 'react'; import { Button } from '@components/Button'; import { Loading } from '@components/Loading'; +import { usePools } from '@contexts/pool-context'; import { useWallet } from '@contexts/wallet-context'; import { contractClient as loanManagerClient } from '@contracts/loan_manager'; import { formatAmount, toDollarsFormatted } from '@lib/formatting'; @@ -57,7 +58,8 @@ interface TableRowProps { } const TableRow = ({ liabilities, ticker }: TableRowProps) => { - const { wallet, prices, signTransaction, refetchBalances } = useWallet(); + const { wallet, signTransaction, refetchBalances } = useWallet(); + const { prices } = usePools(); const [isRepaying, setIsRepaying] = useState(false); if (liabilities === 0n) return null; diff --git a/src/components/WalletCard/WalletCard.tsx b/src/components/WalletCard/WalletCard.tsx index 2761df7..b37408f 100644 --- a/src/components/WalletCard/WalletCard.tsx +++ b/src/components/WalletCard/WalletCard.tsx @@ -1,4 +1,5 @@ import { Loading } from '@components/Loading'; +import { usePools } from '@contexts/pool-context'; import { type PositionsRecord, type PriceRecord, useWallet } from '@contexts/wallet-context'; import { formatCentAmount, toCents } from '@lib/formatting'; import type { SupportedCurrency } from 'currencies'; @@ -13,7 +14,8 @@ const ASSET_MODAL_ID = 'assets-modal'; const LOANS_MODAL_ID = 'loans-modal'; const WalletCard = () => { - const { wallet, openConnectWalletModal, positions, prices } = useWallet(); + const { wallet, openConnectWalletModal, positions } = useWallet(); + const { prices } = usePools(); if (!wallet) { return ( diff --git a/src/contexts/pool-context.tsx b/src/contexts/pool-context.tsx new file mode 100644 index 0000000..7898750 --- /dev/null +++ b/src/contexts/pool-context.tsx @@ -0,0 +1,97 @@ +import { type PropsWithChildren, createContext, useCallback, useContext, useEffect, useState } from 'react'; + +import { contractClient as loanManagerClient } from '@contracts/loan_manager'; +import type { SupportedCurrency } from 'currencies'; +import { CURRENCY_BINDINGS } from 'src/currency-bindings'; + +export type PriceRecord = { + [K in SupportedCurrency]: bigint; +}; + +export type PoolState = { + totalBalance: bigint; + availableBalance: bigint; + apr: bigint; +}; + +export type PoolRecord = { + [K in SupportedCurrency]: PoolState; +}; + +export type PoolContext = { + prices: PriceRecord | null; + pools: PoolRecord | null; + refetchPools: () => void; +}; + +const Context = createContext({ + prices: null, + pools: null, + refetchPools: () => {}, +}); + +const fetchAllPrices = async (): Promise => { + const [XLM, wBTC, wETH, USDC, EURC] = await Promise.all([ + fetchPriceData('XLM'), + fetchPriceData('BTC'), + fetchPriceData('ETH'), + fetchPriceData('USDC'), + fetchPriceData('EURC'), + ]); + return { XLM, wBTC, wETH, USDC, EURC }; +}; + +const fetchPriceData = async (ticker: string): Promise => { + try { + const { result } = await loanManagerClient.get_price({ token: ticker }); + return result; + } catch (error) { + console.error(`Error fetching price data: for ${ticker}`, error); + return 0n; + } +}; + +const fetchPoolState = async (ticker: SupportedCurrency): Promise => { + const { contractClient } = CURRENCY_BINDINGS[ticker]; + const { result: totalBalance } = await contractClient.get_contract_balance(); + const { result: availableBalance } = await contractClient.get_available_balance(); + const { result: apr } = await contractClient.get_interest(); + return { totalBalance, availableBalance, apr }; +}; + +const fetchPools = async (): Promise => { + const [XLM, wBTC, wETH, USDC, EURC] = await Promise.all([ + fetchPoolState('XLM'), + fetchPoolState('wBTC'), + fetchPoolState('wETH'), + fetchPoolState('USDC'), + fetchPoolState('EURC'), + ]); + return { XLM, wBTC, wETH, USDC, EURC }; +}; + +export const PoolProvider = ({ children }: PropsWithChildren) => { + const [prices, setPrices] = useState(null); + const [pools, setPools] = useState(null); + + const refetchPools = useCallback(() => { + fetchAllPrices() + .then((res) => setPrices(res)) + .catch((err) => console.error('Error fetching prices', err)); + fetchPools() + .then((res) => setPools(res)) + .catch((err) => console.error('Error fetching pools', err)); + }, []); + + useEffect(() => { + refetchPools(); + + // Set up a timer for every ledger (~6 secs) to refetch state. + const intervalId = setInterval(refetchPools, 6000); + return () => clearInterval(intervalId); + }, [refetchPools]); + + return {children}; +}; + +export const usePools = (): PoolContext => useContext(Context); diff --git a/src/contexts/wallet-context.tsx b/src/contexts/wallet-context.tsx index 06a2b2f..9369328 100644 --- a/src/contexts/wallet-context.tsx +++ b/src/contexts/wallet-context.tsx @@ -1,8 +1,7 @@ import { FREIGHTER_ID, StellarWalletsKit, WalletNetwork, allowAllModules } from '@creit.tech/stellar-wallets-kit'; import type * as StellarSdk from '@stellar/stellar-sdk'; -import { type PropsWithChildren, createContext, useContext, useEffect, useState } from 'react'; +import { type PropsWithChildren, createContext, useCallback, useContext, useEffect, useState } from 'react'; -import { contractClient as loanManagerClient } from '@contracts/loan_manager'; import { getBalances } from '@lib/horizon'; import { type SupportedCurrency, isSupportedCurrency } from 'currencies'; import { CURRENCY_BINDINGS_ARR } from '../currency-bindings'; @@ -41,7 +40,6 @@ export type WalletContext = { wallet: Wallet | null; walletBalances: BalanceRecord | null; positions: PositionsRecord; - prices: PriceRecord | null; openConnectWalletModal: () => void; disconnectWallet: () => void; refetchBalances: () => void; @@ -66,7 +64,6 @@ const Context = createContext({ wallet: null, walletBalances: null, positions: {}, - prices: null, openConnectWalletModal: () => {}, disconnectWallet: () => {}, refetchBalances: () => {}, @@ -114,27 +111,6 @@ const createBalanceRecord = (balances: StellarSdk.Horizon.HorizonApi.BalanceLine } as BalanceRecord, ); -const fetchAllPrices = async (): Promise => { - const [XLM, wBTC, wETH, USDC, EURC] = await Promise.all([ - fetchPriceData('XLM'), - fetchPriceData('BTC'), - fetchPriceData('ETH'), - fetchPriceData('USDC'), - fetchPriceData('EURC'), - ]); - return { XLM, wBTC, wETH, USDC, EURC }; -}; - -const fetchPriceData = async (token: string): Promise => { - try { - const { result } = await loanManagerClient.get_price({ token }); - return result; - } catch (error) { - console.error(`Error fetching price data: for ${token}`, error); - return 0n; - } -}; - interface WalletState { name: string; timeout: Date; @@ -163,9 +139,8 @@ export const WalletProvider = ({ children }: PropsWithChildren) => { const [wallet, setWallet] = useState(null); const [walletBalances, setWalletBalances] = useState(null); const [positions, setPositions] = useState({}); - const [prices, setPrices] = useState(null); - const loadWallet = async (name: string) => { + const loadWallet = useCallback(async (name: string) => { try { const { address } = await kit.getAddress(); setWallet(createWalletObj(name, address)); @@ -180,21 +155,16 @@ export const WalletProvider = ({ children }: PropsWithChildren) => { console.error('Loading wallet failed', err); deleteWalletState(); } - }; + }, []); // Set initial wallet on load. - // biome-ignore lint: useEffect is ass useEffect(() => { const walletState = loadWalletState(); if (walletState && new Date().getTime() < walletState.timeout.getTime()) { loadWallet(walletState.name); } - - fetchAllPrices() - .then((res) => setPrices(res)) - .catch((err) => console.error('Error fetching prices', err)); - }, []); + }, [loadWallet]); const signTransaction: SignTransaction = async (tx, opts) => { return kit.signTransaction(tx, opts); @@ -234,7 +204,6 @@ export const WalletProvider = ({ children }: PropsWithChildren) => { wallet, walletBalances, positions, - prices, openConnectWalletModal, disconnectWallet, refetchBalances, diff --git a/src/pages/_borrow/BorrowModal/BorrowModal.tsx b/src/pages/_borrow/BorrowModal/BorrowModal.tsx index 419b2cd..f8f9f32 100644 --- a/src/pages/_borrow/BorrowModal/BorrowModal.tsx +++ b/src/pages/_borrow/BorrowModal/BorrowModal.tsx @@ -1,3 +1,4 @@ +import { usePools } from '@contexts/pool-context'; import { useWallet } from '@contexts/wallet-context'; import type { CurrencyBinding } from 'src/currency-bindings'; import { BorrowStep } from './BorrowStep'; @@ -12,7 +13,8 @@ export interface BorrowModalProps { export const BorrowModal = ({ modalId, onClose, currency, totalSupplied }: BorrowModalProps) => { const { name, ticker } = currency; - const { wallet, walletBalances, signTransaction, refetchBalances, prices } = useWallet(); + const { wallet, walletBalances, signTransaction, refetchBalances } = useWallet(); + const { prices } = usePools(); const closeModal = () => { refetchBalances(); diff --git a/src/pages/_borrow/BorrowableAsset.tsx b/src/pages/_borrow/BorrowableAsset.tsx index 100bfbe..c9e219f 100644 --- a/src/pages/_borrow/BorrowableAsset.tsx +++ b/src/pages/_borrow/BorrowableAsset.tsx @@ -1,12 +1,12 @@ import { isNil } from 'ramda'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useMemo } from 'react'; import { Button } from '@components/Button'; import { Loading } from '@components/Loading'; +import { usePools } from '@contexts/pool-context'; import { useWallet } from '@contexts/wallet-context'; -import { contractClient as loanManagerClient } from '@contracts/loan_manager'; import { isBalanceZero } from '@lib/converters'; -import { formatAPR } from '@lib/formatting'; +import { formatAPR, formatAmount, toDollarsFormatted } from '@lib/formatting'; import type { CurrencyBinding } from 'src/currency-bindings'; import { BorrowModal } from './BorrowModal/BorrowModal'; @@ -15,15 +15,14 @@ interface BorrowableAssetCardProps { } export const BorrowableAsset = ({ currency }: BorrowableAssetCardProps) => { - const { icon, name, ticker, contractClient } = currency; + const { icon, name, ticker } = currency; const modalId = `borrow-modal-${ticker}`; const { wallet, walletBalances } = useWallet(); - - const [poolAPR, setPoolAPR] = useState(null); - const [totalSupplied, setTotalSupplied] = useState(null); - const [totalSuppliedPrice, setTotalSuppliedPrice] = useState(null); + const { prices, pools } = usePools(); + const price = prices?.[ticker]; + const pool = pools?.[ticker]; // Does the user have some other token in their wallet to use as a collateral? const isCollateral = !walletBalances @@ -32,92 +31,7 @@ export const BorrowableAsset = ({ currency }: BorrowableAssetCardProps) => { .filter(([t, _b]) => t !== ticker) .some(([_t, b]) => b.trustLine && !isBalanceZero(b.balanceLine.balance)); - const borrowDisabled = !wallet || !isCollateral || !totalSupplied; - - const formatPoolAPR = useCallback((apr: bigint | null) => { - if (apr === null) return ; - return formatAPR(apr); - }, []); - - const fetchAvailableContractBalance = useCallback(async () => { - if (!contractClient) return; - - try { - const { result } = await contractClient.get_available_balance(); - setTotalSupplied(result); - } catch (error) { - console.error('Error fetching contract data:', error); - } - }, [contractClient]); // Dependency on loanPoolContract - - const fetchPoolAPR = useCallback(async () => { - if (!contractClient) return; - - try { - const { result } = await contractClient.get_interest(); - setPoolAPR(result); - } catch (error) { - console.error('Error fetching APR data', error); - } - }, [contractClient]); - - const formatSuppliedAmount = useCallback((amount: bigint | null) => { - if (amount === BigInt(0)) return '0'; - if (!amount) return ; - - const ten_k = BigInt(10_000 * 10_000_000); - const one_m = BigInt(1_000_000 * 10_000_000); - switch (true) { - case amount > one_m: - return `${(Number(amount) / (1_000_000 * 10_000_000)).toFixed(2)}M`; - case amount > ten_k: - return `${(Number(amount) / (1_000 * 10_000_000)).toFixed(1)}K`; - default: - return `${(Number(amount) / 10_000_000).toFixed(1)}`; - } - }, []); - - const fetchPriceData = useCallback(async () => { - if (!loanManagerClient) return; - - try { - const { result } = await loanManagerClient.get_price({ token: currency.ticker }); - setTotalSuppliedPrice(result); - } catch (error) { - console.error('Error fetching price data:', error); - } - }, [currency.ticker]); - - const formatSuppliedAmountPrice = useCallback( - (price: bigint | null) => { - if (totalSupplied === BigInt(0)) return '$0'; - if (!totalSupplied || !price) return null; - - const ten_k = BigInt(10_000 * 10_000_000); - const one_m = BigInt(1_000_000 * 10_000_000); - const total_price = ((price / BigInt(10_000_000)) * totalSupplied) / BigInt(10_000_000); - switch (true) { - case total_price > one_m: - return `$${(Number(total_price) / (1_000_000 * 10_000_000)).toFixed(2)}M`; - case total_price > ten_k: - return `$${(Number(total_price) / (1_000 * 10_000_000)).toFixed(1)}K`; - default: - return `$${(Number(total_price) / 10_000_000).toFixed(1)}`; - } - }, - [totalSupplied], - ); - - useEffect(() => { - // Fetch contract data immediately and set an interval to run every 6 seconds - fetchAvailableContractBalance(); - fetchPriceData(); - fetchPoolAPR(); - const intervalId = setInterval(fetchAvailableContractBalance, 6000); - - // Cleanup function to clear the interval on component unmount - return () => clearInterval(intervalId); - }, [fetchAvailableContractBalance, fetchPriceData, fetchPoolAPR]); // Now dependent on the memoized function + const borrowDisabled = !wallet || !isCollateral || !pool || pool.availableBalance === 0n; const openModal = () => { const modalEl = document.getElementById(modalId) as HTMLDialogElement; @@ -130,11 +44,12 @@ export const BorrowableAsset = ({ currency }: BorrowableAssetCardProps) => { }; const tooltip = useMemo(() => { - if (!totalSupplied) return 'The pool has no assets to borrow'; + if (!pool) return 'The pool is loading'; + if (pool.availableBalance === 0n) return 'the pool has no assets to borrow'; if (!wallet) return 'Connect a wallet first'; if (!isCollateral) return 'Another token needed for the collateral'; return 'Something odd happened.'; - }, [totalSupplied, wallet, isCollateral]); + }, [pool, wallet, isCollateral]); return ( @@ -148,12 +63,14 @@ export const BorrowableAsset = ({ currency }: BorrowableAssetCardProps) => { -

{formatSuppliedAmount(totalSupplied)}

-

{formatSuppliedAmountPrice(totalSuppliedPrice)}

+

+ {pool ? formatAmount(pool.availableBalance) : } +

+

{pool && price ? toDollarsFormatted(price, pool.availableBalance) : null}

-

{formatPoolAPR(poolAPR)}

+

{pool ? formatAPR(pool.apr) : }

@@ -167,8 +84,8 @@ export const BorrowableAsset = ({ currency }: BorrowableAssetCardProps) => { )} - {!isNil(totalSupplied) && ( - + {!isNil(pool) && ( + )} ); diff --git a/src/pages/_lend/DepositModal.tsx b/src/pages/_lend/DepositModal.tsx index 93f41ee..fe310c3 100644 --- a/src/pages/_lend/DepositModal.tsx +++ b/src/pages/_lend/DepositModal.tsx @@ -1,6 +1,7 @@ import { Button } from '@components/Button'; import { CryptoAmountSelector } from '@components/CryptoAmountSelector'; import { Loading } from '@components/Loading'; +import { usePools } from '@contexts/pool-context'; import { useWallet } from '@contexts/wallet-context'; import { getIntegerPart, to7decimals } from '@lib/converters'; import { SCALAR_7, toCents } from '@lib/formatting'; @@ -16,7 +17,8 @@ export interface DepositModalProps { export const DepositModal = ({ modalId, onClose, currency }: DepositModalProps) => { const { contractClient, name, ticker } = currency; - const { wallet, walletBalances, prices, signTransaction, refetchBalances } = useWallet(); + const { wallet, walletBalances, signTransaction, refetchBalances } = useWallet(); + const { prices } = usePools(); const [isDepositing, setIsDepositing] = useState(false); const [amount, setAmount] = useState('0'); diff --git a/src/pages/_lend/LendableAsset.tsx b/src/pages/_lend/LendableAsset.tsx index c033df1..4b77373 100644 --- a/src/pages/_lend/LendableAsset.tsx +++ b/src/pages/_lend/LendableAsset.tsx @@ -1,10 +1,10 @@ import { Button } from '@components/Button'; import { Loading } from '@components/Loading'; +import { usePools } from '@contexts/pool-context'; import { type Balance, useWallet } from '@contexts/wallet-context'; import { isBalanceZero } from '@lib/converters'; import { formatAPY, formatAmount, toDollarsFormatted } from '@lib/formatting'; import { isNil } from 'ramda'; -import { useCallback, useEffect, useState } from 'react'; import type { CurrencyBinding } from 'src/currency-bindings'; import { DepositModal } from './DepositModal'; @@ -13,11 +13,11 @@ export interface LendableAssetProps { } export const LendableAsset = ({ currency }: LendableAssetProps) => { - const { icon, name, ticker, contractClient } = currency; + const { icon, name, ticker } = currency; - const { wallet, walletBalances, prices } = useWallet(); - const [totalSupplied, setTotalSupplied] = useState(null); - const [poolAPR, setPoolAPR] = useState(null); + const { wallet, walletBalances } = useWallet(); + const { prices, pools, refetchPools } = usePools(); + const pool = pools?.[ticker]; const price = prices?.[ticker]; @@ -27,53 +27,6 @@ export const LendableAsset = ({ currency }: LendableAssetProps) => { const isPoor = !balance?.trustLine || isBalanceZero(balance.balanceLine.balance); - const fetchPoolAPR = useCallback(async () => { - if (!contractClient) return; - - try { - const { result } = await contractClient.get_interest(); - setPoolAPR(result); - } catch (error) { - console.error('Error fetching APR data', error); - } - }, [contractClient]); - - const formatPoolAPY = useCallback((apr: bigint | null) => { - if (apr === null) return ; - return formatAPY(apr); - }, []); - - const fetchAvailableContractBalance = useCallback(async () => { - if (!contractClient) return; - - try { - const { result } = await contractClient.get_contract_balance(); - setTotalSupplied(result); - } catch (error) { - console.error('Error fetching contract data:', error); - } - }, [contractClient]); // Dependency on loanPoolContract - - const formatSuppliedAmount = useCallback((amount: bigint | null) => { - if (amount === null) return ; - return formatAmount(amount); - }, []); - - useEffect(() => { - // Fetch contract data immediately and set an interval to run every 6 seconds - function fetch() { - fetchAvailableContractBalance(); - fetchPoolAPR(); - } - - fetch(); - - const intervalId = setInterval(fetch, 6000); - - // Cleanup function to clear the interval on component unmount - return () => clearInterval(intervalId); - }, [fetchAvailableContractBalance, fetchPoolAPR]); // Now dependent on the memoized function - const openModal = () => { const modalEl = document.getElementById(modalId) as HTMLDialogElement; modalEl.showModal(); @@ -82,7 +35,7 @@ export const LendableAsset = ({ currency }: LendableAssetProps) => { const closeModal = () => { const modalEl = document.getElementById(modalId) as HTMLDialogElement; modalEl.close(); - fetchAvailableContractBalance(); + refetchPools(); }; return ( @@ -97,12 +50,14 @@ export const LendableAsset = ({ currency }: LendableAssetProps) => { -

{formatSuppliedAmount(totalSupplied)}

-

{!isNil(price) && !isNil(totalSupplied) && toDollarsFormatted(price, totalSupplied)}

+

+ {pool ? formatAmount(pool.totalBalance) : } +

+

{!isNil(price) && !isNil(pool) && toDollarsFormatted(price, pool.totalBalance)}

-

{formatPoolAPY(poolAPR)}

+

{pool ? formatAPY(pool.apr) : }

From b46e58c87fa24ed5bf0da34f0804e607a14aa13f Mon Sep 17 00:00:00 2001 From: Konsta Purtsi Date: Sun, 8 Dec 2024 09:29:18 +0200 Subject: [PATCH 3/3] feat: Show apr in borrow modal (and little refactoring) --- src/pages/_borrow/BorrowModal/BorrowModal.tsx | 29 ++++--------------- src/pages/_borrow/BorrowModal/BorrowStep.tsx | 25 ++++++++-------- .../_borrow/BorrowModal/TrustlineStep.tsx | 11 +++---- src/pages/_borrow/BorrowableAsset.tsx | 4 +-- 4 files changed, 24 insertions(+), 45 deletions(-) diff --git a/src/pages/_borrow/BorrowModal/BorrowModal.tsx b/src/pages/_borrow/BorrowModal/BorrowModal.tsx index f8f9f32..dde8a27 100644 --- a/src/pages/_borrow/BorrowModal/BorrowModal.tsx +++ b/src/pages/_borrow/BorrowModal/BorrowModal.tsx @@ -1,4 +1,3 @@ -import { usePools } from '@contexts/pool-context'; import { useWallet } from '@contexts/wallet-context'; import type { CurrencyBinding } from 'src/currency-bindings'; import { BorrowStep } from './BorrowStep'; @@ -8,45 +7,27 @@ export interface BorrowModalProps { modalId: string; onClose: () => void; currency: CurrencyBinding; - totalSupplied: bigint; } -export const BorrowModal = ({ modalId, onClose, currency, totalSupplied }: BorrowModalProps) => { +export const BorrowModal = ({ modalId, onClose, currency }: BorrowModalProps) => { const { name, ticker } = currency; - const { wallet, walletBalances, signTransaction, refetchBalances } = useWallet(); - const { prices } = usePools(); + const { walletBalances, refetchBalances } = 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; + const isTrustline = walletBalances?.[ticker].trustLine; return (

Borrow {name}

{!isTrustline ? ( - + ) : ( - + )}
{/* Invisible backdrop that closes the modal on click */} diff --git a/src/pages/_borrow/BorrowModal/BorrowStep.tsx b/src/pages/_borrow/BorrowModal/BorrowStep.tsx index 9e47072..2ac10aa 100644 --- a/src/pages/_borrow/BorrowModal/BorrowStep.tsx +++ b/src/pages/_borrow/BorrowModal/BorrowStep.tsx @@ -1,10 +1,11 @@ import { Button } from '@components/Button'; import { CryptoAmountSelector } from '@components/CryptoAmountSelector'; import { Loading } from '@components/Loading'; -import { type BalanceRecord, type PriceRecord, type Wallet, useWallet } from '@contexts/wallet-context'; +import { usePools } from '@contexts/pool-context'; +import { useWallet } from '@contexts/wallet-context'; import { contractClient as loanManagerClient } from '@contracts/loan_manager'; import { getIntegerPart, to7decimals } from '@lib/converters'; -import { SCALAR_7, fromCents, toCents } from '@lib/formatting'; +import { SCALAR_7, formatAPR, fromCents, toCents } from '@lib/formatting'; import type { SupportedCurrency } from 'currencies'; import { type ChangeEvent, useState } from 'react'; import { CURRENCY_BINDINGS, CURRENCY_BINDINGS_ARR, type CurrencyBinding } from 'src/currency-bindings'; @@ -17,21 +18,22 @@ const HEALTH_FACTOR_EXCELLENT_THRESHOLD = 2.0; export interface BorrowStepProps { onClose: () => void; currency: CurrencyBinding; - totalSupplied: bigint; - wallet: Wallet; - walletBalances: BalanceRecord; - prices: PriceRecord; } -export const BorrowStep = ({ onClose, currency, totalSupplied, wallet, walletBalances, prices }: BorrowStepProps) => { +export const BorrowStep = ({ onClose, currency }: BorrowStepProps) => { const { name, ticker, contractId: loanCurrencyId } = currency; - const { signTransaction } = useWallet(); + const { signTransaction, wallet, walletBalances } = useWallet(); + const { pools, prices } = usePools(); const [isBorrowing, setIsBorrowing] = useState(false); const [loanAmount, setLoanAmount] = useState('0'); const [collateralTicker, setCollateralTicker] = useState('XLM'); const [collateralAmount, setCollateralAmount] = useState('0'); + if (!pools || !prices || !walletBalances) return null; + + const { apr, availableBalance } = pools[ticker]; + const collateralOptions: SupportedCurrency[] = CURRENCY_BINDINGS_ARR.filter((c) => c.ticker !== ticker).map( ({ ticker }) => ticker, ); @@ -50,9 +52,6 @@ export const BorrowStep = ({ onClose, currency, totalSupplied, wallet, walletBal const healthFactor = loanAmountCents && loanAmountCents > 0n ? Number(collateralAmountCents) / Number(loanAmountCents) : 0; - // TODO: get this from the contract. - const interestRate = '7.5%'; - const handleCancel = () => { setLoanAmount('0'); setCollateralAmount('0'); @@ -124,7 +123,7 @@ export const BorrowStep = ({ onClose, currency, totalSupplied, wallet, walletBal const isBorrowDisabled = !isTrustline || loanAmount === '0' || collateralAmount === '0' || healthFactor < HEALTH_FACTOR_MIN_THRESHOLD; - const maxLoan = (totalSupplied / 10_000_000n).toString(); + const maxLoan = (availableBalance / 10_000_000n).toString(); const maxCollateral = getIntegerPart(collateralBalance.trustLine ? collateralBalance.balanceLine.balance : '0'); @@ -147,7 +146,7 @@ export const BorrowStep = ({ onClose, currency, totalSupplied, wallet, walletBal 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}.

+

The annual interest rate is currently {formatAPR(apr)}.

Amount to borrow

void; currency: CurrencyBinding; - wallet: Wallet; - signTransaction: SignTransaction; - refetchBalances: () => void; } -export const TrustLineStep = ({ onClose, currency, wallet, signTransaction, refetchBalances }: TrustLineStepProps) => { +export const TrustLineStep = ({ onClose, currency }: TrustLineStepProps) => { const { name, ticker } = currency; + const { wallet, signTransaction, refetchBalances } = useWallet(); const [isCreating, setIsCreating] = useState(false); + // Modal is impossible to open without a wallet connection. + if (!wallet) return null; + const handleAddTrustlineClick = async () => { try { setIsCreating(true); diff --git a/src/pages/_borrow/BorrowableAsset.tsx b/src/pages/_borrow/BorrowableAsset.tsx index c9e219f..4d888b0 100644 --- a/src/pages/_borrow/BorrowableAsset.tsx +++ b/src/pages/_borrow/BorrowableAsset.tsx @@ -84,9 +84,7 @@ export const BorrowableAsset = ({ currency }: BorrowableAssetCardProps) => { )} - {!isNil(pool) && ( - - )} + {!isNil(pool) && } ); };