From 85aebf97a9fc6e2e117f9473ebb585d1d221a82b Mon Sep 17 00:00:00 2001 From: Konsta Purtsi Date: Thu, 12 Sep 2024 10:39:13 +0300 Subject: [PATCH 1/3] feat: Add balances to the state. --- src/stellar-wallet.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/stellar-wallet.tsx b/src/stellar-wallet.tsx index d7ce17c6..ff9623cf 100644 --- a/src/stellar-wallet.tsx +++ b/src/stellar-wallet.tsx @@ -1,5 +1,10 @@ import { FREIGHTER_ID, StellarWalletsKit, WalletNetwork, allowAllModules } from '@creit.tech/stellar-wallets-kit'; import { type PropsWithChildren, createContext, useContext, useState } from 'react'; +import * as StellarSdk from '@stellar/stellar-sdk'; + +const HorizonServer = new StellarSdk.Horizon.Server("https://horizon-testnet.stellar.org/"); + +export type Balance = StellarSdk.Horizon.HorizonApi.BalanceLine export type Wallet = { address: string; @@ -8,6 +13,7 @@ export type Wallet = { export type WalletContext = { wallet: Wallet | null; + balances: Balance[]; openConnectWalletModal: () => void; signTransaction: SignTransaction; }; @@ -24,8 +30,9 @@ type SignTransaction = ( type XDR_BASE64 = string; const Context = createContext({ - openConnectWalletModal: () => {}, wallet: null, + balances: [], + openConnectWalletModal: () => { }, signTransaction: () => Promise.reject(), }); @@ -42,6 +49,7 @@ const createWalletObj = (address: string): Wallet => ({ export const WalletProvider = ({ children }: PropsWithChildren) => { const [address, setAddress] = useState(null); + const [balances, setBalances] = useState([]); const signTransaction: SignTransaction = async (tx, opts) => { const { signedTxXdr } = await kit.signTransaction(tx, opts); @@ -54,7 +62,9 @@ export const WalletProvider = ({ children }: PropsWithChildren) => { kit.setWallet(option.id); try { const { address } = await kit.getAddress(); + const { balances } = await HorizonServer.loadAccount(address) setAddress(address); + setBalances(balances); } catch (err) { console.error('Error connecting wallet: ', err); } @@ -68,6 +78,7 @@ export const WalletProvider = ({ children }: PropsWithChildren) => { Date: Thu, 12 Sep 2024 14:59:05 +0300 Subject: [PATCH 2/3] feat: Add deposit modal, install DaisyUI --- package-lock.json | 74 +++++++++++++++++- package.json | 2 + src/components/Button.tsx | 9 ++- src/currencies.ts | 5 +- src/layouts/Layout.astro | 2 +- src/lib/utils.ts | 6 ++ src/pages/_lend/DepositModal.tsx | 107 ++++++++++++++++++++++++++ src/pages/_lend/LendableAssetCard.tsx | 49 ++++++++---- src/stellar-wallet.tsx | 38 ++++++--- tailwind.config.mjs | 16 +++- 10 files changed, 270 insertions(+), 38 deletions(-) create mode 100644 src/lib/utils.ts create mode 100644 src/pages/_lend/DepositModal.tsx diff --git a/package-lock.json b/package-lock.json index 1e90ac01..60d42300 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,9 @@ "devDependencies": { "@biomejs/biome": "1.8.3", "@vitejs/plugin-basic-ssl": "^1.1.0", + "daisyui": "^4.12.10", "dotenv": "^16.4.5", + "postcss": "^8.4.45", "prettier": "^3.3.3", "prettier-plugin-astro": "^0.14.1" } @@ -4244,6 +4246,17 @@ } } }, + "node_modules/css-selector-tokenizer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz", + "integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -4262,6 +4275,36 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/culori": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz", + "integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/daisyui": { + "version": "4.12.10", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.10.tgz", + "integrity": "sha512-jp1RAuzbHhGdXmn957Z2XsTZStXGHzFfF0FgIOZj3Wv9sH7OZgLfXTRZNfKVYxltGUOBsG1kbWAdF5SrqjebvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-selector-tokenizer": "^0.8", + "culori": "^3", + "picocolors": "^1", + "postcss-js": "^4" + }, + "engines": { + "node": ">=16.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/daisyui" + } + }, "node_modules/debug": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", @@ -4699,6 +4742,13 @@ "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", "license": "MIT" }, + "node_modules/fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -7412,9 +7462,9 @@ } }, "node_modules/postcss": { - "version": "8.4.41", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", - "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "version": "8.4.45", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz", + "integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==", "funding": [ { "type": "opencollective", @@ -8852,6 +8902,24 @@ "s.color": "0.0.15" } }, + "node_modules/sugarss": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-4.0.1.tgz", + "integrity": "sha512-WCjS5NfuVJjkQzK10s8WOBY+hhDxxNt/N6ZaGwxFZ+wN3/lKKFSaaKUNecULcTTvE4urLcKaZFQD8vO0mOZujw==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", diff --git a/package.json b/package.json index 8a2fa26b..212c6f7d 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "devDependencies": { "@biomejs/biome": "1.8.3", "@vitejs/plugin-basic-ssl": "^1.1.0", + "daisyui": "^4.12.10", "dotenv": "^16.4.5", + "postcss": "^8.4.45", "prettier": "^3.3.3", "prettier-plugin-astro": "^0.14.1" }, diff --git a/src/components/Button.tsx b/src/components/Button.tsx index bc9d7dda..4632793b 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -2,14 +2,15 @@ import type { PropsWithChildren } from 'react'; import { Link } from 'react-router-dom'; export interface ButtonProps { - onClick: () => void; + onClick?: () => void; className?: string; + disabled?: boolean; } -const buttonStyle = 'bg-black font-semibold text-white rounded-full px-8 py-2 hover:bg-grey-dark transition'; +const buttonStyle = 'btn btn-neutral font-semibold text-base rounded-full px-8 py-2'; -export const Button = ({ onClick, className = '', children }: PropsWithChildren) => ( - ); diff --git a/src/currencies.ts b/src/currencies.ts index c35d6d1d..ea3f6105 100644 --- a/src/currencies.ts +++ b/src/currencies.ts @@ -2,10 +2,11 @@ import XLMPoolContract from '@contracts/loan_pool'; import USDCPoolContract from '@contracts/usdc_pool'; import StellarIcon from '@images/Stellar_Symbol.png'; import USDCIcon from '@images/usdc.svg'; +import type { SupportedCurrency } from './stellar-wallet'; export type Currency = { name: string; - symbol: string; // could be a type union of currency symbols. + symbol: SupportedCurrency; icon: string; loanPoolContract: typeof XLMPoolContract; }; @@ -23,4 +24,4 @@ export const CURRENCIES: Currency[] = [ icon: USDCIcon.src, loanPoolContract: USDCPoolContract, }, -]; +] as const; diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index 05bf6a85..27241b8a 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -12,7 +12,7 @@ const { title, description } = Astro.props; --- - + diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 00000000..9ad0df42 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/src/pages/_lend/DepositModal.tsx b/src/pages/_lend/DepositModal.tsx new file mode 100644 index 00000000..2a8b877f --- /dev/null +++ b/src/pages/_lend/DepositModal.tsx @@ -0,0 +1,107 @@ +import { Button } from '@components/Button'; +import { type ChangeEvent, useState } from 'react'; +import type { Currency } from 'src/currencies'; +import { useWallet } from 'src/stellar-wallet'; + +export interface DepositModalProps { + modalId: string; + onClose: () => void; + currency: Currency; +} + +export const DepositModal = ({ modalId, onClose, currency }: DepositModalProps) => { + const { loanPoolContract, name, symbol } = currency; + + const { wallet, balances, signTransaction } = useWallet(); + const [isDepositing, setIsDepositing] = useState(false); + const [amount, setAmount] = useState('0'); + + const balance = balances[symbol]; + + if (!balance) return null; + + const handleDepositClick = async () => { + if (!wallet) { + alert('Please connect your wallet first!'); + return; + } + + setIsDepositing(true); + + 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, + }); + + try { + const { result } = await tx.signAndSend({ signTransaction }); + alert(`Deposit successful, result: ${result}`); + onClose(); + } catch (err) { + alert(`Error depositing: ${JSON.stringify(err)}`); + } + setIsDepositing(false); + }; + + const handleAmountChange = (ev: ChangeEvent) => { + setAmount(ev.target.value); + }; + + return ( + +
+

Deposit {name}

+ +
+
+

Amount to deposit

+ +
+ | + | + | + | + | +
+
+
+ +

+ {amount} {symbol} out of {balance.balance} {symbol} +

+ +
+ + {!isDepositing ? ( + + ) : ( + + )} +
+
+ {/* Invisible backdrop that closes the modal on click */} +
+ +
+
+ ); +}; diff --git a/src/pages/_lend/LendableAssetCard.tsx b/src/pages/_lend/LendableAssetCard.tsx index 853adfa0..3071bbc7 100644 --- a/src/pages/_lend/LendableAssetCard.tsx +++ b/src/pages/_lend/LendableAssetCard.tsx @@ -5,19 +5,26 @@ import type { xdr } from '@stellar/stellar-base'; import { Api as RpcApi } from '@stellar/stellar-sdk/rpc'; import { useCallback, useEffect, useState } from 'react'; import type { Currency } from 'src/currencies'; -import { useWallet } from 'src/stellar-wallet'; +import { type Balance, useWallet } from 'src/stellar-wallet'; +import { DepositModal } from './DepositModal'; export interface LendableAssetCardProps { currency: Currency; } +const DEPOSIT_MODAL_ID = 'modal-id'; + export const LendableAssetCard = ({ currency }: LendableAssetCardProps) => { const { icon, name, symbol, loanPoolContract } = currency; - const { wallet, signTransaction } = useWallet(); + const { wallet, balances } = useWallet(); const [totalSupplied, setTotalSupplied] = useState(null); const [totalSuppliedPrice, setTotalSuppliedPrice] = useState(null); + const balance: Balance | undefined = balances[symbol]; + + const isPoor = !balance?.balance || balance.balance === '0'; + const fetchAvailableContractBalance = useCallback(async () => { if (!loanPoolContract) return; @@ -103,23 +110,24 @@ export const LendableAssetCard = ({ currency }: LendableAssetCardProps) => { return () => clearInterval(intervalId); }, [fetchAvailableContractBalance, fetchPriceData]); // Now dependent on the memoized function - const handleDepositClick = async () => { + const buttonLabel = (): string => { if (!wallet) { - alert('Please connect your wallet first!'); - return; + return 'Connect a wallet first.'; } + if (isPoor) { + return 'Not enough funds in the wallet.'; + } + return ''; + }; - loanPoolContract.options.publicKey = wallet.address; - - const amount = BigInt(2000000); - const tx = await loanPoolContract.deposit({ user: wallet.address, amount }); + const openModal = () => { + const modalEl = document.getElementById(DEPOSIT_MODAL_ID) as HTMLDialogElement; + modalEl.showModal(); + }; - try { - const { result } = await tx.signAndSend({ signTransaction }); - alert(`Deposit successful, result: ${result}`); - } catch (err) { - alert(`Error depositing: ${JSON.stringify(err)}`); - } + const closeModal = () => { + const modalEl = document.getElementById(DEPOSIT_MODAL_ID) as HTMLDialogElement; + modalEl.close(); fetchAvailableContractBalance(); }; @@ -143,7 +151,16 @@ export const LendableAssetCard = ({ currency }: LendableAssetCardProps) => {

1.23%

- {wallet && } + {isPoor ? ( +
+ +
+ ) : ( + + )} + ); }; diff --git a/src/stellar-wallet.tsx b/src/stellar-wallet.tsx index ff9623cf..5ac6c31b 100644 --- a/src/stellar-wallet.tsx +++ b/src/stellar-wallet.tsx @@ -1,19 +1,25 @@ import { FREIGHTER_ID, StellarWalletsKit, WalletNetwork, allowAllModules } from '@creit.tech/stellar-wallets-kit'; -import { type PropsWithChildren, createContext, useContext, useState } from 'react'; import * as StellarSdk from '@stellar/stellar-sdk'; +import { type PropsWithChildren, createContext, useContext, useState } from 'react'; -const HorizonServer = new StellarSdk.Horizon.Server("https://horizon-testnet.stellar.org/"); - -export type Balance = StellarSdk.Horizon.HorizonApi.BalanceLine +const HorizonServer = new StellarSdk.Horizon.Server('https://horizon-testnet.stellar.org/'); export type Wallet = { address: string; displayName: string; }; +export type SupportedCurrency = 'XLM' | 'USDC'; + +export type Balance = StellarSdk.Horizon.HorizonApi.BalanceLine; + +export type BalanceRecord = { + [K in SupportedCurrency]?: Balance; +}; + export type WalletContext = { wallet: Wallet | null; - balances: Balance[]; + balances: BalanceRecord; openConnectWalletModal: () => void; signTransaction: SignTransaction; }; @@ -31,8 +37,8 @@ type XDR_BASE64 = string; const Context = createContext({ wallet: null, - balances: [], - openConnectWalletModal: () => { }, + balances: {}, + openConnectWalletModal: () => {}, signTransaction: () => Promise.reject(), }); @@ -47,9 +53,19 @@ const createWalletObj = (address: string): Wallet => ({ displayName: `${address.slice(0, 4)}...${address.slice(-4)}`, }); +const createBalanceRecord = (balances: Balance[]): BalanceRecord => + balances.reduce((acc, balance) => { + if (balance.asset_type === 'native') { + acc.XLM = balance; + } else if (balance.asset_type === 'credit_alphanum4' && balance.asset_code === 'USDC') { + acc.USDC = balance; + } + return acc; + }, {} as BalanceRecord); + export const WalletProvider = ({ children }: PropsWithChildren) => { const [address, setAddress] = useState(null); - const [balances, setBalances] = useState([]); + const [balances, setBalances] = useState({}); const signTransaction: SignTransaction = async (tx, opts) => { const { signedTxXdr } = await kit.signTransaction(tx, opts); @@ -62,9 +78,9 @@ export const WalletProvider = ({ children }: PropsWithChildren) => { kit.setWallet(option.id); try { const { address } = await kit.getAddress(); - const { balances } = await HorizonServer.loadAccount(address) + const { balances } = await HorizonServer.loadAccount(address); setAddress(address); - setBalances(balances); + setBalances(createBalanceRecord(balances)); } catch (err) { console.error('Error connecting wallet: ', err); } @@ -78,7 +94,7 @@ export const WalletProvider = ({ children }: PropsWithChildren) => { Date: Thu, 12 Sep 2024 15:11:56 +0300 Subject: [PATCH 3/3] feat: Make background white, increase dropshadow on cards. --- src/App.tsx | 4 ++-- src/layouts/Layout.astro | 6 +++--- tailwind.config.mjs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index c7da3e50..e69e4c4f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,13 +15,13 @@ const PageWrapper = () => { const isIndex = pathname === '/'; return ( - +
); }; diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index 27241b8a..ea50ccc1 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -21,7 +21,9 @@ const { title, description } = Astro.props; {title} - + + + - - diff --git a/tailwind.config.mjs b/tailwind.config.mjs index 57002852..4ab1c685 100644 --- a/tailwind.config.mjs +++ b/tailwind.config.mjs @@ -29,7 +29,7 @@ export default { full: '9999px', }, dropShadow: { - DEFAULT: '0 4px 4px rgba(0, 0, 0, 0.25)', + DEFAULT: '0 12px 12px rgba(0, 0, 0, 0.25)', }, extend: { spacing: {