Skip to content

Commit 1f8a568

Browse files
authored
Merge pull request #138 from Husten150/fix/husten150-issues
Implement all open issues assigned to Husten150 (#109, #102, #96, #95, #94)
2 parents 510a884 + f078dda commit 1f8a568

40 files changed

Lines changed: 3906 additions & 3 deletions

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@
2424
"test:waterfall": "tsx src/lib/__tests__/invalidationRegistry.test.ts",
2525
"test:redis-cache": "tsx src/lib/cache/__tests__/redisCache.test.ts",
2626
"test:horizon-pagination": "tsx src/lib/horizon/__tests__/useHorizonPagination.test.ts",
27-
"test:redis-cache": "tsx src/lib/cache/__tests__/redisCache.test.ts",
28-
"test:all": "tsx src/lib/__tests__/offlineQueue.test.ts && tsx src/lib/__tests__/OptimisticTransactionManager.test.ts && tsx src/services/__tests__/localCache.test.ts && tsx src/lib/__tests__/txQueue.test.ts && tsx src/lib/__tests__/slidingWindow.test.ts && tsx src/lib/__tests__/idbClient.test.ts && tsx src/services/__tests__/sharedStateSync.test.ts && tsx src/store/__tests__/workspaceStore.test.ts && tsx src/lib/offline/tests/networkCache.test.ts && tsx src/lib/__tests__/invalidationRegistry.test.ts && tsx src/lib/horizon/__tests__/useHorizonPagination.test.ts && tsx src/lib/cache/__tests__/redisCache.test.ts"
2927
"test:backup": "tsx src/lib/backup/__tests__/backupRestore.test.ts",
3028
"test:backup-scheduler": "tsx src/lib/backup/__tests__/scheduler.test.ts",
31-
"test:all": "tsx src/lib/__tests__/offlineQueue.test.ts && tsx src/lib/__tests__/OptimisticTransactionManager.test.ts && tsx src/services/__tests__/localCache.test.ts && tsx src/lib/__tests__/txQueue.test.ts && tsx src/lib/__tests__/slidingWindow.test.ts && tsx src/lib/__tests__/idbClient.test.ts && tsx src/services/__tests__/sharedStateSync.test.ts && tsx src/store/__tests__/workspaceStore.test.ts && tsx src/lib/offline/tests/networkCache.test.ts && tsx src/lib/__tests__/invalidationRegistry.test.ts && tsx src/lib/horizon/__tests__/useHorizonPagination.test.ts && tsx src/lib/backup/__tests__/backupRestore.test.ts && tsx src/lib/backup/__tests__/scheduler.test.ts"
29+
"test:scheduler": "tsx src/lib/scheduler/__tests__/scheduler.test.ts",
30+
"test:approval-manager": "tsx src/lib/approvalManager/__tests__/approvalManager.test.ts",
31+
"test:bridge": "tsx src/lib/bridge/__tests__/bridgeStore.test.ts",
32+
"test:staking": "tsx src/lib/staking/__tests__/stakingCalculator.test.ts",
33+
"test:all": "tsx src/lib/__tests__/offlineQueue.test.ts && tsx src/lib/__tests__/OptimisticTransactionManager.test.ts && tsx src/services/__tests__/localCache.test.ts && tsx src/lib/__tests__/txQueue.test.ts && tsx src/lib/__tests__/slidingWindow.test.ts && tsx src/lib/__tests__/idbClient.test.ts && tsx src/services/__tests__/sharedStateSync.test.ts && tsx src/store/__tests__/workspaceStore.test.ts && tsx src/lib/offline/tests/networkCache.test.ts && tsx src/lib/__tests__/invalidationRegistry.test.ts && tsx src/lib/horizon/__tests__/useHorizonPagination.test.ts && tsx src/lib/cache/__tests__/redisCache.test.ts && tsx src/lib/backup/__tests__/backupRestore.test.ts && tsx src/lib/backup/__tests__/scheduler.test.ts && tsx src/lib/scheduler/__tests__/scheduler.test.ts && tsx src/lib/approvalManager/__tests__/approvalManager.test.ts && tsx src/lib/bridge/__tests__/bridgeStore.test.ts && tsx src/lib/staking/__tests__/stakingCalculator.test.ts"
3234
},
3335
"dependencies": {
3436
"@monaco-editor/react": "^4.7.0",
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"use client"
2+
3+
import dynamic from "next/dynamic"
4+
5+
const BridgeDashboard = dynamic(
6+
() =>
7+
import("@/src/components/bridge/BridgeDashboard").then(
8+
(mod) => mod.BridgeDashboard,
9+
),
10+
{ ssr: false },
11+
)
12+
13+
export function BridgeDashboardClient() {
14+
return <BridgeDashboard />
15+
}

src/app/bridge/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { BridgeDashboardClient } from "./BridgeDashboardClient"
2+
3+
export default function BridgePage() {
4+
return <BridgeDashboardClient />
5+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"use client"
2+
3+
import dynamic from "next/dynamic"
4+
5+
const StakingDashboard = dynamic(
6+
() =>
7+
import("@/src/components/staking/StakingDashboard").then(
8+
(mod) => mod.StakingDashboard,
9+
),
10+
{ ssr: false },
11+
)
12+
13+
export function StakingDashboardClient() {
14+
return <StakingDashboard />
15+
}

src/app/staking/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { StakingDashboardClient } from "./StakingDashboardClient"
2+
3+
export default function StakingPage() {
4+
return <StakingDashboardClient />
5+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"use client";
2+
3+
import { useState, useCallback } from "react";
4+
import { TransactionMonitor } from "./TransactionMonitor";
5+
import { TransactionDetail } from "./TransactionDetail";
6+
import { useBridgeTx } from "@/src/hooks/useBridgeTx";
7+
import type { BridgeTransaction } from "@/src/lib/bridge";
8+
9+
const DEMO_WALLET = "GABCDEF1234567890";
10+
11+
export function BridgeDashboard() {
12+
const [selectedTx, setSelectedTx] = useState<BridgeTransaction | null>(null);
13+
const { transactions, isLoading, error, retryTransaction, refetch } = useBridgeTx({
14+
walletAddress: DEMO_WALLET,
15+
});
16+
17+
const handleSelect = useCallback((tx: BridgeTransaction) => {
18+
setSelectedTx(tx);
19+
}, []);
20+
21+
const handleRetry = useCallback(
22+
(id: string) => {
23+
retryTransaction(id);
24+
setSelectedTx(null);
25+
},
26+
[retryTransaction],
27+
);
28+
29+
const handleCloseDetail = useCallback(() => {
30+
setSelectedTx(null);
31+
}, []);
32+
33+
return (
34+
<div className="mx-auto max-w-6xl space-y-6 px-4 py-8">
35+
<div className="flex items-center justify-between">
36+
<div>
37+
<h1 className="text-2xl font-bold text-foreground">Cross-Chain Bridge</h1>
38+
<p className="mt-1 text-sm text-muted">
39+
Monitor bridge transactions across Ethereum, Polygon, Arbitrum, and Stellar Soroban.
40+
</p>
41+
</div>
42+
<button
43+
onClick={() => refetch()}
44+
disabled={isLoading}
45+
className="rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-text transition hover:bg-primary-hover disabled:cursor-not-allowed disabled:opacity-50"
46+
>
47+
{isLoading ? "Refreshing..." : "Refresh"}
48+
</button>
49+
</div>
50+
51+
{error && (
52+
<div className="rounded-md border border-error-border bg-error-bg p-4 text-sm text-danger-text">
53+
Failed to load bridge transactions. Please try again.
54+
</div>
55+
)}
56+
57+
{selectedTx ? (
58+
<TransactionDetail
59+
transaction={selectedTx}
60+
onRetry={handleRetry}
61+
onClose={handleCloseDetail}
62+
/>
63+
) : (
64+
<div className="rounded-lg border border-border bg-surface">
65+
<div className="border-b border-border px-6 py-4">
66+
<h2 className="text-lg font-semibold text-foreground">
67+
Transactions ({transactions.length})
68+
</h2>
69+
</div>
70+
<TransactionMonitor onSelect={handleSelect} onRetry={handleRetry} />
71+
</div>
72+
)}
73+
</div>
74+
);
75+
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
"use client";
2+
3+
import type { BridgeTransaction, BridgeStatus } from "@/src/lib/bridge";
4+
5+
const STATUS_STEPS: { status: BridgeStatus; label: string }[] = [
6+
{ status: "Initiated", label: "Transaction Initiated" },
7+
{ status: "SourceConfirmed", label: "Source Chain Confirmed" },
8+
{ status: "BridgeRelayed", label: "Bridge Relayed" },
9+
{ status: "DestinationPending", label: "Destination Pending" },
10+
{ status: "DestinationConfirmed", label: "Destination Confirmed" },
11+
{ status: "Complete", label: "Complete" },
12+
];
13+
14+
interface TimelineEntry {
15+
status: BridgeStatus
16+
label: string
17+
timestamp: number | undefined
18+
isReached: boolean
19+
isActive: boolean
20+
isFailed: boolean
21+
}
22+
23+
function formatTime(ts: number | undefined): string {
24+
if (!ts) return "";
25+
return new Date(ts).toLocaleString();
26+
}
27+
28+
function DetailRow({ label, value }: { label: string; value: string }) {
29+
return (
30+
<div className="flex items-center justify-between border-b border-border py-2 last:border-0">
31+
<span className="text-sm text-muted">{label}</span>
32+
<span className="text-sm font-medium text-foreground">{value}</span>
33+
</div>
34+
);
35+
}
36+
37+
interface TransactionDetailProps {
38+
transaction: BridgeTransaction
39+
onRetry?: (id: string) => void
40+
onClose?: () => void
41+
}
42+
43+
export function TransactionDetail({ transaction, onRetry, onClose }: TransactionDetailProps) {
44+
const entries: TimelineEntry[] = STATUS_STEPS.map((step, idx) => {
45+
const timestampKey = {
46+
Initiated: "initiatedAt",
47+
SourceConfirmed: "sourceConfirmedAt",
48+
BridgeRelayed: "bridgeRelayedAt",
49+
DestinationPending: "destinationPendingAt",
50+
DestinationConfirmed: "destinationConfirmedAt",
51+
Complete: "completedAt",
52+
}[step.status] as keyof BridgeTransaction;
53+
54+
const ts = transaction[timestampKey] as number | undefined;
55+
const isFailed = transaction.status === "Failed";
56+
const currentIdx = STATUS_STEPS.findIndex((s) => s.status === transaction.status);
57+
const isReached = idx <= currentIdx && !isFailed;
58+
const isActive = idx === currentIdx && !isFailed;
59+
60+
return {
61+
status: step.status,
62+
label: step.label,
63+
timestamp: ts,
64+
isReached,
65+
isActive,
66+
isFailed: isFailed && idx === currentIdx,
67+
};
68+
});
69+
70+
const chainName = (id: string) => id.charAt(0).toUpperCase() + id.slice(1);
71+
72+
return (
73+
<div className="rounded-lg border border-border bg-surface">
74+
<div className="flex items-center justify-between border-b border-border px-6 py-4">
75+
<h2 className="text-lg font-semibold text-foreground">Transaction Detail</h2>
76+
{onClose && (
77+
<button
78+
onClick={onClose}
79+
className="rounded p-1 text-muted transition hover:bg-surface-alt hover:text-foreground"
80+
>
81+
82+
</button>
83+
)}
84+
</div>
85+
86+
<div className="grid grid-cols-2 gap-4 border-b border-border px-6 py-4">
87+
<DetailRow label="Token" value={`${transaction.amount} ${transaction.token}`} />
88+
<DetailRow label="Route" value={`${chainName(transaction.sourceChain)} \u2192 ${chainName(transaction.destinationChain)}`} />
89+
<DetailRow label="Source TX" value={transaction.sourceTxHash.slice(0, 18) + "..."} />
90+
<DetailRow
91+
label="Destination TX"
92+
value={transaction.destinationTxHash ? transaction.destinationTxHash.slice(0, 18) + "..." : "Pending"}
93+
/>
94+
<DetailRow
95+
label="Confirmations"
96+
value={`${transaction.currentConfirmations}/${transaction.requiredConfirmations}`}
97+
/>
98+
<DetailRow label="Gas Used" value={transaction.gasUsed ?? "N/A"} />
99+
<DetailRow
100+
label="Actual Time"
101+
value={transaction.actualTimeMs ? `${(transaction.actualTimeMs / 1000).toFixed(1)}s` : "In progress"}
102+
/>
103+
<DetailRow
104+
label="Estimated Time"
105+
value={`${(transaction.estimatedTimeMs / 1000).toFixed(0)}s`}
106+
/>
107+
</div>
108+
109+
<div className="px-6 py-4">
110+
<h3 className="mb-4 text-sm font-semibold text-foreground">Status Timeline</h3>
111+
<div className="relative">
112+
{entries.map((entry, idx) => (
113+
<div key={entry.status} className="relative flex gap-4 pb-6 last:pb-0">
114+
<div className="flex flex-col items-center">
115+
<div
116+
className={`z-10 flex h-7 w-7 items-center justify-center rounded-full text-xs font-bold ${
117+
entry.isActive
118+
? "border-2 border-primary bg-surface text-primary"
119+
: entry.isReached
120+
? "bg-primary text-primary-text"
121+
: entry.isFailed
122+
? "border-2 border-danger bg-error-bg text-danger-text"
123+
: "border border-border bg-surface text-muted"
124+
}`}
125+
>
126+
{entry.isFailed ? "!" : entry.isReached ? "\u2713" : idx + 1}
127+
</div>
128+
{idx < entries.length - 1 && (
129+
<div
130+
className={`mt-0 h-full w-0.5 ${
131+
entry.isReached ? "bg-primary" : "bg-border"
132+
}`}
133+
/>
134+
)}
135+
</div>
136+
<div className="flex-1 pb-2">
137+
<p
138+
className={`text-sm font-medium ${
139+
entry.isReached || entry.isActive ? "text-foreground" : "text-muted"
140+
}`}
141+
>
142+
{entry.label}
143+
</p>
144+
{entry.timestamp && (
145+
<p className="text-xs text-muted">{formatTime(entry.timestamp)}</p>
146+
)}
147+
</div>
148+
</div>
149+
))}
150+
</div>
151+
</div>
152+
153+
{transaction.status === "Failed" && (
154+
<div className="mx-6 mb-4 rounded-md border border-error-border bg-error-bg p-4">
155+
<p className="text-sm font-medium text-danger-text">
156+
{transaction.errorReason ?? "Unknown error"}
157+
</p>
158+
{transaction.recommendedAction === "retry" && onRetry && (
159+
<button
160+
onClick={() => onRetry(transaction.id)}
161+
className="mt-2 rounded bg-primary px-3 py-1 text-xs font-semibold text-primary-text transition hover:bg-primary-hover"
162+
>
163+
Retry Transaction
164+
</button>
165+
)}
166+
{transaction.recommendedAction === "contact_support" && (
167+
<p className="mt-1 text-xs text-muted">
168+
Please contact support to resolve this issue.
169+
</p>
170+
)}
171+
</div>
172+
)}
173+
</div>
174+
);
175+
}

0 commit comments

Comments
 (0)