@@ -505,6 +505,7 @@ function EditHtmlTemplate(): JSX.Element {
505505 const isVM = ! ! activeDapp ?. contract ?. chainId && activeDapp . contract . chainId . toString ( ) . startsWith ( 'vm' ) ;
506506
507507 const [ isCurrentProviderVM , setIsCurrentProviderVM ] = useState ( false ) ;
508+ const [ vmContractStatus , setVmContractStatus ] = useState < 'checking' | 'deployed' | 'not-found' > ( 'checking' ) ;
508509
509510 useEffect ( ( ) => {
510511 if ( ! plugin ) return ;
@@ -519,6 +520,82 @@ function EditHtmlTemplate(): JSX.Element {
519520 checkVM ( ) ;
520521 } , [ plugin , activeDapp ] ) ;
521522
523+ useEffect ( ( ) => {
524+ if ( ! isVM || ! isCurrentProviderVM || ! plugin || ! activeDapp ?. contract ?. address ) {
525+ setVmContractStatus ( 'checking' ) ;
526+ return ;
527+ }
528+
529+ let cancelled = false ;
530+
531+ const tryCallContract = async ( ) : Promise < boolean > => {
532+ const abi = activeDapp . contract . abi ;
533+ if ( ! abi || ! Array . isArray ( abi ) ) return false ;
534+
535+ const viewFn = abi . find ( ( item : any ) =>
536+ item . type === 'function' &&
537+ ( item . stateMutability === 'view' || item . stateMutability === 'pure' ) &&
538+ ( ! item . inputs || item . inputs . length === 0 )
539+ ) ;
540+ if ( ! viewFn ) return false ;
541+
542+ const inputTypes = ( viewFn . inputs || [ ] ) . map ( ( i : any ) => i . type ) . join ( ',' ) ;
543+ const sig = `${ viewFn . name } (${ inputTypes } )` ;
544+ const hexSig = '0x' + Array . from ( new TextEncoder ( ) . encode ( sig ) )
545+ . map ( b => b . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) ;
546+ const selectorHex = await plugin . call ( 'blockchain' , 'sendRpc' , 'web3_sha3' , [ hexSig ] ) ;
547+ const selector = typeof selectorHex === 'string' ? selectorHex . substring ( 0 , 10 ) : '0x' ;
548+
549+ const callResult = await plugin . call ( 'blockchain' , 'sendRpc' , 'eth_call' , [ {
550+ to : activeDapp . contract . address ,
551+ data : selector
552+ } , 'latest' ] ) ;
553+
554+ return typeof callResult === 'string' && callResult . length > 2 ;
555+ } ;
556+
557+ const checkWithRetry = async ( ) => {
558+ // Log getCode for reference — Remix VM returns 0x even when contract is functional
559+ try {
560+ const code = await plugin . call ( 'blockchain' , 'getCode' , activeDapp . contract . address ) ;
561+ console . log ( `[QuickDapp] getCode(${ activeDapp . contract . address } ):` , code ) ;
562+ } catch ( _ ) { }
563+
564+ // getCode is unreliable in Remix VM, so we use eth_call with a view function instead.
565+ // The VM also needs time to load state after workspace switch, so we retry.
566+ const MAX_RETRIES = 3 ;
567+ const RETRY_DELAY_MS = 2000 ;
568+
569+ for ( let attempt = 0 ; attempt < MAX_RETRIES ; attempt ++ ) {
570+ if ( cancelled ) return ;
571+ if ( attempt > 0 ) {
572+ await new Promise ( resolve => setTimeout ( resolve , RETRY_DELAY_MS ) ) ;
573+ if ( cancelled ) return ;
574+ }
575+
576+ try {
577+ if ( await tryCallContract ( ) ) {
578+ setVmContractStatus ( 'deployed' ) ;
579+ return ;
580+ }
581+ } catch ( e ) {
582+ const errStr = String ( e ) ;
583+ if ( errStr . includes ( 'revert' ) || errStr . includes ( 'execution reverted' ) ) {
584+ setVmContractStatus ( 'deployed' ) ;
585+ return ;
586+ }
587+ }
588+ }
589+
590+ if ( ! cancelled ) {
591+ setVmContractStatus ( 'not-found' ) ;
592+ }
593+ } ;
594+
595+ checkWithRetry ( ) ;
596+ return ( ) => { cancelled = true ; } ;
597+ } , [ isVM , isCurrentProviderVM , plugin , activeDapp ?. contract ?. address ] ) ;
598+
522599 useEffect ( ( ) => {
523600 let isMounted = true ;
524601
@@ -640,28 +717,20 @@ function EditHtmlTemplate(): JSX.Element {
640717 ) }
641718
642719 { isVM && (
643- < div className = " alert alert-warning py-2 px-3 mb-2 small shadow-sm border-warning d-flex align-items-start" >
644- < i className = " fas fa-exclamation-triangle me-2 mt-1 text-warning" > </ i >
720+ < div className = { ` alert py-2 px-3 mb-2 small shadow-sm d-flex align-items-start ${ vmContractStatus === 'not-found' ? 'alert-danger border-danger' : 'alert-warning border-warning' } ` } >
721+ < i className = { ` fas ${ vmContractStatus === 'not-found' ? ' fa-times-circle text-danger' : 'fa- exclamation-triangle text-warning' } me-2 mt-1` } > </ i >
645722 < div >
646723 < div className = "fw-bold mb-1" > Remix VM — Local Only</ div >
647- { activeDapp . sourceWorkspace ?. name && (
648- < div >
649- To run this DApp, switch to the contract workspace:{ ' ' }
650- < button
651- className = "btn btn-link btn-sm p-0 text-decoration-underline"
652- onClick = { async ( ) => {
653- try {
654- await plugin . call ( 'filePanel' , 'switchToWorkspace' , {
655- name : activeDapp . sourceWorkspace ! . name ,
656- isLocalhost : false ,
657- } ) ;
658- } catch ( e ) {
659- console . warn ( '[QuickDapp] Failed to switch workspace:' , e ) ;
660- }
661- } }
662- >
663- < strong > { activeDapp . sourceWorkspace . name } </ strong >
664- </ button >
724+ { vmContractStatus === 'not-found' && (
725+ < div className = "text-danger mb-1" >
726+ < i className = "fas fa-exclamation-circle me-1" > </ i >
727+ No contract found at < code > { activeDapp . contract . address } </ code > . The VM state may have been reset. Please redeploy the contract.
728+ </ div >
729+ ) }
730+ { vmContractStatus === 'checking' && isCurrentProviderVM && (
731+ < div className = "mb-1" >
732+ < i className = "fas fa-spinner fa-spin me-1" > </ i >
733+ Checking contract status...
665734 </ div >
666735 ) }
667736 < div className = "mt-1 text-danger" >
0 commit comments