Skip to content

Commit 0142894

Browse files
committed
Use the remix-vm dapp without changing the workspace
1 parent bb7e667 commit 0142894

File tree

2 files changed

+125
-20
lines changed

2 files changed

+125
-20
lines changed

libs/remix-ui/quick-dapp-v2/src/lib/components/EditHtmlTemplate/index.tsx

Lines changed: 89 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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">

libs/remix-ui/quick-dapp-v2/src/lib/utils/DappManager.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,11 +265,47 @@ export class DappManager {
265265
workspaceName
266266
);
267267

268+
// Preserve VM state: the VM reads .states/{provider}/state.json per workspace.
269+
// Without copying, switching to a new workspace resets the VM and loses deployed contracts.
270+
let vmStateSnapshot: string | null = null;
271+
let vmProviderName: string | null = null;
272+
try {
273+
vmProviderName = await (this.plugin as any).call('blockchain', 'getProvider') as string;
274+
if (vmProviderName && vmProviderName.startsWith('vm-')) {
275+
// Flush the latest in-memory state to disk first
276+
try { await (this.plugin as any).call('blockchain', 'dumpState'); } catch (_) { /* non-critical */ }
277+
await new Promise(resolve => setTimeout(resolve, 100));
278+
279+
const statePath = `.states/${vmProviderName}/state.json`;
280+
const stateExists = await (this.plugin as any).call('fileManager', 'exists', statePath);
281+
if (stateExists) {
282+
vmStateSnapshot = await this.plugin.call('fileManager', 'readFile', statePath) as string;
283+
}
284+
}
285+
} catch (e) {
286+
console.warn('[DappManager] Could not capture VM state (non-critical):', e);
287+
}
288+
268289
await this.plugin.call('filePanel', 'createWorkspace', workspaceName, true);
269290

270291
await this.switchToWorkspace(workspaceName);
271292
await new Promise(resolve => setTimeout(resolve, 300));
272293

294+
// Restore VM state in the new DApp workspace
295+
if (vmStateSnapshot && vmProviderName) {
296+
try {
297+
try { await this.plugin.call('fileManager', 'mkdir', '.states'); } catch (_) {}
298+
try { await this.plugin.call('fileManager', 'mkdir', `.states/${vmProviderName}`); } catch (_) {}
299+
await this.plugin.call(
300+
'fileManager', 'writeFile',
301+
`.states/${vmProviderName}/state.json`,
302+
vmStateSnapshot
303+
);
304+
} catch (e) {
305+
console.warn('[DappManager] Failed to restore VM state (non-critical):', e);
306+
}
307+
}
308+
273309
await this.focusPlugin();
274310

275311
const initialConfig: DappConfig = {

0 commit comments

Comments
 (0)