diff --git a/apps/namadillo/src/App/App.tsx b/apps/namadillo/src/App/App.tsx
index c64606d513..25099f0b65 100644
--- a/apps/namadillo/src/App/App.tsx
+++ b/apps/namadillo/src/App/App.tsx
@@ -16,8 +16,8 @@ export function App(): JSX.Element {
useExtensionEvents();
useTransactionNotifications();
useTransactionCallback();
- useRegistryFeatures();
useTransactionWatcher();
+ useRegistryFeatures();
useServerSideEvents();
return (
diff --git a/apps/namadillo/src/App/Common/SelectOptionModal.tsx b/apps/namadillo/src/App/Common/SelectOptionModal.tsx
index 98a5fa1868..1cfd69ef06 100644
--- a/apps/namadillo/src/App/Common/SelectOptionModal.tsx
+++ b/apps/namadillo/src/App/Common/SelectOptionModal.tsx
@@ -1,4 +1,5 @@
import { Modal } from "@namada/components";
+import { Fragment } from "react";
import { AssetsModalCard } from "./AssetsModalCard";
import { ModalContainer } from "./ModalContainer";
@@ -30,14 +31,14 @@ export const SelectOptionModal = ({
>
{items.map((item, index) => (
- <>
- -
+
+
-
{item.children}
{index + 1 < items.length && (
)}
- >
+
))}
diff --git a/apps/namadillo/src/App/Common/Timeline.tsx b/apps/namadillo/src/App/Common/Timeline.tsx
index 4c616e3334..029e618f70 100644
--- a/apps/namadillo/src/App/Common/Timeline.tsx
+++ b/apps/namadillo/src/App/Common/Timeline.tsx
@@ -15,42 +15,25 @@ type TransactionTimelineProps = {
complete?: boolean;
};
-type DisabledProps = {
- disabled: boolean;
-};
-
-const StepConnector = ({ disabled }: DisabledProps): JSX.Element => (
-
+const StepConnector = (): JSX.Element => (
+
);
-const StepBullet = ({ disabled }: DisabledProps): JSX.Element => (
-
+const StepBullet = (): JSX.Element => (
+
);
const StepContent = ({
children,
isCurrentStep,
hasError,
- disabled,
}: React.PropsWithChildren & {
isCurrentStep: boolean;
hasError: boolean;
- disabled: boolean;
}): JSX.Element => (
{children}
@@ -206,19 +189,15 @@ export const Timeline = ({
className={twMerge(
clsx(
"flex flex-col gap-1 items-center",
- "text-center transition-all duration-150"
+ "text-center transition-all duration-150",
+ { "opacity-20": index > currentStepIndex && !hasError }
)
)}
>
- {index > 0 && (
-
currentStepIndex} />
- )}
- {step.bullet && (
- currentStepIndex} />
- )}
+ {index > 0 && }
+ {step.bullet && }
currentStepIndex}
hasError={!!hasError}
>
{step.children}
diff --git a/apps/namadillo/src/atoms/transactions/atoms.ts b/apps/namadillo/src/atoms/transactions/atoms.ts
index 82fab52383..c5bcef3c56 100644
--- a/apps/namadillo/src/atoms/transactions/atoms.ts
+++ b/apps/namadillo/src/atoms/transactions/atoms.ts
@@ -1,4 +1,5 @@
import { defaultAccountAtom } from "atoms/accounts";
+import { indexerApiAtom } from "atoms/api";
import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";
import { Address, TransferTransactionData } from "types";
@@ -6,6 +7,7 @@ import {
filterCompleteTransactions,
filterPendingTransactions,
} from "./functions";
+import { fetchTransaction } from "./services";
export const transactionStorageKey = "namadillo:transactions";
@@ -31,3 +33,8 @@ export const completeTransactionsHistoryAtom = atom((get) => {
const myTransactions = get(myTransactionHistoryAtom);
return myTransactions.filter(filterCompleteTransactions);
});
+
+export const fetchTransactionAtom = atom((get) => {
+ const api = get(indexerApiAtom);
+ return (hash: string) => fetchTransaction(api, hash);
+});
diff --git a/apps/namadillo/src/atoms/transactions/services.ts b/apps/namadillo/src/atoms/transactions/services.ts
index 4143415fd6..10d408fe98 100644
--- a/apps/namadillo/src/atoms/transactions/services.ts
+++ b/apps/namadillo/src/atoms/transactions/services.ts
@@ -1,5 +1,7 @@
import { IndexedTx, StargateClient } from "@cosmjs/stargate";
+import { DefaultApi, WrapperTransaction } from "@namada/indexer-client";
import { IbcTransferTransactionData } from "types";
+import { sanitizeAddress } from "utils/address";
type SearchByTagsQuery = {
key: string;
@@ -57,3 +59,11 @@ export const queryForAck = async (
): Promise => {
return await client.searchTx(getAckPacketsParams(ibcTx));
};
+
+export const fetchTransaction = async (
+ api: DefaultApi,
+ hash: string
+): Promise => {
+ // indexer only accepts the hash as lowercase
+ return (await api.apiV1ChainWrapperTxIdGet(sanitizeAddress(hash))).data;
+};
diff --git a/apps/namadillo/src/hooks/useTransactionCallbacks.tsx b/apps/namadillo/src/hooks/useTransactionCallbacks.tsx
index bb24b4f44f..2acdd6e33e 100644
--- a/apps/namadillo/src/hooks/useTransactionCallbacks.tsx
+++ b/apps/namadillo/src/hooks/useTransactionCallbacks.tsx
@@ -3,10 +3,8 @@ import { shouldUpdateBalanceAtom, shouldUpdateProposalAtom } from "atoms/etc";
import { claimableRewardsAtom, clearClaimRewards } from "atoms/staking";
import { useAtomValue, useSetAtom } from "jotai";
import { TransferStep } from "types";
-import {
- useTransactionEventListener,
- useTransactionEventListListener,
-} from "utils";
+import { EventData } from "types/events";
+import { useTransactionEventListener } from "utils";
import { useTransactionActions } from "./useTransactionActions";
export const useTransactionCallback = (): void => {
@@ -35,9 +33,7 @@ export const useTransactionCallback = (): void => {
useTransactionEventListener("Unbond.Success", onBalanceUpdate);
useTransactionEventListener("Withdraw.Success", onBalanceUpdate);
useTransactionEventListener("Redelegate.Success", onBalanceUpdate);
- useTransactionEventListener("ClaimRewards.Success", onBalanceUpdate, [
- account?.address,
- ]);
+ useTransactionEventListener("ClaimRewards.Success", onBalanceUpdate);
useTransactionEventListener("VoteProposal.Success", () => {
shouldUpdateProposal(true);
@@ -48,28 +44,16 @@ export const useTransactionCallback = (): void => {
setTimeout(() => shouldUpdateProposal(false), timePolling);
});
- useTransactionEventListListener(
- [
- "TransparentTransfer.Success",
- "ShieldedTransfer.Success",
- "ShieldingTransfer.Success",
- "UnshieldingTransfer.Success",
- ],
- (e) => {
- e.detail.data.forEach((dataList) => {
- dataList.data.forEach((props) => {
- const sourceAddress = "source" in props ? props.source : "";
- sourceAddress &&
- changeTransaction(
- e.detail.tx.hash,
- {
- status: "success",
- currentStep: TransferStep.Complete,
- },
- sourceAddress
- );
- });
+ const onTransferSuccess = (e: EventData): void => {
+ e.detail.tx.forEach(({ hash }) => {
+ changeTransaction(hash, {
+ status: "success",
+ currentStep: TransferStep.Complete,
});
- }
- );
+ });
+ };
+ useTransactionEventListener("TransparentTransfer.Success", onTransferSuccess);
+ useTransactionEventListener("ShieldedTransfer.Success", onTransferSuccess);
+ useTransactionEventListener("ShieldingTransfer.Success", onTransferSuccess);
+ useTransactionEventListener("UnshieldingTransfer.Success", onTransferSuccess);
};
diff --git a/apps/namadillo/src/hooks/useTransactionNotifications.tsx b/apps/namadillo/src/hooks/useTransactionNotifications.tsx
index dc7b5f0a1b..6b634ae249 100644
--- a/apps/namadillo/src/hooks/useTransactionNotifications.tsx
+++ b/apps/namadillo/src/hooks/useTransactionNotifications.tsx
@@ -13,10 +13,8 @@ import { searchAllStoredTxByHash } from "atoms/transactions";
import BigNumber from "bignumber.js";
import invariant from "invariant";
import { useSetAtom } from "jotai";
-import {
- useTransactionEventListener,
- useTransactionEventListListener,
-} from "utils";
+import { EventData } from "types/events";
+import { useTransactionEventListener } from "utils";
type TxWithAmount = { amount: BigNumber };
@@ -28,7 +26,7 @@ const getTotalAmountFromTransactionList = (txs: TxWithAmount[]): BigNumber =>
}, new BigNumber(0));
const parseTxsData = (
- tx: TxProps,
+ tx: TxProps | TxProps[],
data: T[]
): { id: string; total: BigNumber } => {
const id = createNotificationId(tx);
@@ -349,72 +347,59 @@ export const useTransactionNotifications = (): void => {
});
});
- useTransactionEventListListener(
- [
- "TransparentTransfer.Error",
- "ShieldedTransfer.Error",
- "ShieldingTransfer.Error",
- "UnshieldingTransfer.Error",
- ],
- (e) => {
- const tx = e.detail.tx;
- const data: TxWithAmount[] = e.detail.data[0].data;
- const { id } = parseTxsData(tx, data);
- clearPendingNotifications(id);
- const storedTx = searchAllStoredTxByHash(tx.hash);
- dispatchNotification({
- id,
- type: "error",
- title: "Transfer transaction failed",
- description:
- storedTx ?
- <>
- Your transfer transaction of{" "}
- {" "}
- to {shortenAddress(storedTx.destinationAddress, 8, 8)} has failed
- >
- : "Your transfer transaction has failed",
- details:
- e.detail.error?.message && failureDetails(e.detail.error.message),
- });
- }
- );
+ const onTransferError = (e: EventData): void => {
+ const id = createNotificationId(e.detail.tx);
+ clearPendingNotifications(id);
+ const storedTx = searchAllStoredTxByHash(e.detail.tx[0].hash);
+ dispatchNotification({
+ id,
+ type: "error",
+ title: "Transfer transaction failed",
+ description:
+ storedTx ?
+ <>
+ Your transfer transaction of{" "}
+ {" "}
+ to {shortenAddress(storedTx.destinationAddress, 8, 8)} has failed
+ >
+ : "Your transfer transaction has failed",
+ details:
+ e.detail.error?.message && failureDetails(e.detail.error.message),
+ });
+ };
+ useTransactionEventListener("TransparentTransfer.Error", onTransferError);
+ useTransactionEventListener("ShieldedTransfer.Error", onTransferError);
+ useTransactionEventListener("ShieldingTransfer.Error", onTransferError);
+ useTransactionEventListener("UnshieldingTransfer.Error", onTransferError);
- useTransactionEventListListener(
- [
- "TransparentTransfer.Success",
- "ShieldedTransfer.Success",
- "ShieldingTransfer.Success",
- "UnshieldingTransfer.Success",
- ],
- (e) => {
- const tx = e.detail.tx;
- const data: TxWithAmount[] = e.detail.data[0].data;
- const { id } = parseTxsData(tx, data);
- clearPendingNotifications(id);
- const storedTx = searchAllStoredTxByHash(tx.hash);
- dispatchNotification({
- id,
- title: "Transfer transaction succeeded",
- description:
- storedTx ?
- <>
- Your transfer transaction of{" "}
- {" "}
- to {shortenAddress(storedTx.destinationAddress, 8, 8)} has
- succeeded
- >
- : "Your transfer transaction has succeeded",
- type: "success",
- });
- }
- );
+ const onTransferSuccess = (e: EventData): void => {
+ const id = createNotificationId(e.detail.tx);
+ clearPendingNotifications(id);
+ const storedTx = searchAllStoredTxByHash(e.detail.tx[0].hash);
+ dispatchNotification({
+ id,
+ title: "Transfer transaction succeeded",
+ description:
+ storedTx ?
+ <>
+ Your transfer transaction of{" "}
+ {" "}
+ to {shortenAddress(storedTx.destinationAddress, 8, 8)} has succeeded
+ >
+ : "Your transfer transaction has succeeded",
+ type: "success",
+ });
+ };
+ useTransactionEventListener("TransparentTransfer.Success", onTransferSuccess);
+ useTransactionEventListener("ShieldedTransfer.Success", onTransferSuccess);
+ useTransactionEventListener("ShieldingTransfer.Success", onTransferSuccess);
+ useTransactionEventListener("UnshieldingTransfer.Success", onTransferSuccess);
useTransactionEventListener("IbcTransfer.Success", (e) => {
invariant(e.detail.hash, "Notification error: Invalid Tx hash");
diff --git a/apps/namadillo/src/hooks/useTransactionWatcher.tsx b/apps/namadillo/src/hooks/useTransactionWatcher.tsx
index 754ac43054..fa9728a287 100644
--- a/apps/namadillo/src/hooks/useTransactionWatcher.tsx
+++ b/apps/namadillo/src/hooks/useTransactionWatcher.tsx
@@ -1,16 +1,21 @@
+import { WrapperTransactionExitCodeEnum } from "@namada/indexer-client";
import { useQuery } from "@tanstack/react-query";
import {
updateIbcTransferStatus,
updateIbcWithdrawalStatus,
} from "atoms/integrations";
-import { pendingTransactionsHistoryAtom } from "atoms/transactions";
+import {
+ fetchTransactionAtom,
+ pendingTransactionsHistoryAtom,
+} from "atoms/transactions";
import { useAtomValue } from "jotai";
-import { IbcTransferTransactionData } from "types";
+import { IbcTransferTransactionData, TransferStep } from "types";
import { useTransactionActions } from "./useTransactionActions";
export const useTransactionWatcher = (): void => {
const { changeTransaction } = useTransactionActions();
const pendingTransactions = useAtomValue(pendingTransactionsHistoryAtom);
+ const fetchTransaction = useAtomValue(fetchTransactionAtom);
useQuery({
queryKey: ["transaction-status", pendingTransactions],
@@ -19,6 +24,33 @@ export const useTransactionWatcher = (): void => {
return Promise.allSettled(
pendingTransactions.map(async (tx) => {
switch (tx.type) {
+ case "TransparentToTransparent":
+ case "TransparentToShielded":
+ case "ShieldedToTransparent":
+ case "ShieldedToShielded":
+ {
+ const hash = tx.hash ?? "";
+ const response = await fetchTransaction(hash);
+ const hasRejectedTx = response.innerTransactions.find(
+ ({ exitCode }) =>
+ // indexer api is returning as "Rejected", but sdk type is "rejected"
+ exitCode.toLowerCase() ===
+ WrapperTransactionExitCodeEnum.Rejected.toLowerCase()
+ );
+ if (hasRejectedTx) {
+ changeTransaction(hash, {
+ status: "error",
+ errorMessage: "Transaction rejected",
+ });
+ } else {
+ changeTransaction(hash, {
+ status: "success",
+ currentStep: TransferStep.Complete,
+ });
+ }
+ }
+ break;
+
case "IbcToTransparent":
case "IbcToShielded":
await updateIbcTransferStatus(
@@ -27,6 +59,7 @@ export const useTransactionWatcher = (): void => {
changeTransaction
);
break;
+
case "TransparentToIbc":
await updateIbcWithdrawalStatus(
tx as IbcTransferTransactionData,
diff --git a/apps/namadillo/src/types.ts b/apps/namadillo/src/types.ts
index 49deac9efb..718f54383c 100644
--- a/apps/namadillo/src/types.ts
+++ b/apps/namadillo/src/types.ts
@@ -235,22 +235,25 @@ export enum TransferStep {
// Defines the steps in the Namada <> Namada transfer progress for tracking transaction stages.
export const namadaTransferStages = {
TransparentToShielded: [
- TransferStep.Sign,
TransferStep.ZkProof,
+ TransferStep.Sign,
TransferStep.TransparentToShielded,
TransferStep.Complete,
] as const,
ShieldedToTransparent: [
+ TransferStep.ZkProof,
TransferStep.Sign,
TransferStep.ShieldedToTransparent,
TransferStep.Complete,
] as const,
ShieldedToShielded: [
+ TransferStep.ZkProof,
TransferStep.Sign,
TransferStep.ShieldedToShielded,
TransferStep.Complete,
] as const,
TransparentToTransparent: [
+ TransferStep.ZkProof,
TransferStep.Sign,
TransferStep.TransparentToTransparent,
TransferStep.Complete,
diff --git a/apps/namadillo/src/types/events.ts b/apps/namadillo/src/types/events.ts
index c5506c61b9..c0532e3f50 100644
--- a/apps/namadillo/src/types/events.ts
+++ b/apps/namadillo/src/types/events.ts
@@ -32,7 +32,7 @@ export type TransactionEventHandlers = {
export interface EventData extends CustomEvent {
detail: {
- tx: TxProps;
+ tx: TxProps[];
data: T[];
// If event is for PartialSuccess, use the following:
successData?: T[];
diff --git a/apps/namadillo/src/utils/index.ts b/apps/namadillo/src/utils/index.ts
index 74e2529ca1..bde30209b9 100644
--- a/apps/namadillo/src/utils/index.ts
+++ b/apps/namadillo/src/utils/index.ts
@@ -4,7 +4,7 @@ import { localnetConfigAtom } from "atoms/integrations/atoms";
import BigNumber from "bignumber.js";
import { getDefaultStore } from "jotai";
import namadaAssets from "namada-chain-registry/namada/assetlist.json";
-import { useEffect } from "react";
+import { useEffect, useRef } from "react";
export const proposalStatusToString = (status: ProposalStatus): string => {
const statusText: Record = {
@@ -38,32 +38,19 @@ export const proposalIdToString = (proposalId: bigint): string =>
export const useTransactionEventListener = (
event: T,
- handler: (this: Window, ev: WindowEventMap[T]) => void,
- deps: React.DependencyList = []
+ handler: (event: WindowEventMap[T]) => void
): void => {
- useEffect(() => {
- window.addEventListener(event, handler);
- return () => {
- window.removeEventListener(event, handler);
- };
- }, deps);
-};
+ // `handlerRef` is useful to avoid recreating the listener every time
+ const handlerRef = useRef(handler);
+ handlerRef.current = handler;
-export const useTransactionEventListListener = (
- events: T[],
- handler: (this: Window, ev: WindowEventMap[T]) => void,
- deps: React.DependencyList = []
-): void => {
useEffect(() => {
- events.forEach((event) => {
- window.addEventListener(event, handler);
- });
+ const callback: typeof handler = (event) => handlerRef.current(event);
+ window.addEventListener(event, callback);
return () => {
- events.forEach((event) => {
- window.removeEventListener(event, handler);
- });
+ window.removeEventListener(event, callback);
};
- }, deps);
+ }, [event]);
};
export const sumBigNumberArray = (numbers: BigNumber[]): BigNumber => {