Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Adding Feature update balance when having new block #707

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/namada-interface/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"license": "MIT",
"private": true,
"dependencies": {
"@cosmjs/socket": "^0.32.3",
"@cosmjs/tendermint-rpc": "^0.32.3",
"@namada/components": "0.2.1",
"@namada/hooks": "0.2.1",
"@namada/integrations": "0.2.1",
Expand Down Expand Up @@ -38,7 +40,8 @@
"stream": "^0.0.2",
"styled-components": "^5.3.3",
"typescript": "^5.1.3",
"web-vitals": "^2.1.4"
"web-vitals": "^2.1.4",
"xstream": "^11.14.0"
},
"scripts": {
"bump": "yarn workspace namada run bump --target apps/namada-interface",
Expand Down
19 changes: 17 additions & 2 deletions apps/namada-interface/src/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ import {
import { Account } from "@namada/types";
import { Toasts } from "App/Toast";
import { Outlet } from "react-router-dom";
import { addAccounts, fetchBalances } from "slices/accounts";
import {
addAccounts,
fetchBalances,
fetchTransparentBalances,
} from "slices/accounts";
import { setChain } from "slices/chain";
import { SettingsState } from "slices/settings";
import { persistor, store, useAppDispatch, useAppSelector } from "store";
Expand All @@ -37,6 +41,7 @@ import { TopNavigation } from "./TopNavigation";

import { chainAtom } from "slices/chain";

import useCatchEventBlock from "hooks/useCatchEventBlock";
import {
useOnAccountsChanged,
useOnChainChanged,
Expand Down Expand Up @@ -67,7 +72,7 @@ export const AnimatedTransition = (props: {
function App(): JSX.Element {
useOnNamadaExtensionAttached();
useOnNamadaExtensionConnected();
useOnAccountsChanged();
const { refreshBalances } = useOnAccountsChanged();
useOnChainChanged();

const dispatch = useAppDispatch();
Expand All @@ -89,6 +94,16 @@ function App(): JSX.Element {

useEffect(() => storeColorMode(colorMode), [colorMode]);

const updateNewBlock = (): void => {
dispatch(fetchTransparentBalances());
};

useCatchEventBlock({
rpcAddress: chain.rpc,
triggerCallback: updateNewBlock,
refreshBalancesAtom: refreshBalances,
});

const extensionAttachStatus = useUntilIntegrationAttached(chain);
const currentExtensionAttachStatus =
extensionAttachStatus[chain.extension.id];
Expand Down
4 changes: 3 additions & 1 deletion apps/namada-interface/src/App/fetchEffects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ export const useOnNamadaExtensionConnected = (): void => {
}, [connected]);
};

export const useOnAccountsChanged = (): void => {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export const useOnAccountsChanged = () => {
const accountsLoadable = useAtomValue(loadable(accountsAtom));

const refreshBalances = useSetAtom(balancesAtom);
Expand All @@ -67,4 +68,5 @@ export const useOnAccountsChanged = (): void => {
refreshPublicKeys();
}
}, [accountsLoadable]);
return { refreshBalances };
};
74 changes: 74 additions & 0 deletions apps/namada-interface/src/hooks/useCatchEventBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Comet38Client } from "@cosmjs/tendermint-rpc";
import { useEffect, useState } from "react";
import { useAppSelector } from "store";
import { Subscription } from "xstream";

import { AccountsState } from "slices/accounts";
import {
connectWebsocketClient,
subscribeNewBlock,
validateConnection,
} from "utils/subscribeNewBlock";

type TUseCatchEventBlockProps = {
rpcAddress: string;
refreshBalancesAtom: () => void;
triggerCallback: () => void;
};

function useCatchEventBlock({
rpcAddress,
refreshBalancesAtom,
triggerCallback,
}: TUseCatchEventBlockProps): {
connectState: boolean;
tmClient: Comet38Client | undefined;
subscription: Subscription | undefined;
} {
const [connectState, setConnectState] = useState<boolean>(false);
const [tmClient, setTmClient] = useState<Comet38Client>();
const [subscription, setSubscription] = useState<Subscription>();

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

const connect = async (address: string): Promise<void> => {
try {
const isValid = await validateConnection(address);
if (!isValid) {
throw new Error("Invalid network!");
}

const tmClient = await connectWebsocketClient(address);
if (!tmClient) {
throw new Error("Can't connect to client!");
}

setConnectState(true);
setTmClient(tmClient);
} catch (err) {
console.error(err);
return;
}
};

useEffect(() => {
connect(rpcAddress);
}, [rpcAddress]);

useEffect(() => {
if (tmClient) {
// triggerCallback is function want to trigger when having new block
const subscription = subscribeNewBlock(tmClient, triggerCallback);
setSubscription(subscription);
}
}, [tmClient]);

useEffect(() => {
// only refresh balance in account slice is not enouge. Need call function refresh balance below
refreshBalancesAtom();
}, [derived]);

return { connectState, tmClient, subscription };
}

export default useCatchEventBlock;
22 changes: 22 additions & 0 deletions apps/namada-interface/src/slices/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const INITIAL_STATE = {

enum AccountsThunkActions {
FetchBalances = "fetchBalances",
FetchTransparentBalances = "fetchTransparentBalances",
FetchBalance = "fetchBalance",
}

Expand All @@ -59,6 +60,27 @@ export const fetchBalances = createAsyncThunk<void, void, { state: RootState }>(
}
);

export const fetchTransparentBalances = createAsyncThunk<
void,
void,
{ state: RootState }
>(
`${ACCOUNTS_ACTIONS_BASE}/${AccountsThunkActions.FetchTransparentBalances}`,
async (_, thunkApi) => {
const { id } = chains.namada;

const accounts: Account[] = Object.values(
thunkApi.getState().accounts.derived[id]
);

accounts.forEach((account) => {
if (account.details.type === "mnemonic") {
thunkApi.dispatch(fetchBalance(account));
}
});
}
);

// TODO: fetchBalance is broken for integrations other than Namada. This
// function should be removed and new code should use the jotai atoms instead.
export const fetchBalance = createAsyncThunk<
Expand Down
73 changes: 73 additions & 0 deletions apps/namada-interface/src/utils/subscribeNewBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { StreamingSocket } from "@cosmjs/socket";
import {
Comet38Client,
NewBlockEvent,
WebsocketClient,
} from "@cosmjs/tendermint-rpc";
import { Subscription } from "xstream";

const replaceHTTPtoWebsocket = (url: string): string => {
return url.replace("http", "ws");
};

export function subscribeNewBlock(
tmClient: Comet38Client,
callback: (event: NewBlockEvent) => void
): Subscription {
const stream = tmClient.subscribeNewBlock();
const subscription = stream.subscribe({
next: (event) => {
callback(event);
},
error: (err) => {
console.error(err);
subscription.unsubscribe();
},
});

return subscription;
}

export async function validateConnection(rpcAddress: string): Promise<boolean> {
return new Promise((resolve) => {
const wsUrl = replaceHTTPtoWebsocket(rpcAddress);
const path = wsUrl.endsWith("/") ? "websocket" : "/websocket";
const socket = new StreamingSocket(wsUrl + path, 3000);
console.log(socket);
socket.events.subscribe({
error: () => {
resolve(false);
},
});

socket.connect();
socket.connected.then(() => resolve(true)).catch(() => resolve(false));
return true;
});
}

export async function connectWebsocketClient(
rpcAddress: string
): Promise<Comet38Client> {
return new Promise(async (resolve, reject) => {
try {
const wsUrl = replaceHTTPtoWebsocket(rpcAddress);
const wsClient = new WebsocketClient(wsUrl, (err) => {
reject(err);
});
const tmClient = await Comet38Client.create(wsClient);
if (!tmClient) {
reject(new Error("cannot create tendermint client"));
}

const status = await tmClient.status();
if (!status) {
reject(new Error("cannot get client status"));
}

resolve(tmClient);
} catch (err) {
reject(err);
}
});
}
Loading