@@ -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 >
0 commit comments