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 = ({ > 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 => {