Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 47 additions & 4 deletions app/history/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,44 @@ import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.share

const ITEMS_PER_PAGE = 5;

/**
* Check if two transfers are the same by comparing their unique identifiers.
* For V2 transfers, the backend returns database-generated IDs, while locally
* created transfers use messageId/txHash as ID. This function matches by
* transaction hash or extrinsic hash to handle this case.
*/
const isSameTransfer = (t1: Transfer, t2: Transfer): boolean => {
// Match by ID if both have non-empty IDs
if (t1.id === t2.id && t1.id.length > 0) return true;

// For ToPolkadotTransferResult (E->P), match by transaction hash
if (t1.sourceType === "ethereum" && t2.sourceType === "ethereum") {
const t1Casted = t1 as historyV2.ToPolkadotTransferResult;
const t2Casted = t2 as historyV2.ToPolkadotTransferResult;
if (
t1Casted.submitted.transactionHash ===
t2Casted.submitted.transactionHash &&
t1Casted.submitted.transactionHash.length > 0
) {
return true;
}
}

// For ToEthereumTransferResult (P->E), match by extrinsic hash
if (t1.sourceType === "substrate" && t2.sourceType === "substrate") {
const t1Casted = t1 as historyV2.ToEthereumTransferResult;
const t2Casted = t2 as historyV2.ToEthereumTransferResult;
if (
t1Casted.submitted.extrinsic_hash === t2Casted.submitted.extrinsic_hash &&
t1Casted.submitted.extrinsic_hash.length > 0
) {
return true;
}
}

return false;
};

const getExplorerLinks = (
transfer: Transfer,
source: assetsV2.TransferLocation,
Expand Down Expand Up @@ -451,9 +489,8 @@ export default function History() {
const oldTransferCutoff = new Date().getTime() - 4 * 60 * 60 * 1000; // 4 hours
for (let i = 0; i < transfersPendingLocal.length; ++i) {
if (
transferHistoryCache.find(
(h) =>
h.id?.toLowerCase() === transfersPendingLocal[i].id?.toLowerCase(),
transferHistoryCache.find((h) =>
isSameTransfer(h, transfersPendingLocal[i]),
) ||
new Date(transfersPendingLocal[i].info.when).getTime() <
oldTransferCutoff
Expand All @@ -479,7 +516,13 @@ export default function History() {
]);
const allTransfers: Transfer[] = [];
for (const pending of transfersPendingLocal) {
if (transferHistoryCache.find((t) => t.id === pending.id)) {
// Check if this pending transfer already exists in the cache
// Match by ID, transaction hash, or message ID to handle V2 transfers
const isDuplicate = transferHistoryCache.find((t) =>
isSameTransfer(t, pending),
);

if (isDuplicate) {
continue;
}
allTransfers.push(pending);
Expand Down
53 changes: 52 additions & 1 deletion app/txcomplete/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { Transfer } from "@/store/transferHistory";
import base64url from "base64url";
import { LucideLoaderCircle } from "lucide-react";
import { useSearchParams } from "next/navigation";
import { Suspense, useContext, useMemo } from "react";
import { Suspense, useContext, useMemo, useState } from "react";
import { TransferStatusBadge } from "@/components/history/TransferStatusBadge";
import { Button } from "@/components/ui/button";
import Link from "next/link";
Expand All @@ -38,6 +38,7 @@ import { useAtom, useAtomValue } from "jotai";
import { walletTxChecker } from "@/utils/addresses";
import { NeuroWebUnwrapForm } from "@/components/transfer/NeuroWebUnwrapStep";
import { ethereumAccountAtom, ethereumAccountsAtom } from "@/store/ethereum";
import { AddTipDialog } from "@/components/AddTipDialog";

const Loading = () => {
return (
Expand All @@ -58,6 +59,11 @@ function TxCard(props: TxCardProps) {
const { transfer, refresh, inHistory, registry } = props;
const { destination } = getEnvDetail(transfer, registry);
const links: { name: string; link: string }[] = [];
const [tipDialogOpen, setTipDialogOpen] = useState(false);
const [tipParams, setTipParams] = useState<{
direction: "Inbound" | "Outbound";
nonce: number;
} | null>(null);

const token =
registry.ethereumChains[registry.ethChainId].assets[
Expand Down Expand Up @@ -199,6 +205,51 @@ function TxCard(props: TxCardProps) {
</ul>
</div>
{neuroWeb}
<div
className={cn(
transfer.status !== historyV2.TransferStatus.Complete
? ""
: "hidden",
)}
>
<Button
variant="outline"
onClick={() => {
// Get nonce and direction based on transfer type
let nonce: number | undefined;
let direction: "Inbound" | "Outbound" | undefined;

if (transfer.sourceType === "ethereum") {
const t = transfer as historyV2.ToPolkadotTransferResult;
nonce = t.submitted.nonce;
direction = "Inbound";
} else if (transfer.sourceType === "substrate") {
const t = transfer as historyV2.ToEthereumTransferResult;
nonce = t.bridgeHubMessageAccepted?.nonce;
direction = "Outbound";
}

if (nonce !== undefined && direction) {
setTipParams({ direction, nonce });
setTipDialogOpen(true);
} else {
alert(
"Tip cannot be added yet. The transfer must reach the bridge hub first.",
);
}
}}
>
Add Tip
</Button>
{tipParams && (
<AddTipDialog
open={tipDialogOpen}
onOpenChange={setTipDialogOpen}
direction={tipParams.direction}
nonce={tipParams.nonce}
/>
)}
</div>
<div className="flex justify-between items-center">
<Link
className={cn("underline text-sm", !inHistory ? "hidden" : "")}
Expand Down
Loading