{
key={`my-validator-currency-${validator.address}`}
className="text-right leading-tight"
>
-
+
,
- {myValidator.timeLeft}
+ {entry.timeLeft}
,
-
+
,
],
});
- }
+ });
}
return rowsList;
- }, [myUnbonds]);
+ }, [myValidators]);
return (
{
const change = {
validatorId: myValidator.validator.address,
- amount: myValidator.withdrawableAmount!,
+ amount: unbondingEntry.amount,
};
const { gasPrice } = useGasEstimate();
@@ -49,29 +51,26 @@ export const WithdrawalButton = ({
};
}, []);
- const onWithdraw = useCallback(
- async (myValidator: MyValidator) => {
- invariant(
- account,
- "Extension is not connected or you don't have an account"
- );
- invariant(gasPrice, "Gas price loading is still pending");
- invariant(gasLimits.isSuccess, "Gas limit loading is still pending");
- invariant(
- myValidator.withdrawableAmount,
- "Validator doesn't have amounts available for withdrawal"
- );
- createWithdrawTx({
- changes: [change],
- gasConfig: {
- gasPrice: gasPrice!,
- gasLimit: gasLimits.data!.Withdraw.native,
- },
- account: account!,
- });
- },
- [myValidator.withdrawableAmount, gasPrice, gasLimits.isSuccess]
- );
+ const onWithdraw = useCallback(async () => {
+ invariant(
+ account,
+ "Extension is not connected or you don't have an account"
+ );
+ invariant(gasPrice, "Gas price loading is still pending");
+ invariant(gasLimits.isSuccess, "Gas limit loading is still pending");
+ invariant(
+ unbondingEntry.amount,
+ "Validator doesn't have amounts available for withdrawal"
+ );
+ createWithdrawTx({
+ changes: [change],
+ gasConfig: {
+ gasPrice: gasPrice,
+ gasLimit: gasLimits.data.Withdraw.native,
+ },
+ account,
+ });
+ }, [unbondingEntry.amount, change, gasPrice, gasLimits.isSuccess]);
const dispatchNotification = useSetAtom(dispatchToastNotificationAtom);
@@ -133,8 +132,8 @@ export const WithdrawalButton = ({
onWithdraw(myValidator)}
+ disabled={!unbondingEntry.canWithdraw || isPending || isSuccess}
+ onClick={() => onWithdraw()}
>
{isSuccess && "Claimed"}
{isPending && "Processing"}
diff --git a/apps/namadillo/src/atoms/syncStatus/atoms.ts b/apps/namadillo/src/atoms/syncStatus/atoms.ts
index be2fd3c4e..f50f58dde 100644
--- a/apps/namadillo/src/atoms/syncStatus/atoms.ts
+++ b/apps/namadillo/src/atoms/syncStatus/atoms.ts
@@ -1,11 +1,7 @@
import { accountBalanceAtom } from "atoms/accounts/atoms";
import { allProposalsAtom, votedProposalIdsAtom } from "atoms/proposals/atoms";
import { indexerHeartbeatAtom, rpcHeartbeatAtom } from "atoms/settings/atoms";
-import {
- allValidatorsAtom,
- myUnbondsAtom,
- myValidatorsAtom,
-} from "atoms/validators/atoms";
+import { allValidatorsAtom, myValidatorsAtom } from "atoms/validators/atoms";
import { atom } from "jotai";
export const syncStatusAtom = atom((get) => {
@@ -17,7 +13,6 @@ export const syncStatusAtom = atom((get) => {
// Staking
get(accountBalanceAtom),
get(myValidatorsAtom),
- get(myUnbondsAtom),
get(allValidatorsAtom),
// Governance
diff --git a/apps/namadillo/src/atoms/validators/atoms.ts b/apps/namadillo/src/atoms/validators/atoms.ts
index f1c8a50ce..6583e9f4c 100644
--- a/apps/namadillo/src/atoms/validators/atoms.ts
+++ b/apps/namadillo/src/atoms/validators/atoms.ts
@@ -3,17 +3,13 @@ import { indexerApiAtom } from "atoms/api";
import { chainParametersAtom } from "atoms/chain";
import { shouldUpdateBalanceAtom } from "atoms/etc";
import { queryDependentFn } from "atoms/utils";
-import BigNumber from "bignumber.js";
-import {
- AtomWithQueryResult,
- UndefinedInitialDataOptions,
- atomWithQuery,
-} from "jotai-tanstack-query";
-import { MyUnbondingValidator, MyValidator, Validator } from "types";
+import { atomWithQuery } from "jotai-tanstack-query";
+import { MyValidator, Validator } from "types";
+import { toMyValidators } from "./functions";
import {
fetchAllValidators,
- fetchMyUnbonds,
- fetchMyValidators,
+ fetchMyBondedAmounts,
+ fetchMyUnbondedAmounts,
fetchVotingPower,
} from "./services";
@@ -51,85 +47,20 @@ export const myValidatorsAtom = atomWithQuery((get) => {
return {
queryKey: ["my-validators", account.data?.address],
refetchInterval: enablePolling ? 1000 : false,
- ...queryDependentFn(
- async (): Promise =>
- fetchMyValidators(
- api,
- account.data!,
- chainParameters.data!,
- votingPower.data!
- ),
- [account, chainParameters, votingPower]
- ),
+ ...queryDependentFn(async (): Promise => {
+ const bondedAmountsQuery = fetchMyBondedAmounts(api, account.data!);
+ const unbondedAmountsQuery = fetchMyUnbondedAmounts(api, account.data!);
+ const [unbondedAmounts, bondedAmounts] = await Promise.all([
+ unbondedAmountsQuery,
+ bondedAmountsQuery,
+ ]);
+ return toMyValidators(
+ bondedAmounts,
+ unbondedAmounts,
+ votingPower.data!,
+ chainParameters.data!.epochInfo,
+ chainParameters.data!.apr
+ );
+ }, [account, chainParameters, votingPower]),
};
});
-
-export const myUnbondsAtom = atomWithQuery((get) => {
- const chainParameters = get(chainParametersAtom);
- const account = get(defaultAccountAtom);
- const votingPower = get(votingPowerAtom);
- const api = get(indexerApiAtom);
-
- // TODO: Refactor after this event subscription is enabled in the indexer
- const enablePolling = get(shouldUpdateBalanceAtom);
- return {
- queryKey: ["my-unbonds", account.data?.address],
- refetchInterval: enablePolling ? 1000 : false,
- ...queryDependentFn(
- async (): Promise =>
- fetchMyUnbonds(
- api,
- account.data!,
- chainParameters.data!,
- votingPower.data!
- ),
- [account, chainParameters, votingPower]
- ),
- };
-});
-
-export const unbondedAmountByAddressAtom = atomWithQuery((get) =>
- deriveFromMyValidatorsAtom(
- "unbonded-amount",
- "unbondedAmount",
- get(myUnbondsAtom)
- )
-);
-
-export const withdrawableAmountByAddressAtom = atomWithQuery((get) =>
- deriveFromMyValidatorsAtom(
- "withdrawable-amount",
- "withdrawableAmount",
- get(myUnbondsAtom)
- )
-);
-
-export const stakedAmountByAddressAtom = atomWithQuery((get) =>
- deriveFromMyValidatorsAtom(
- "staked-amount",
- "stakedAmount",
- get(myValidatorsAtom)
- )
-);
-
-const deriveFromMyValidatorsAtom = (
- key: string,
- property: "stakedAmount" | "unbondedAmount" | "withdrawableAmount",
- myValidators: AtomWithQueryResult<
- (MyValidator | MyUnbondingValidator)[],
- Error
- >
-): UndefinedInitialDataOptions> => {
- return {
- queryKey: [key, myValidators.data],
- enabled: myValidators.isSuccess,
- queryFn: async () => {
- return myValidators.data!.reduce((prev, current) => {
- if (current[property]?.gt(0)) {
- return { ...prev, [current.validator.address]: current[property] };
- }
- return prev;
- }, {});
- },
- };
-};
diff --git a/apps/namadillo/src/atoms/validators/functions.ts b/apps/namadillo/src/atoms/validators/functions.ts
index b3974d086..abbadc6da 100644
--- a/apps/namadillo/src/atoms/validators/functions.ts
+++ b/apps/namadillo/src/atoms/validators/functions.ts
@@ -6,7 +6,7 @@ import {
} from "@anomaorg/namada-indexer-client";
import { singleUnitDurationFromInterval } from "@namada/utils";
import BigNumber from "bignumber.js";
-import { EpochInfo, MyUnbondingValidator, MyValidator, Validator } from "types";
+import { Address, EpochInfo, MyValidator, UnbondEntry, Validator } from "types";
export const toValidator = (
indexerValidator: IndexerValidator,
@@ -45,67 +45,85 @@ export const toValidator = (
};
};
+export const calculateUnbondingTimeLeft = (unbond: IndexerUnbond): string => {
+ const timeNow = Math.round(Date.now() / 1000);
+ const withdrawTime = Number(unbond.withdrawTime);
+ const canWithdraw = unbond.canWithdraw;
+ const timeLeft =
+ canWithdraw ? ""
+ // If can't withdraw but estimation is incorrect display withdraw epoch
+ : withdrawTime < timeNow ? `Epoch ${unbond.withdrawEpoch}`
+ : singleUnitDurationFromInterval(timeNow, withdrawTime);
+ return timeLeft;
+};
+
+/**
+ * Parses the results returned by the indexer into a MyValidator structure, returning
+ * an array of MyValidators objects
+ */
export const toMyValidators = (
indexerBonds: IndexerBond[],
+ indexerUnbonds: IndexerUnbond[],
totalVotingPower: IndexerVotingPower,
epochInfo: EpochInfo,
apr: BigNumber
): MyValidator[] => {
- return indexerBonds.map((indexerBond) => {
- const validator = toValidator(
- indexerBond.validator,
- totalVotingPower,
- epochInfo,
- apr
- );
+ const myValidators: Record = {};
- return {
- uuid: String(indexerBond.validator.validatorId),
- stakingStatus: "bonded",
- stakedAmount: BigNumber(indexerBond.amount),
- unbondedAmount: BigNumber(0),
- withdrawableAmount: BigNumber(0),
- validator,
- };
- });
-};
+ const createEntryIfDoesntExist = (validator: IndexerValidator): void => {
+ if (!myValidators.hasOwnProperty(validator.address)) {
+ myValidators[validator.address] = {
+ withdrawableAmount: new BigNumber(0),
+ stakedAmount: new BigNumber(0),
+ unbondedAmount: new BigNumber(0),
+ bondItems: [],
+ unbondItems: [],
+ validator: toValidator(validator, totalVotingPower, epochInfo, apr),
+ };
+ }
+ };
-export const toUnbondingValidators = (
- indexerBonds: IndexerUnbond[],
- totalVotingPower: IndexerVotingPower,
- epochInfo: EpochInfo,
- apr: BigNumber
-): MyUnbondingValidator[] => {
- const timeNow = Math.round(Date.now() / 1000);
+ const addBondToAddress = (
+ address: Address,
+ key: "bondItems" | "unbondItems",
+ bond: IndexerBond | IndexerUnbond
+ ): void => {
+ const { validator: _, ...bondsWithoutValidator } = bond;
+ myValidators[address]![key].push(bondsWithoutValidator);
+ };
- return indexerBonds.map((indexerUnbond) => {
- const validator = toValidator(
- indexerUnbond.validator,
- totalVotingPower,
- epochInfo,
- apr
- );
- const withdrawTime = Number(indexerUnbond.withdrawTime);
+ const incrementAmount = (
+ address: Address,
+ prop: keyof Pick<
+ MyValidator,
+ "stakedAmount" | "withdrawableAmount" | "unbondedAmount"
+ >,
+ amount: BigNumber | string
+ ): void => {
+ myValidators[address][prop] = myValidators[address][prop]!.plus(amount);
+ };
- const canWithdraw = indexerUnbond.canWithdraw;
- const timeLeft =
- canWithdraw ? ""
- // If can't withdraw but estimation is incorrect display withdraw epoch
- : withdrawTime < timeNow ? `Epoch ${indexerUnbond.withdrawEpoch}`
- : singleUnitDurationFromInterval(timeNow, withdrawTime);
+ for (const bond of indexerBonds) {
+ const { address } = bond.validator;
+ createEntryIfDoesntExist(bond.validator);
+ incrementAmount(address, "stakedAmount", bond.amount);
+ addBondToAddress(address, "bondItems", { ...bond });
+ }
- const amountValue = BigNumber(indexerUnbond.amount);
- const amount = {
- [canWithdraw ? "withdrawableAmount" : "unbondedAmount"]: amountValue,
+ for (const unbond of indexerUnbonds) {
+ const { address } = unbond.validator;
+ createEntryIfDoesntExist(unbond.validator);
+ const unbondingDetails: UnbondEntry = {
+ ...unbond,
+ timeLeft: calculateUnbondingTimeLeft(unbond),
};
+ addBondToAddress(address, "unbondItems", unbondingDetails);
+ if (unbond.canWithdraw) {
+ incrementAmount(address, "withdrawableAmount", unbond.amount);
+ } else {
+ incrementAmount(address, "unbondedAmount", unbond.amount);
+ }
+ }
- return {
- uuid: String(indexerUnbond.validator.validatorId),
- stakingStatus: "unbonded",
- stakedAmount: BigNumber(0),
- timeLeft,
- validator,
- ...amount,
- };
- });
+ return Object.values(myValidators);
};
diff --git a/apps/namadillo/src/atoms/validators/services.ts b/apps/namadillo/src/atoms/validators/services.ts
index 7ae8dd3db..842001c3b 100644
--- a/apps/namadillo/src/atoms/validators/services.ts
+++ b/apps/namadillo/src/atoms/validators/services.ts
@@ -2,20 +2,13 @@ import {
DefaultApi,
ValidatorStatus as IndexerValidatorStatus,
VotingPower as IndexerVotingPower,
+ MergedBond,
+ Unbond,
VotingPower,
} from "@anomaorg/namada-indexer-client";
import { Account } from "@namada/types";
-import {
- ChainParameters,
- MyUnbondingValidator,
- MyValidator,
- Validator,
-} from "types";
-import {
- toMyValidators,
- toUnbondingValidators,
- toValidator,
-} from "./functions";
+import { ChainParameters, Validator } from "types";
+import { toValidator } from "./functions";
export const fetchVotingPower = async (
api: DefaultApi
@@ -41,40 +34,22 @@ export const fetchAllValidators = async (
);
};
-export const fetchMyValidators = async (
+export const fetchMyBondedAmounts = async (
api: DefaultApi,
- account: Account,
- chainParameters: ChainParameters,
- votingPower: IndexerVotingPower
-): Promise => {
- const epochInfo = chainParameters.epochInfo;
- const apr = chainParameters.apr;
+ account: Account
+): Promise => {
const bondsResponse = await api.apiV1PosMergedBondsAddressGet(
account.address
);
- return toMyValidators(
- bondsResponse.data.results,
- votingPower,
- epochInfo,
- apr
- );
+ return bondsResponse.data.results;
};
-export const fetchMyUnbonds = async (
+export const fetchMyUnbondedAmounts = async (
api: DefaultApi,
- account: Account,
- chainParameters: ChainParameters,
- votingPower: IndexerVotingPower
-): Promise => {
- const epochInfo = chainParameters.epochInfo;
- const apr = chainParameters.apr;
+ account: Account
+): Promise => {
const unbondsResponse = await api.apiV1PosMergedUnbondsAddressGet(
account.address
);
- return toUnbondingValidators(
- unbondsResponse.data.results,
- votingPower,
- epochInfo,
- apr
- );
+ return unbondsResponse.data.results;
};
diff --git a/apps/namadillo/src/types.d.ts b/apps/namadillo/src/types.d.ts
index a41fe1d6b..863b2159b 100644
--- a/apps/namadillo/src/types.d.ts
+++ b/apps/namadillo/src/types.d.ts
@@ -1,3 +1,7 @@
+import {
+ Bond as IndexerBond,
+ Unbond as IndexerUnbond,
+} from "@anomaorg/namada-indexer-client";
import { ChainKey, ExtensionKey } from "@namada/types";
import BigNumber from "bignumber.js";
@@ -75,16 +79,22 @@ export type Validator = Unique & {
imageUrl?: string;
};
+export type UnbondEntry = Omit<
+ | (IndexerUnbond & {
+ timeLeft: string;
+ })
+ | "validator"
+>;
+
+export type BondEntry = Omit;
+
export type MyValidator = {
- stakingStatus: string;
stakedAmount?: BigNumber;
unbondedAmount?: BigNumber;
withdrawableAmount?: BigNumber;
validator: Validator;
-};
-
-export type MyUnbondingValidator = MyValidator & {
- timeLeft: string;
+ bondItems: BondEntry[];
+ unbondItems: UnbondEntry[];
};
export type StakingTotals = {
From c423c0c2d9c6016091a7aa59bb87094e1b6a7a6a Mon Sep 17 00:00:00 2001
From: Eric Corson
Date: Tue, 27 Aug 2024 14:38:38 +0900
Subject: [PATCH 3/6] fix: start toast timeout when toast type changes (#1049)
This fixes toasts not automatically closing when a pending toast turns
into a success/failure toast.
---
apps/namadillo/src/App/Common/Toast.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/namadillo/src/App/Common/Toast.tsx b/apps/namadillo/src/App/Common/Toast.tsx
index 81fbba365..224ec402a 100644
--- a/apps/namadillo/src/App/Common/Toast.tsx
+++ b/apps/namadillo/src/App/Common/Toast.tsx
@@ -61,7 +61,7 @@ const Toast = ({ notification, onClose }: ToastProps): JSX.Element => {
useEffect(() => {
startTimeout();
- }, []);
+ }, [notification.type]);
return (
Date: Tue, 27 Aug 2024 14:49:44 +0900
Subject: [PATCH 4/6] fix: set default timeout for success/error toasts (#1049)
---
apps/namadillo/src/App/Common/Toast.tsx | 12 +++++++++---
apps/namadillo/src/App/Staking/IncrementBonding.tsx | 1 -
apps/namadillo/src/App/Staking/ReDelegate.tsx | 1 -
.../src/hooks/useTransactionNotifications.tsx | 9 ---------
4 files changed, 9 insertions(+), 14 deletions(-)
diff --git a/apps/namadillo/src/App/Common/Toast.tsx b/apps/namadillo/src/App/Common/Toast.tsx
index 224ec402a..132c27524 100644
--- a/apps/namadillo/src/App/Common/Toast.tsx
+++ b/apps/namadillo/src/App/Common/Toast.tsx
@@ -40,22 +40,28 @@ const Toast = ({ notification, onClose }: ToastProps): JSX.Element => {
const [viewDetails, setViewDetails] = useState(false);
const interval = useRef();
+ const timeout =
+ notification.timeout ??
+ (notification.type === "success" || notification.type === "error" ?
+ 5000
+ : undefined);
+
const closeNotification = (): void => {
onClose(notification);
};
const keepNotification = (): void => {
- if (notification.timeout && interval.current) {
+ if (interval.current) {
clearTimeout(interval.current);
interval.current = undefined;
}
};
const startTimeout = (): void => {
- if (notification.timeout && !interval.current) {
+ if (typeof timeout !== "undefined" && !interval.current) {
interval.current = setTimeout(() => {
closeNotification();
- }, notification.timeout);
+ }, timeout);
}
};
diff --git a/apps/namadillo/src/App/Staking/IncrementBonding.tsx b/apps/namadillo/src/App/Staking/IncrementBonding.tsx
index 2f4a6dc95..b970ca2f3 100644
--- a/apps/namadillo/src/App/Staking/IncrementBonding.tsx
+++ b/apps/namadillo/src/App/Staking/IncrementBonding.tsx
@@ -151,7 +151,6 @@ const IncrementBonding = (): JSX.Element => {
bondTransactionError instanceof Error ?
bondTransactionError.message
: undefined,
- timeout: 5000,
type: "error",
});
}
diff --git a/apps/namadillo/src/App/Staking/ReDelegate.tsx b/apps/namadillo/src/App/Staking/ReDelegate.tsx
index f910ee022..e19def6a2 100644
--- a/apps/namadillo/src/App/Staking/ReDelegate.tsx
+++ b/apps/namadillo/src/App/Staking/ReDelegate.tsx
@@ -113,7 +113,6 @@ export const ReDelegate = (): JSX.Element => {
redelegateTxError.message
: undefined,
type: "error",
- timeout: 5000,
});
}
}, [isError]);
diff --git a/apps/namadillo/src/hooks/useTransactionNotifications.tsx b/apps/namadillo/src/hooks/useTransactionNotifications.tsx
index e66dc6b8b..744fe75c0 100644
--- a/apps/namadillo/src/hooks/useTransactionNotifications.tsx
+++ b/apps/namadillo/src/hooks/useTransactionNotifications.tsx
@@ -86,7 +86,6 @@ export const useTransactionNotifications = (): void => {
>
),
details: e.detail.error?.message,
- timeout: 5000,
});
});
@@ -104,7 +103,6 @@ export const useTransactionNotifications = (): void => {
),
details: getAmountByValidatorList(e.detail.data),
type: "success",
- timeout: 5000,
});
});
@@ -121,7 +119,6 @@ export const useTransactionNotifications = (): void => {
),
details: getAmountByValidatorList(e.detail.data),
type: "success",
- timeout: 5000,
});
});
@@ -149,7 +146,6 @@ export const useTransactionNotifications = (): void => {
title: "Withdrawal Success",
description: `Your withdrawal transaction has succeeded`,
type: "success",
- timeout: 5000,
});
});
@@ -162,7 +158,6 @@ export const useTransactionNotifications = (): void => {
description: <>Your withdrawal transaction has failed>,
details: e.detail.error?.message,
type: "error",
- timeout: 5000,
});
});
@@ -179,7 +174,6 @@ export const useTransactionNotifications = (): void => {
>
),
type: "error",
- timeout: 5000,
});
});
@@ -197,7 +191,6 @@ export const useTransactionNotifications = (): void => {
),
details: getReDelegateDetailList(e.detail.data),
type: "success",
- timeout: 5000,
});
});
@@ -210,7 +203,6 @@ export const useTransactionNotifications = (): void => {
title: "Staking transaction failed",
description: <>Your vote transaction has failed.>,
details: e.detail.error?.message,
- timeout: 5000,
});
});
@@ -222,7 +214,6 @@ export const useTransactionNotifications = (): void => {
title: "Staking transaction succeeded",
description: `Your vote transaction has succeeded`,
type: "success",
- timeout: 5000,
});
});
};
From 96860cd440edc856efd0a235ab530825061260e8 Mon Sep 17 00:00:00 2001
From: Eric Corson
Date: Tue, 27 Aug 2024 16:39:35 +0900
Subject: [PATCH 5/6] fix: don't flash lock screen when opening extension
(#1050)
This fixes a Firefox bug where the saved password prompt pops up when
opening the unlocked extension.
Fixes #968.
---
apps/extension/src/App/App.tsx | 22 ++++++++++-------
apps/extension/src/context/AccountContext.tsx | 6 ++---
apps/extension/src/context/VaultContext.tsx | 24 +++++++++++--------
3 files changed, 31 insertions(+), 21 deletions(-)
diff --git a/apps/extension/src/App/App.tsx b/apps/extension/src/App/App.tsx
index 82e93722d..f993c3fcc 100644
--- a/apps/extension/src/App/App.tsx
+++ b/apps/extension/src/App/App.tsx
@@ -9,36 +9,42 @@ import routes from "./routes";
export const App: React.FC = () => {
const location = useLocation();
- const { isLocked, unlock, passwordInitialized } = useVaultContext();
+ const { lockStatus, unlock, passwordInitialized } = useVaultContext();
const displayReturnButton = (): boolean => {
const setupRoute = routes.setup();
const indexRoute = routes.viewAccountList();
return Boolean(
- !isLocked &&
- isLocked !== undefined &&
+ lockStatus === "unlocked" &&
!matchPath(setupRoute, location.pathname) &&
!matchPath(indexRoute, location.pathname)
);
};
- const shouldLock = passwordInitialized && isLocked;
- if (passwordInitialized === undefined) return null;
+ if (passwordInitialized === undefined || lockStatus === "pending") {
+ return null;
+ }
+
+ const shouldLock = passwordInitialized && lockStatus === "locked";
return (
}
>
- {shouldLock ? : }
+ {shouldLock ?
+
+ : }
);
};
diff --git a/apps/extension/src/context/AccountContext.tsx b/apps/extension/src/context/AccountContext.tsx
index 60636062f..111e66133 100644
--- a/apps/extension/src/context/AccountContext.tsx
+++ b/apps/extension/src/context/AccountContext.tsx
@@ -64,7 +64,7 @@ export const AccountContextWrapper = ({
children,
}: AccountContextProps): JSX.Element => {
const requester = useRequester();
- const { isLocked, logout } = useVaultContext();
+ const { lockStatus, logout } = useVaultContext();
const [accounts, setAccounts] = useState([]);
const [parentAccounts, setParentAccounts] = useState([]);
@@ -166,11 +166,11 @@ export const AccountContextWrapper = ({
};
useEffect(() => {
- if (!isLocked) {
+ if (lockStatus === "unlocked") {
void fetchAll();
void fetchActiveAccountId();
}
- }, [isLocked]);
+ }, [lockStatus]);
useEffect(() => {
setParentAccounts(accounts.filter((account) => !account.parentId));
diff --git a/apps/extension/src/context/VaultContext.tsx b/apps/extension/src/context/VaultContext.tsx
index 7513c6caa..8ed27dd67 100644
--- a/apps/extension/src/context/VaultContext.tsx
+++ b/apps/extension/src/context/VaultContext.tsx
@@ -17,13 +17,15 @@ import React, { createContext, useContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Ports } from "router";
+type LockStatus = "locked" | "unlocked" | "pending";
+
// Add types here
type VaultContextType = {
lock: () => Promise;
unlock: (password: string) => Promise;
logout: () => Promise;
checkPassword: (password: string) => Promise;
- isLocked: boolean;
+ lockStatus: LockStatus;
changePassword: (
oldPassword: string,
newPassword: string
@@ -38,7 +40,7 @@ const createVaultContext = (): VaultContextType => {
unlock: async (_password: string) => false,
logout: async () => {},
checkPassword: async (_password: string) => false,
- isLocked: true,
+ lockStatus: "locked",
changePassword: async (_oldPassword: string, _newPassword: string) =>
Result.ok(null),
passwordInitialized: undefined,
@@ -57,7 +59,7 @@ export const VaultContextWrapper = ({
}: VaultContextWrapperProps): JSX.Element => {
const requester = useRequester();
const navigate = useNavigate();
- const [locked, setLocked] = useState(true);
+ const [lockStatus, setLockStatus] = useState("pending");
const [passwordInitialized, setPasswordInitialized] = useState<
undefined | boolean
>();
@@ -69,20 +71,20 @@ export const VaultContextWrapper = ({
);
if (unlocked) {
- setLocked(!unlocked);
+ setLockStatus("unlocked");
}
return unlocked;
};
const lock = async (): Promise => {
await requester.sendMessage(Ports.Background, new LockVaultMsg());
- setLocked(true);
+ setLockStatus("locked");
};
const logout = async (): Promise => {
await requester.sendMessage(Ports.Background, new LogoutMsg());
setPasswordInitialized(false);
- setLocked(true);
+ setLockStatus("locked");
navigate(routes.setup());
};
@@ -114,13 +116,15 @@ export const VaultContextWrapper = ({
};
const queryIsLocked = async (): Promise => {
- setLocked(
- await requester.sendMessage(Ports.Background, new CheckIsLockedMsg())
+ const isLocked = await requester.sendMessage(
+ Ports.Background,
+ new CheckIsLockedMsg()
);
+ setLockStatus(isLocked ? "locked" : "unlocked");
};
useEventListener(Events.ExtensionLocked, () => {
- setLocked(true);
+ setLockStatus("locked");
});
useEffect(() => {
@@ -133,7 +137,7 @@ export const VaultContextWrapper = ({
value={{
lock,
unlock,
- isLocked: locked,
+ lockStatus,
checkPassword,
passwordInitialized,
changePassword,
From 976fe32e559e880a486a09b3e4d885e0100a8a9a Mon Sep 17 00:00:00 2001
From: Mateusz Jasiuk
Date: Thu, 29 Aug 2024 10:26:40 +0200
Subject: [PATCH 6/6] bug: governance and unbonding fixes (#1044)
* refactor: use new api client
* feat: use maxBlockTime to calculate unbonding period
* feat: properly disable vote button and poll proposal changes
* chore: fix PR comments
---
apps/namadillo/package.json | 2 +-
.../src/App/Governance/ProposalHeader.tsx | 4 +-
.../App/Governance/ProposalStatusSummary.tsx | 7 ++-
.../src/App/Governance/SubmitVote.tsx | 3 +-
apps/namadillo/src/atoms/api.ts | 6 ++-
apps/namadillo/src/atoms/chain/services.ts | 1 +
apps/namadillo/src/atoms/etc.ts | 9 +++-
apps/namadillo/src/atoms/proposals/atoms.ts | 51 +++++++++++--------
.../src/atoms/proposals/functions.ts | 11 ++--
apps/namadillo/src/atoms/settings/services.ts | 5 +-
.../src/atoms/validators/functions.ts | 8 +--
.../src/hooks/useTransactionCallbacks.tsx | 13 ++++-
apps/namadillo/src/types.d.ts | 1 +
yarn.lock | 33 +++++++++---
14 files changed, 108 insertions(+), 46 deletions(-)
diff --git a/apps/namadillo/package.json b/apps/namadillo/package.json
index 39f6905f6..5d2a3ebd6 100644
--- a/apps/namadillo/package.json
+++ b/apps/namadillo/package.json
@@ -7,7 +7,7 @@
"license": "MIT",
"private": true,
"dependencies": {
- "@anomaorg/namada-indexer-client": "0.0.21",
+ "@anomaorg/namada-indexer-client": "0.0.23",
"@cosmjs/encoding": "^0.32.3",
"@tailwindcss/container-queries": "^0.1.1",
"@tanstack/react-query": "^5.40.0",
diff --git a/apps/namadillo/src/App/Governance/ProposalHeader.tsx b/apps/namadillo/src/App/Governance/ProposalHeader.tsx
index 314ae66e4..b50e9fdee 100644
--- a/apps/namadillo/src/App/Governance/ProposalHeader.tsx
+++ b/apps/namadillo/src/App/Governance/ProposalHeader.tsx
@@ -296,7 +296,9 @@ const VoteButton: React.FC<{
}> = ({ proposal, voted, proposalId }) => {
const navigate = useNavigate();
const isExtensionConnected = useAtomValue(namadaExtensionConnectedAtom);
- const canVote = useAtomValue(canVoteAtom);
+ const canVote = useAtomValue(
+ canVoteAtom(proposal.data?.startEpoch || BigInt(-1))
+ );
if (!isExtensionConnected) {
return null;
diff --git a/apps/namadillo/src/App/Governance/ProposalStatusSummary.tsx b/apps/namadillo/src/App/Governance/ProposalStatusSummary.tsx
index 5b51f2473..25a902df6 100644
--- a/apps/namadillo/src/App/Governance/ProposalStatusSummary.tsx
+++ b/apps/namadillo/src/App/Governance/ProposalStatusSummary.tsx
@@ -1,7 +1,7 @@
import BigNumber from "bignumber.js";
import clsx from "clsx";
import { AnimatePresence, motion } from "framer-motion";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import { PieChart, PieChartData, Stack } from "@namada/components";
import { formatPercentage } from "@namada/utils";
@@ -165,6 +165,11 @@ const Loaded: React.FC<{
highestVoteType
);
+ useEffect(() => {
+ // Reset the hovered vote type when the highest vote type changes(on data poll)
+ setHoveredVoteType(highestVoteType);
+ }, [highestVoteType]);
+
const votedProportion =
totalVotingPower.isEqualTo(0) ?
BigNumber(0)
diff --git a/apps/namadillo/src/App/Governance/SubmitVote.tsx b/apps/namadillo/src/App/Governance/SubmitVote.tsx
index 546d32372..f373c5f67 100644
--- a/apps/namadillo/src/App/Governance/SubmitVote.tsx
+++ b/apps/namadillo/src/App/Governance/SubmitVote.tsx
@@ -73,11 +73,12 @@ export const WithProposalId: React.FC<{ proposalId: bigint }> = ({
const [selectedVoteType, setSelectedVoteType] = useState();
const proposalQueryResult = useAtomValue(proposalFamily(proposalId));
- const canVote = useAtomValue(canVoteAtom);
const proposal =
proposalQueryResult.isSuccess ? proposalQueryResult.data : null;
+ const canVote = useAtomValue(canVoteAtom(proposal?.startEpoch || BigInt(-1)));
+
const onCloseModal = (): void => navigate(-1);
const dispatchPendingNotification = (txs: TxProps[]): void => {
diff --git a/apps/namadillo/src/atoms/api.ts b/apps/namadillo/src/atoms/api.ts
index 0c158192b..24257cbad 100644
--- a/apps/namadillo/src/atoms/api.ts
+++ b/apps/namadillo/src/atoms/api.ts
@@ -1,4 +1,4 @@
-import { DefaultApi } from "@anomaorg/namada-indexer-client";
+import { Configuration, DefaultApi } from "@anomaorg/namada-indexer-client";
import { Atom, atom, getDefaultStore } from "jotai";
import { indexerUrlAtom } from "./settings";
@@ -14,5 +14,7 @@ export const getIndexerApi = (): DefaultApi => {
// Helper function to use outside of hooks
const getApi = (get: (atom: Atom) => Value): DefaultApi => {
const indexerUrl = get(indexerUrlAtom);
- return new DefaultApi({ basePath: indexerUrl });
+ const configuration = new Configuration({ basePath: indexerUrl });
+
+ return new DefaultApi(configuration);
};
diff --git a/apps/namadillo/src/atoms/chain/services.ts b/apps/namadillo/src/atoms/chain/services.ts
index 3692f883c..b36025858 100644
--- a/apps/namadillo/src/atoms/chain/services.ts
+++ b/apps/namadillo/src/atoms/chain/services.ts
@@ -23,6 +23,7 @@ export const fetchChainParameters = async (
1,
minEpochDuration: Number(parameters.minDuration),
minNumOfBlocks: Number(parameters.minNumOfBlocks),
+ maxBlockTime: Number(parameters.maxBlockTime),
epochSwitchBlocksDelay: Number(parameters.epochSwitchBlocksDelay),
},
apr: BigNumber(parameters.apr),
diff --git a/apps/namadillo/src/atoms/etc.ts b/apps/namadillo/src/atoms/etc.ts
index e3e2f615d..c27577ce0 100644
--- a/apps/namadillo/src/atoms/etc.ts
+++ b/apps/namadillo/src/atoms/etc.ts
@@ -6,6 +6,7 @@ import { atomWithStorage } from "jotai/utils";
type ControlRoutineProps = {
shouldUpdateAmount: boolean;
+ shouldUpdateProposal: boolean;
lastBlockHeight: BigNumber | undefined;
};
@@ -13,6 +14,7 @@ export const controlRoutineAtom = atomWithStorage(
"namadillo:etc",
{
shouldUpdateAmount: false,
+ shouldUpdateProposal: false,
lastBlockHeight: undefined,
}
);
@@ -29,7 +31,12 @@ export const shouldUpdateBalanceAtom = atom(
changeProps("shouldUpdateAmount")
);
+export const shouldUpdateProposalAtom = atom(
+ (get) => get(controlRoutineAtom).shouldUpdateProposal,
+ changeProps("shouldUpdateProposal")
+);
+
export const lastBlockHeightAtom = atom(
- (get) => get(controlRoutineAtom).shouldUpdateAmount,
+ (get) => get(controlRoutineAtom).lastBlockHeight,
changeProps("lastBlockHeight")
);
diff --git a/apps/namadillo/src/atoms/proposals/atoms.ts b/apps/namadillo/src/atoms/proposals/atoms.ts
index de5756839..86df789a3 100644
--- a/apps/namadillo/src/atoms/proposals/atoms.ts
+++ b/apps/namadillo/src/atoms/proposals/atoms.ts
@@ -21,15 +21,16 @@ import {
fetchVotedProposalIds,
} from "./functions";
-import {
- Bond as NamadaIndexerBond,
- BondStatusEnum as NamadaIndexerBondStatusEnum,
-} from "@anomaorg/namada-indexer-client";
+import { Bond as NamadaIndexerBond } from "@anomaorg/namada-indexer-client";
+import { shouldUpdateProposalAtom } from "atoms/etc";
export const proposalFamily = atomFamily((id: bigint) =>
atomWithQuery((get) => {
const api = get(indexerApiAtom);
+ const enablePolling = get(shouldUpdateProposalAtom);
return {
+ // TODO: subscribe to indexer events when it's done
+ refetchInterval: enablePolling ? 1000 : false,
queryKey: ["proposal", id.toString()],
queryFn: () => fetchProposalById(api, id),
};
@@ -96,7 +97,11 @@ export const paginatedProposalsFamily = atomFamily(
export const votedProposalIdsAtom = atomWithQuery((get) => {
const account = get(defaultAccountAtom);
const api = get(indexerApiAtom);
+ const enablePolling = get(shouldUpdateProposalAtom);
+
return {
+ // TODO: subscribe to indexer events when it's done
+ refetchInterval: enablePolling ? 1000 : false,
queryKey: ["voted-proposal-ids", account.data],
...queryDependentFn(async () => {
if (typeof account.data === "undefined") {
@@ -107,25 +112,29 @@ export const votedProposalIdsAtom = atomWithQuery((get) => {
};
});
-export const canVoteAtom = atomWithQuery((get) => {
- const account = get(defaultAccountAtom);
- const api = get(indexerApiAtom);
+export const canVoteAtom = atomFamily((proposalStartEpoch: bigint) =>
+ atomWithQuery((get) => {
+ const account = get(defaultAccountAtom);
+ const api = get(indexerApiAtom);
- return {
- queryKey: ["can-vote"],
- enabled: account.isSuccess,
- queryFn: async () => {
- const all_bonds = await api.apiV1PosBondAddressGet(account.data!.address);
+ return {
+ queryKey: ["can-vote", account.data, api],
+ enabled: account.isSuccess,
+ queryFn: async () => {
+ const all_bonds = await api.apiV1PosBondAddressGet(
+ account.data!.address
+ );
- return all_bonds.data.results.reduce(
- (acc: boolean, current: NamadaIndexerBond) => {
- return acc || current.status === NamadaIndexerBondStatusEnum.Active;
- },
- false
- );
- },
- };
-});
+ return all_bonds.data.results.reduce(
+ (acc: boolean, current: NamadaIndexerBond) => {
+ return acc || Number(current.startEpoch) <= proposalStartEpoch;
+ },
+ false
+ );
+ },
+ };
+ })
+);
type CreateVoteTxArgs = {
proposalId: bigint;
diff --git a/apps/namadillo/src/atoms/proposals/functions.ts b/apps/namadillo/src/atoms/proposals/functions.ts
index 435c2e55c..cc4bbc1a6 100644
--- a/apps/namadillo/src/atoms/proposals/functions.ts
+++ b/apps/namadillo/src/atoms/proposals/functions.ts
@@ -1,4 +1,5 @@
import {
+ ApiV1GovProposalGetStatusEnum as ApiIndexerProposalStatusEnum,
DefaultApi,
Proposal as IndexerProposal,
ProposalStatusEnum as IndexerProposalStatusEnum,
@@ -257,16 +258,16 @@ const fromIndexerStatus = (
const toIndexerStatus = (
proposalStatus: ProposalStatus
-): IndexerProposalStatusEnum => {
+): ApiIndexerProposalStatusEnum => {
switch (proposalStatus) {
case "pending":
- return IndexerProposalStatusEnum.Pending;
+ return ApiIndexerProposalStatusEnum.Pending;
case "ongoing":
- return IndexerProposalStatusEnum.Voting;
+ return ApiIndexerProposalStatusEnum.VotingPeriod;
case "passed":
- return IndexerProposalStatusEnum.Passed;
+ return ApiIndexerProposalStatusEnum.Passed;
case "rejected":
- return IndexerProposalStatusEnum.Rejected;
+ return ApiIndexerProposalStatusEnum.Rejected;
default:
return assertNever(proposalStatus);
}
diff --git a/apps/namadillo/src/atoms/settings/services.ts b/apps/namadillo/src/atoms/settings/services.ts
index 2dbbff95b..3ac430f7a 100644
--- a/apps/namadillo/src/atoms/settings/services.ts
+++ b/apps/namadillo/src/atoms/settings/services.ts
@@ -1,4 +1,4 @@
-import { DefaultApi } from "@anomaorg/namada-indexer-client";
+import { Configuration, DefaultApi } from "@anomaorg/namada-indexer-client";
import { isUrlValid } from "@namada/utils";
import toml from "toml";
import { SettingsTomlOptions } from "types";
@@ -8,7 +8,8 @@ export const isIndexerAlive = async (url: string): Promise => {
return false;
}
try {
- const api = new DefaultApi({ basePath: url });
+ const configuration = new Configuration({ basePath: url });
+ const api = new DefaultApi(configuration);
const response = await api.healthGet();
return response.status === 200;
} catch {
diff --git a/apps/namadillo/src/atoms/validators/functions.ts b/apps/namadillo/src/atoms/validators/functions.ts
index abbadc6da..a458dd06f 100644
--- a/apps/namadillo/src/atoms/validators/functions.ts
+++ b/apps/namadillo/src/atoms/validators/functions.ts
@@ -1,5 +1,6 @@
import {
Bond as IndexerBond,
+ MergedBond as IndexerMergedBond,
Unbond as IndexerUnbond,
Validator as IndexerValidator,
VotingPower as IndexerVotingPower,
@@ -18,10 +19,9 @@ export const toValidator = (
const expectedApr = nominalApr.times(1 - commission.toNumber());
// Because epoch duration is in reality longer by epochSwitchBlocksDelay we have to account for that
- const timePerBlock = epochInfo.minEpochDuration / epochInfo.minNumOfBlocks;
const realMinEpochDuration =
epochInfo.minEpochDuration +
- timePerBlock * epochInfo.epochSwitchBlocksDelay;
+ epochInfo.maxBlockTime * epochInfo.epochSwitchBlocksDelay;
const unbondingPeriod = singleUnitDurationFromInterval(
0,
@@ -62,7 +62,7 @@ export const calculateUnbondingTimeLeft = (unbond: IndexerUnbond): string => {
* an array of MyValidators objects
*/
export const toMyValidators = (
- indexerBonds: IndexerBond[],
+ indexerBonds: IndexerBond[] | IndexerMergedBond[],
indexerUnbonds: IndexerUnbond[],
totalVotingPower: IndexerVotingPower,
epochInfo: EpochInfo,
@@ -86,7 +86,7 @@ export const toMyValidators = (
const addBondToAddress = (
address: Address,
key: "bondItems" | "unbondItems",
- bond: IndexerBond | IndexerUnbond
+ bond: IndexerBond | IndexerMergedBond | IndexerUnbond
): void => {
const { validator: _, ...bondsWithoutValidator } = bond;
myValidators[address]![key].push(bondsWithoutValidator);
diff --git a/apps/namadillo/src/hooks/useTransactionCallbacks.tsx b/apps/namadillo/src/hooks/useTransactionCallbacks.tsx
index a0d38a6d1..4b837cd6b 100644
--- a/apps/namadillo/src/hooks/useTransactionCallbacks.tsx
+++ b/apps/namadillo/src/hooks/useTransactionCallbacks.tsx
@@ -1,5 +1,5 @@
import { accountBalanceAtom } from "atoms/accounts";
-import { shouldUpdateBalanceAtom } from "atoms/etc";
+import { shouldUpdateBalanceAtom, shouldUpdateProposalAtom } from "atoms/etc";
import { useAtomValue, useSetAtom } from "jotai";
import { useTransactionEventListener } from "utils";
@@ -19,4 +19,15 @@ export const useTransactionCallback = (): void => {
useTransactionEventListener("Bond.Success", onBalanceUpdate);
useTransactionEventListener("Unbond.Success", onBalanceUpdate);
useTransactionEventListener("Withdraw.Success", onBalanceUpdate);
+
+ const shouldUpdateProposal = useSetAtom(shouldUpdateProposalAtom);
+
+ useTransactionEventListener("VoteProposal.Success", () => {
+ shouldUpdateProposal(true);
+
+ // This does not guarantee that the proposal will be updated,
+ // but because this is temporary solution(don't quote me on this), it should be fine :)
+ const timePolling = 12 * 1000;
+ setTimeout(() => shouldUpdateProposal(false), timePolling);
+ });
};
diff --git a/apps/namadillo/src/types.d.ts b/apps/namadillo/src/types.d.ts
index 863b2159b..18a5ec784 100644
--- a/apps/namadillo/src/types.d.ts
+++ b/apps/namadillo/src/types.d.ts
@@ -47,6 +47,7 @@ export type EpochInfo = {
unbondingPeriodInEpochs: number;
minEpochDuration: number;
minNumOfBlocks: number;
+ maxBlockTime: number;
epochSwitchBlocksDelay: number;
};
diff --git a/yarn.lock b/yarn.lock
index 3bd7bed2b..7fc6f8e61 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -45,12 +45,12 @@ __metadata:
languageName: node
linkType: hard
-"@anomaorg/namada-indexer-client@npm:0.0.21":
- version: 0.0.21
- resolution: "@anomaorg/namada-indexer-client@npm:0.0.21"
+"@anomaorg/namada-indexer-client@npm:0.0.23":
+ version: 0.0.23
+ resolution: "@anomaorg/namada-indexer-client@npm:0.0.23"
dependencies:
- axios: "npm:^0.21.1"
- checksum: 4b9632145eee1d20672ad65eef447ed90e3abfc2fee09a5c3e40fc5fd9321ff83aff03acf4be75abe1c74c83fc635216f46f5c58155c11a291656bb822f05465
+ axios: "npm:^1.6.1"
+ checksum: 1249a851ad5ad09daa88bc621fbc33fe520a3f5912ab02d472c4d7b52f26fb83cee2f7818c634a55abb137dfdf5226d0ecd057e24895573199da069a5111e1cd
languageName: node
linkType: hard
@@ -8448,7 +8448,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@namada/namadillo@workspace:apps/namadillo"
dependencies:
- "@anomaorg/namada-indexer-client": "npm:0.0.21"
+ "@anomaorg/namada-indexer-client": "npm:0.0.23"
"@cosmjs/encoding": "npm:^0.32.3"
"@playwright/test": "npm:^1.24.1"
"@release-it/keep-a-changelog": "npm:^5.0.0"
@@ -12954,6 +12954,17 @@ __metadata:
languageName: node
linkType: hard
+"axios@npm:^1.6.1":
+ version: 1.7.4
+ resolution: "axios@npm:1.7.4"
+ dependencies:
+ follow-redirects: "npm:^1.15.6"
+ form-data: "npm:^4.0.0"
+ proxy-from-env: "npm:^1.1.0"
+ checksum: 5ea1a93140ca1d49db25ef8e1bd8cfc59da6f9220159a944168860ad15a2743ea21c5df2967795acb15cbe81362f5b157fdebbea39d53117ca27658bab9f7f17
+ languageName: node
+ linkType: hard
+
"axobject-query@npm:^2.2.0":
version: 2.2.0
resolution: "axobject-query@npm:2.2.0"
@@ -19364,6 +19375,16 @@ __metadata:
languageName: node
linkType: hard
+"follow-redirects@npm:^1.15.6":
+ version: 1.15.6
+ resolution: "follow-redirects@npm:1.15.6"
+ peerDependenciesMeta:
+ debug:
+ optional: true
+ checksum: 9ff767f0d7be6aa6870c82ac79cf0368cd73e01bbc00e9eb1c2a16fbb198ec105e3c9b6628bb98e9f3ac66fe29a957b9645bcb9a490bb7aa0d35f908b6b85071
+ languageName: node
+ linkType: hard
+
"for-each@npm:^0.3.3":
version: 0.3.3
resolution: "for-each@npm:0.3.3"