Skip to content

Commit

Permalink
fix: add fetch effects and improve fetch behaviour (#641)
Browse files Browse the repository at this point in the history
- Add effects to handle accounts changed, chain changed, extension
  attached, and extension connected. Replaces fetches interspersed in
  App and AccountOverview

- Refresh public keys on Tx completed instead of toast change

- Use setter for fetching public keys to allow more control over when
  the fetch happens

- Check for native token early in minimum gas price fetch
  • Loading branch information
emccorson committed Mar 4, 2024
1 parent 1059477 commit dad5b77
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 71 deletions.
40 changes: 26 additions & 14 deletions apps/namada-interface/src/App/AccountOverview/AccountOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,17 @@ import {
useIntegrationConnection,
useUntilIntegrationAttached,
} from "@namada/integrations";
import { Account, Chain, ExtensionKey, Extensions, TokenType, Tokens } from "@namada/types";
import {
Account,
Chain,
ExtensionKey,
Extensions,
TokenType,
Tokens,
} from "@namada/types";
import { TopLevelRoute } from "App/types";
import { AccountsState, addAccounts, fetchBalances } from "slices/accounts";
import { setIsConnected } from "slices/settings";
import { namadaExtensionConnectedAtom, setIsConnected } from "slices/settings";
import { useAppDispatch, useAppSelector } from "store";
import {
AccountOverviewContainer,
Expand All @@ -25,16 +32,18 @@ import {
} from "./AccountOverview.components";
import { DerivedAccounts } from "./DerivedAccounts";

import { useAtomValue, useSetAtom } from "jotai";
import { accountsAtom, balancesAtom } from "slices/accounts";
import BigNumber from "bignumber.js";
import { useAtomValue, useSetAtom } from "jotai";
import { balancesAtom } from "slices/accounts";

//TODO: move to utils when we have one
const isEmptyObject = (object: Record<string, unknown>): boolean => {
return object ? Object.keys(object).length === 0 : true;
};

export const AccountOverview = (): JSX.Element => {
const setNamadaExtensionConnected = useSetAtom(namadaExtensionConnectedAtom);

const navigate = useNavigate();
const dispatch = useAppDispatch();
const chain = useAppSelector<Chain>((state) => state.chain.config);
Expand All @@ -46,9 +55,6 @@ export const AccountOverview = (): JSX.Element => {
metamask: false,
});

const refreshAccounts = useSetAtom(accountsAtom);
const refreshBalances = useSetAtom(balancesAtom);

const { derived } = useAppSelector<AccountsState>((state) => state.accounts);

const [integration, isConnectingToExtension, withConnection] =
Expand All @@ -65,16 +71,14 @@ export const AccountOverview = (): JSX.Element => {

const balances = useAtomValue(balancesAtom);
const totalNativeBalance = Object.values(balances).reduce((acc, balance) => {
return acc.plus(balance[chain.currency.symbol as TokenType] || BigNumber(0));
return acc.plus(
balance[chain.currency.symbol as TokenType] || BigNumber(0)
);
}, BigNumber(0));

const handleConnectExtension = async (): Promise<void> => {
withConnection(
async () => {
// jotai
refreshAccounts();
refreshBalances();

const accounts = await integration?.accounts();
if (accounts) {
dispatch(addAccounts(accounts as Account[]));
Expand All @@ -86,12 +90,16 @@ export const AccountOverview = (): JSX.Element => {
...isExtensionConnected,
[chain.extension.id]: true,
});

setNamadaExtensionConnected(true);
},
async () => {
setIsExtensionConnected({
...isExtensionConnected,
[chain.extension.id]: false,
});

setNamadaExtensionConnected(false);
}
);
};
Expand All @@ -108,8 +116,12 @@ export const AccountOverview = (): JSX.Element => {
<TotalContainer>
{!isEmptyObject(derived[chain.id]) && (
<TotalAmount>
<TotalAmountFiat>{Tokens[chain.currency.symbol as TokenType].symbol}</TotalAmountFiat>
<TotalAmountValue>{totalNativeBalance.toString()}</TotalAmountValue>
<TotalAmountFiat>
{Tokens[chain.currency.symbol as TokenType].symbol}
</TotalAmountFiat>
<TotalAmountValue>
{totalNativeBalance.toString()}
</TotalAmountValue>
</TotalAmount>
)}
</TotalContainer>
Expand Down
49 changes: 17 additions & 32 deletions apps/namada-interface/src/App/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable max-len */
import { AnimatePresence } from "framer-motion";
import { createBrowserHistory } from "history";
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
import { useAtomValue } from "jotai";
import { useEffect, useState } from "react";
import { PersistGate } from "redux-persist/integration/react";
import { ThemeProvider } from "styled-components";
Expand All @@ -23,13 +23,7 @@ import { Outlet } from "react-router-dom";
import { addAccounts, fetchBalances } from "slices/accounts";
import { setChain } from "slices/chain";
import { SettingsState } from "slices/settings";
import {
persistor,
reduxStoreAtom,
store,
useAppDispatch,
useAppSelector,
} from "store";
import { persistor, store, useAppDispatch, useAppSelector } from "store";
import {
AppContainer,
AppLoader,
Expand All @@ -41,9 +35,14 @@ import {
} from "./App.components";
import { TopNavigation } from "./TopNavigation";

import { accountsAtom, balancesAtom } from "slices/accounts";
import { chainAtom } from "slices/chain";
import { minimumGasPriceAtom } from "slices/fees";

import {
useOnAccountsChanged,
useOnChainChanged,
useOnNamadaExtensionAttached,
useOnNamadaExtensionConnected,
} from "./fetchEffects";

export const history = createBrowserHistory({ window });

Expand All @@ -65,11 +64,12 @@ export const AnimatedTransition = (props: {
);
};

// TODO: can be moved to slices/notifications once redux is removed
// Defining it there currently causes a unit test error related to redux-persist
const toastsAtom = atom((get) => get(reduxStoreAtom).notifications.toasts);

function App(): JSX.Element {
useOnNamadaExtensionAttached();
useOnNamadaExtensionConnected();
useOnAccountsChanged();
useOnChainChanged();

const dispatch = useAppDispatch();
const initialColorMode = loadColorMode();
const [colorMode, setColorMode] = useState<ColorMode>(initialColorMode);
Expand All @@ -79,10 +79,7 @@ function App(): JSX.Element {
setColorMode((currentMode) => (currentMode === "dark" ? "light" : "dark"));
};

const [chain, refreshChain] = useAtom(chainAtom);
const refreshAccounts = useSetAtom(accountsAtom);
const refreshBalances = useSetAtom(balancesAtom);
const refreshMinimumGasPrice = useSetAtom(minimumGasPriceAtom);
const chain = useAtomValue(chainAtom);

const { connectedChains } = useAppSelector<SettingsState>(
(state) => state.settings
Expand All @@ -96,12 +93,9 @@ function App(): JSX.Element {
const currentExtensionAttachStatus =
extensionAttachStatus[chain.extension.id];

// TODO: remove this effect once redux has been replaced by jotai
useEffect(() => {
const fetchAccounts = async (): Promise<void> => {
// jotai
refreshAccounts();
refreshBalances();

const accounts = await integration?.accounts();
if (accounts) {
dispatch(addAccounts(accounts as Account[]));
Expand All @@ -113,16 +107,13 @@ function App(): JSX.Element {
connectedChains.includes(chain.id)
) {
fetchAccounts();
refreshMinimumGasPrice();
}
}, [chain]);

// TODO: remove this effect once redux has been replaced by jotai
useEffect(() => {
(async () => {
if (currentExtensionAttachStatus === "attached") {
// jotai
refreshChain();

const chain = await integration.getChain();
if (chain) {
dispatch(setChain(chain));
Expand All @@ -131,12 +122,6 @@ function App(): JSX.Element {
})();
}, [currentExtensionAttachStatus]);

const toasts = useAtomValue(toastsAtom);
useEffect(() => {
// TODO: this could be more conservative about how often it fetches balances
refreshBalances();
}, [toasts]);

return (
<ThemeProvider theme={theme}>
<PersistGate loading={null} persistor={persistor}>
Expand Down
70 changes: 70 additions & 0 deletions apps/namada-interface/src/App/fetchEffects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { useAtomValue, useSetAtom } from "jotai";
import { loadable } from "jotai/utils";

import { useEffectSkipFirstRender } from "@namada/hooks";
import {
Namada,
useIntegration,
useUntilIntegrationAttached,
} from "@namada/integrations";
import { namadaExtensionConnectedAtom } from "slices/settings";

import { accountsAtom, balancesAtom } from "slices/accounts";
import { chainAtom } from "slices/chain";
import { isRevealPkNeededAtom, minimumGasPriceAtom } from "slices/fees";

export const useOnChainChanged = (): void => {
const chain = useAtomValue(chainAtom);

const refreshMinimumGasPrice = useSetAtom(minimumGasPriceAtom);
const refreshPublicKeys = useSetAtom(isRevealPkNeededAtom);

useEffectSkipFirstRender(() => {
refreshMinimumGasPrice();
refreshPublicKeys();
}, [chain]);
};

export const useOnNamadaExtensionAttached = (): void => {
const setNamadaExtensionConnected = useSetAtom(namadaExtensionConnectedAtom);
const chain = useAtomValue(chainAtom); // should always be namada
const { namada: attachStatus } = useUntilIntegrationAttached(chain);
const integration = useIntegration("namada") as Namada;

useEffectSkipFirstRender(() => {
(async () => {
if (attachStatus === "attached") {
const connected = !!(await integration.isConnected());
setNamadaExtensionConnected(connected);
}
})();
}, [attachStatus]);
};

export const useOnNamadaExtensionConnected = (): void => {
const connected = useAtomValue(namadaExtensionConnectedAtom);

const refreshChain = useSetAtom(chainAtom);
const refreshAccounts = useSetAtom(accountsAtom);

useEffectSkipFirstRender(() => {
if (connected) {
refreshChain();
refreshAccounts();
}
}, [connected]);
};

export const useOnAccountsChanged = (): void => {
const accountsLoadable = useAtomValue(loadable(accountsAtom));

const refreshBalances = useSetAtom(balancesAtom);
const refreshPublicKeys = useSetAtom(isRevealPkNeededAtom);

useEffectSkipFirstRender(() => {
if (accountsLoadable.state === "hasData") {
refreshBalances();
refreshPublicKeys();
}
}, [accountsLoadable]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ export const NamadaProposalsUpdatedHandler =
};

export const NamadaUpdatedBalancesHandler =
(dispatch: Dispatch<unknown>) => async () => {
(dispatch: Dispatch<unknown>, refreshBalances: () => void) => async () => {
dispatch(fetchBalances());
refreshBalances();
};

export const NamadaUpdatedStakingHandler =
Expand All @@ -67,7 +68,8 @@ export const NamadaTxStartedHandler =
};

export const NamadaTxCompletedHandler =
(dispatch: Dispatch<unknown>) => async (event: CustomEventInit) => {
(dispatch: Dispatch<unknown>, refreshPublicKeys: () => void) =>
async (event: CustomEventInit) => {
const { msgId, txType, success, payload } = event.detail;
if (!success) {
console.warn(`${txType} failed:`, payload);
Expand All @@ -80,4 +82,15 @@ export const NamadaTxCompletedHandler =
error: payload || "",
})
);
refreshPublicKeys();
};

export const NamadaConnectionRevokedHandler =
(
integration: Namada,
setNamadaExtensionConnected: (connected: boolean) => void
) =>
async () => {
const connected = !!(await integration.isConnected());
setNamadaExtensionConnected(connected);
};
26 changes: 23 additions & 3 deletions apps/namada-interface/src/services/extensionEvents/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
MetamaskAccountChangedHandler,
MetamaskBridgeTransferCompletedHandler,
NamadaAccountChangedHandler,
NamadaConnectionRevokedHandler,
NamadaNetworkChangedHandler,
NamadaProposalsUpdatedHandler,
NamadaTxCompletedHandler,
Expand All @@ -19,8 +20,10 @@ import {
} from "./handlers";

import { useSetAtom } from "jotai";
import { accountsAtom } from "slices/accounts";
import { accountsAtom, balancesAtom } from "slices/accounts";
import { chainAtom } from "slices/chain";
import { isRevealPkNeededAtom } from "slices/fees";
import { namadaExtensionConnectedAtom } from "slices/settings";

export const ExtensionEventsContext = createContext({});

Expand All @@ -32,6 +35,9 @@ export const ExtensionEventsProvider: React.FC = (props): JSX.Element => {

const refreshAccounts = useSetAtom(accountsAtom);
const refreshChain = useSetAtom(chainAtom);
const refreshBalances = useSetAtom(balancesAtom);
const refreshPublicKeys = useSetAtom(isRevealPkNeededAtom);
const setNamadaExtensionConnected = useSetAtom(namadaExtensionConnectedAtom);

// Instantiate handlers:
const namadaAccountChangedHandler = NamadaAccountChangedHandler(
Expand All @@ -45,10 +51,20 @@ export const ExtensionEventsProvider: React.FC = (props): JSX.Element => {
refreshChain
);
const namadaTxStartedHandler = NamadaTxStartedHandler(dispatch);
const namadaTxCompletedHandler = NamadaTxCompletedHandler(dispatch);
const namadaUpdatedBalancesHandler = NamadaUpdatedBalancesHandler(dispatch);
const namadaTxCompletedHandler = NamadaTxCompletedHandler(
dispatch,
refreshPublicKeys
);
const namadaUpdatedBalancesHandler = NamadaUpdatedBalancesHandler(
dispatch,
refreshBalances
);
const namadaUpdatedStakingHandler = NamadaUpdatedStakingHandler(dispatch);
const namadaProposalsUpdatedHandler = NamadaProposalsUpdatedHandler(dispatch);
const namadaConnectionRevokedHandler = NamadaConnectionRevokedHandler(
namadaIntegration as Namada,
setNamadaExtensionConnected
);

// Keplr handlers
const keplrAccountChangedHandler = KeplrAccountChangedHandler(
Expand All @@ -73,6 +89,10 @@ export const ExtensionEventsProvider: React.FC = (props): JSX.Element => {
useEventListenerOnce(Events.TxStarted, namadaTxStartedHandler);
useEventListenerOnce(Events.TxCompleted, namadaTxCompletedHandler);
useEventListenerOnce(Events.ProposalsUpdated, namadaProposalsUpdatedHandler);
useEventListenerOnce(
Events.ConnectionRevoked,
namadaConnectionRevokedHandler
);
useEventListenerOnce(KeplrEvents.AccountChanged, keplrAccountChangedHandler);
useEventListenerOnce(
MetamaskEvents.AccountChanged,
Expand Down
Loading

0 comments on commit dad5b77

Please sign in to comment.