diff --git a/nym-wallet/package.json b/nym-wallet/package.json
index b3964e4947..09541d0e09 100644
--- a/nym-wallet/package.json
+++ b/nym-wallet/package.json
@@ -27,6 +27,7 @@
},
"dependencies": {
"@babel/helper-simple-access": "^7.25.9",
+ "@emotion/cache": "^11.14.0",
"@emotion/react": "^11.7.0",
"@emotion/styled": "^11.6.0",
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
@@ -38,6 +39,7 @@
"@nymproject/node-tester": "^1.3.1",
"@nymproject/react": "^1.0.0",
"@nymproject/types": "^1.0.0",
+ "@tanstack/react-query": "^5.62.0",
"@tauri-apps/api": "^2.10.1",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
"@tauri-apps/plugin-opener": "^2.5.3",
@@ -46,7 +48,6 @@
"@tauri-apps/tauri-forage": "^1.0.0-beta.2",
"big.js": "^6.2.1",
"bs58": "^4.0.1",
- "@emotion/cache": "^11.14.0",
"clsx": "^1.1.1",
"date-fns": "^2.28.0",
"joi": "^17.11.0",
@@ -138,4 +139,4 @@
"@types/minimatch": "5.1.2"
},
"private": false
-}
\ No newline at end of file
+}
diff --git a/nym-wallet/src-tauri/src/log.rs b/nym-wallet/src-tauri/src/log.rs
index f9b61aaff2..ae9e897190 100644
--- a/nym-wallet/src-tauri/src/log.rs
+++ b/nym-wallet/src-tauri/src/log.rs
@@ -62,9 +62,16 @@ pub fn setup_logging(app_handle: tauri::AppHandle) -> Result<(), log::SetLoggerE
message: record.args().to_string(),
level: record.level().into(),
};
- // Tauri 2: target the log webview explicitly so the dedicated window receives events.
- if let Some(log_win) = log_window_app.get_webview_window("log") {
- let _ = log_win.emit("log://log", msg);
+ let app = log_window_app.clone();
+ let app_for_emit = app.clone();
+ if let Err(e) = app.run_on_main_thread(move || {
+ if let Some(log_win) = app_for_emit.get_webview_window("log") {
+ if let Err(err) = log_win.emit("log://log", msg) {
+ log::warn!("failed to emit log line to log webview: {err}");
+ }
+ }
+ }) {
+ log::warn!("failed to schedule log line for log webview: {e}");
}
}));
diff --git a/nym-wallet/src-tauri/src/operations/app/window.rs b/nym-wallet/src-tauri/src/operations/app/window.rs
index 1d5482e409..a40c1d9688 100644
--- a/nym-wallet/src-tauri/src/operations/app/window.rs
+++ b/nym-wallet/src-tauri/src/operations/app/window.rs
@@ -34,6 +34,7 @@ async fn create_window(
)
.title("Nym Wallet")
.background_color(NYM_WALLET_WEBVIEW_BG)
+ .use_https_scheme(true)
.build()
{
Ok(window) => {
diff --git a/nym-wallet/src-tauri/src/operations/help/log.rs b/nym-wallet/src-tauri/src/operations/help/log.rs
index 08a90a0085..81a1d7fc0a 100644
--- a/nym-wallet/src-tauri/src/operations/help/log.rs
+++ b/nym-wallet/src-tauri/src/operations/help/log.rs
@@ -1,5 +1,6 @@
use crate::error::BackendError;
use crate::webview_theme::NYM_WALLET_WEBVIEW_BG;
+use tauri::webview::PageLoadEvent;
use tauri::Manager;
#[tauri::command]
@@ -20,6 +21,22 @@ pub fn help_log_toggle_window(app_handle: tauri::AppHandle) -> Result<(), Backen
)
.title("Nym Wallet Logs")
.background_color(NYM_WALLET_WEBVIEW_BG)
+ .use_https_scheme(true)
+ .on_page_load(|window, payload| match payload.event() {
+ PageLoadEvent::Started => {
+ log::debug!("Log webview load started: {}", payload.url());
+ }
+ PageLoadEvent::Finished => {
+ log::info!("Log webview load finished: {}", payload.url());
+ if std::env::var("NYM_WALLET_LOG_WEBVIEW_DEVTOOLS")
+ .ok()
+ .as_deref()
+ == Some("1")
+ {
+ window.open_devtools();
+ }
+ }
+ })
.build()
{
Ok(window) => {
diff --git a/nym-wallet/src/common.tsx b/nym-wallet/src/common.tsx
index 03b90c8d62..15f1739f34 100644
--- a/nym-wallet/src/common.tsx
+++ b/nym-wallet/src/common.tsx
@@ -1,4 +1,5 @@
import React, { ComponentType, useEffect } from 'react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';
import { BrowserRouter, HashRouter } from 'react-router-dom';
import { SnackbarProvider } from 'notistack';
@@ -11,6 +12,15 @@ import { useTauriTextEditingClipboard } from './hooks/useTauriTextEditingClipboa
type RouterComponent = ComponentType<{ children?: React.ReactNode }>;
+const walletQueryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: 1,
+ refetchOnWindowFocus: false,
+ },
+ },
+});
+
const ClipboardBridge: FCWithChildren = ({ children }) => {
useTauriTextEditingClipboard();
return children;
@@ -48,18 +58,20 @@ export const AppCommon = ({
return (
-
-
-
- {children}
-
-
-
+
+
+
+
+ {children}
+
+
+
+
);
diff --git a/nym-wallet/src/components/LogViewer/index.tsx b/nym-wallet/src/components/LogViewer/index.tsx
index 424280f15d..370c801615 100644
--- a/nym-wallet/src/components/LogViewer/index.tsx
+++ b/nym-wallet/src/components/LogViewer/index.tsx
@@ -3,16 +3,24 @@ import type { UnlistenFn } from '@tauri-apps/api/event';
import { getCurrentWebview } from '@tauri-apps/api/webview';
import {
Box,
- Paper,
+ Button,
Chip,
+ Paper,
+ Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
+ Typography,
useTheme,
} from '@mui/material';
+import type { Theme } from '@mui/material/styles';
+import { writeText } from '@tauri-apps/plugin-clipboard-manager';
+import { useSnackbar } from 'notistack';
+import { helpLogToggleWindow } from 'src/requests/logging';
+import { Console } from 'src/utils/console';
// see https://github.com/tauri-apps/tauri-plugin-log/blob/dev/webview-src/index.ts#L4
enum LogLevel {
@@ -40,7 +48,7 @@ const getLogLevelName = (value: LogLevel) => {
}
};
-const getLogLevelColor = (level: LogLevel, theme: any) => {
+const getLogLevelColor = (level: LogLevel, theme: Theme) => {
switch (level) {
case LogLevel.Trace:
return {
@@ -90,6 +98,7 @@ interface RecordPayload {
export const LogViewer: FC = () => {
const theme = useTheme();
+ const { enqueueSnackbar } = useSnackbar();
const unlisten = useRef(null);
const [messages, setMessages] = useState([]);
const [messageCount, setMessageCount] = useState(0);
@@ -131,6 +140,44 @@ export const LogViewer: FC = () => {
};
}, []);
+ const handleCloseLogs = async () => {
+ try {
+ await helpLogToggleWindow();
+ } catch (e) {
+ Console.error(e);
+ enqueueSnackbar('Could not close the log window', { variant: 'error' });
+ }
+ };
+
+ const formatLine = (m: RecordPayload) => `${getLogLevelName(m.level)}\t${m.message}`;
+
+ const handleCopyAll = async () => {
+ if (messages.length === 0) return;
+ try {
+ const chronological = [...messages].reverse();
+ await writeText(chronological.map(formatLine).join('\n'));
+ } catch (e) {
+ Console.error(e);
+ enqueueSnackbar('Could not copy logs to the clipboard', { variant: 'error' });
+ }
+ };
+
+ const handleCopyLatest = async () => {
+ const latest = messages[0];
+ if (!latest) return;
+ try {
+ await writeText(formatLine(latest));
+ } catch (e) {
+ Console.error(e);
+ enqueueSnackbar('Could not copy to the clipboard', { variant: 'error' });
+ }
+ };
+
+ const handleClearList = () => {
+ setMessages([]);
+ setMessageCount(0);
+ };
+
return (
{
- {messages.map((m, index) => {
- const levelColors = getLogLevelColor(m.level, theme);
- return (
-
-
+
+
+ No log lines yet. The wallet streams new lines here in real time - older log history is not
+ replayed. Use the wallet or set RUST_LOG for more detail. Close with the button below or use Open
+ logs again in Settings - Advanced (same action toggles this window).
+
+
+
+ ) : (
+ messages.map((m, index) => {
+ const levelColors = getLogLevelColor(m.level, theme);
+ return (
+
-
-
-
- {m.message}
-
-
- );
- })}
+ >
+
+
+
+ {m.message}
+
+
+ );
+ })
+ )}
@@ -235,14 +298,32 @@ export const LogViewer: FC = () => {
- {messageCount} log entries since opening this window
+
+
+
+ {messageCount} log entries since opening this window
+
+
+
+
+
+
+
+
+
);
diff --git a/nym-wallet/src/context/delegationQuery.ts b/nym-wallet/src/context/delegationQuery.ts
new file mode 100644
index 0000000000..3431db38cd
--- /dev/null
+++ b/nym-wallet/src/context/delegationQuery.ts
@@ -0,0 +1,44 @@
+import type { DelegationWithEverything, WrappedDelegationEvent } from '@nymproject/types';
+import { getAllPendingDelegations, getDelegationSummary } from 'src/requests';
+import { decCoinToDisplay } from 'src/utils';
+
+export type DelegationSummaryBundle = {
+ delegations: (DelegationWithEverything | WrappedDelegationEvent)[];
+ pendingDelegations: WrappedDelegationEvent[];
+ totalDelegations: string;
+ totalRewards: string;
+ totalDelegationsAndRewards: string;
+};
+
+export async function fetchDelegationSummaryQuery(): Promise {
+ const data = await getDelegationSummary();
+ const pending = await getAllPendingDelegations();
+
+ const pendingOnNewNodes = pending.filter((event) => {
+ const some = data.delegations.some(({ node_identity }) => node_identity === event.node_identity);
+ return !some;
+ });
+ const items = data.delegations.map((delegation) => ({
+ ...delegation,
+ amount: decCoinToDisplay(delegation.amount),
+ unclaimed_rewards: delegation.unclaimed_rewards && decCoinToDisplay(delegation.unclaimed_rewards),
+ cost_params: delegation.cost_params && {
+ ...delegation.cost_params,
+ interval_operating_cost: decCoinToDisplay(delegation.cost_params.interval_operating_cost),
+ },
+ }));
+
+ const td = parseFloat(data.total_delegations.amount);
+ const tr = parseFloat(data.total_rewards.amount);
+ const delegationsAndRewards = Number.isFinite(td) && Number.isFinite(tr) ? (td + tr).toFixed(6) : '0';
+
+ return {
+ delegations: [...items, ...pendingOnNewNodes],
+ pendingDelegations: pending,
+ totalDelegations: `${data.total_delegations.amount} ${data.total_delegations.denom}`,
+ totalRewards: `${data.total_rewards.amount} ${data.total_rewards.denom}`,
+ totalDelegationsAndRewards: `${delegationsAndRewards} ${data.total_delegations.denom}`,
+ };
+}
+
+export { delegationQueryKeys } from './delegationQueryKeys';
diff --git a/nym-wallet/src/context/delegationQueryKeys.test.ts b/nym-wallet/src/context/delegationQueryKeys.test.ts
new file mode 100644
index 0000000000..12197eb769
--- /dev/null
+++ b/nym-wallet/src/context/delegationQueryKeys.test.ts
@@ -0,0 +1,7 @@
+import { delegationQueryKeys } from './delegationQueryKeys';
+
+describe('delegationQueryKeys', () => {
+ it('builds a stable summary key per client address', () => {
+ expect(delegationQueryKeys.summary('nyc1test')).toStrictEqual(['delegation', 'summary', 'nyc1test']);
+ });
+});
diff --git a/nym-wallet/src/context/delegationQueryKeys.ts b/nym-wallet/src/context/delegationQueryKeys.ts
new file mode 100644
index 0000000000..19e4ce54b2
--- /dev/null
+++ b/nym-wallet/src/context/delegationQueryKeys.ts
@@ -0,0 +1,4 @@
+export const delegationQueryKeys = {
+ all: ['delegation'] as const,
+ summary: (clientAddress: string) => [...delegationQueryKeys.all, 'summary', clientAddress] as const,
+};
diff --git a/nym-wallet/src/context/delegations.tsx b/nym-wallet/src/context/delegations.tsx
index d77e52467e..351be2049e 100644
--- a/nym-wallet/src/context/delegations.tsx
+++ b/nym-wallet/src/context/delegations.tsx
@@ -1,5 +1,7 @@
-import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useState } from 'react';
-import { getDelegationSummary, undelegateFromMixnode } from 'src/requests/delegation';
+import React, { createContext, FC, useCallback, useContext, useEffect, useMemo } from 'react';
+import { useLocation } from 'react-router-dom';
+import { useQuery, useQueryClient } from '@tanstack/react-query';
+import { undelegateFromMixnode } from 'src/requests/delegation';
import {
DecCoin,
DelegationWithEverything,
@@ -9,19 +11,19 @@ import {
WrappedDelegationEvent,
} from '@nymproject/types';
import type { Network } from 'src/types';
-import {
- delegateToMixnode,
- getAllPendingDelegations,
- vestingDelegateToMixnode,
- vestingUndelegateFromMixnode,
-} from 'src/requests';
+import { delegateToMixnode, vestingDelegateToMixnode, vestingUndelegateFromMixnode } from 'src/requests';
import { TPoolOption } from 'src/components';
-import { decCoinToDisplay } from 'src/utils';
import { Console } from 'src/utils/console';
+import { AppContext } from 'src/context/main';
+import { delegationQueryKeys } from './delegationQueryKeys';
+import { fetchDelegationSummaryQuery } from './delegationQuery';
export type TDelegationContext = {
delegationItemErrors?: { nodeId: string; errors: string };
isLoading: boolean;
+ isFetching: boolean;
+ isError: boolean;
+ lastUpdatedAtMs: number;
delegations?: TDelegations;
pendingDelegations?: WrappedDelegationEvent[];
totalDelegations?: string;
@@ -52,7 +54,10 @@ export const isDelegation = (delegation: DelegationWithEvent): delegation is Del
'owner' in delegation;
export const DelegationContext = createContext({
- isLoading: true,
+ isLoading: false,
+ isFetching: false,
+ isError: false,
+ lastUpdatedAtMs: 0,
refresh: async () => undefined,
addDelegation: async () => {
throw new Error('Not implemented');
@@ -66,18 +71,47 @@ export const DelegationContext = createContext({
setDelegationItemErrors: () => undefined,
});
+function isDelegationRoutePath(pathname: string): boolean {
+ return pathname === '/delegation' || pathname.endsWith('/delegation');
+}
+
export const DelegationContextProvider: FC<{
network?: Network;
children: React.ReactNode;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
}> = ({ network, children }) => {
- const [isLoading, setIsLoading] = useState(true);
- const [delegationItemErrors, setDelegationItemErrors] = useState<{ nodeId: string; errors: string }>();
- const [delegations, setDelegations] = useState();
- const [totalDelegations, setTotalDelegations] = useState();
- const [totalRewards, setTotalRewards] = useState();
- const [totalDelegationsAndRewards, setTotalDelegationsAndRewards] = useState();
- const [pendingDelegations, setPendingDelegations] = useState();
+ const location = useLocation();
+ const queryClient = useQueryClient();
+ const { clientDetails } = useContext(AppContext);
+ const clientAddress = clientDetails?.client_address;
+ const onDelegationRoute = isDelegationRoutePath(location.pathname);
+
+ const [delegationItemErrors, setDelegationItemErrors] = React.useState<{ nodeId: string; errors: string }>();
+
+ const query = useQuery({
+ queryKey: delegationQueryKeys.summary(clientAddress ?? ''),
+ queryFn: fetchDelegationSummaryQuery,
+ enabled: Boolean(clientAddress) && onDelegationRoute,
+ staleTime: 5 * 60 * 1000,
+ gcTime: 30 * 60 * 1000,
+ });
+
+ useEffect(() => {
+ if (!clientAddress) {
+ queryClient.removeQueries({ queryKey: delegationQueryKeys.all });
+ }
+ }, [clientAddress, queryClient]);
+
+ const bundle = clientAddress && onDelegationRoute ? query.data : undefined;
+
+ const refresh = useCallback(async () => {
+ if (!clientAddress || !onDelegationRoute) {
+ return;
+ }
+ await queryClient.invalidateQueries({
+ queryKey: delegationQueryKeys.summary(clientAddress),
+ });
+ }, [clientAddress, onDelegationRoute, queryClient]);
const addDelegation = async (data: { mix_id: number; amount: DecCoin }, tokenPool: TPoolOption, fee?: FeeDetails) => {
try {
@@ -91,51 +125,29 @@ export const DelegationContextProvider: FC<{
return tx;
} catch (e) {
- throw new Error(e as string);
+ const message = e instanceof Error ? e.message : String(e);
+ throw new Error(message);
}
};
- const refresh = useCallback(async () => {
- setIsLoading(true);
- try {
- const data = await getDelegationSummary();
- const pending = await getAllPendingDelegations();
+ const delegations = bundle?.delegations;
+ const pendingDelegations = bundle?.pendingDelegations;
+ const totalDelegations = bundle?.totalDelegations;
+ const totalRewards = bundle?.totalRewards;
+ const totalDelegationsAndRewards = bundle?.totalDelegationsAndRewards;
- const pendingOnNewNodes = pending.filter((event) => {
- const some = data.delegations.some(({ node_identity }) => node_identity === event.node_identity);
- return !some;
- });
- const items = data.delegations.map((delegation) => ({
- ...delegation,
- amount: decCoinToDisplay(delegation.amount),
- unclaimed_rewards: delegation.unclaimed_rewards && decCoinToDisplay(delegation.unclaimed_rewards),
- cost_params: delegation.cost_params && {
- ...delegation.cost_params,
- interval_operating_cost: decCoinToDisplay(delegation.cost_params.interval_operating_cost),
- },
- }));
-
- const delegationsAndRewards = (+data.total_delegations.amount + +data.total_rewards.amount).toFixed(6);
-
- setPendingDelegations(pending);
- setDelegations([...items, ...pendingOnNewNodes]);
- setTotalDelegations(`${data.total_delegations.amount} ${data.total_delegations.denom}`);
- setTotalRewards(`${data.total_rewards.amount} ${data.total_rewards.denom}`);
- setTotalDelegationsAndRewards(`${delegationsAndRewards} ${data.total_delegations.denom}`);
- } catch (e) {
- Console.error(e);
- }
- setIsLoading(false);
- }, []);
-
- useEffect(() => {
- refresh();
- }, []);
+ const isLoading = Boolean(clientAddress) && onDelegationRoute && query.isPending;
+ const isFetching = Boolean(clientAddress) && onDelegationRoute && query.isFetching;
+ const isError = Boolean(clientAddress) && onDelegationRoute && query.isError && !query.data;
+ const lastUpdatedAtMs = bundle ? query.dataUpdatedAt : 0;
const memoizedValue = useMemo(
() => ({
delegationItemErrors,
isLoading,
+ isFetching,
+ isError,
+ lastUpdatedAtMs,
delegations,
pendingDelegations,
totalDelegations,
@@ -148,16 +160,26 @@ export const DelegationContextProvider: FC<{
undelegateVesting: vestingUndelegateFromMixnode,
}),
[
- isLoading,
- delegations,
delegationItemErrors,
+ isLoading,
+ isFetching,
+ isError,
+ lastUpdatedAtMs,
+ delegations,
pendingDelegations,
totalDelegations,
totalRewards,
totalDelegationsAndRewards,
+ refresh,
],
);
+ useEffect(() => {
+ if (query.isError && query.error) {
+ Console.error(query.error);
+ }
+ }, [query.isError, query.error]);
+
return {children};
};
diff --git a/nym-wallet/src/context/mocks/delegations.tsx b/nym-wallet/src/context/mocks/delegations.tsx
index f2aea600bc..27c15d3e1d 100644
--- a/nym-wallet/src/context/mocks/delegations.tsx
+++ b/nym-wallet/src/context/mocks/delegations.tsx
@@ -7,7 +7,7 @@ import {
FeeDetails,
TransactionExecuteResult,
} from '@nymproject/types';
-import { DelegationContext, TDelegationTransaction } from '../delegations';
+import { DelegationContext } from '../delegations';
import { mockSleep } from './utils';
import { TPoolOption } from '../../components';
@@ -71,6 +71,9 @@ export const MockDelegationContextProvider: FCWithChildren = ({ children }) => {
const [error, setError] = useState();
const [delegations, setDelegations] = useState();
const [totalDelegations, setTotalDelegations] = useState();
+ const [totalRewards, setTotalRewards] = useState();
+ const [totalDelegationsAndRewards, setTotalDelegationsAndRewards] = useState();
+ const [lastUpdatedAtMs, setLastUpdatedAtMs] = useState(0);
const [delegationItemErrors, setDelegationItemErrors] = useState<{ nodeId: string; errors: string }>();
const triggerStateUpdate = () => setTrigger(new Date());
@@ -81,8 +84,16 @@ export const MockDelegationContextProvider: FCWithChildren = ({ children }) => {
const recalculate = async () => {
const newDelegations = await getDelegations();
const newTotalDelegations = `${newDelegations.length * 100} NYM`;
+ const rewardsSum = newDelegations.reduce((acc, d) => {
+ const n = parseFloat(d.unclaimed_rewards?.amount ?? '0');
+ return acc + (Number.isFinite(n) ? n : 0);
+ }, 0);
+ const newTotalRewards = `${rewardsSum} nym`;
setDelegations(newDelegations);
setTotalDelegations(newTotalDelegations);
+ setTotalRewards(newTotalRewards);
+ setTotalDelegationsAndRewards(`${newTotalDelegations} + ${newTotalRewards}`);
+ setLastUpdatedAtMs(Date.now());
};
const addDelegation = async (
@@ -91,7 +102,6 @@ export const MockDelegationContextProvider: FCWithChildren = ({ children }) => {
_fee?: FeeDetails,
): Promise => {
await mockSleep(SLEEP_MS);
- // mockDelegations.push({ ...newDelegation });
await recalculate();
triggerStateUpdate();
@@ -118,52 +128,6 @@ export const MockDelegationContextProvider: FCWithChildren = ({ children }) => {
};
};
- const updateDelegation = async (
- newDelegation: DelegationWithEverything,
- ignorePendingForStorybook?: boolean,
- ): Promise => {
- if (ignorePendingForStorybook) {
- mockDelegations = mockDelegations.map((d) => {
- if (d.node_identity === newDelegation.node_identity) {
- return { ...newDelegation };
- }
- return d;
- });
- await recalculate();
- triggerStateUpdate();
- return {
- transactionUrl:
- 'https://sandbox-blocks.nymtech.net/transactions/55303CD4B91FAC4C2715E40EBB52BB3B92829D9431B3A279D37B5CC58432E354',
- };
- }
-
- await mockSleep(SLEEP_MS);
- mockDelegations = mockDelegations.map((d) => {
- if (d.node_identity === newDelegation.node_identity) {
- return { ...newDelegation, isPending: { blockHeight: 1234, actionType: 'delegate' } };
- }
- return d;
- });
- await recalculate();
- triggerStateUpdate();
-
- setTimeout(async () => {
- mockDelegations = mockDelegations.map((d) => {
- if (d.node_identity === newDelegation.node_identity) {
- return { ...d, isPending: undefined };
- }
- return d;
- });
- await recalculate();
- triggerStateUpdate();
- }, 3000);
-
- return {
- transactionUrl:
- 'https://sandbox-blocks.nymtech.net/transactions/55303CD4B91FAC4C2715E40EBB52BB3B92829D9431B3A279D37B5CC58432E354',
- };
- };
-
const undelegate = async (mix_id: number, _fee?: Fee): Promise => {
await mockSleep(SLEEP_MS);
mockDelegations = mockDelegations.map((d) => {
@@ -193,10 +157,8 @@ export const MockDelegationContextProvider: FCWithChildren = ({ children }) => {
};
};
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const undelegateVesting = async (mix_id: number, _fee?: FeeDetails) => ({
+ const undelegateVesting = async (_mix_id: number): Promise => ({
msg_responses_json: '',
- data_json: '',
logs_json: '',
transaction_hash: '',
gas_info: {
@@ -210,6 +172,9 @@ export const MockDelegationContextProvider: FCWithChildren = ({ children }) => {
setIsLoading(true);
setError(undefined);
setTotalDelegations(undefined);
+ setTotalRewards(undefined);
+ setTotalDelegationsAndRewards(undefined);
+ setLastUpdatedAtMs(0);
setDelegations([]);
};
@@ -227,7 +192,6 @@ export const MockDelegationContextProvider: FCWithChildren = ({ children }) => {
}, []);
useEffect(() => {
- // reset state and refresh
resetState();
refresh();
}, []);
@@ -237,17 +201,34 @@ export const MockDelegationContextProvider: FCWithChildren = ({ children }) => {
delegationItemErrors,
setDelegationItemErrors,
isLoading,
- error,
+ isFetching: isLoading,
+ isError: Boolean(error),
+ lastUpdatedAtMs,
delegations,
+ pendingDelegations: [],
totalDelegations,
+ totalRewards,
+ totalDelegationsAndRewards,
refresh,
- getDelegations,
addDelegation,
- updateDelegation,
undelegate,
undelegateVesting,
}),
- [isLoading, error, delegations, totalDelegations, trigger],
+ [
+ isLoading,
+ error,
+ delegations,
+ totalDelegations,
+ totalRewards,
+ totalDelegationsAndRewards,
+ lastUpdatedAtMs,
+ refresh,
+ addDelegation,
+ undelegate,
+ undelegateVesting,
+ delegationItemErrors,
+ trigger,
+ ],
);
return {children};
diff --git a/nym-wallet/src/context/rewards.tsx b/nym-wallet/src/context/rewards.tsx
index dc6b72a44f..060f5d3e48 100644
--- a/nym-wallet/src/context/rewards.tsx
+++ b/nym-wallet/src/context/rewards.tsx
@@ -3,25 +3,29 @@ import { FeeDetails, TransactionExecuteResult } from '@nymproject/types';
import { useDelegationContext } from './delegations';
import { claimDelegatorRewards } from '../requests';
+export type TRewardsTransaction = {
+ transactionUrl: string;
+ transactionHash: string;
+};
+
type TRewardsContext = {
isLoading: boolean;
error?: string;
totalRewards?: string;
refresh: () => Promise;
claimRewards: (mixId: number, fee?: FeeDetails) => Promise;
-};
-
-export type TRewardsTransaction = {
- transactionUrl: string;
- transactionHash: string;
+ redeemAllRewards: () => Promise;
};
export const RewardsContext = createContext({
- isLoading: true,
+ isLoading: false,
refresh: async () => undefined,
claimRewards: async () => {
throw new Error('Not implemented');
},
+ redeemAllRewards: async () => {
+ throw new Error('Not implemented');
+ },
});
export const RewardsContextProvider: FCWithChildren = ({ children }) => {
@@ -47,7 +51,7 @@ export const RewardsContextProvider: FCWithChildren = ({ children }) => {
throw new Error('Not implemented');
},
}),
- [isLoading, error, totalRewards],
+ [isLoading, error, totalRewards, refresh],
);
return {children};
diff --git a/nym-wallet/src/log.tsx b/nym-wallet/src/log.tsx
index 8fc9f166c1..24330d7f26 100644
--- a/nym-wallet/src/log.tsx
+++ b/nym-wallet/src/log.tsx
@@ -1,15 +1,18 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { ErrorBoundary } from 'react-error-boundary';
+import { SnackbarProvider } from 'notistack';
import { LogViewer } from './components/LogViewer';
import { ErrorFallback } from './components';
import { NymWalletTheme } from './theme';
const Log = () => (
-
-
-
+
+
+
+
+
);
diff --git a/nym-wallet/src/pages/delegation/index.tsx b/nym-wallet/src/pages/delegation/index.tsx
index 7b3abbfc54..6d367b48b5 100644
--- a/nym-wallet/src/pages/delegation/index.tsx
+++ b/nym-wallet/src/pages/delegation/index.tsx
@@ -1,4 +1,4 @@
-import React, { FC, useContext, useEffect, useState } from 'react';
+import React, { FC, useCallback, useContext, useEffect, useRef, useState } from 'react';
import { OpenInNew } from '@mui/icons-material';
import { Alert, AlertTitle, Box, Button, CircularProgress, LinearProgress, Stack, Typography } from '@mui/material';
import { alpha, useTheme } from '@mui/material/styles';
@@ -11,6 +11,7 @@ import { Console } from 'src/utils/console';
import { OverSaturatedBlockerModal } from 'src/components/Delegation/DelegateBlocker';
import { getSpendableCoins, migrateVestedDelegations, userBalance } from 'src/requests';
import { LoadingModal } from 'src/components/Modals/LoadingModal';
+import { format } from 'date-fns';
import { getIntervalAsDate, toPercentIntegerString } from 'src/utils';
import { DelegationContextProvider, isDelegation, TDelegations, useDelegationContext } from '../../context/delegations';
import { RewardsContextProvider, useRewardsContext } from '../../context/rewards';
@@ -44,6 +45,9 @@ export const Delegation: FC = () => {
const {
delegations,
isLoading,
+ isFetching,
+ isError,
+ lastUpdatedAtMs,
addDelegation,
undelegate,
undelegateVesting,
@@ -55,9 +59,11 @@ export const Delegation: FC = () => {
[delegations],
);
- const { refresh: refreshRewards, claimRewards } = useRewardsContext();
+ const { claimRewards } = useRewardsContext();
- const refresh = async () => Promise.all([refreshDelegations(), refreshRewards()]);
+ const refreshWithData = useCallback(async () => {
+ await refreshDelegations();
+ }, [refreshDelegations]);
// If an action modal is open, don't show the loading modal
const isActionModalOpen =
@@ -83,36 +89,54 @@ export const Delegation: FC = () => {
};
};
- const getNextInterval = async () => {
+ const getNextInterval = useCallback(async () => {
try {
const { nextEpoch: newNextEpoch } = await getIntervalAsDate();
setNextEpoch(newNextEpoch);
} catch {
setNextEpoch(Error());
}
- };
+ }, []);
- const refreshWithIntervalUpdate = async () => {
- refresh();
- getNextInterval();
- };
+ const refreshWithIntervalUpdate = useCallback(async () => {
+ await refreshWithData();
+ await getNextInterval();
+ }, [refreshWithData, getNextInterval]);
+
+ const refreshWithIntervalUpdateRef = useRef(refreshWithIntervalUpdate);
+ refreshWithIntervalUpdateRef.current = refreshWithIntervalUpdate;
- // Refresh the rewards and delegations periodically when page is mounted
useEffect(() => {
- const timer = setInterval(refreshWithIntervalUpdate, 5 * 60 * 1000); // every 5 minutes
+ const timer = setInterval(() => {
+ refreshWithIntervalUpdateRef.current().catch((err) => {
+ Console.error(err);
+ });
+ }, 5 * 60 * 1000);
return () => clearInterval(timer);
}, []);
const doMigrateNow = async () => {
setShowVestingMigrationProgressModal(true);
await migrateVestedDelegations();
- await refresh();
+ await refreshDelegations();
setShowVestingMigrationProgressModal(false);
};
useEffect(() => {
- refreshWithIntervalUpdate();
- }, [clientDetails, confirmationModalProps]);
+ getNextInterval().catch((err) => {
+ Console.error(err);
+ });
+ }, [clientDetails, getNextInterval]);
+
+ const prevConfirmationModalProps = useRef(undefined);
+ useEffect(() => {
+ if (prevConfirmationModalProps.current !== undefined && confirmationModalProps === undefined) {
+ refreshWithIntervalUpdate().catch((err) => {
+ Console.error(err);
+ });
+ }
+ prevConfirmationModalProps.current = confirmationModalProps;
+ }, [confirmationModalProps, refreshWithIntervalUpdate]);
const handleDelegationItemActionClick = (item: DelegationWithEverything, action: DelegationListItemActions) => {
if (
@@ -409,6 +433,14 @@ export const Delegation: FC = () => {
}}
>
+ {isError && (
+ Could not load delegation data. Check your connection and try again.
+ )}
+ {lastUpdatedAtMs > 0 && (
+
+ Last updated: {format(lastUpdatedAtMs, 'yyyy-MM-dd HH:mm:ss')}
+
+ )}
{!!delegations?.length && (
{
)}
- {isLoading && delegations !== undefined && !isActionModalOpen && (
+ {isFetching && delegations !== undefined && !isActionModalOpen && (