Skip to content

Commit 6e4052f

Browse files
feat: CCTP V2 Integration for USDC deposits (Ticket 4.3) (#67)
* feat: implement CCTP V2 integration for USDC deposits (Ticket 4.3) * style: run prettier format to fix CI * style: fix cargo fmt to pass CI
1 parent cee32f0 commit 6e4052f

28 files changed

Lines changed: 1386 additions & 458 deletions

app/package-lock.json

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
"react": "^18.3.1",
2929
"react-countup": "^6.5.3",
3030
"react-dom": "^18.3.1",
31-
"react-router-dom": "^6.30.4"
31+
"react-router-dom": "^6.30.4",
32+
"viem": "^2.53.1"
3233
},
3334
"devDependencies": {
3435
"@eslint/js": "^9.39.4",
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { useState } from "react";
2+
import { motion, AnimatePresence } from "framer-motion";
3+
import { CCTPDepositResult } from "@/lib/cctp";
4+
5+
export interface DepositModalProps {
6+
isOpen: boolean;
7+
onClose: () => void;
8+
depositState: CCTPDepositResult | null;
9+
progressMessage: string;
10+
onDeposit: (amount: number) => void;
11+
}
12+
13+
export function DepositModal({
14+
isOpen,
15+
onClose,
16+
depositState,
17+
progressMessage,
18+
onDeposit,
19+
}: DepositModalProps) {
20+
const [amount, setAmount] = useState<string>("");
21+
22+
const handleDeposit = () => {
23+
const val = parseFloat(amount);
24+
if (!isNaN(val) && val > 0) {
25+
onDeposit(val);
26+
}
27+
};
28+
29+
const isPending =
30+
depositState &&
31+
["initiating", "pending_attestation", "minting"].includes(
32+
depositState.status,
33+
);
34+
const isCompleted = depositState?.status === "completed";
35+
36+
return (
37+
<AnimatePresence>
38+
{isOpen && (
39+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
40+
<motion.div
41+
className="absolute inset-0 bg-flux-background/80 backdrop-blur-sm"
42+
initial={{ opacity: 0 }}
43+
animate={{ opacity: 1 }}
44+
exit={{ opacity: 0 }}
45+
onClick={onClose}
46+
/>
47+
<motion.div
48+
className="relative bg-[#1A1A24] border border-flux-border/30 rounded-xl shadow-2xl w-full max-w-md p-6 overflow-hidden"
49+
initial={{ opacity: 0, scale: 0.95, y: 20 }}
50+
animate={{ opacity: 1, scale: 1, y: 0 }}
51+
exit={{ opacity: 0, scale: 0.95, y: 20 }}
52+
>
53+
<div className="flex justify-between items-center mb-6">
54+
<h2 className="text-xl font-bold text-flux-text">
55+
Cross-Chain Deposit (CCTP)
56+
</h2>
57+
<button
58+
onClick={onClose}
59+
className="text-flux-text-dim hover:text-flux-text transition-colors"
60+
>
61+
62+
</button>
63+
</div>
64+
65+
{!depositState || depositState.status === "failed" ? (
66+
<div className="flex flex-col gap-4">
67+
<div className="flex flex-col gap-2">
68+
<label className="text-[13px] font-bold text-flux-text-dim uppercase tracking-[0.03em]">
69+
Amount (USDC)
70+
</label>
71+
<div className="relative">
72+
<input
73+
type="number"
74+
value={amount}
75+
onChange={(e) => setAmount(e.target.value)}
76+
placeholder="0.00"
77+
className="w-full bg-flux-surface/50 border border-flux-border/20 rounded-lg py-3 px-4 text-flux-text font-mono focus:outline-none focus:border-flux-accent"
78+
/>
79+
<div className="absolute right-4 top-1/2 -translate-y-1/2 text-flux-text-dim font-bold text-[13px]">
80+
USDC
81+
</div>
82+
</div>
83+
</div>
84+
85+
{depositState?.status === "failed" && (
86+
<div className="text-flux-short text-[13px] bg-flux-short/10 p-3 rounded border border-flux-short/20">
87+
{depositState.error}
88+
</div>
89+
)}
90+
91+
<button
92+
onClick={handleDeposit}
93+
disabled={!amount || parseFloat(amount) <= 0}
94+
className="w-full bg-flux-accent text-white font-bold py-3 rounded-lg hover:bg-flux-accent/90 transition-colors disabled:opacity-50 mt-2"
95+
>
96+
Initiate Deposit
97+
</button>
98+
<p className="text-[12px] text-flux-text-dim text-center mt-2">
99+
Powered by Circle CCTP V2. This is a non-blocking process.
100+
</p>
101+
</div>
102+
) : (
103+
<div className="flex flex-col items-center justify-center py-6 gap-4">
104+
{isPending && (
105+
<div className="w-12 h-12 border-4 border-flux-surface rounded-full border-t-flux-accent animate-spin mb-2"></div>
106+
)}
107+
{isCompleted && (
108+
<div className="w-12 h-12 bg-flux-long/20 text-flux-long rounded-full flex items-center justify-center text-2xl mb-2">
109+
110+
</div>
111+
)}
112+
113+
<h3 className="text-[16px] font-bold text-flux-text text-center">
114+
{progressMessage}
115+
</h3>
116+
117+
<div className="w-full bg-flux-surface/40 border border-flux-border/20 rounded p-4 flex flex-col gap-3 mt-4">
118+
{depositState.sourceTxHash && (
119+
<div className="flex flex-col gap-1">
120+
<span className="text-[11px] text-flux-text-dim uppercase">
121+
Source Tx Hash
122+
</span>
123+
<span className="text-[12px] font-mono text-flux-accent break-all">
124+
{depositState.sourceTxHash}
125+
</span>
126+
</div>
127+
)}
128+
{depositState.solanaTxSignature && (
129+
<div className="flex flex-col gap-1">
130+
<span className="text-[11px] text-flux-text-dim uppercase">
131+
Solana Signature
132+
</span>
133+
<span className="text-[12px] font-mono text-flux-long break-all">
134+
{depositState.solanaTxSignature}
135+
</span>
136+
</div>
137+
)}
138+
</div>
139+
140+
{isPending && (
141+
<button
142+
onClick={onClose}
143+
className="w-full bg-flux-surface border border-flux-border/30 text-flux-text font-bold py-2.5 rounded-lg hover:bg-flux-surface/80 transition-colors mt-4 text-[14px]"
144+
>
145+
Close & Continue Trading
146+
</button>
147+
)}
148+
149+
{isCompleted && (
150+
<button
151+
onClick={onClose}
152+
className="w-full bg-flux-long/20 text-flux-long font-bold py-2.5 rounded-lg hover:bg-flux-long/30 transition-colors mt-4 text-[14px]"
153+
>
154+
Done
155+
</button>
156+
)}
157+
</div>
158+
)}
159+
</motion.div>
160+
</div>
161+
)}
162+
</AnimatePresence>
163+
);
164+
}

app/src/components/ErrorBoundary.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, ReactNode } from 'react';
1+
import { Component, ReactNode } from "react";
22

