diff --git a/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx b/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx
index f422efdc8d..18c45ec968 100644
--- a/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx
+++ b/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx
@@ -25,6 +25,7 @@ interface Props {
export const UnbondModal = ({ node, onConfirm, onClose, onError }: Props) => {
const { fee, isFeeLoading, getFee, feeError } = useGetFee();
const unbondReturn = formatOperatorUnbondReturn(node);
+ const compoundedRewards = unbondReturn.hasCompoundedRewards ? unbondReturn.operatorRewards : null;
useEffect(() => {
if (feeError) {
@@ -66,14 +67,10 @@ export const UnbondModal = ({ node, onConfirm, onClose, onError }: Props) => {
Could not calculate exact return - check your wallet balance after unbonding.
)}
- {unbondReturn.hasCompoundedRewards ? (
+ {compoundedRewards ? (
<>
-
+
Delegator stake is returned to delegators separately and is not included in this total.
diff --git a/nym-wallet/src/hooks/balanceRefreshOrchestration.test.ts b/nym-wallet/src/hooks/balanceRefreshOrchestration.test.ts
new file mode 100644
index 0000000000..0dfc513cfb
--- /dev/null
+++ b/nym-wallet/src/hooks/balanceRefreshOrchestration.test.ts
@@ -0,0 +1,13 @@
+import { runBalanceRefreshWithoutNestedLoading } from './balanceRefreshOrchestration';
+
+describe('runBalanceRefreshWithoutNestedLoading', () => {
+ it('delegates loading ownership to the caller by disabling nested loading toggles', async () => {
+ const fetchBalance = jest.fn(async () => undefined);
+ const fetchTokenAllocation = jest.fn(async () => undefined);
+
+ await runBalanceRefreshWithoutNestedLoading(fetchBalance, fetchTokenAllocation);
+
+ expect(fetchBalance).toHaveBeenCalledWith(false);
+ expect(fetchTokenAllocation).toHaveBeenCalledWith(false, false);
+ });
+});
diff --git a/nym-wallet/src/hooks/balanceRefreshOrchestration.ts b/nym-wallet/src/hooks/balanceRefreshOrchestration.ts
new file mode 100644
index 0000000000..1e29a27d9f
--- /dev/null
+++ b/nym-wallet/src/hooks/balanceRefreshOrchestration.ts
@@ -0,0 +1,7 @@
+export async function runBalanceRefreshWithoutNestedLoading(
+ fetchBalance: (manageLoading?: boolean) => Promise,
+ fetchTokenAllocation: (isBackgroundPoll?: boolean, manageLoading?: boolean) => Promise,
+): Promise {
+ await fetchBalance(false);
+ await fetchTokenAllocation(false, false);
+}
diff --git a/nym-wallet/src/hooks/useGetBalance.ts b/nym-wallet/src/hooks/useGetBalance.ts
index c226793e2d..aabf6fd0fb 100644
--- a/nym-wallet/src/hooks/useGetBalance.ts
+++ b/nym-wallet/src/hooks/useGetBalance.ts
@@ -13,6 +13,7 @@ import {
userBalance,
} from '../requests';
import { Console } from '../utils/console';
+import { runBalanceRefreshWithoutNestedLoading } from './balanceRefreshOrchestration';
type TTokenAllocation = {
[key in
@@ -98,16 +99,22 @@ export const useGetBalance = (clientDetails?: Account): TUseuserBalance => {
setVestingAccountInfo(vestingAccountDetail);
};
- const fetchTokenAllocation = async (isBackgroundPoll = false) => {
- setIsLoading(true);
+ const fetchTokenAllocation = async (isBackgroundPoll = false, manageLoading = true) => {
+ if (manageLoading) {
+ setIsLoading(true);
+ }
if (!clientDetails?.client_address) {
- setIsLoading(false);
+ if (manageLoading) {
+ setIsLoading(false);
+ }
return;
}
if (vestingAccountStatusRef.current === 'absent') {
if (isBackgroundPoll) {
- setIsLoading(false);
+ if (manageLoading) {
+ setIsLoading(false);
+ }
return;
}
vestingAccountStatusRef.current = 'unknown';
@@ -204,12 +211,16 @@ export const useGetBalance = (clientDetails?: Account): TUseuserBalance => {
clearVestingUiState();
Console.error(e as string);
} finally {
- setIsLoading(false);
+ if (manageLoading) {
+ setIsLoading(false);
+ }
}
};
- const fetchBalance = useCallback(async () => {
- setIsLoading(true);
+ const fetchBalance = useCallback(async (manageLoading = true) => {
+ if (manageLoading) {
+ setIsLoading(true);
+ }
setError(undefined);
try {
const bal = await userBalance();
@@ -217,7 +228,9 @@ export const useGetBalance = (clientDetails?: Account): TUseuserBalance => {
} catch (err) {
setError(err as string);
} finally {
- setIsLoading(false);
+ if (manageLoading) {
+ setIsLoading(false);
+ }
}
}, []);
@@ -236,9 +249,10 @@ export const useGetBalance = (clientDetails?: Account): TUseuserBalance => {
clearAll();
return;
}
+ setIsLoading(true);
+ setError(undefined);
try {
- await fetchBalance();
- await fetchTokenAllocation();
+ await runBalanceRefreshWithoutNestedLoading(fetchBalance, fetchTokenAllocation);
} finally {
setIsLoading(false);
}
@@ -249,8 +263,6 @@ export const useGetBalance = (clientDetails?: Account): TUseuserBalance => {
clearAll();
return;
}
- setIsLoading(true);
- setError(undefined);
refreshBalances().catch((e) => Console.error(String(e)));
}, [clientDetails?.client_address]);
diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralNymNodeSettings.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralNymNodeSettings.tsx
index 4f5a9c2371..c48f41e02e 100644
--- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralNymNodeSettings.tsx
+++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralNymNodeSettings.tsx
@@ -16,7 +16,7 @@ import { useBondingContext, AppContext } from 'src/context';
import { TBondedNymNode } from 'src/requests/nymNodeDetails';
import { settingsValidationSchema } from 'src/components/Bonding/forms/nym-node/settingsValidationSchema';
import { simulateUpdateNymNodeConfig } from 'src/requests';
-import { getHostnameUpdateErrorMessage } from 'src/utils/hostnameUpdateError';
+import { getNodeSettingsUpdateErrorMessage } from 'src/utils/nodeSettingsUpdateError';
export const GeneralNymNodeSettings = ({ bondedNode }: { bondedNode: TBondedNymNode }) => {
const [openConfirmationModal, setOpenConfirmationModal] = useState(false);
@@ -57,7 +57,7 @@ export const GeneralNymNodeSettings = ({ bondedNode }: { bondedNode: TBondedNymN
try {
const tx = await updateNymNodeConfig(configUpdate, fee);
- const errorMessage = getHostnameUpdateErrorMessage(tx);
+ const errorMessage = getNodeSettingsUpdateErrorMessage(tx);
if (errorMessage) {
setSubmitError(errorMessage);
resetFeeState();
@@ -68,7 +68,7 @@ export const GeneralNymNodeSettings = ({ bondedNode }: { bondedNode: TBondedNymN
setOpenConfirmationModal(true);
} catch (error) {
Console.error(error);
- setSubmitError(getHostnameUpdateErrorMessage(undefined, String(error)));
+ setSubmitError(getNodeSettingsUpdateErrorMessage(undefined, String(error)));
resetFeeState();
}
};
diff --git a/nym-wallet/src/utils/hostnameUpdateError.test.ts b/nym-wallet/src/utils/nodeSettingsUpdateError.test.ts
similarity index 50%
rename from nym-wallet/src/utils/hostnameUpdateError.test.ts
rename to nym-wallet/src/utils/nodeSettingsUpdateError.test.ts
index 7313d8a9bf..292959971e 100644
--- a/nym-wallet/src/utils/hostnameUpdateError.test.ts
+++ b/nym-wallet/src/utils/nodeSettingsUpdateError.test.ts
@@ -1,12 +1,12 @@
-import { getHostnameUpdateErrorMessage } from './hostnameUpdateError';
+import { getNodeSettingsUpdateErrorMessage } from './nodeSettingsUpdateError';
-describe('getHostnameUpdateErrorMessage', () => {
+describe('getNodeSettingsUpdateErrorMessage', () => {
it('returns undefined when the transaction succeeded', () => {
expect(
- getHostnameUpdateErrorMessage({
+ getNodeSettingsUpdateErrorMessage({
transaction_hash: 'abc123',
- logs_json: '',
- msg_responses_json: '',
+ logs_json: '[]',
+ msg_responses_json: '[]',
gas_info: {
gas_wanted: { gas_units: BigInt(1) },
gas_used: { gas_units: BigInt(1) },
@@ -17,23 +17,23 @@ describe('getHostnameUpdateErrorMessage', () => {
});
it('returns context error when provided', () => {
- expect(getHostnameUpdateErrorMessage(undefined, 'an error occurred: insufficient funds')).toBe(
+ expect(getNodeSettingsUpdateErrorMessage(undefined, 'an error occurred: insufficient funds')).toBe(
'an error occurred: insufficient funds',
);
});
it('returns a generic message when the update failed without context error', () => {
- expect(getHostnameUpdateErrorMessage(undefined)).toBe(
+ expect(getNodeSettingsUpdateErrorMessage(undefined)).toBe(
'Unable to update node settings. Check your balance and try again.',
);
});
it('returns an error message when transaction_hash is an empty string', () => {
expect(
- getHostnameUpdateErrorMessage({
+ getNodeSettingsUpdateErrorMessage({
transaction_hash: '',
- logs_json: '',
- msg_responses_json: '',
+ logs_json: '[]',
+ msg_responses_json: '[]',
gas_info: {
gas_wanted: { gas_units: BigInt(1) },
gas_used: { gas_units: BigInt(1) },
@@ -42,4 +42,19 @@ describe('getHostnameUpdateErrorMessage', () => {
}),
).toBe('Unable to update node settings. Check your balance and try again.');
});
+
+ it('returns an error when gas_used is zero despite a non-empty hash', () => {
+ expect(
+ getNodeSettingsUpdateErrorMessage({
+ transaction_hash: 'abc123',
+ logs_json: '[]',
+ msg_responses_json: '[]',
+ gas_info: {
+ gas_wanted: { gas_units: BigInt(1) },
+ gas_used: { gas_units: BigInt(0) },
+ },
+ fee: { amount: '1', denom: 'nym' },
+ }),
+ ).toBe('Unable to update node settings. Check your balance and try again.');
+ });
});
diff --git a/nym-wallet/src/utils/hostnameUpdateError.ts b/nym-wallet/src/utils/nodeSettingsUpdateError.ts
similarity index 64%
rename from nym-wallet/src/utils/hostnameUpdateError.ts
rename to nym-wallet/src/utils/nodeSettingsUpdateError.ts
index 1f323ab264..b838b09b79 100644
--- a/nym-wallet/src/utils/hostnameUpdateError.ts
+++ b/nym-wallet/src/utils/nodeSettingsUpdateError.ts
@@ -1,10 +1,11 @@
import { TransactionExecuteResult } from '@nymproject/types';
+import { isTransactionExecuteSuccessful } from './transactionExecuteSuccess';
-export const getHostnameUpdateErrorMessage = (
+export const getNodeSettingsUpdateErrorMessage = (
tx: TransactionExecuteResult | undefined,
contextError?: string,
): string | undefined => {
- if (tx?.transaction_hash && tx.transaction_hash.length > 0) {
+ if (isTransactionExecuteSuccessful(tx)) {
return undefined;
}
if (contextError) {
diff --git a/nym-wallet/src/utils/transactionExecuteSuccess.test.ts b/nym-wallet/src/utils/transactionExecuteSuccess.test.ts
new file mode 100644
index 0000000000..9e28e424ca
--- /dev/null
+++ b/nym-wallet/src/utils/transactionExecuteSuccess.test.ts
@@ -0,0 +1,53 @@
+import { TransactionExecuteResult } from '@nymproject/types';
+import { isTransactionExecuteSuccessful } from './transactionExecuteSuccess';
+
+const baseTx = {
+ transaction_hash: 'abc123',
+ logs_json: '[]',
+ msg_responses_json: '[]',
+ gas_info: {
+ gas_wanted: { gas_units: BigInt(100) },
+ gas_used: { gas_units: BigInt(50) },
+ },
+ fee: { amount: '1', denom: 'nym' },
+} as TransactionExecuteResult;
+
+describe('isTransactionExecuteSuccessful', () => {
+ it('returns true for a structurally valid execution result', () => {
+ expect(isTransactionExecuteSuccessful(baseTx)).toBe(true);
+ });
+
+ it('returns true when log and response JSON are empty strings', () => {
+ expect(
+ isTransactionExecuteSuccessful({
+ ...baseTx,
+ logs_json: '',
+ msg_responses_json: '',
+ }),
+ ).toBe(true);
+ });
+
+ it('returns false when transaction_hash is empty', () => {
+ expect(isTransactionExecuteSuccessful({ ...baseTx, transaction_hash: '' })).toBe(false);
+ });
+
+ it('returns false when gas_used is zero', () => {
+ expect(
+ isTransactionExecuteSuccessful({
+ ...baseTx,
+ gas_info: {
+ gas_wanted: { gas_units: BigInt(100) },
+ gas_used: { gas_units: BigInt(0) },
+ },
+ }),
+ ).toBe(false);
+ });
+
+ it('returns false when msg_responses_json is not valid JSON', () => {
+ expect(isTransactionExecuteSuccessful({ ...baseTx, msg_responses_json: 'not-json' })).toBe(false);
+ });
+
+ it('returns false when logs_json is not a JSON array', () => {
+ expect(isTransactionExecuteSuccessful({ ...baseTx, logs_json: '{"error":true}' })).toBe(false);
+ });
+});
diff --git a/nym-wallet/src/utils/transactionExecuteSuccess.ts b/nym-wallet/src/utils/transactionExecuteSuccess.ts
new file mode 100644
index 0000000000..024730cd4b
--- /dev/null
+++ b/nym-wallet/src/utils/transactionExecuteSuccess.ts
@@ -0,0 +1,40 @@
+import { TransactionExecuteResult } from '@nymproject/types';
+
+const parseJsonArray = (raw: string): unknown[] | null => {
+ const trimmed = raw.trim();
+ if (!trimmed) {
+ return [];
+ }
+ try {
+ const parsed = JSON.parse(trimmed);
+ return Array.isArray(parsed) ? parsed : null;
+ } catch {
+ return null;
+ }
+};
+
+/**
+ * Rust IPC only returns `TransactionExecuteResult` after DeliverTx succeeds (code 0).
+ * This helper centralizes the TS-side interpretation: non-empty hash plus structurally
+ * valid execution metadata, not hash presence alone.
+ */
+export const isTransactionExecuteSuccessful = (tx: TransactionExecuteResult | undefined): boolean => {
+ if (!tx?.transaction_hash || tx.transaction_hash.length === 0) {
+ return false;
+ }
+
+ const gasUsed = tx.gas_info?.gas_used?.gas_units;
+ if (gasUsed === undefined || gasUsed <= 0n) {
+ return false;
+ }
+
+ if (parseJsonArray(tx.msg_responses_json) === null) {
+ return false;
+ }
+
+ if (parseJsonArray(tx.logs_json) === null) {
+ return false;
+ }
+
+ return true;
+};