diff --git a/chain-registry b/chain-registry index 44cfc8bf..a3bacffd 160000 --- a/chain-registry +++ b/chain-registry @@ -1 +1 @@ -Subproject commit 44cfc8bfaf43737539b7afbbf3143758b3fd595a +Subproject commit a3bacffd17c93771515b05d7b5e6cca162436105 diff --git a/src/components/RouteDisplay.tsx b/src/components/RouteDisplay.tsx index 29a501b9..8f70cca1 100644 --- a/src/components/RouteDisplay.tsx +++ b/src/components/RouteDisplay.tsx @@ -90,18 +90,25 @@ function RouteEnd({ amount, symbol, logo, chain }: RouteEndProps) { } interface TransferStepProps { + actions: Action[]; action: TransferAction; id: string; statusData?: ReturnType["data"]; } -function TransferStep({ action, id, statusData }: TransferStepProps) { +function TransferStep({ action, actions, id, statusData }: TransferStepProps) { const { data: sourceChain } = useChainByID(action.sourceChain); const { data: destinationChain } = useChainByID(action.destinationChain); // format: operationType-- const operationCount = Number(id.split("-")[1]); + const operationIndex = Number(id.split("-")[2]); const transfer = statusData?.transferSequence[operationCount]; + const isNextOpSwap = + actions + // We can assume that the swap operation by the previous transfer + .find((x) => Number(x.id.split("-")[2]) === operationIndex + 1) + ?.id.split("-")[0] === "swap"; // We can assume that the transfer is successful when the state is TRANSFER_SUCCESS or TRANSFER_RECEIVED const renderTransferState = useMemo(() => { @@ -137,18 +144,20 @@ function TransferStep({ action, id, statusData }: TransferStepProps) { }, [transfer?.state]); const renderExplorerLink = useMemo(() => { - if (!transfer?.explorerLink) return null; + const packetTx = + operationIndex === 0 ? transfer?.txs.sendTx : isNextOpSwap ? transfer?.txs.sendTx : transfer?.txs.receiveTx; + if (!packetTx?.explorerLink) return null; return ( - {transfer.explorerLink.split("/").at(-1)?.slice(0, 6)}…{transfer.explorerLink.split("/").at(-1)?.slice(-6)} + {packetTx.explorerLink.split("/").at(-1)?.slice(0, 6)}…{packetTx.explorerLink.split("/").at(-1)?.slice(-6)} ); - }, [transfer?.explorerLink]); + }, [isNextOpSwap, operationIndex, transfer?.txs.receiveTx, transfer?.txs.sendTx]); const { getAsset } = useAssets(); @@ -266,19 +275,20 @@ function SwapStep({ action, actions, id, statusData }: SwapStepProps) { }, [swap?.state]); const renderExplorerLink = useMemo(() => { - if (!swap?.explorerLink) return null; + const receiveTx = swap?.txs.receiveTx; + if (!receiveTx) return null; if (swap?.state !== "TRANSFER_SUCCESS") return null; return ( - {swap.explorerLink.split("/").at(-1)?.slice(0, 6)}…{swap.explorerLink.split("/").at(-1)?.slice(-6)} + {receiveTx.explorerLink.split("/").at(-1)?.slice(0, 6)}…{receiveTx.explorerLink.split("/").at(-1)?.slice(-6)} ); - }, [swap?.explorerLink, swap?.state]); + }, [swap?.state, swap?.txs.receiveTx]); if (!assetIn && assetOut) { return ( @@ -542,6 +552,7 @@ function RouteDisplay({ route, isRouteExpanded, setIsRouteExpanded, broadcastedT {action.type === "TRANSFER" && ( diff --git a/src/components/TransactionDialog/TransactionDialogContent.tsx b/src/components/TransactionDialog/TransactionDialogContent.tsx index 416c6b5c..95ccad5a 100644 --- a/src/components/TransactionDialog/TransactionDialogContent.tsx +++ b/src/components/TransactionDialog/TransactionDialogContent.tsx @@ -15,6 +15,7 @@ import { useFinalityTimeEstimate } from "@/hooks/useFinalityTimeEstimate"; import { useSkipClient } from "@/solve"; import { isUserRejectedRequestError } from "@/utils/error"; import { getExplorerUrl } from "@/utils/explorer"; +import { randomId } from "@/utils/random"; import RouteDisplay from "../RouteDisplay"; import { SpinnerIcon } from "../SpinnerIcon"; @@ -58,7 +59,6 @@ function TransactionDialogContent({ const [txComplete, setTxComplete] = useState(false); const [isRouteExpanded, setIsRouteExpanded] = useState(false); const [broadcastedTxs, setBroadcastedTxs] = useState([]); - const [historyId, setHistoryId] = useState(); const [txStatuses, setTxStatuses] = useState(() => Array.from({ length: transactionCount }, () => ({ @@ -76,7 +76,7 @@ function TransactionDialogContent({ async function onSubmit() { setTransacting(true); setIsRouteExpanded(true); - + const historyId = randomId(); try { const userAddresses: Record = {}; @@ -156,15 +156,8 @@ function TransactionDialogContent({ onTransactionBroadcast: async (txStatus) => { const makeExplorerUrl = await getExplorerUrl(txStatus.chainID); const explorerLink = makeExplorerUrl?.(txStatus.txHash); - const hId = (() => { - if (!historyId) { - const [_historyId] = txHistory.add({ route }); - setHistoryId(_historyId); - return _historyId; - } - return historyId; - })(); - txHistory.addStatus(hId, { + + txHistory.addStatus(historyId, route, { chainId: txStatus.chainID, txHash: txStatus.txHash, explorerLink: explorerLink || "#", @@ -275,7 +268,6 @@ function TransactionDialogContent({ } finally { setTransacting(false); setBroadcastedTxs([]); - setHistoryId(undefined); } } diff --git a/src/components/TransactionDialog/index.tsx b/src/components/TransactionDialog/index.tsx index 3df6052d..192fd284 100644 --- a/src/components/TransactionDialog/index.tsx +++ b/src/components/TransactionDialog/index.tsx @@ -1,6 +1,7 @@ import { RouteResponse } from "@skip-router/core"; import { clsx } from "clsx"; import { Fragment, useEffect, useState } from "react"; +import toast from "react-hot-toast"; import { useDisclosureKey } from "@/context/disclosures"; @@ -31,9 +32,9 @@ function TransactionDialog({ onAllTransactionComplete, }: Props) { const [hasDisplayedWarning, setHasDisplayedWarning] = useState(false); - const [isOpen, { set: setIsOpen }] = useDisclosureKey("confirmSwapDialog"); + const [isOpen, confirmControl] = useDisclosureKey("confirmSwapDialog"); - const [, control] = useDisclosureKey("priceImpactDialog"); + const [, priceImpactControl] = useDisclosureKey("priceImpactDialog"); useEffect(() => { if (!isOpen) { @@ -46,10 +47,25 @@ function TransactionDialog({ } if (shouldShowPriceImpactWarning) { - control.open(); + priceImpactControl.open(); setHasDisplayedWarning(true); } - }, [control, setHasDisplayedWarning, isOpen, hasDisplayedWarning, shouldShowPriceImpactWarning]); + + if (isOpen && !route) { + priceImpactControl.close(); + confirmControl.close(); + toast.error( +

+ Something went wrong! +
+ Your transaction may or may not be processed. +

, + ); + return; + } + // reason: ignoring control handlers + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [hasDisplayedWarning, isOpen, route, shouldShowPriceImpactWarning]); return ( @@ -61,26 +77,24 @@ function TransactionDialog({ "enabled:hover:rotate-1 enabled:hover:scale-105", )} disabled={!route || (typeof isLoading === "boolean" && isLoading)} - onClick={() => setIsOpen(true)} + onClick={() => confirmControl.open()} > Preview Route - {isOpen && ( + {isOpen && route && (
- {route && ( - setIsOpen(false)} - isAmountError={isAmountError} - transactionCount={transactionCount} - onAllTransactionComplete={onAllTransactionComplete} - /> - )} +
)} setIsOpen(false)} + onGoBack={confirmControl.close} message={routeWarningMessage} title={routeWarningTitle} /> diff --git a/src/context/tx-history.ts b/src/context/tx-history.ts index 4c32a601..eee3e612 100644 --- a/src/context/tx-history.ts +++ b/src/context/tx-history.ts @@ -46,6 +46,18 @@ export const txHistory = { return [id, newItem] as const; }, + update: (id: string, input: Partial) => { + useTxHistory.setState((state) => { + const current = state[id]; + + const latest: TxHistoryItem = { + ...current, + ...input, + }; + + return { [id]: latest }; + }); + }, success: (id: string) => { useTxHistory.setState((state) => { const current = state[id]; @@ -79,10 +91,19 @@ export const txHistory = { return newState; }, true); }, - addStatus: (id: string, txStatus: TxStatus) => { + addStatus: (id: string, route: RouteResponse, txStatus: TxStatus) => { useTxHistory.setState((state) => { const current = state[id]; - if (!current) return state; + if (!current) { + const newItem: TxHistoryItem = { + txStatus: [txStatus], + timestamp: new Date().toISOString(), + status: "pending", + route, + }; + + return { [id]: newItem }; + } const newTxStatus = current.txStatus.concat(txStatus); diff --git a/src/hooks/useAccount.ts b/src/hooks/useAccount.ts index 65122f3a..9b7d587f 100644 --- a/src/hooks/useAccount.ts +++ b/src/hooks/useAccount.ts @@ -1,4 +1,4 @@ -import { useChain as useCosmosChain } from "@cosmos-kit/react"; +import { useManager as useCosmosManager } from "@cosmos-kit/react"; import { useMemo } from "react"; import { useAccount as useWagmiAccount } from "wagmi"; @@ -10,12 +10,13 @@ export function useAccount(context: TrackWalletCtx) { const trackedWallet = useTrackWallet(context); const { data: chain } = useChainByID(trackedWallet?.chainID); - - const { walletRepo } = useCosmosChain(chain?.chainType === "cosmos" ? chain.chainName : "cosmoshub"); + const { getWalletRepo } = useCosmosManager(); const cosmosWallet = useMemo(() => { - return walletRepo.wallets.find((w) => w.walletName === trackedWallet?.walletName); - }, [trackedWallet?.walletName, walletRepo.wallets]); + if (chain?.chainType !== "cosmos") return; + const { wallets } = getWalletRepo(chain.chainName); + return wallets.find((w) => w.walletName === trackedWallet?.walletName); + }, [chain?.chainName, chain?.chainType, getWalletRepo, trackedWallet?.walletName]); const wagmiAccount = useWagmiAccount(); diff --git a/src/solve/queries.ts b/src/solve/queries.ts index 634641d5..0d31a38e 100644 --- a/src/solve/queries.ts +++ b/src/solve/queries.ts @@ -1,4 +1,4 @@ -import { AssetsRequest, SwapVenue, TransferState } from "@skip-router/core"; +import { AssetsRequest, ChainTransaction, SwapVenue, TransferState } from "@skip-router/core"; import { useQuery } from "@tanstack/react-query"; import { useEffect, useMemo, useState } from "react"; @@ -9,7 +9,10 @@ import { useSkipClient } from "./hooks"; interface TransferSequence { srcChainID: string; destChainID: string; - explorerLink: string | undefined; + txs: { + sendTx: ChainTransaction | null; + receiveTx: ChainTransaction | null; + }; state: TransferState; } @@ -184,7 +187,10 @@ export const useBroadcastedTxsStatus = ({ return { srcChainID: transfer.ibcTransfer.srcChainID, destChainID: transfer.ibcTransfer.dstChainID, - explorerLink: transfer.ibcTransfer.packetTXs.sendTx?.explorerLink, + txs: { + sendTx: transfer.ibcTransfer.packetTXs.sendTx, + receiveTx: transfer.ibcTransfer.packetTXs.receiveTx, + }, state: transfer.ibcTransfer.state, }; } @@ -202,11 +208,24 @@ export const useBroadcastedTxsStatus = ({ return "TRANSFER_UNKNOWN"; } })(); - + if ("contractCallWithTokenTxs" in transfer.axelarTransfer.txs) { + return { + srcChainID: transfer.axelarTransfer.srcChainID, + destChainID: transfer.axelarTransfer.dstChainID, + txs: { + sendTx: transfer.axelarTransfer.txs.contractCallWithTokenTxs.sendTx, + receiveTx: transfer.axelarTransfer.txs.contractCallWithTokenTxs.executeTx, + }, + state: axelarState, + }; + } return { srcChainID: transfer.axelarTransfer.srcChainID, destChainID: transfer.axelarTransfer.dstChainID, - explorerLink: transfer.axelarTransfer.axelarScanLink, + txs: { + sendTx: transfer.axelarTransfer.txs.sendTokenTxs.sendTx, + receiveTx: transfer.axelarTransfer.txs.sendTokenTxs.executeTx, + }, state: axelarState, }; }); diff --git a/src/utils/wallet.ts b/src/utils/wallet.ts index 2c4fc7e0..1ee5fb3d 100644 --- a/src/utils/wallet.ts +++ b/src/utils/wallet.ts @@ -3,8 +3,6 @@ import { ChainWalletBase } from "@cosmos-kit/core"; import { ChainId } from "@/chains/types"; import { MergedWalletClient } from "@/lib/cosmos-kit"; -import { isMobile } from "./os"; - export async function gracefullyConnect( wallet: ChainWalletBase, { @@ -14,10 +12,9 @@ export async function gracefullyConnect( } = {}, ) { const client = wallet.client as MergedWalletClient; - const canAddChain = !isMobile() || !("snapInstalled" in client); - if (canAddChain) { - await wallet.client - .addChain?.({ + if (client && "addChain" in client) { + await client + ?.addChain?.({ chain: { bech32_prefix: wallet.chain.bech32_prefix, chain_id: wallet.chain.chain_id,