33
interface Props {
44
children: ReactNode;
@@ -23,7 +23,7 @@ export class ErrorBoundary extends Component<Props, State> {
2323
}
2424

2525
override componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
26-
console.error('ErrorBoundary caught:', error, errorInfo);
26+
console.error("ErrorBoundary caught:", error, errorInfo);
2727
}
2828

2929
override render(): ReactNode {
@@ -35,11 +35,14 @@ export class ErrorBoundary extends Component<Props, State> {
3535
<div className="w-10 h-10 rounded-lg bg-flux-short/10 flex items-center justify-center">
3636
<span className="text-[18px]">⚠️</span>
3737
</div>
38-
<h2 className="text-[16px] font-bold text-flux-text">Something went wrong</h2>
38+
<h2 className="text-[16px] font-bold text-flux-text">
39+
Something went wrong
40+
</h2>
3941
</div>
4042

4143
<p className="text-[13px] text-flux-text-dim mb-4 leading-relaxed">
42-
An error occurred while rendering this page. Please try refreshing or navigate back.
44+
An error occurred while rendering this page. Please try refreshing
45+
or navigate back.
4346
</p>
4447

4548
{this.state.error && (
@@ -53,7 +56,7 @@ export class ErrorBoundary extends Component<Props, State> {
5356
<button
5457
onClick={() => {
5558
this.setState({ hasError: false, error: null });
56-
window.location.href = '/';
59+
window.location.href = "/";
5760
}}
5861
className="w-full py-2 px-3 rounded bg-flux-accent/10 text-flux-accent hover:bg-flux-accent/20 font-semibold text-[13px] transition-colors"
5962
>

app/src/components/Footer.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@ export function Footer() {
44
return (
55
<footer className="border-t border-flux-border px-6 py-4 text-[11px] text-flux-text-faint">
66
<div className="mx-auto flex max-w-[1400px] flex-col items-center justify-between gap-2 sm:flex-row">
7-
<span>FluxDEX — hybrid CLOB + PMM DEX on Solana, powered by MagicBlock Ephemeral Rollups.</span>
7+
<span>
8+
FluxDEX — hybrid CLOB + PMM DEX on Solana, powered by MagicBlock
9+
Ephemeral Rollups.
10+
</span>
811
<div className="flex items-center gap-4">
912
<span className="uppercase tracking-[0.05em] font-semibold">
10-
Network: <span className="text-flux-text-dim font-mono">{SOLANA_CLUSTER}</span>
13+
Network:{" "}
14+
<span className="text-flux-text-dim font-mono">
15+
{SOLANA_CLUSTER}
16+
</span>
1117
</span>
1218
<span className="text-flux-text-faint">·</span>
1319
<span className="font-mono">v0.1.0</span>

app/src/components/Header.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ const NAV_ITEMS = [
1010
] as const;
1111

1212
// Mock sparkline data for header
13-
const HEADER_SPARK = [40, 42, 38, 45, 50, 48, 55, 52, 58, 60, 57, 63, 65, 62, 68, 70, 72, 69, 75, 78];
13+
const HEADER_SPARK = [
14+
40, 42, 38, 45, 50, 48, 55, 52, 58, 60, 57, 63, 65, 62, 68, 70, 72, 69, 75,
15+
78,
16+
];
1417

1518
export function Header() {
1619
return (
@@ -21,7 +24,12 @@ export function Header() {
2124
<span className="text-[17px] font-bold tracking-tight text-flux-text">
2225
Flux<span className="text-flux-accent">DEX</span>
2326
</span>
24-
<Sparkline data={HEADER_SPARK} width={56} height={20} color="#F97316" />
27+
<Sparkline
28+
data={HEADER_SPARK}
29+
width={56}
30+
height={20}
31+
color="#F97316"
32+
/>
2533
</NavLink>
2634

2735
<div className="h-5 w-px bg-flux-border hidden sm:block" />
@@ -69,4 +77,3 @@ export function Header() {
6977
</header>
7078
);
7179
}
72-

0 commit comments

Comments
 (0)