diff --git a/web/src/components/DepositCard/DepositCard.tsx b/web/src/components/DepositCard/DepositCard.tsx index 63089dd..4025f78 100644 --- a/web/src/components/DepositCard/DepositCard.tsx +++ b/web/src/components/DepositCard/DepositCard.tsx @@ -5,7 +5,10 @@ import { Dec, DecUtils } from "@keplr-wallet/unit"; import AnimatedArrowSpacer from "components/AnimatedDownArrowSpacer/AnimatedDownArrowSpacer"; import Dropdown from "components/Dropdown/Dropdown"; import { useConfig } from "config"; -import { useEvmChainSelection } from "features/EthWallet"; +import { + AddERC20ToWalletButton, + useEvmChainSelection, +} from "features/EthWallet"; import { padDecimal, sendIbcTransfer, @@ -411,6 +414,11 @@ export default function DepositCard(): React.ReactElement { Balance:

)} + {selectedEvmCurrencyOption?.value?.erc20ContractAddress && ( + + )} )} {recipientAddressOverride && !isRecipientAddressEditable && ( diff --git a/web/src/components/Dropdown/Dropdown.tsx b/web/src/components/Dropdown/Dropdown.tsx index 5f3e225..93eb100 100644 --- a/web/src/components/Dropdown/Dropdown.tsx +++ b/web/src/components/Dropdown/Dropdown.tsx @@ -38,7 +38,7 @@ interface DropdownProps { valueOverride?: DropdownOption | null; } -function Dropdown({ +export default function Dropdown({ options, onSelect, placeholder = "Select an option", @@ -199,5 +199,3 @@ function Dropdown({ ); } - -export default Dropdown; diff --git a/web/src/components/Footer/Footer.tsx b/web/src/components/Footer/Footer.tsx index 039ac24..452568e 100644 --- a/web/src/components/Footer/Footer.tsx +++ b/web/src/components/Footer/Footer.tsx @@ -5,7 +5,24 @@ export default function Footer(): React.ReactElement { diff --git a/web/src/components/WithdrawCard/WithdrawCard.tsx b/web/src/components/WithdrawCard/WithdrawCard.tsx index 14e0460..137dcfe 100644 --- a/web/src/components/WithdrawCard/WithdrawCard.tsx +++ b/web/src/components/WithdrawCard/WithdrawCard.tsx @@ -5,6 +5,7 @@ import { useConfig } from "config"; import AnimatedArrowSpacer from "components/AnimatedDownArrowSpacer/AnimatedDownArrowSpacer"; import Dropdown from "components/Dropdown/Dropdown"; import { + AddERC20ToWalletButton, getAstriaWithdrawerService, useEthWallet, useEvmChainSelection, @@ -15,7 +16,7 @@ import { NotificationType, useNotifications } from "features/Notifications"; export default function WithdrawCard(): React.ReactElement { const { evmChains, ibcChains } = useConfig(); const { addNotification } = useNotifications(); - const { selectedWallet } = useEthWallet(); + const { provider } = useEthWallet(); const { evmAccountAddress: fromAddress, @@ -163,7 +164,7 @@ export default function WithdrawCard(): React.ReactElement { } const recipientAddress = recipientAddressOverride || ibcAccountAddress; - if (!selectedWallet || !fromAddress || !recipientAddress) { + if (!provider || !fromAddress || !recipientAddress) { addNotification({ toastOpts: { toastType: NotificationType.WARNING, @@ -194,7 +195,7 @@ export default function WithdrawCard(): React.ReactElement { selectedEvmCurrency.nativeTokenWithdrawerContractAddress || ""; const withdrawerSvc = getAstriaWithdrawerService( - selectedWallet.provider, + provider, contractAddress, Boolean(selectedEvmCurrency.erc20ContractAddress), ); @@ -334,6 +335,9 @@ export default function WithdrawCard(): React.ReactElement { Balance:

)} + {selectedEvmCurrency?.erc20ContractAddress && ( + + )} )} diff --git a/web/src/features/EthWallet/components/AddERC20ToWalletButton/AddERC20ToWalletButton.tsx b/web/src/features/EthWallet/components/AddERC20ToWalletButton/AddERC20ToWalletButton.tsx new file mode 100644 index 0000000..8ecc28c --- /dev/null +++ b/web/src/features/EthWallet/components/AddERC20ToWalletButton/AddERC20ToWalletButton.tsx @@ -0,0 +1,57 @@ +import type { EvmCurrency } from "config"; + +import { useEthWallet } from "../../hooks/useEthWallet"; + +interface AddERC20ToWalletButtonProps { + evmCurrency: EvmCurrency; + buttonClassNameOverride?: string; +} + +export default function AddERC20ToWalletButton({ + evmCurrency, + buttonClassNameOverride, +}: AddERC20ToWalletButtonProps) { + const { provider } = useEthWallet(); + const buttonClassName = + buttonClassNameOverride ?? "p-0 is-size-7 has-text-light is-ghost"; + + const addCoinToWallet = async () => { + if (!provider) { + return; + } + try { + const wasAdded = await provider.send("wallet_watchAsset", { + type: "ERC20", + options: { + address: evmCurrency.erc20ContractAddress, + symbol: evmCurrency.coinDenom, + decimals: evmCurrency.coinDecimals, + }, + }); + + if (wasAdded) { + console.debug("ERC20 token added: ", evmCurrency.erc20ContractAddress); + } else { + console.debug( + "User declined to add ERC20 token: ", + evmCurrency.erc20ContractAddress, + ); + } + } catch (error) { + console.error(error); + } + }; + + return ( + <> + + + ); +} diff --git a/web/src/features/EthWallet/contexts/EthWalletContext.tsx b/web/src/features/EthWallet/contexts/EthWalletContext.tsx index f482211..7b7d1ae 100644 --- a/web/src/features/EthWallet/contexts/EthWalletContext.tsx +++ b/web/src/features/EthWallet/contexts/EthWalletContext.tsx @@ -10,11 +10,10 @@ import { formatBalance } from "features/EthWallet/utils/utils"; export interface EthWalletContextProps { providers: EIP6963ProviderDetail[]; - selectedWallet: EIP6963ProviderDetail | undefined; // TODO - refactor to be an ethers.Provider to make things easier? + selectedWallet: EIP6963ProviderDetail | undefined; + provider: ethers.BrowserProvider | undefined; userAccount: UserAccount | undefined; selectedChain: EvmChainInfo | undefined; - provider: ethers.BrowserProvider | undefined; - signer: ethers.Signer | undefined; handleConnect: ( providerWithInfo: EIP6963ProviderDetail, defaultChain: EvmChainInfo, @@ -32,7 +31,6 @@ export const EthWalletContextProvider: React.FC<{ children: ReactNode }> = ({ const [selectedProvider, setSelectedProvider] = useState(); const [userAccount, setUserAccount] = useState(); - const [signer, setSigner] = useState(); const providers = useSyncWalletProviders(); const [selectedChain, setSelectedChain] = useState< EvmChainInfo | undefined @@ -138,13 +136,11 @@ export const EthWalletContextProvider: React.FC<{ children: ReactNode }> = ({ // use first account const address = accounts[0]; - // create an ethers provider and signer + // create an ethers provider from eip1193 provider const ethersProvider = new ethers.BrowserProvider( providerWithInfo.provider, ); setSelectedProvider(ethersProvider); - const ethersSigner = await ethersProvider.getSigner(); - setSigner(ethersSigner); // get balance using ethers const balance = await ethersProvider.getBalance(address); @@ -178,7 +174,6 @@ export const EthWalletContextProvider: React.FC<{ children: ReactNode }> = ({ selectedWallet, userAccount, selectedChain, - signer, handleConnect, }} > diff --git a/web/src/features/EthWallet/hooks/useEvmChainSelection.tsx b/web/src/features/EthWallet/hooks/useEvmChainSelection.tsx index 17dafd9..90f8595 100644 --- a/web/src/features/EthWallet/hooks/useEvmChainSelection.tsx +++ b/web/src/features/EthWallet/hooks/useEvmChainSelection.tsx @@ -19,14 +19,15 @@ import { NotificationType, useNotifications } from "features/Notifications"; import { useEthWallet } from "features/EthWallet/hooks/useEthWallet"; import EthWalletConnector from "features/EthWallet/components/EthWalletConnector/EthWalletConnector"; import { - AstriaErc20WithdrawerService, + type AstriaErc20WithdrawerService, getAstriaWithdrawerService, } from "features/EthWallet/services/AstriaWithdrawerService/AstriaWithdrawerService"; import { formatBalance } from "features/EthWallet/utils/utils"; +import { useBalancePolling } from "features/GetBalancePolling"; export function useEvmChainSelection(evmChains: EvmChains) { const { addNotification } = useNotifications(); - const { selectedWallet, userAccount } = useEthWallet(); + const { provider, userAccount } = useEthWallet(); const [selectedEvmChain, setSelectedEvmChain] = useState( null, @@ -37,72 +38,83 @@ export function useEvmChainSelection(evmChains: EvmChains) { null, ); - const [evmBalance, setEvmBalance] = useState(null); - const [isLoadingEvmBalance, setIsLoadingEvmBalance] = - useState(false); - const resetState = useCallback(() => { setSelectedEvmChain(null); setSelectedEvmCurrency(null); setEvmAccountAddress(null); - setEvmBalance(null); - setIsLoadingEvmBalance(false); }, []); - useEffect(() => { - async function getAndSetBalance() { - if ( - !selectedWallet || - !userAccount || - !selectedEvmChain || - !selectedEvmCurrency || - !evmAccountAddress - ) { - return; - } - if (!evmCurrencyBelongsToChain(selectedEvmCurrency, selectedEvmChain)) { - return; - } - setIsLoadingEvmBalance(true); - try { - const contractAddress = - selectedEvmCurrency.erc20ContractAddress || - selectedEvmCurrency.nativeTokenWithdrawerContractAddress || - ""; - const withdrawerSvc = getAstriaWithdrawerService( - selectedWallet.provider, - contractAddress, - Boolean(selectedEvmCurrency.erc20ContractAddress), - ); - if (withdrawerSvc instanceof AstriaErc20WithdrawerService) { - const balanceRes = await withdrawerSvc.getBalance(evmAccountAddress); - const balanceStr = formatBalance( - balanceRes.toString(), - selectedEvmCurrency.coinDecimals, - ); - const balance = `${balanceStr} ${selectedEvmCurrency.coinDenom}`; - setEvmBalance(balance); - } else { - // for native token balance - const balance = `${userAccount.balance} ${selectedEvmCurrency.coinDenom}`; - setEvmBalance(balance); - } - setIsLoadingEvmBalance(false); - } catch (e) { - console.error("Failed to get balance from EVM", e); - setIsLoadingEvmBalance(false); - } + const getBalanceCallback = useCallback(async () => { + if ( + !provider || + !userAccount || + !selectedEvmChain || + !selectedEvmCurrency || + !evmAccountAddress + ) { + console.log( + "provider, userAccount, chain, currency, or address is null", + { + provider, + userAccount, + selectedEvmChain, + selectedEvmCurrency, + evmAccountAddress, + }, + ); + return null; + } + if (!evmCurrencyBelongsToChain(selectedEvmCurrency, selectedEvmChain)) { + return null; + } + if (selectedEvmCurrency.erc20ContractAddress) { + const withdrawerSvc = getAstriaWithdrawerService( + provider, + selectedEvmCurrency.erc20ContractAddress, + true, + ) as AstriaErc20WithdrawerService; + const balanceRes = await withdrawerSvc.getBalance(evmAccountAddress); + const balanceStr = formatBalance( + balanceRes.toString(), + selectedEvmCurrency.coinDecimals, + ); + return `${balanceStr} ${selectedEvmCurrency.coinDenom}`; } - getAndSetBalance().then((_) => {}); + return `${userAccount.balance} ${selectedEvmCurrency.coinDenom}`; }, [ + provider, + userAccount, selectedEvmChain, selectedEvmCurrency, - selectedWallet, - userAccount, evmAccountAddress, ]); + const pollingConfig = useMemo( + () => ({ + enabled: Boolean( + provider && + userAccount && + selectedEvmChain && + selectedEvmCurrency && + evmAccountAddress, + ), + intervalMS: 10_000, + onError: (error: Error) => { + console.error("Failed to get balance from EVM wallet", error); + }, + }), + [ + provider, + userAccount, + selectedEvmChain, + selectedEvmCurrency, + evmAccountAddress, + ], + ); + const { balance: evmBalance, isLoading: isLoadingEvmBalance } = + useBalancePolling(getBalanceCallback, pollingConfig); + const selectedEvmChainNativeToken = useMemo(() => { return selectedEvmChain?.currencies[0]; }, [selectedEvmChain]); @@ -175,7 +187,7 @@ export function useEvmChainSelection(evmChains: EvmChains) { // create refs to hold the latest state values const latestState = useRef({ userAccount, - selectedWallet, + provider, evmAccountAddress, selectedEvmChain, }); @@ -184,11 +196,11 @@ export function useEvmChainSelection(evmChains: EvmChains) { useEffect(() => { latestState.current = { userAccount, - selectedWallet, + provider, evmAccountAddress, selectedEvmChain, }; - }, [userAccount, selectedWallet, evmAccountAddress, selectedEvmChain]); + }, [userAccount, provider, evmAccountAddress, selectedEvmChain]); const connectEVMWallet = async () => { if (!selectedEvmChain) { @@ -206,8 +218,8 @@ export function useEvmChainSelection(evmChains: EvmChains) { const currentState = latestState.current; setEvmAccountAddress(""); setSelectedEvmChain(null); - if (currentState.selectedWallet) { - currentState.selectedWallet = undefined; + if (currentState.provider) { + currentState.provider = undefined; } }, onConfirm: () => { diff --git a/web/src/features/EthWallet/index.ts b/web/src/features/EthWallet/index.ts index 9ef35cf..0e8c840 100644 --- a/web/src/features/EthWallet/index.ts +++ b/web/src/features/EthWallet/index.ts @@ -1,3 +1,4 @@ +import AddERC20ToWalletButton from "./components/AddERC20ToWalletButton/AddERC20ToWalletButton"; import EthWalletConnector from "./components/EthWalletConnector/EthWalletConnector"; import { EthWalletContextProvider } from "./contexts/EthWalletContext"; import { useEthWallet } from "./hooks/useEthWallet"; @@ -6,6 +7,7 @@ import { getAstriaWithdrawerService } from "./services/AstriaWithdrawerService/A export { getAstriaWithdrawerService, + AddERC20ToWalletButton, EthWalletConnector, EthWalletContextProvider, useEthWallet, diff --git a/web/src/features/EthWallet/services/AstriaWithdrawerService/AstriaWithdrawerService.test.ts b/web/src/features/EthWallet/services/AstriaWithdrawerService/AstriaWithdrawerService.test.ts index 025fe0e..b46b5b5 100644 --- a/web/src/features/EthWallet/services/AstriaWithdrawerService/AstriaWithdrawerService.test.ts +++ b/web/src/features/EthWallet/services/AstriaWithdrawerService/AstriaWithdrawerService.test.ts @@ -50,11 +50,11 @@ describe("AstriaWithdrawerService and AstriaErc20WithdrawerService", () => { describe("AstriaWithdrawerService", () => { it("should create a singleton instance", () => { const service1 = getAstriaWithdrawerService( - {} as ethers.Eip1193Provider, + {} as ethers.BrowserProvider, mockContractAddress, ); const service2 = getAstriaWithdrawerService( - {} as ethers.Eip1193Provider, + {} as ethers.BrowserProvider, mockContractAddress, ); @@ -63,8 +63,8 @@ describe("AstriaWithdrawerService and AstriaErc20WithdrawerService", () => { }); it("should update provider when a new one is supplied", () => { - const initialProvider = {} as ethers.Eip1193Provider; - const newProvider = {} as ethers.Eip1193Provider; + const initialProvider = {} as ethers.BrowserProvider; + const newProvider = {} as ethers.BrowserProvider; const service1 = getAstriaWithdrawerService( initialProvider, @@ -76,17 +76,13 @@ describe("AstriaWithdrawerService and AstriaErc20WithdrawerService", () => { ); expect(service1).toBe(service2); - expect(ethers.BrowserProvider).toHaveBeenCalledTimes(2); - expect(ethers.BrowserProvider).toHaveBeenNthCalledWith( - 1, - initialProvider, - ); - expect(ethers.BrowserProvider).toHaveBeenNthCalledWith(2, newProvider); }); it("should call withdrawToIbcChain with correct parameters", async () => { + const provider = new ethers.BrowserProvider({} as ethers.Eip1193Provider); + const service = getAstriaWithdrawerService( - {} as ethers.Eip1193Provider, + provider, mockContractAddress, ) as AstriaWithdrawerService; @@ -113,12 +109,12 @@ describe("AstriaWithdrawerService and AstriaErc20WithdrawerService", () => { describe("AstriaErc20WithdrawerService", () => { it("should create a singleton instance", () => { const service1 = getAstriaWithdrawerService( - {} as ethers.Eip1193Provider, + {} as ethers.BrowserProvider, mockContractAddress, true, ); const service2 = getAstriaWithdrawerService( - {} as ethers.Eip1193Provider, + {} as ethers.BrowserProvider, mockContractAddress, true, ); @@ -128,8 +124,8 @@ describe("AstriaWithdrawerService and AstriaErc20WithdrawerService", () => { }); it("should update provider when a new one is supplied", () => { - const initialProvider = {} as ethers.Eip1193Provider; - const newProvider = {} as ethers.Eip1193Provider; + const initialProvider = {} as ethers.BrowserProvider; + const newProvider = {} as ethers.BrowserProvider; const service1 = getAstriaWithdrawerService( initialProvider, @@ -143,17 +139,13 @@ describe("AstriaWithdrawerService and AstriaErc20WithdrawerService", () => { ); expect(service1).toBe(service2); - expect(ethers.BrowserProvider).toHaveBeenCalledTimes(2); - expect(ethers.BrowserProvider).toHaveBeenNthCalledWith( - 1, - initialProvider, - ); - expect(ethers.BrowserProvider).toHaveBeenNthCalledWith(2, newProvider); }); - it("should call withdrawToIbcChain with correct parameters", async () => { + it("should call withdrawToIbcChain for erc20 with correct parameters", async () => { + const provider = new ethers.BrowserProvider({} as ethers.Eip1193Provider); + const service = getAstriaWithdrawerService( - {} as ethers.Eip1193Provider, + provider, mockContractAddress, true, ) as AstriaErc20WithdrawerService; @@ -179,7 +171,7 @@ describe("AstriaWithdrawerService and AstriaErc20WithdrawerService", () => { describe("getAstriaWithdrawerService", () => { it("should return AstriaWithdrawerService when isErc20 is false", () => { const service = getAstriaWithdrawerService( - {} as ethers.Eip1193Provider, + {} as ethers.BrowserProvider, mockContractAddress, false, ); @@ -188,7 +180,7 @@ describe("AstriaWithdrawerService and AstriaErc20WithdrawerService", () => { it("should return AstriaErc20WithdrawerService when isErc20 is true", () => { const service = getAstriaWithdrawerService( - {} as ethers.Eip1193Provider, + {} as ethers.BrowserProvider, mockContractAddress, true, ); diff --git a/web/src/features/EthWallet/services/AstriaWithdrawerService/AstriaWithdrawerService.ts b/web/src/features/EthWallet/services/AstriaWithdrawerService/AstriaWithdrawerService.ts index 810b29c..5551a3e 100644 --- a/web/src/features/EthWallet/services/AstriaWithdrawerService/AstriaWithdrawerService.ts +++ b/web/src/features/EthWallet/services/AstriaWithdrawerService/AstriaWithdrawerService.ts @@ -7,7 +7,7 @@ export class AstriaWithdrawerService extends GenericContractService { ]; public static override getInstance( - provider: ethers.Eip1193Provider, + provider: ethers.BrowserProvider, contractAddress: string, ): AstriaWithdrawerService { /* biome-ignore lint/complexity/noThisInStatic: */ @@ -49,7 +49,7 @@ export class AstriaErc20WithdrawerService extends GenericContractService { ]; public static override getInstance( - provider: ethers.Eip1193Provider, + provider: ethers.BrowserProvider, contractAddress: string, ): AstriaErc20WithdrawerService { /* biome-ignore lint/complexity/noThisInStatic: */ @@ -86,7 +86,7 @@ export class AstriaErc20WithdrawerService extends GenericContractService { // Helper function to get AstriaWithdrawerService instance export const getAstriaWithdrawerService = ( - provider: ethers.Eip1193Provider, + provider: ethers.BrowserProvider, contractAddress: string, isErc20 = false, ): AstriaWithdrawerService | AstriaErc20WithdrawerService => { diff --git a/web/src/features/EthWallet/services/GenericContractService.ts b/web/src/features/EthWallet/services/GenericContractService.ts index df8e70b..ba45e7a 100644 --- a/web/src/features/EthWallet/services/GenericContractService.ts +++ b/web/src/features/EthWallet/services/GenericContractService.ts @@ -9,10 +9,10 @@ export default class GenericContractService { protected contractPromise: Promise | null = null; protected constructor( - walletProvider: ethers.Eip1193Provider, + walletProvider: ethers.BrowserProvider, contractAddress: string, ) { - this.walletProvider = new ethers.BrowserProvider(walletProvider); + this.walletProvider = walletProvider; this.contractAddress = contractAddress; this.abi = (this.constructor as typeof GenericContractService).ABI; } @@ -23,7 +23,7 @@ export default class GenericContractService { } public static getInstance( - provider: ethers.Eip1193Provider, + provider: ethers.BrowserProvider, contractAddress: string, ): GenericContractService { /* biome-ignore lint/complexity/noThisInStatic: */ @@ -43,8 +43,8 @@ export default class GenericContractService { return instance; } - protected updateProvider(provider: ethers.Eip1193Provider): void { - this.walletProvider = new ethers.BrowserProvider(provider); + protected updateProvider(provider: ethers.BrowserProvider): void { + this.walletProvider = provider; this.contractPromise = null; } diff --git a/web/src/features/GetBalancePolling/hooks/useGetBalancePolling.ts b/web/src/features/GetBalancePolling/hooks/useGetBalancePolling.ts new file mode 100644 index 0000000..6349866 --- /dev/null +++ b/web/src/features/GetBalancePolling/hooks/useGetBalancePolling.ts @@ -0,0 +1,57 @@ +import { useCallback, useEffect, useState } from "react"; + +interface PollingConfig { + enabled: boolean; + intervalMS?: number; + onError?: (error: Error) => void; +} + +/** + * Hook to poll for balance at a given interval + * @param fetchBalance + * @param config + */ +export default function useBalancePolling( + fetchBalance: () => Promise, + config: PollingConfig, +) { + const [balance, setBalance] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const getBalance = useCallback(async () => { + if (!config.enabled) return; + + setIsLoading(true); + try { + const result = await fetchBalance(); + setBalance(result); + setError(null); + } catch (e) { + const error = + e instanceof Error ? e : new Error("Failed to fetch balance"); + setError(error); + config.onError?.(error); + } finally { + setIsLoading(false); + } + }, [fetchBalance, config.enabled, config.onError]); + + useEffect(() => { + getBalance().then((_) => {}); + + // setup polling if enabled + if (config.enabled && config.intervalMS) { + const intervalId = setInterval(getBalance, config.intervalMS); + return () => clearInterval(intervalId); + } + return undefined; + }, [getBalance, config.enabled, config.intervalMS]); + + return { + balance, + isLoading, + error, + refetch: getBalance, + }; +} diff --git a/web/src/features/GetBalancePolling/index.ts b/web/src/features/GetBalancePolling/index.ts new file mode 100644 index 0000000..3134cc7 --- /dev/null +++ b/web/src/features/GetBalancePolling/index.ts @@ -0,0 +1,3 @@ +import useBalancePolling from "./hooks/useGetBalancePolling"; + +export { useBalancePolling }; diff --git a/web/src/features/KeplrWallet/hooks/useIbcChainSelection.tsx b/web/src/features/KeplrWallet/hooks/useIbcChainSelection.tsx index 10a0978..96dffc1 100644 --- a/web/src/features/KeplrWallet/hooks/useIbcChainSelection.tsx +++ b/web/src/features/KeplrWallet/hooks/useIbcChainSelection.tsx @@ -14,6 +14,7 @@ import { getBalanceFromKeplr, getKeplrFromWindow, } from "features/KeplrWallet/services/ibc"; +import { useBalancePolling } from "features/GetBalancePolling"; /** * Custom hook to manage the selection of an IBC chain and currency. @@ -34,43 +35,35 @@ export function useIbcChainSelection(ibcChains: IbcChains) { null, ); - const [ibcBalance, setIbcBalance] = useState(null); - const [isLoadingIbcBalance, setIsLoadingIbcBalance] = - useState(false); - const resetState = useCallback(() => { setSelectedIbcChain(null); setSelectedIbcCurrency(null); setIbcAccountAddress(null); - setIbcBalance(null); - setIsLoadingIbcBalance(false); }, []); - useEffect(() => { - async function getAndSetBalance() { - if (!selectedIbcChain || !selectedIbcCurrency) { - return; - } - if (!ibcCurrencyBelongsToChain(selectedIbcCurrency, selectedIbcChain)) { - return; - } - setIsLoadingIbcBalance(true); - try { - const balance = await getBalanceFromKeplr( - selectedIbcChain, - selectedIbcCurrency, - ); - setIbcBalance(balance); - setIsLoadingIbcBalance(false); - } catch (e) { - console.error("Failed to get balance from Keplr", e); - setIsLoadingIbcBalance(false); - } + const getBalanceCallback = useCallback(async () => { + if (!selectedIbcChain || !selectedIbcCurrency) { + return null; } - - getAndSetBalance().then((_) => {}); + if (!ibcCurrencyBelongsToChain(selectedIbcCurrency, selectedIbcChain)) { + return null; + } + return getBalanceFromKeplr(selectedIbcChain, selectedIbcCurrency); }, [selectedIbcChain, selectedIbcCurrency]); + const pollingConfig = useMemo( + () => ({ + enabled: !!selectedIbcChain && !!selectedIbcCurrency, + intervalMS: 10_000, + onError: (error: Error) => { + console.error("Failed to get balance from Keplr", error); + }, + }), + [selectedIbcChain, selectedIbcCurrency], + ); + const { balance: ibcBalance, isLoading: isLoadingIbcBalance } = + useBalancePolling(getBalanceCallback, pollingConfig); + useEffect(() => { async function getAddress() { if (!selectedIbcChain) { diff --git a/web/src/styles/footer-customizations.scss b/web/src/styles/footer-customizations.scss index d7d3143..1bdb544 100644 --- a/web/src/styles/footer-customizations.scss +++ b/web/src/styles/footer-customizations.scss @@ -4,6 +4,9 @@ .content { a { color: $astria-orange-soft; + &:hover { + text-decoration: underline; + } } } } diff --git a/web/src/styles/index.scss b/web/src/styles/index.scss index 53d3428..3930952 100644 --- a/web/src/styles/index.scss +++ b/web/src/styles/index.scss @@ -51,7 +51,7 @@ body { .is-fullheight-with-navbar-and-footer { // viewport height minus navbar and footer height - min-height: calc(100vh - 85px - 56px); + min-height: calc(100vh - 85px - 96px); } // custom styles for the side tag component