Skip to content

Commit

Permalink
Merge pull request #77 from laina-protocol/refactor/pool-context
Browse files Browse the repository at this point in the history
refactor: Move pool interactions into a React Context
  • Loading branch information
kovipu authored Dec 8, 2024
2 parents 4d3f0d8 + b46e58c commit 7594c29
Show file tree
Hide file tree
Showing 15 changed files with 183 additions and 251 deletions.
7 changes: 5 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ 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';
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 (
Expand Down Expand Up @@ -39,7 +40,9 @@ const App = () => {
return (
<React.StrictMode>
<WalletProvider>
<RouterProvider router={router} />
<PoolProvider>
<RouterProvider router={router} />
</PoolProvider>
</WalletProvider>
</React.StrictMode>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/CryptoAmountSelector.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Nav.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
6 changes: 4 additions & 2 deletions src/components/WalletCard/AssetsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
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';
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;
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions src/components/WalletCard/LoansModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ 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';
import type { SupportedCurrency } from 'currencies';
import { CURRENCY_BINDINGS } from 'src/currency-bindings';
import { useWallet } from 'src/stellar-wallet';

export interface AssetsModalProps {
modalId: string;
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions src/components/WalletCard/WalletCard.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
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';
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';
Expand All @@ -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 (
Expand Down
97 changes: 97 additions & 0 deletions src/contexts/pool-context.tsx
Original file line number Diff line number Diff line change
@@ -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<PoolContext>({
prices: null,
pools: null,
refetchPools: () => {},
});

const fetchAllPrices = async (): Promise<PriceRecord> => {
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<bigint> => {
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<PoolState> => {
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<PoolRecord> => {
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<PriceRecord | null>(null);
const [pools, setPools] = useState<PoolRecord | null>(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 <Context.Provider value={{ prices, pools, refetchPools }}>{children}</Context.Provider>;
};

export const usePools = (): PoolContext => useContext(Context);
41 changes: 5 additions & 36 deletions src/stellar-wallet.tsx → src/contexts/wallet-context.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
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';
import { CURRENCY_BINDINGS_ARR } from '../currency-bindings';

const WALLET_TIMEOUT_DAYS = 3;

Expand Down Expand Up @@ -41,7 +40,6 @@ export type WalletContext = {
wallet: Wallet | null;
walletBalances: BalanceRecord | null;
positions: PositionsRecord;
prices: PriceRecord | null;
openConnectWalletModal: () => void;
disconnectWallet: () => void;
refetchBalances: () => void;
Expand All @@ -66,7 +64,6 @@ const Context = createContext<WalletContext>({
wallet: null,
walletBalances: null,
positions: {},
prices: null,
openConnectWalletModal: () => {},
disconnectWallet: () => {},
refetchBalances: () => {},
Expand Down Expand Up @@ -114,27 +111,6 @@ const createBalanceRecord = (balances: StellarSdk.Horizon.HorizonApi.BalanceLine
} as BalanceRecord,
);

const fetchAllPrices = async (): Promise<PriceRecord> => {
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<bigint> => {
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;
Expand Down Expand Up @@ -163,9 +139,8 @@ export const WalletProvider = ({ children }: PropsWithChildren) => {
const [wallet, setWallet] = useState<Wallet | null>(null);
const [walletBalances, setWalletBalances] = useState<BalanceRecord | null>(null);
const [positions, setPositions] = useState<PositionsRecord>({});
const [prices, setPrices] = useState<PriceRecord | null>(null);

const loadWallet = async (name: string) => {
const loadWallet = useCallback(async (name: string) => {
try {
const { address } = await kit.getAddress();
setWallet(createWalletObj(name, address));
Expand All @@ -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);
Expand Down Expand Up @@ -234,7 +204,6 @@ export const WalletProvider = ({ children }: PropsWithChildren) => {
wallet,
walletBalances,
positions,
prices,
openConnectWalletModal,
disconnectWallet,
refetchBalances,
Expand Down
29 changes: 6 additions & 23 deletions src/pages/_borrow/BorrowModal/BorrowModal.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,33 @@
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';

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, prices } = useWallet();
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 (
<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}
/>
<TrustLineStep onClose={closeModal} currency={currency} />
) : (
<BorrowStep
onClose={closeModal}
currency={currency}
totalSupplied={totalSupplied}
wallet={wallet}
walletBalances={walletBalances}
prices={prices}
/>
<BorrowStep onClose={closeModal} currency={currency} />
)}
</div>
{/* Invisible backdrop that closes the modal on click */}
Expand Down
Loading

0 comments on commit 7594c29

Please sign in to comment.