diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx index ce040842c..e4c2aba3e 100644 --- a/src/app/_layout.tsx +++ b/src/app/_layout.tsx @@ -40,7 +40,7 @@ import release from "../generated/release"; import en from "../i18n/en.json"; import es from "../i18n/es.json"; import e2e from "../utils/e2e"; -import queryClient, { persister } from "../utils/queryClient"; +import queryClient, { persistOptions } from "../utils/queryClient"; import reportError from "../utils/reportError"; import exaConfig from "../utils/wagmi/exa"; import ownerConfig from "../utils/wagmi/owner"; @@ -169,7 +169,7 @@ export default wrap(function RootLayout() { return ( - + diff --git a/src/assets/images/optimism.svg b/src/assets/images/optimism.svg deleted file mode 100644 index 765428cf6..000000000 --- a/src/assets/images/optimism.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/activity/details/ReceivedActivity.tsx b/src/components/activity/details/ReceivedActivity.tsx index 686dd6238..fe2b84c96 100644 --- a/src/components/activity/details/ReceivedActivity.tsx +++ b/src/components/activity/details/ReceivedActivity.tsx @@ -5,7 +5,6 @@ import { ArrowDownToLine } from "@tamagui/lucide-icons"; import { Square, XStack, YStack } from "tamagui"; import TransactionDetails from "./TransactionDetails"; -import assetLogos from "../../../utils/assetLogos"; import AssetLogo from "../../shared/AssetLogo"; import Text from "../../shared/Text"; @@ -38,7 +37,7 @@ export default function ReceivedActivity({ item }: { item: Omit - + diff --git a/src/components/activity/details/RepayActivity.tsx b/src/components/activity/details/RepayActivity.tsx index c17fa0599..c648772fc 100644 --- a/src/components/activity/details/RepayActivity.tsx +++ b/src/components/activity/details/RepayActivity.tsx @@ -5,7 +5,6 @@ import { ArrowUpFromLine } from "@tamagui/lucide-icons"; import { Square, XStack, YStack } from "tamagui"; import TransactionDetails from "./TransactionDetails"; -import assetLogos from "../../../utils/assetLogos"; import AssetLogo from "../../shared/AssetLogo"; import Text from "../../shared/Text"; @@ -38,7 +37,7 @@ export default function RepayActivity({ item }: { item: Omit - + diff --git a/src/components/activity/details/SentActivity.tsx b/src/components/activity/details/SentActivity.tsx index 8c6a21651..36f1367d5 100644 --- a/src/components/activity/details/SentActivity.tsx +++ b/src/components/activity/details/SentActivity.tsx @@ -7,7 +7,6 @@ import { Square, XStack, YStack } from "tamagui"; import shortenHex from "@exactly/common/shortenHex"; import TransactionDetails from "./TransactionDetails"; -import assetLogos from "../../../utils/assetLogos"; import AssetLogo from "../../shared/AssetLogo"; import Text from "../../shared/Text"; @@ -44,7 +43,7 @@ export default function SentActivity({ item }: { item: Omit - + diff --git a/src/components/activity/details/TransactionDetails.tsx b/src/components/activity/details/TransactionDetails.tsx index b3b527684..01ab6a915 100644 --- a/src/components/activity/details/TransactionDetails.tsx +++ b/src/components/activity/details/TransactionDetails.tsx @@ -13,9 +13,9 @@ import { format } from "date-fns"; import chain from "@exactly/common/generated/chain"; import shortenHex from "@exactly/common/shortenHex"; -import OptimismImage from "../../../assets/images/optimism.svg"; import openBrowser from "../../../utils/openBrowser"; import reportError from "../../../utils/reportError"; +import ChainLogo from "../../shared/ChainLogo"; import Text from "../../shared/Text"; import type { ActivityItem } from "../../../utils/queryClient"; @@ -55,7 +55,7 @@ export default function TransactionDetails({ {chain.name} - + {item?.type === "sent" && ( diff --git a/src/components/add-funds/AddCrypto.tsx b/src/components/add-funds/AddCrypto.tsx index e1b0ea7f4..45a465522 100644 --- a/src/components/add-funds/AddCrypto.tsx +++ b/src/components/add-funds/AddCrypto.tsx @@ -12,21 +12,19 @@ import chain from "@exactly/common/generated/chain"; import shortenHex from "@exactly/common/shortenHex"; import SupportedAssetsSheet from "./SupportedAssetsSheet"; -import OptimismImage from "../../assets/images/optimism.svg"; import assetLogos from "../../utils/assetLogos"; import { presentArticle } from "../../utils/intercom"; import reportError from "../../utils/reportError"; import useAccount from "../../utils/useAccount"; import AssetLogo from "../shared/AssetLogo"; +import ChainLogo from "../shared/ChainLogo"; import CopyAddressSheet from "../shared/CopyAddressSheet"; import SafeView from "../shared/SafeView"; import Button from "../shared/StyledButton"; import Text from "../shared/Text"; import View from "../shared/View"; -const supportedAssets = Object.entries(assetLogos) - .filter(([symbol]) => symbol !== "USDC.e" && symbol !== "DAI") - .map(([symbol, image]) => ({ symbol, image })); +const supportedAssets = Object.keys(assetLogos).filter((s) => s !== "USDC.e" && s !== "DAI"); export default function AddCrypto() { const router = useRouter(); @@ -128,7 +126,7 @@ export default function AddCrypto() { - + {chain.name} @@ -144,13 +142,11 @@ export default function AddCrypto() { setSupportedAssetsShown(true); }} > - {supportedAssets.map(({ symbol, image }, index) => { - return ( - - - - ); - })} + {supportedAssets.map((symbol, index) => ( + + + + ))} diff --git a/src/components/add-funds/AddFunds.tsx b/src/components/add-funds/AddFunds.tsx index 399757e3b..21ca3eba9 100644 --- a/src/components/add-funds/AddFunds.tsx +++ b/src/components/add-funds/AddFunds.tsx @@ -14,9 +14,9 @@ import chain from "@exactly/common/generated/chain"; import shortenHex from "@exactly/common/shortenHex"; import AddFundsOption from "./AddFundsOption"; -import OptimismImage from "../../assets/images/optimism.svg"; import { presentArticle } from "../../utils/intercom"; import reportError from "../../utils/reportError"; +import ChainLogo from "../shared/ChainLogo"; import SafeView from "../shared/SafeView"; import Text from "../shared/Text"; import View from "../shared/View"; @@ -70,7 +70,7 @@ export default function AddFunds() { }} /> } + icon={} title={t("From another wallet")} subtitle={t("On {{chain}}", { chain: chain.name })} onPress={() => { diff --git a/src/components/add-funds/AssetSelectSheet.tsx b/src/components/add-funds/AssetSelectSheet.tsx index ef346786a..bb2dcf869 100644 --- a/src/components/add-funds/AssetSelectSheet.tsx +++ b/src/components/add-funds/AssetSelectSheet.tsx @@ -7,7 +7,7 @@ import { ScrollView, XStack, YStack } from "tamagui"; import { formatUnits } from "viem"; -import TokenLogo from "./TokenLogo"; +import AssetLogo from "../shared/AssetLogo"; import Input from "../shared/Input"; import ModalSheet from "../shared/ModalSheet"; import SafeView from "../shared/SafeView"; @@ -131,7 +131,7 @@ export default function AssetSelectSheet({ gap="$s3_5" > - + {token.symbol} diff --git a/src/components/add-funds/Bridge.tsx b/src/components/add-funds/Bridge.tsx index 73359daa8..28e533a04 100644 --- a/src/components/add-funds/Bridge.tsx +++ b/src/components/add-funds/Bridge.tsx @@ -31,8 +31,6 @@ import shortenHex from "@exactly/common/shortenHex"; import { WAD } from "@exactly/lib"; import AssetSelectSheet from "./AssetSelectSheet"; -import TokenLogo from "./TokenLogo"; -import OptimismImage from "../../assets/images/optimism.svg"; import { getBridgeSources, getRouteFrom, tokenCorrelation, type BridgeSources, type RouteFrom } from "../../utils/lifi"; import openBrowser from "../../utils/openBrowser"; import queryClient from "../../utils/queryClient"; @@ -40,6 +38,7 @@ import reportError from "../../utils/reportError"; import useAccount from "../../utils/useAccount"; import ownerConfig, { addChains } from "../../utils/wagmi/owner"; import AssetLogo from "../shared/AssetLogo"; +import ChainLogo from "../shared/ChainLogo"; import GradientScrollView from "../shared/GradientScrollView"; import SafeView from "../shared/SafeView"; import Skeleton from "../shared/Skeleton"; @@ -537,7 +536,7 @@ export default function Bridge() { - + {`${Number( formatUnits(bridgePreview.sourceAmount, bridgePreview.sourceToken.decimals), @@ -660,7 +659,6 @@ export default function Bridge() { onUseMax={(maxAmount) => { setSourceAmount(maxAmount); }} - chainLogoUri={selectedGroup?.chain.id === 10 ? undefined : selectedGroup?.chain.logoURI} /> )} {insufficientBalance && ( @@ -699,23 +697,17 @@ export default function Bridge() { - {destinationToken.logoURI ? ( - - ) : ( - - )} + - + diff --git a/src/components/add-funds/SupportedAssetsSheet.tsx b/src/components/add-funds/SupportedAssetsSheet.tsx index 003e5a1b4..afe23a45f 100644 --- a/src/components/add-funds/SupportedAssetsSheet.tsx +++ b/src/components/add-funds/SupportedAssetsSheet.tsx @@ -16,9 +16,7 @@ import SafeView from "../shared/SafeView"; import Text from "../shared/Text"; import View from "../shared/View"; -const supportedAssets = Object.entries(assetLogos) - .filter(([symbol]) => symbol !== "USDC.e" && symbol !== "DAI") - .map(([symbol, image]) => ({ symbol, image })); +const supportedAssets = Object.keys(assetLogos).filter((symbol) => symbol !== "USDC.e" && symbol !== "DAI"); export default function SupportedAssetsSheet({ open, onClose }: { onClose: () => void; open: boolean }) { const { t } = useTranslation(); @@ -40,26 +38,24 @@ export default function SupportedAssetsSheet({ open, onClose }: { onClose: () => - {supportedAssets.map(({ symbol, image }) => { - return ( - - - - {symbol} - - - ); - })} + {supportedAssets.map((symbol) => ( + + + + {symbol} + + + ))} ; - const label = token?.symbol[0]?.toUpperCase() ?? "?"; - return ( - - - {label} - - - ); -} diff --git a/src/components/home/AssetList.tsx b/src/components/home/AssetList.tsx index 7bcb0e8ef..3077d9c4e 100644 --- a/src/components/home/AssetList.tsx +++ b/src/components/home/AssetList.tsx @@ -10,9 +10,8 @@ import { previewerAddress, ratePreviewerAddress } from "@exactly/common/generate import { useReadPreviewerExactly, useReadRatePreviewerSnapshot } from "@exactly/common/generated/hooks"; import { floatingDepositRates } from "@exactly/lib"; -import assetLogos from "../../utils/assetLogos"; import useAccount from "../../utils/useAccount"; -import useAccountAssets from "../../utils/useAccountAssets"; +import usePortfolio from "../../utils/usePortfolio"; import AssetLogo from "../shared/AssetLogo"; import Skeleton from "../shared/Skeleton"; import Text from "../shared/Text"; @@ -21,7 +20,6 @@ type AssetItem = { amount: bigint; assetName?: string; decimals: number; - logoURI?: string; market?: string; rate?: bigint; symbol: string; @@ -34,11 +32,11 @@ function AssetRow({ asset }: { asset: AssetItem }) { t, i18n: { language }, } = useTranslation(); - const { symbol, logoURI, amount, decimals, usdPrice, usdValue, rate } = asset; + const { symbol, amount, decimals, usdPrice, usdValue, rate } = asset; return ( - + {symbol} @@ -111,7 +109,7 @@ export default function AssetList() { const { t } = useTranslation(); const { address } = useAccount(); const { data: markets } = useReadPreviewerExactly({ address: previewerAddress, args: [address ?? zeroAddress] }); - const { externalAssets } = useAccountAssets(); + const { externalAssets } = usePortfolio(); const { data: snapshots, dataUpdatedAt } = useReadRatePreviewerSnapshot({ address: ratePreviewerAddress, }); @@ -138,10 +136,9 @@ export default function AssetList() { .filter(({ amount, symbol }) => (symbol === "USDC.e" ? amount > 0n : true)) .sort((a, b) => Number(b.usdValue) - Number(a.usdValue)) ?? []; - const externalAssetItems = externalAssets.map(({ symbol, name, logoURI, amount, decimals, usdValue, priceUSD }) => ({ + const externalAssetItems = externalAssets.map(({ symbol, name, amount, decimals, usdValue, priceUSD }) => ({ symbol, name, - logoURI, amount: amount ?? 0n, decimals, usdValue: parseUnits(usdValue.toFixed(18), 18), diff --git a/src/components/home/CardLimits.tsx b/src/components/home/CardLimits.tsx index db9a807f6..b244fe191 100644 --- a/src/components/home/CardLimits.tsx +++ b/src/components/home/CardLimits.tsx @@ -13,7 +13,6 @@ import { marketUSDCAddress, previewerAddress } from "@exactly/common/generated/c import { useReadPreviewerExactly } from "@exactly/common/generated/hooks"; import { borrowLimit, WAD, withdrawLimit } from "@exactly/lib"; -import assetLogos from "../../utils/assetLogos"; import useAccount from "../../utils/useAccount"; import AssetLogo from "../shared/AssetLogo"; import Text from "../shared/Text"; @@ -34,7 +33,7 @@ export default function CardLimits({ onPress }: { onPress: () => void }) { - {isCredit ? null : } + {isCredit ? null : } diff --git a/src/components/loans/Asset.tsx b/src/components/loans/Asset.tsx index d6c8e90ac..14b2741bf 100644 --- a/src/components/loans/Asset.tsx +++ b/src/components/loans/Asset.tsx @@ -12,7 +12,6 @@ import { zeroAddress } from "viem"; import { previewerAddress } from "@exactly/common/generated/chain"; import { useReadPreviewerExactly } from "@exactly/common/generated/hooks"; -import assetLogos from "../../utils/assetLogos"; import { presentArticle } from "../../utils/intercom"; import queryClient, { type Loan } from "../../utils/queryClient"; import reportError from "../../utils/reportError"; @@ -97,11 +96,7 @@ export default function Asset() { {selected && } - + {assetSymbol} diff --git a/src/components/loans/CreditLine.tsx b/src/components/loans/CreditLine.tsx index 52dac2024..f18fe3d7c 100644 --- a/src/components/loans/CreditLine.tsx +++ b/src/components/loans/CreditLine.tsx @@ -13,7 +13,6 @@ import { marketUSDCAddress, previewerAddress } from "@exactly/common/generated/c import { useReadPreviewerExactly } from "@exactly/common/generated/hooks"; import { borrowLimit } from "@exactly/lib"; -import assetLogos from "../../utils/assetLogos"; import queryClient, { type Loan } from "../../utils/queryClient"; import useAccount from "../../utils/useAccount"; import useInstallments from "../../utils/useInstallments"; @@ -44,7 +43,7 @@ export default function CreditLine() { - + {(markets ? Number(formatUnits(borrowLimit(markets, marketUSDCAddress), 6)) : 0).toLocaleString(language, { minimumFractionDigits: 2, diff --git a/src/components/loans/LoanSummary.tsx b/src/components/loans/LoanSummary.tsx index 6c15e4b9c..e5a448a00 100644 --- a/src/components/loans/LoanSummary.tsx +++ b/src/components/loans/LoanSummary.tsx @@ -10,7 +10,6 @@ import { previewerAddress } from "@exactly/common/generated/chain"; import { useReadPreviewerPreviewBorrowAtMaturity } from "@exactly/common/generated/hooks"; import { MATURITY_INTERVAL, WAD } from "@exactly/lib"; -import assetLogos from "../../utils/assetLogos"; import useAccount from "../../utils/useAccount"; import useAsset from "../../utils/useAsset"; import useInstallments from "../../utils/useInstallments"; @@ -27,7 +26,7 @@ export default function LoanSummary({ loan }: { loan: Loan }) { } = useTranslation(); const { address } = useAccount(); const { data: bytecode } = useBytecode({ address: previewerAddress, query: { enabled: !!address } }); - const { market } = useAsset(loan.market); + const { market, isFetching: isMarketFetching } = useAsset(loan.market); const symbol = market?.symbol.slice(3) === "WETH" ? "ETH" : market?.symbol.slice(3); const isBorrow = loan.installments === 1; const timestamp = useMemo(() => Math.floor(Date.now() / 1000), []); @@ -45,7 +44,7 @@ export default function LoanSummary({ loan }: { loan: Loan }) { enabled: isBorrow && !!loan.amount && !!loan.market && !!address && !!bytecode, }, }); - const pending = isInstallmentsPending || isBorrowPending; + const pending = isMarketFetching || isInstallmentsPending || isBorrowPending; const apr = useMemo(() => { const value = !isBorrow && installments @@ -74,11 +73,7 @@ export default function LoanSummary({ loan }: { loan: Loan }) { ) : ( - + {!isBorrow && installments ? (Number(installments.amounts.reduce((a, b) => a + b, 0n)) / 1e6).toLocaleString(language, { diff --git a/src/components/loans/Review.tsx b/src/components/loans/Review.tsx index f16ec6466..8e136ad21 100644 --- a/src/components/loans/Review.tsx +++ b/src/components/loans/Review.tsx @@ -25,7 +25,6 @@ import ProposalType from "@exactly/common/ProposalType"; import shortenHex from "@exactly/common/shortenHex"; import { MATURITY_INTERVAL, WAD } from "@exactly/lib"; -import assetLogos from "../../utils/assetLogos"; import { presentArticle } from "../../utils/intercom"; import reportError from "../../utils/reportError"; import useAccount from "../../utils/useAccount"; @@ -234,11 +233,7 @@ export default function Review() { {receiver === address ? t("You receive") : t("You send")} - + {(Number(amount ?? 0n) / 10 ** (assetMarket?.decimals ?? 6)).toLocaleString(language, { minimumFractionDigits: 2, @@ -258,11 +253,7 @@ export default function Review() { })} - + {(Number(feeAmount) / 10 ** (assetMarket?.decimals ?? 6)).toLocaleString(language, { minimumFractionDigits: 2, @@ -279,11 +270,7 @@ export default function Review() { - + {( Number(singleInstallment ? totalAmount : installmentsAmount) / @@ -310,11 +297,7 @@ export default function Review() { {t("Total")} - + {(Number(totalAmount) / 10 ** (assetMarket?.decimals ?? 6)).toLocaleString(language, { minimumFractionDigits: 2, @@ -447,11 +430,7 @@ export default function Review() { {statusMessage} - + {(Number(totalAmount) / 10 ** (assetMarket?.decimals ?? 6)).toLocaleString(language, { minimumFractionDigits: 2, diff --git a/src/components/pay-mode/OverduePayments.tsx b/src/components/pay-mode/OverduePayments.tsx index 8cbb3f3eb..2a5938de8 100644 --- a/src/components/pay-mode/OverduePayments.tsx +++ b/src/components/pay-mode/OverduePayments.tsx @@ -17,7 +17,6 @@ import ProposalType, { } from "@exactly/common/ProposalType"; import { WAD } from "@exactly/lib"; -import assetLogos from "../../utils/assetLogos"; import useAccount from "../../utils/useAccount"; import AssetLogo from "../shared/AssetLogo"; import Text from "../shared/Text"; @@ -100,7 +99,7 @@ export default function OverduePayments({ onSelect }: { onSelect: (maturity: big - + {(Number(amount) / 1e6).toLocaleString(language, { minimumFractionDigits: 2, diff --git a/src/components/pay-mode/Pay.tsx b/src/components/pay-mode/Pay.tsx index d12d3c842..e9233be3e 100644 --- a/src/components/pay-mode/Pay.tsx +++ b/src/components/pay-mode/Pay.tsx @@ -43,13 +43,12 @@ import SafeView from "../../components/shared/SafeView"; import Button from "../../components/shared/StyledButton"; import Text from "../../components/shared/Text"; import View from "../../components/shared/View"; -import assetLogos from "../../utils/assetLogos"; import { getRoute, getRouteFrom } from "../../utils/lifi"; import queryClient from "../../utils/queryClient"; import reportError from "../../utils/reportError"; import useAccount from "../../utils/useAccount"; -import useAccountAssets from "../../utils/useAccountAssets"; import useAsset from "../../utils/useAsset"; +import usePortfolio from "../../utils/usePortfolio"; import useSimulateProposal from "../../utils/useSimulateProposal"; import exa from "../../utils/wagmi/exa"; import AssetLogo from "../shared/AssetLogo"; @@ -66,7 +65,7 @@ export default function Pay() { } = useTranslation(); const { address: account } = useAccount(); const router = useRouter(); - const { accountAssets } = useAccountAssets({ sortBy: "usdcFirst" }); + const { assets } = usePortfolio(undefined, { sortBy: "usdcFirst" }); const { market: exaUSDC } = useAsset(marketUSDCAddress); const [enableSimulations, setEnableSimulations] = useState(true); const [assetSelectionOpen, setAssetSelectionOpen] = useState(false); @@ -74,7 +73,18 @@ export default function Pay() { (state: Record, tool: string) => (state[tool] ? state : { ...state, [tool]: true }), {}, ); - const [selectedAsset, setSelectedAsset] = useState<{ address?: Address; external: boolean }>({ external: true }); + const [manuallySelectedAsset, setManuallySelectedAsset] = useState<{ address?: Address; external: boolean }>({ + external: true, + }); + const selectedAsset = useMemo(() => { + if (manuallySelectedAsset.address) return manuallySelectedAsset; + if (!assets[0]) return manuallySelectedAsset; + const { type } = assets[0]; + return { + address: type === "external" ? parse(Address, assets[0].address) : parse(Address, assets[0].market), + external: type === "external", + }; + }, [manuallySelectedAsset, assets]); const [selectedRepayAssets, setSelectedRepayAssets] = useState(); const { markets, @@ -487,16 +497,8 @@ export default function Pay() { const isSuccess = mode === "external" ? isExternalRepaySuccess : isRepaySuccess; const writeError = mode === "external" ? externalRepayError : writeContractError; - if (!selectedAsset.address && accountAssets[0]) { - const { type } = accountAssets[0]; - setSelectedAsset({ - address: type === "external" ? parse(Address, accountAssets[0].address) : parse(Address, accountAssets[0].market), - external: type === "external", - }); - } - const handleAssetSelect = useCallback((address: Address, external: boolean) => { - setSelectedAsset({ address, external }); + setManuallySelectedAsset({ address, external }); }, []); const isLatestPlugin = installedPlugins?.[0] === exaPluginAddress; @@ -555,7 +557,7 @@ export default function Pay() { {t("Debt")} - + {(Number(positionValue) / 1e6).toLocaleString(language, { minimumFractionDigits: 0, @@ -588,7 +590,7 @@ export default function Pay() { {t("Subtotal")} - + {isRouteFetching ? ( ) : ( @@ -637,7 +639,7 @@ export default function Pay() { )} - + - + {isRouteFetching ? ( ) : ( @@ -728,18 +730,7 @@ export default function Pay() { setAssetSelectionOpen(true); }} > - + {symbol} @@ -753,18 +744,7 @@ export default function Pay() { - + {isFetchingAsset ? ( ) : ( @@ -793,7 +773,7 @@ export default function Pay() { - + {isRouteFromFetching ? ( ) : ( diff --git a/src/components/pay-mode/PaySelector.tsx b/src/components/pay-mode/PaySelector.tsx index 29585ea78..ac588da6a 100644 --- a/src/components/pay-mode/PaySelector.tsx +++ b/src/components/pay-mode/PaySelector.tsx @@ -15,7 +15,6 @@ import MAX_INSTALLMENTS from "@exactly/common/MAX_INSTALLMENTS"; import { borrowLimit, WAD, withdrawLimit } from "@exactly/lib"; import ManualRepaymentSheet from "./ManualRepaymentSheet"; -import assetLogos from "../../utils/assetLogos"; import { presentArticle } from "../../utils/intercom"; import queryClient from "../../utils/queryClient"; import reportError from "../../utils/reportError"; @@ -325,7 +324,7 @@ function InstallmentButton({ 0 ? "$uiNeutralSecondary" : "$uiNeutralPrimary"}> {installment > 0 ? `${installment}x` : t("Pay Now")} - {installment > 0 && } + {installment > 0 && } {installment > 0 && (isInstallmentsFetching || (installment === 1 && isBorrowPreviewLoading) ? ( diff --git a/src/components/pay-mode/RepayAmountSelector.tsx b/src/components/pay-mode/RepayAmountSelector.tsx index 5e479f4ed..02d83f3d5 100644 --- a/src/components/pay-mode/RepayAmountSelector.tsx +++ b/src/components/pay-mode/RepayAmountSelector.tsx @@ -7,7 +7,6 @@ import { formatUnits, parseUnits } from "viem"; import { min } from "@exactly/lib"; -import assetLogos from "../../utils/assetLogos"; import AssetLogo from "../shared/AssetLogo"; import Input from "../shared/Input"; import Text from "../shared/Text"; @@ -118,7 +117,7 @@ export default function RepayAmountSelector({ maxWidth="80%" height={60} > - + - + { @@ -146,7 +144,6 @@ export default function Amount() { return { amount: formatUnits(formAmount, market.decimals), external: false, - logoURI: assetLogos[symbol as keyof typeof assetLogos], symbol, usdValue: formatUnits((formAmount * market.usdPrice) / WAD, market.decimals), }; @@ -154,7 +151,6 @@ export default function Amount() { return { amount: formatUnits(formAmount, external?.decimals ?? 0), external: true, - logoURI: external?.logoURI, symbol: external?.symbol, usdValue: formatUnits((formAmount * parseUnits(external?.priceUSD ?? "0", 18)) / WAD, external?.decimals ?? 0), }; @@ -333,8 +329,6 @@ export default function Amount() { { setReviewOpen(false); @@ -422,17 +416,7 @@ export default function Amount() {  {details.symbol}  - + diff --git a/src/components/send-funds/ReviewSheet.tsx b/src/components/send-funds/ReviewSheet.tsx index 172345857..9e1050464 100644 --- a/src/components/send-funds/ReviewSheet.tsx +++ b/src/components/send-funds/ReviewSheet.tsx @@ -9,7 +9,6 @@ import { zeroAddress } from "viem"; import shortenHex from "@exactly/common/shortenHex"; -import assetLogos from "../../utils/assetLogos"; import AssetLogo from "../shared/AssetLogo"; import Blocky from "../shared/Blocky"; import ModalSheet from "../shared/ModalSheet"; @@ -22,9 +21,7 @@ import type { Address } from "@exactly/common/validation"; export default function ReviewSheet({ amount, - external, isFirstSend, - logoURI, onClose, onSend, open, @@ -34,9 +31,7 @@ export default function ReviewSheet({ usdValue, }: { amount: string; - external: boolean; isFirstSend: boolean; - logoURI?: string; onClose: () => void; onSend: () => void; open: boolean; @@ -72,13 +67,7 @@ export default function ReviewSheet({ {t("Sending")} - + {amount} {symbol} diff --git a/src/components/shared/AddressDialog.tsx b/src/components/shared/AddressDialog.tsx index 6f56cde94..2b1b13024 100644 --- a/src/components/shared/AddressDialog.tsx +++ b/src/components/shared/AddressDialog.tsx @@ -7,9 +7,9 @@ import { AlertDialog, XStack, YStack } from "tamagui"; import chain from "@exactly/common/generated/chain"; import Button from "./Button"; +import ChainLogo from "./ChainLogo"; import Text from "./Text"; import View from "./View"; -import OptimismImage from "../../assets/images/optimism.svg"; import useAspectRatio from "../../utils/useAspectRatio"; export default function AddressDialog({ @@ -60,7 +60,7 @@ export default function AddressDialog({ - + diff --git a/src/components/shared/AssetLogo.tsx b/src/components/shared/AssetLogo.tsx index a0e05f454..b99ffa487 100644 --- a/src/components/shared/AssetLogo.tsx +++ b/src/components/shared/AssetLogo.tsx @@ -1,17 +1,46 @@ +import React from "react"; import { Platform } from "react-native"; import { Image } from "expo-image"; -import { styled } from "tamagui"; +import { styled, View } from "tamagui"; +import { useQuery } from "@tanstack/react-query"; + +import Text from "./Text"; +import { getTokenLogoURI } from "../../utils/assetLogos"; +import { lifiTokensOptions } from "../../utils/queryClient"; import reportError from "../../utils/reportError"; -export default styled(Image, { +const StyledImage = styled(Image, { name: "AssetLogo", cachePolicy: "memory-disk", contentFit: "contain", transition: Platform.OS === "web" ? "smooth" : undefined, placeholderContentFit: "cover", borderRadius: "$r_0", + overflow: "hidden", onError: reportError, }); + +export default function AssetLogo({ height, symbol, width }: { height: number; symbol: string; width: number }) { + const { data: tokens = [] } = useQuery(lifiTokensOptions); + const uri = getTokenLogoURI(tokens, symbol); + if (!uri) { + return ( + + + {symbol.slice(0, 2).toUpperCase()} + + + ); + } + return ; +} diff --git a/src/components/shared/AssetSelector.tsx b/src/components/shared/AssetSelector.tsx index 1872cb305..8b222e476 100644 --- a/src/components/shared/AssetSelector.tsx +++ b/src/components/shared/AssetSelector.tsx @@ -14,9 +14,8 @@ import { withdrawLimit } from "@exactly/lib"; import AssetLogo from "./AssetLogo"; import Skeleton from "./Skeleton"; -import assetLogos from "../../utils/assetLogos"; import useAccount from "../../utils/useAccount"; -import useAccountAssets from "../../utils/useAccountAssets"; +import usePortfolio from "../../utils/usePortfolio"; import Text from "../shared/Text"; import View from "../shared/View"; @@ -34,10 +33,10 @@ export default function AssetSelector({ const [selectedMarket, setSelectedMarket] = useState
(); const { address: account } = useAccount(); const { data: markets } = useReadPreviewerExactly({ address: previewerAddress, args: [account ?? zeroAddress] }); - const { accountAssets, externalAssets, isPending } = useAccountAssets({ sortBy }); + const { assets, externalAssets, isPending } = usePortfolio(undefined, { sortBy }); - if (accountAssets.length === 0) { - if (isPending) { + if (assets.length === 0) { + if (isPending || !markets) { return ( @@ -67,7 +66,7 @@ export default function AssetSelector({ onSubmit(output, isExternal); }} > - {accountAssets.map((asset) => { + {assets.map((asset) => { const availableBalance = asset.type === "external" ? Number(asset.amount ?? 0n) / 10 ** asset.decimals @@ -109,14 +108,7 @@ export default function AssetSelector({ borderRadius="$r3" > - + {symbol} diff --git a/src/components/shared/ChainLogo.tsx b/src/components/shared/ChainLogo.tsx new file mode 100644 index 000000000..5a3cc524e --- /dev/null +++ b/src/components/shared/ChainLogo.tsx @@ -0,0 +1,37 @@ +import React from "react"; + +import { YStack } from "tamagui"; + +import { useQuery } from "@tanstack/react-query"; + +import chain from "@exactly/common/generated/chain"; + +import Image from "./Image"; +import Text from "./Text"; +import { lifiChainsOptions } from "../../utils/queryClient"; + +export default function ChainLogo({ chainId, size }: { chainId?: number; size: number }) { + const targetChainId = chainId ?? chain.id; + const { data } = useQuery({ + ...lifiChainsOptions, + select: (chains) => chains.find((c) => c.id === targetChainId), + }); + if (!data?.logoURI) { + const name = data?.name ?? chain.name; + return ( + + + {name.slice(0, 2).toUpperCase()} + + + ); + } + return ; +} diff --git a/src/components/shared/CopyAddressSheet.tsx b/src/components/shared/CopyAddressSheet.tsx index a91a5f51a..75bbb1f7f 100644 --- a/src/components/shared/CopyAddressSheet.tsx +++ b/src/components/shared/CopyAddressSheet.tsx @@ -7,17 +7,15 @@ import { ScrollView, XStack, YStack } from "tamagui"; import chain from "@exactly/common/generated/chain"; import AssetLogo from "./AssetLogo"; +import ChainLogo from "./ChainLogo"; import ModalSheet from "./ModalSheet"; -import OptimismImage from "../../assets/images/optimism.svg"; import assetLogos from "../../utils/assetLogos"; import useAccount from "../../utils/useAccount"; import Button from "../shared/Button"; import SafeView from "../shared/SafeView"; import Text from "../shared/Text"; -const supportedAssets = Object.entries(assetLogos) - .filter(([symbol]) => symbol !== "USDC.e" && symbol !== "DAI") - .map(([symbol, image]) => ({ symbol, image })); +const supportedAssets = Object.keys(assetLogos).filter((s) => s !== "USDC.e" && s !== "DAI"); export default function CopyAddressSheet({ open, onClose }: { onClose: () => void; open: boolean }) { const { address } = useAccount(); @@ -65,7 +63,7 @@ export default function CopyAddressSheet({ open, onClose }: { onClose: () => voi - + {chain.name} @@ -77,13 +75,11 @@ export default function CopyAddressSheet({ open, onClose }: { onClose: () => voi padding="$s3_5" alignSelf="flex-end" > - {supportedAssets.map(({ symbol, image }, index) => { - return ( - - - - ); - })} + {supportedAssets.map((symbol, index) => ( + + + + ))} diff --git a/src/components/shared/Failure.tsx b/src/components/shared/Failure.tsx index c7715ea2b..791b255a0 100644 --- a/src/components/shared/Failure.tsx +++ b/src/components/shared/Failure.tsx @@ -11,8 +11,6 @@ import { marketUSDCAddress } from "@exactly/common/generated/chain"; import GradientScrollView from "./GradientScrollView"; import SafeView from "./SafeView"; -import assetLogos from "../../utils/assetLogos"; -import useAsset from "../../utils/useAsset"; import AssetLogo from "../shared/AssetLogo"; import Text from "../shared/Text"; import View from "../shared/View"; @@ -34,7 +32,6 @@ export default function Failure({ repayAssets: bigint; selectedAsset?: Hex; }) { - const { externalAsset } = useAsset(selectedAsset); const { t, i18n: { language }, @@ -82,7 +79,7 @@ export default function Failure({  USDC  - + {currency !== "USDC" && ( @@ -97,18 +94,7 @@ export default function Failure({  {currency}  - + {currency && } )} diff --git a/src/components/shared/InstallmentSelector.tsx b/src/components/shared/InstallmentSelector.tsx index a226fa7a7..28922b811 100644 --- a/src/components/shared/InstallmentSelector.tsx +++ b/src/components/shared/InstallmentSelector.tsx @@ -13,7 +13,6 @@ import MAX_INSTALLMENTS from "@exactly/common/MAX_INSTALLMENTS"; import AssetLogo from "./AssetLogo"; import Skeleton from "./Skeleton"; import Text from "./Text"; -import assetLogos from "../../utils/assetLogos"; import useAsset from "../../utils/useAsset"; import useInstallments from "../../utils/useInstallments"; @@ -116,11 +115,7 @@ function Installment({ - + {hasInstallments && (isInstallmentsPending || isBorrowPending ? ( diff --git a/src/components/shared/PaymentScheduleSheet.tsx b/src/components/shared/PaymentScheduleSheet.tsx index bd6ae4dee..5ea0383d2 100644 --- a/src/components/shared/PaymentScheduleSheet.tsx +++ b/src/components/shared/PaymentScheduleSheet.tsx @@ -10,7 +10,6 @@ import { MATURITY_INTERVAL } from "@exactly/lib"; import AssetLogo from "./AssetLogo"; import ModalSheet from "./ModalSheet"; -import assetLogos from "../../utils/assetLogos"; import useAccount from "../../utils/useAccount"; import useAsset from "../../utils/useAsset"; import SafeView from "../shared/SafeView"; @@ -31,7 +30,6 @@ export default function PaymentScheduleSheet({ const { address } = useAccount(); const { data: loan } = useQuery({ queryKey: ["loan"], enabled: !!address }); const { market } = useAsset(loan?.market); - const symbol = market?.symbol.slice(3) === "WETH" ? "ETH" : market?.symbol.slice(3); const { t, i18n: { language }, @@ -62,17 +60,14 @@ export default function PaymentScheduleSheet({ {Array.from({ length: loan.installments }).map((_, index) => { const maturity = Number(loan.maturity) + index * MATURITY_INTERVAL; + const symbol = market.symbol.slice(3) === "WETH" ? "ETH" : market.symbol.slice(3); return ( {index + 1} - + {(Number(installmentsAmount) / 10 ** market.decimals).toLocaleString(language, { minimumFractionDigits: 2, diff --git a/src/components/shared/Pending.tsx b/src/components/shared/Pending.tsx index c13473225..c90505c04 100644 --- a/src/components/shared/Pending.tsx +++ b/src/components/shared/Pending.tsx @@ -10,8 +10,6 @@ import { isAfter } from "date-fns"; import { marketUSDCAddress } from "@exactly/common/generated/chain"; import GradientScrollView from "./GradientScrollView"; -import assetLogos from "../../utils/assetLogos"; -import useAsset from "../../utils/useAsset"; import AssetLogo from "../shared/AssetLogo"; import ExaSpinner from "../shared/Spinner"; import Text from "../shared/Text"; @@ -38,7 +36,6 @@ export default function Pending({ t, i18n: { language }, } = useTranslation(); - const { externalAsset } = useAsset(selectedAsset); return ( @@ -82,7 +79,7 @@ export default function Pending({  USDC  - + {currency !== "USDC" && ( @@ -97,18 +94,7 @@ export default function Pending({  {currency}  - + {currency && } )} diff --git a/src/components/shared/Success.tsx b/src/components/shared/Success.tsx index 33819f93e..34a60ce9b 100644 --- a/src/components/shared/Success.tsx +++ b/src/components/shared/Success.tsx @@ -15,9 +15,7 @@ import { useReadUpgradeableModularAccountGetInstalledPlugins } from "@exactly/co import GradientScrollView from "./GradientScrollView"; import SafeView from "./SafeView"; import View from "./View"; -import assetLogos from "../../utils/assetLogos"; import useAccount from "../../utils/useAccount"; -import useAsset from "../../utils/useAsset"; import AssetLogo from "../shared/AssetLogo"; import Text from "../shared/Text"; import TransactionDetails from "../shared/TransactionDetails"; @@ -43,7 +41,6 @@ export default function Success({ t, i18n: { language }, } = useTranslation(); - const { externalAsset } = useAsset(selectedAsset); const { address } = useAccount(); const { data: bytecode } = useBytecode({ address: address ?? zeroAddress, query: { enabled: !!address } }); const { data: installedPlugins } = useReadUpgradeableModularAccountGetInstalledPlugins({ @@ -102,7 +99,7 @@ export default function Success({  USDC  - + {currency !== "USDC" && ( @@ -117,17 +114,7 @@ export default function Success({  {currency}  - + {currency && } )} diff --git a/src/components/shared/TransactionDetails.tsx b/src/components/shared/TransactionDetails.tsx index a5a395b4e..a053700b8 100644 --- a/src/components/shared/TransactionDetails.tsx +++ b/src/components/shared/TransactionDetails.tsx @@ -12,7 +12,7 @@ import { format } from "date-fns"; import chain from "@exactly/common/generated/chain"; import shortenHex from "@exactly/common/shortenHex"; -import OptimismImage from "../../assets/images/optimism.svg"; +import ChainLogo from "./ChainLogo"; import openBrowser from "../../utils/openBrowser"; import reportError from "../../utils/reportError"; import Text from "../shared/Text"; @@ -44,7 +44,7 @@ export default function TransactionDetails({ hash }: { hash?: string }) { {chain.name} - + {hash && ( diff --git a/src/components/shared/WeightedRate.tsx b/src/components/shared/WeightedRate.tsx index 82e505357..82b7f162e 100644 --- a/src/components/shared/WeightedRate.tsx +++ b/src/components/shared/WeightedRate.tsx @@ -7,7 +7,6 @@ import { XStack } from "tamagui"; import AssetLogo from "./AssetLogo"; import Text from "./Text"; -import assetLogos from "../../utils/assetLogos"; export default function WeightedRate({ averageRate, @@ -46,14 +45,11 @@ export default function WeightedRate({ {displayLogos && ( - {depositMarkets.map(({ market, symbol }, index, array) => { - const uri = assetLogos[symbol as keyof typeof assetLogos]; - return ( - - - - ); - })} + {depositMarkets.map(({ market, symbol }, index, array) => ( + + + + ))} )} diff --git a/src/components/swaps/Failure.tsx b/src/components/swaps/Failure.tsx index 436158334..af2913eb0 100644 --- a/src/components/swaps/Failure.tsx +++ b/src/components/swaps/Failure.tsx @@ -86,7 +86,7 @@ export default function Failure({ {fromToken.symbol} - + @@ -99,7 +99,7 @@ export default function Failure({ {toToken.symbol} - + diff --git a/src/components/swaps/Pending.tsx b/src/components/swaps/Pending.tsx index 40ab65241..e9aa77234 100644 --- a/src/components/swaps/Pending.tsx +++ b/src/components/swaps/Pending.tsx @@ -79,7 +79,7 @@ export default function Pending({ {`$${fromUsdAmount.toLocaleString(language, { style: "decimal", minimumFractionDigits: 2, maximumFractionDigits: 2 })}`} - + {Number(formatUnits(fromAmount, fromToken.decimals)).toFixed(8)} @@ -89,7 +89,7 @@ export default function Pending({ {`$${toUsdAmount.toLocaleString(language, { style: "decimal", minimumFractionDigits: 2, maximumFractionDigits: 2 })}`} - + {Number(formatUnits(toAmount, toToken.decimals)).toFixed(8)} diff --git a/src/components/swaps/SelectorModal.tsx b/src/components/swaps/SelectorModal.tsx index 0787c125e..634a6dcd9 100644 --- a/src/components/swaps/SelectorModal.tsx +++ b/src/components/swaps/SelectorModal.tsx @@ -7,7 +7,7 @@ import { ButtonIcon, XStack, YStack } from "tamagui"; import { formatUnits } from "viem"; -import useAccountAssets from "../../utils/useAccountAssets"; +import usePortfolio, { type PortfolioAsset } from "../../utils/usePortfolio"; import AssetLogo from "../shared/AssetLogo"; import Button from "../shared/Button"; import Input from "../shared/Input"; @@ -24,18 +24,14 @@ function TokenListItem({ isSelected, onPress, language, + matchingAsset, }: { isSelected: boolean; language: string; + matchingAsset?: PortfolioAsset; onPress: () => void; token: Token; }) { - const { accountAssets } = useAccountAssets(); - const matchingAsset = accountAssets.find( - (asset) => - (asset.type === "protocol" && asset.asset === token.address) || - (asset.type === "external" && asset.address === token.address), - ); return ( - + @@ -105,25 +101,28 @@ export default function TokenSelectModal({ withBalanceOnly?: boolean; }) { const [searchQuery, setSearchQuery] = useState(""); - const { accountAssets } = useAccountAssets(); + const { assets } = usePortfolio(); const { t, i18n: { language }, } = useTranslation(); + const assetByAddress = useMemo(() => { + const map = new Map(); + for (const asset of assets) { + const address = (asset.type === "protocol" ? asset.asset : asset.address).toLowerCase(); + map.set(address, asset); + } + return map; + }, [assets]); + const filteredTokens = useMemo(() => { const query = searchQuery.toLowerCase().trim(); const matchesQuery = (...fields: (string | undefined)[]) => fields.some((field) => field?.toLowerCase().includes(query)); - const matchesAsset = (token: Token) => - accountAssets.find( - (asset) => - (asset.type === "protocol" && asset.asset === token.address) || - (asset.type === "external" && asset.address === token.address), - ); return tokens.filter((token) => { if (withBalanceOnly) { - const asset = matchesAsset(token); + const asset = assetByAddress.get(token.address.toLowerCase()); if (!asset) return false; return asset.type === "protocol" ? asset.floatingDepositAssets > 0n && matchesQuery(asset.symbol, asset.assetName, asset.asset) @@ -131,7 +130,7 @@ export default function TokenSelectModal({ } return matchesQuery(token.symbol, token.name, token.address); }); - }, [searchQuery, tokens, withBalanceOnly, accountAssets]); + }, [searchQuery, tokens, withBalanceOnly, assetByAddress]); return ( @@ -181,6 +180,7 @@ export default function TokenSelectModal({ setSearchQuery(""); }} language={language} + matchingAsset={assetByAddress.get(item.address.toLowerCase())} /> )} keyExtractor={(item) => item.address} diff --git a/src/components/swaps/Success.tsx b/src/components/swaps/Success.tsx index d3f34d4f5..293fa7c3c 100644 --- a/src/components/swaps/Success.tsx +++ b/src/components/swaps/Success.tsx @@ -90,7 +90,7 @@ export default function Success({ {`$${fromUsdAmount.toLocaleString(language, { style: "decimal", minimumFractionDigits: 2, maximumFractionDigits: 2 })}`} - + {Number(formatUnits(fromAmount, fromToken.decimals)).toFixed(8)} @@ -100,7 +100,7 @@ export default function Success({ {`$${toUsdAmount.toLocaleString(language, { style: "decimal", minimumFractionDigits: 2, maximumFractionDigits: 2 })}`} - + {Number(formatUnits(toAmount, toToken.decimals)).toFixed(8)} diff --git a/src/components/swaps/Swaps.tsx b/src/components/swaps/Swaps.tsx index de70053f8..d963bf8f0 100644 --- a/src/components/swaps/Swaps.tsx +++ b/src/components/swaps/Swaps.tsx @@ -36,8 +36,8 @@ import openBrowser from "../../utils/openBrowser"; import queryClient from "../../utils/queryClient"; import reportError from "../../utils/reportError"; import useAccount from "../../utils/useAccount"; -import useAccountAssets from "../../utils/useAccountAssets"; import useAsset from "../../utils/useAsset"; +import usePortfolio from "../../utils/usePortfolio"; import useSimulateProposal from "../../utils/useSimulateProposal"; import Button from "../shared/Button"; import SafeView from "../shared/SafeView"; @@ -74,7 +74,7 @@ export default function Swaps() { const insets = useSafeAreaInsets(); const { t } = useTranslation(); const { address: account } = useAccount(); - const { externalAssets, protocolAssets } = useAccountAssets(); + const { externalAssets, protocolAssets } = usePortfolio(); const [acknowledged, setAcknowledged] = useState(false); const [activeInput, setActiveInput] = useState<"from" | "to">("from"); const { data: markets } = useReadPreviewerExactly({ address: previewerAddress, args: [account ?? zeroAddress] }); diff --git a/src/components/swaps/TokenInput.tsx b/src/components/swaps/TokenInput.tsx index 9cba052cb..7f3562ebd 100644 --- a/src/components/swaps/TokenInput.tsx +++ b/src/components/swaps/TokenInput.tsx @@ -9,9 +9,8 @@ import { formatUnits, parseUnits } from "viem"; import { WAD } from "@exactly/lib"; -import OptimismImage from "../../assets/images/optimism.svg"; import AssetLogo from "../shared/AssetLogo"; -import Image from "../shared/Image"; +import ChainLogo from "../shared/ChainLogo"; import Input from "../shared/Input"; import Skeleton from "../shared/Skeleton"; import Text from "../shared/Text"; @@ -33,11 +32,9 @@ export default function TokenInput({ onFocus, onChange, onUseMax, - chainLogoUri, }: { amount: bigint; balance: bigint; - chainLogoUri?: string; disabled?: boolean; isActive: boolean; isDanger?: boolean; @@ -137,7 +134,7 @@ export default function TokenInput({ > {token ? ( <> - + - {chainLogoUri ? ( - - ) : ( - - )} + ) : ( diff --git a/src/utils/assetLogos.ts b/src/utils/assetLogos.ts index 3ea2fd8c6..a13989535 100644 --- a/src/utils/assetLogos.ts +++ b/src/utils/assetLogos.ts @@ -1,4 +1,4 @@ -export default { +const assetLogos = { USDC: "https://app.exact.ly/img/assets/USDC.svg", ETH: "https://app.exact.ly/img/assets/WETH.svg", wstETH: "https://app.exact.ly/img/assets/wstETH.svg", @@ -7,3 +7,13 @@ export default { DAI: "https://app.exact.ly/img/assets/DAI.svg", "USDC.e": "https://app.exact.ly/img/assets/USDC.e.svg", } as const; + +export function getTokenLogoURI(tokens: { logoURI?: string; symbol: string }[], symbol: string): string | undefined { + const search = symbol === "ETH" ? "WETH" : symbol; + return ( + tokens.find((token) => token.symbol === search || token.symbol === symbol)?.logoURI ?? + assetLogos[symbol as keyof typeof assetLogos] + ); +} + +export default assetLogos; diff --git a/src/utils/lifi.ts b/src/utils/lifi.ts index 1dfd9d11c..b95d0fadc 100644 --- a/src/utils/lifi.ts +++ b/src/utils/lifi.ts @@ -23,6 +23,8 @@ import chain, { mockSwapperAbi, swapperAddress } from "@exactly/common/generated import { Address as AddressSchema, Hex } from "@exactly/common/validation"; import publicClient from "./publicClient"; +import queryClient, { lifiChainsOptions, lifiTokensOptions } from "./queryClient"; +import reportError from "./reportError"; let configured = false; function ensureConfig() { @@ -37,6 +39,8 @@ function ensureConfig() { }, }); configured = true; + queryClient.prefetchQuery(lifiTokensOptions).catch(reportError); + queryClient.prefetchQuery(lifiChainsOptions).catch(reportError); } export async function getRoute( @@ -111,8 +115,13 @@ async function getAllTokens(): Promise { ensureConfig(); if (chain.testnet || chain.id === anvil.id) return []; const response = await getTokens({ chains: [chain.id] }); - const exa = await getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B"); - return [exa, ...(response.tokens[chain.id] ?? [])]; + const tokens = response.tokens[chain.id] ?? []; + try { + const exa = await getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B"); + return [exa, ...tokens]; + } catch { + return tokens; + } } export async function getAsset(account: Address) { @@ -173,10 +182,14 @@ const allowList = new Set([ export async function getAllowTokens() { ensureConfig(); if (chain.testnet || chain.id === anvil.id) return []; - const exa = await getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B"); const { tokens } = await getTokens({ chains: [chain.id] }); const allowTokens = tokens[chain.id]?.filter((token) => allowList.has(token.address)) ?? []; - return [exa, ...allowTokens]; + try { + const exa = await getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B"); + return [exa, ...allowTokens]; + } catch { + return allowTokens; + } } export type RouteFrom = { diff --git a/src/utils/queryClient.ts b/src/utils/queryClient.ts index 5f9d8a093..7f1100954 100644 --- a/src/utils/queryClient.ts +++ b/src/utils/queryClient.ts @@ -1,18 +1,82 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import { sdk } from "@farcaster/miniapp-sdk"; +import { ChainType, getChains, getToken, getTokenBalancesByChain, getTokens, type Token } from "@lifi/sdk"; import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister"; import { persistQueryClientRestore, persistQueryClientSubscribe } from "@tanstack/query-persist-client-core"; -import { dehydrate, QueryCache, QueryClient, type Query } from "@tanstack/react-query"; +import { dehydrate, QueryCache, QueryClient, queryOptions, type Query } from "@tanstack/react-query"; +import { anvil, optimism } from "viem/chains"; import { deserialize, serialize } from "wagmi"; import { hashFn, structuralSharing } from "wagmi/query"; +import chain from "@exactly/common/generated/chain"; + import reportError from "./reportError"; import { isAvailable as isOwnerAvailable } from "./wagmi/owner"; import type { getActivity } from "./server"; import type { Address } from "viem"; +export const lifiChainsOptions = queryOptions({ + queryKey: ["lifi", "chains"], + staleTime: Infinity, + gcTime: Infinity, + enabled: !chain.testnet && chain.id !== anvil.id, + queryFn: async () => { + try { + return await getChains({ chainTypes: [ChainType.EVM] }); + } catch (error) { + reportError(error); + return []; + } + }, +}); + +export const lifiTokensOptions = queryOptions({ + queryKey: ["lifi", "tokens"], + staleTime: Infinity, + gcTime: Infinity, + enabled: !chain.testnet && chain.id !== anvil.id, + queryFn: async () => { + try { + const { tokens } = await getTokens({ chainTypes: [ChainType.EVM] }); + const allTokens = Object.values(tokens).flat(); + if (chain.id !== optimism.id) return allTokens; + const exa = await getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B").catch((error: unknown) => { + reportError(error); + }); + return exa ? [exa, ...allTokens] : allTokens; + } catch (error) { + reportError(error); + return [] as Token[]; + } + }, +}); + +export function tokenBalancesOptions(account: Address | undefined) { + return queryOptions({ + queryKey: ["lifi", "tokenBalances", account], + staleTime: 30_000, + gcTime: 60_000, + enabled: !!account && !chain.testnet && chain.id !== anvil.id, + queryFn: async () => { + if (!account) return []; + try { + const allTokens = + queryClient.getQueryData(lifiTokensOptions.queryKey) ?? + (await queryClient.fetchQuery(lifiTokensOptions)); + const tokens = allTokens.filter((token) => (token.chainId as number) === chain.id); + if (tokens.length === 0) return []; + const balances = await getTokenBalancesByChain(account, { [chain.id]: tokens }); + return balances[chain.id]?.filter((balance) => balance.amount && balance.amount > 0n) ?? []; + } catch (error) { + reportError(error); + return []; + } + }, + }); +} + export const persister = createAsyncStoragePersister({ serialize, deserialize, @@ -47,9 +111,14 @@ export const hydrated = const dehydrateOptions = { shouldDehydrateQuery: ({ queryKey, state }: Query) => - state.status === "success" && queryKey[0] !== "activity" && queryKey[0] !== "externalAssets", + state.status === "success" && + queryKey[0] !== "activity" && + queryKey[0] !== "externalAssets" && + queryKey[0] !== "lifi", }; +export const persistOptions = { persister, dehydrateOptions }; + if (typeof window !== "undefined") { const subscribe = () => persistQueryClientSubscribe({ queryClient, persister, dehydrateOptions }); hydrated.then(subscribe, subscribe); diff --git a/src/utils/useAccountAssets.ts b/src/utils/useAccountAssets.ts deleted file mode 100644 index 22bb49c30..000000000 --- a/src/utils/useAccountAssets.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { zeroAddress } from "viem"; -import { anvil } from "viem/chains"; - -import chain, { previewerAddress } from "@exactly/common/generated/chain"; -import { useReadPreviewerExactly } from "@exactly/common/generated/hooks"; -import { withdrawLimit } from "@exactly/lib"; - -import { getTokenBalances } from "./lifi"; -import useAccount from "./useAccount"; - -export type ProtocolAsset = { - asset: string; - assetName: string; - decimals: number; - floatingDepositAssets: bigint; - market: `0x${string}`; - symbol: string; - type: "protocol"; - usdPrice: bigint; - usdValue: number; -}; - -export type ExternalAsset = { - address: string; - amount?: bigint; - decimals: number; - logoURI?: string; - name: string; - priceUSD: string; - symbol: string; - type: "external"; - usdValue: number; -}; - -export default function useAccountAssets(options?: { sortBy?: "usdcFirst" | "usdValue" }) { - const { address: account } = useAccount(); - - const { data: markets } = useReadPreviewerExactly({ address: previewerAddress, args: [account ?? zeroAddress] }); - - const { data: externalAssets, isPending: isExternalAssetsPending } = useQuery({ - queryKey: ["externalAssets", account], - queryFn: async () => { - if (chain.testnet || chain.id === anvil.id || !account) return []; - const balances = await getTokenBalances(account); - return balances.filter( - ({ address }) => markets && !markets.some(({ market }) => address.toLowerCase() === market.toLowerCase()), - ); - }, - enabled: !!account, - }); - - const protocol = (markets ?? []) - .map((market) => ({ - ...market, - usdValue: markets - ? Number((withdrawLimit(markets, market.market) * market.usdPrice) / BigInt(10 ** market.decimals)) / 1e18 - : 0, - type: "protocol", - })) - .filter(({ floatingDepositAssets }) => floatingDepositAssets > 0) as ProtocolAsset[]; - - const external = (externalAssets ?? []).map((externalAsset) => ({ - ...externalAsset, - usdValue: (Number(externalAsset.priceUSD) * Number(externalAsset.amount ?? 0n)) / 10 ** externalAsset.decimals, - type: "external", - })) as ExternalAsset[]; - - const combinedAssets = [...protocol, ...external].sort((a, b) => { - if (options?.sortBy === "usdcFirst") return a.symbol.slice(3) === "USDC" ? -1 : 1; - return b.usdValue - a.usdValue; - }); - - return { - accountAssets: combinedAssets, - protocolAssets: protocol, - externalAssets: external, - isPending: isExternalAssetsPending, - }; -} diff --git a/src/utils/usePortfolio.ts b/src/utils/usePortfolio.ts index 2013acfe5..245401f60 100644 --- a/src/utils/usePortfolio.ts +++ b/src/utils/usePortfolio.ts @@ -1,18 +1,55 @@ import { useMemo } from "react"; +import { useQuery } from "@tanstack/react-query"; import { zeroAddress } from "viem"; import { previewerAddress, ratePreviewerAddress } from "@exactly/common/generated/chain"; import { useReadPreviewerExactly, useReadRatePreviewerSnapshot } from "@exactly/common/generated/hooks"; -import { floatingDepositRates } from "@exactly/lib"; +import { floatingDepositRates, withdrawLimit } from "@exactly/lib"; + +import { tokenBalancesOptions } from "./queryClient"; +import useAccount from "./useAccount"; import type { Hex } from "@exactly/common/validation"; -export default function usePortfolio(account?: Hex) { +export type ProtocolAsset = { + asset: Hex; + assetName: string; + decimals: number; + floatingDepositAssets: bigint; + market: Hex; + symbol: string; + type: "protocol"; + usdPrice: bigint; + usdValue: number; +}; + +export type ExternalAsset = { + address: string; + amount?: bigint; + decimals: number; + name: string; + priceUSD: string; + symbol: string; + type: "external"; + usdValue: number; +}; + +export type PortfolioAsset = ExternalAsset | ProtocolAsset; + +export default function usePortfolio(account?: Hex, options?: { sortBy?: "usdcFirst" | "usdValue" }) { + const { address: connectedAccount } = useAccount(); + const resolvedAccount = account ?? connectedAccount; + const { data: rateSnapshot, dataUpdatedAt: rateDataUpdatedAt } = useReadRatePreviewerSnapshot({ address: ratePreviewerAddress, }); - const { data: markets } = useReadPreviewerExactly({ address: previewerAddress, args: [account ?? zeroAddress] }); + const { data: markets, isPending: isMarketsPending } = useReadPreviewerExactly({ + address: previewerAddress, + args: [resolvedAccount ?? zeroAddress], + }); + + const { data: tokenBalances, isPending: isExternalPending } = useQuery(tokenBalancesOptions(resolvedAccount)); const portfolio = useMemo(() => { if (!markets) return { depositMarkets: [], usdBalance: 0n }; @@ -50,5 +87,52 @@ export default function usePortfolio(account?: Hex) { return weightedRate / usdBalance; }, [portfolio, rates]); - return { portfolio, averageRate }; + const protocolAssets = useMemo(() => { + if (!markets) return []; + return markets + .filter(({ floatingDepositAssets }) => floatingDepositAssets > 0n) + .map((market) => ({ + ...market, + usdValue: + Number((withdrawLimit(markets, market.market) * market.usdPrice) / BigInt(10 ** market.decimals)) / 1e18, + type: "protocol" as const, + })); + }, [markets]); + + const externalAssets = useMemo(() => { + const balances = tokenBalances ?? []; + if (balances.length === 0 || !markets) return []; + + const filtered = balances.filter( + ({ address }) => !markets.some(({ asset }) => address.toLowerCase() === asset.toLowerCase()), + ); + + return filtered.map((externalAsset) => ({ + ...externalAsset, + usdValue: (Number(externalAsset.priceUSD) * Number(externalAsset.amount ?? 0n)) / 10 ** externalAsset.decimals, + type: "external" as const, + })); + }, [tokenBalances, markets]); + + const assets = useMemo(() => { + const combined = [...protocolAssets, ...externalAssets]; + return combined.sort((a, b) => { + if (options?.sortBy === "usdcFirst") { + const aSymbol = a.type === "protocol" ? a.symbol.slice(3) : a.symbol; + const bSymbol = b.type === "protocol" ? b.symbol.slice(3) : b.symbol; + if (aSymbol === "USDC" && bSymbol !== "USDC") return -1; + if (bSymbol === "USDC" && aSymbol !== "USDC") return 1; + } + return b.usdValue - a.usdValue; + }); + }, [protocolAssets, externalAssets, options?.sortBy]); + + return { + portfolio, + averageRate, + assets, + protocolAssets, + externalAssets, + isPending: isExternalPending || isMarketsPending, + }; }