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