Skip to content

Commit e8ada0e

Browse files
committed
feat(bridge): one-click receiver pre-fill; disable page while indexer syncs
1 parent ceb2cbb commit e8ada0e

2 files changed

Lines changed: 110 additions & 39 deletions

File tree

app/src/components/BridgePanel.tsx

Lines changed: 107 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,14 @@ const STATUS_STEPS: Record<string, { label: string; step: number }> = {
9191
manual: { label: 'Escalated for manual review', step: 3 },
9292
};
9393

94-
export default function BridgePanel() {
94+
interface IndexerStatus {
95+
up: boolean;
96+
height: number | null;
97+
nodeHeight: number | null;
98+
synced: boolean;
99+
}
100+
101+
export default function BridgePanel({ indexerEnabled = false }: { indexerEnabled?: boolean }) {
95102
const [config, setConfig] = useState<BridgeConfig | null>(null);
96103
const [fees, setFees] = useState<BridgeFees | null>(null);
97104
const [configError, setConfigError] = useState<string | null>(null);
@@ -106,6 +113,8 @@ export default function BridgePanel() {
106113
const [ethBalance, setEthBalance] = useState<string | null>(null);
107114
const [connecting, setConnecting] = useState(false);
108115
const [mlAddress, setMlAddress] = useState<string | null>(null);
116+
const [addressLoading, setAddressLoading] = useState(false);
117+
const [indexerStatus, setIndexerStatus] = useState<IndexerStatus | null>(null);
109118

110119
const [phase, setPhase] = useState<Phase>('idle');
111120
const [error, setError] = useState<string | null>(null);
@@ -142,7 +151,7 @@ export default function BridgePanel() {
142151
}, []);
143152

144153
// ── Own ML address (receive side for E2M) ───────────────────────────────────
145-
const loadMlAddress = useCallback(async () => {
154+
const fetchMlAddress = useCallback(async (): Promise<string | null> => {
146155
try {
147156
const res = await fetch('/api/rpc', {
148157
method: 'POST',
@@ -156,17 +165,52 @@ export default function BridgePanel() {
156165
ok: boolean;
157166
result?: { address: string; used: boolean; purpose: string }[];
158167
};
159-
if (!data.ok || !Array.isArray(data.result)) return;
168+
if (!data.ok || !Array.isArray(data.result)) return null;
160169
const first = data.result.find((a) => a.purpose === 'Receive' && !a.used) ?? data.result[0];
161-
if (first) setMlAddress(first.address);
170+
return first?.address ?? null;
162171
} catch {
163-
/* dashboard shows wallet errors elsewhere */
172+
return null; /* dashboard shows wallet errors elsewhere */
164173
}
165174
}, []);
175+
176+
const loadMlAddress = useCallback(async () => {
177+
const address = await fetchMlAddress();
178+
if (address) setMlAddress(address);
179+
}, [fetchMlAddress]);
166180
useEffect(() => {
167181
loadMlAddress();
168182
}, [loadMlAddress]);
169183

184+
/** Fill the receiver with a fresh receive address from the ML wallet. */
185+
async function useMlAddress() {
186+
setAddressLoading(true);
187+
try {
188+
const address = await fetchMlAddress();
189+
if (address) {
190+
setMlAddress(address);
191+
setReceiver(address);
192+
setReceiverSuggestion(null);
193+
}
194+
} finally {
195+
setAddressLoading(false);
196+
}
197+
}
198+
199+
// ── Indexer readiness gate ──────────────────────────────────────────────────
200+
useEffect(() => {
201+
if (!indexerEnabled) return;
202+
const load = () =>
203+
fetch('/api/indexer-status')
204+
.then((r) => r.json() as Promise<IndexerStatus>)
205+
.then(setIndexerStatus)
206+
.catch(() => {});
207+
load();
208+
const t = setInterval(load, 10_000);
209+
return () => clearInterval(t);
210+
}, [indexerEnabled]);
211+
212+
const indexerReady = !indexerEnabled || indexerStatus?.synced === true;
213+
170214
// ── MetaMask session restore ────────────────────────────────────────────────
171215
useEffect(() => {
172216
getConnectedAccount().then(setEthAccount);
@@ -348,7 +392,34 @@ export default function BridgePanel() {
348392

349393
const busy = phase === 'approve' || phase === 'deposit' || phase === 'signing' || phase === 'submitting' || phase === 'polling';
350394
const receiverLabel = direction === 'e2m' ? 'Your Mintlayer address (receiver)' : 'Your Ethereum address (receiver)';
351-
const receiverValue = direction === 'e2m' ? mlAddress ?? '' : ethAccount ?? '';
395+
396+
// Indexer still scanning: hide the whole bridge UI to avoid half-working
397+
// state (the m2e side spends from the local wallet, whose view of balances
398+
// is only complete once the indexer caught up with the node).
399+
if (indexerEnabled && indexerStatus && !indexerStatus.synced) {
400+
const height = indexerStatus.height ?? 0;
401+
const nodeHeight = indexerStatus.nodeHeight ?? 0;
402+
const pct = nodeHeight > 0 ? Math.min(100, Math.round((height / nodeHeight) * 100)) : 0;
403+
return (
404+
<div className={card}>
405+
<div className="flex items-center gap-3 mb-3">
406+
<span className="w-2 h-2 rounded-full bg-yellow-400 animate-pulse shrink-0"></span>
407+
<h2 className="text-base font-semibold text-gray-100">The indexer is still syncing</h2>
408+
</div>
409+
<p className="text-sm text-gray-400 mb-3">
410+
The bridge stays disabled until the indexer has caught up with the node, so
411+
you don't act on incomplete balances or history.
412+
</p>
413+
<div className="w-full h-2 rounded-full bg-gray-800 overflow-hidden mb-2">
414+
<div className="h-full bg-mint-600 transition-all" style={{ width: `${pct}%` }}></div>
415+
</div>
416+
<p className="text-xs text-gray-500 font-mono">
417+
indexer block #{height.toLocaleString()} / node block #{nodeHeight.toLocaleString()} ({pct}%)
418+
</p>
419+
<p className="text-xs text-gray-500 mt-2">This page re-checks automatically every 10 seconds.</p>
420+
</div>
421+
);
422+
}
352423

353424
const phaseLabel =
354425
phase === 'approve'
@@ -476,21 +547,35 @@ export default function BridgePanel() {
476547

477548
<div>
478549
<label className="block text-xs text-gray-400 mb-1">{receiverLabel}</label>
479-
{direction === 'e2m' ? (
550+
<div className="flex gap-2">
480551
<input
481552
value={receiver}
482553
onChange={e => { setReceiver(e.target.value); setReceiverSuggestion(null); }}
483-
placeholder="tmt1…"
554+
placeholder={direction === 'e2m' ? 'tmt1…' : '0x…'}
484555
className={input}
485556
/>
486-
) : (
487-
<input
488-
value={receiver}
489-
onChange={e => { setReceiver(e.target.value); setReceiverSuggestion(null); }}
490-
placeholder="0x…"
491-
className={input}
492-
/>
493-
)}
557+
{direction === 'e2m' ? (
558+
<button
559+
type="button"
560+
onClick={useMlAddress}
561+
disabled={addressLoading}
562+
title="Fill in a receive address from your Mintlayer wallet"
563+
className="shrink-0 rounded-lg border border-mint-700 bg-mint-900/30 hover:bg-mint-900/60 px-3 text-xs font-medium text-mint-300 transition-colors disabled:opacity-50"
564+
>
565+
{addressLoading ? '…' : 'Use my ML address'}
566+
</button>
567+
) : (
568+
<button
569+
type="button"
570+
onClick={() => { if (ethAccount) { setReceiver(ethAccount); setReceiverSuggestion(null); } }}
571+
disabled={!ethAccount}
572+
title="Fill in your connected MetaMask address"
573+
className="shrink-0 rounded-lg border border-mint-700 bg-mint-900/30 hover:bg-mint-900/60 px-3 text-xs font-medium text-mint-300 transition-colors disabled:opacity-50"
574+
>
575+
Use MetaMask
576+
</button>
577+
)}
578+
</div>
494579
{receiverSuggestion && (
495580
<div className="mt-2 rounded-lg border border-amber-800 bg-amber-900/20 px-3 py-2 text-xs text-amber-300">
496581
Did you mean{' '}
@@ -505,24 +590,6 @@ export default function BridgePanel() {
505590
</button>
506591
</div>
507592
)}
508-
{direction === 'e2m' && mlAddress && !receiver && (
509-
<button
510-
type="button"
511-
onClick={() => setReceiver(mlAddress)}
512-
className="text-xs text-mint-400 hover:text-mint-300 mt-1"
513-
>
514-
Use your wallet address
515-
</button>
516-
)}
517-
{direction === 'm2e' && ethAccount && !receiver && (
518-
<button
519-
type="button"
520-
onClick={() => setReceiver(ethAccount)}
521-
className="text-xs text-mint-400 hover:text-mint-300 mt-1"
522-
>
523-
Use your MetaMask address
524-
</button>
525-
)}
526593
</div>
527594

528595
{error && (
@@ -542,12 +609,14 @@ export default function BridgePanel() {
542609
{connecting ? 'Connecting…' : 'Connect MetaMask'}
543610
</button>
544611
) : (
545-
<button onClick={submit} disabled={busy || !config} className={primaryBtn}>
612+
<button onClick={submit} disabled={busy || !config || !indexerReady} className={primaryBtn}>
546613
{busy
547614
? 'Working…'
548-
: direction === 'e2m'
549-
? `Deposit ${assetTicker || 'tokens'} to bridge`
550-
: `Send ${assetTicker || 'tokens'} to bridge`}
615+
: !indexerReady
616+
? 'Waiting for the indexer…'
617+
: direction === 'e2m'
618+
? `Deposit ${assetTicker || 'tokens'} to bridge`
619+
: `Send ${assetTicker || 'tokens'} to bridge`}
551620
</button>
552621
)}
553622
</div>

app/src/pages/bridge.astro

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import BridgePanel from '@/components/BridgePanel';
55
if (process.env.NETWORK !== 'mainnet') {
66
return Astro.redirect('/');
77
}
8+
9+
const indexerEnabled = process.env.INDEXER_ENABLED === 'true';
810
---
911

1012
<Layout title="Bridge" activeNav="bridge">
1113
<h1 class="text-2xl font-semibold text-gray-100 mb-1">Bridge</h1>
1214
<p class="text-sm text-gray-500 mb-6">
1315
Move ERC20 tokens between Ethereum and Mintlayer.
1416
</p>
15-
<BridgePanel client:only="react" />
17+
<BridgePanel client:only="react" indexerEnabled={indexerEnabled} />
1618
</Layout>

0 commit comments

Comments
 (0)