Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 42 additions & 25 deletions frontend/src/components/Simulator/ContractItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import {
} from '@heroicons/vue/16/solid';
import { ref, onMounted, nextTick } from 'vue';
import { notify } from '@kyvg/vue3-notification';
import { computed } from 'vue';
import { useShortAddress } from '@/hooks';

const { shorten } = useShortAddress();

const store = useContractsStore();
const defaultContractName = 'New Contract.py';
Expand All @@ -22,6 +26,11 @@ const props = defineProps<{

const emit = defineEmits(['save', 'cancel']);

const deployedContract = computed(() => {
if (!props.contract) return null;
return store.deployedContracts.find((c) => c.contractId === props.contract?.id);
});

const isEditing = ref(false);
const editInput = ref<HTMLInputElement | null>(null);
const editingFileName = ref('');
Expand Down Expand Up @@ -137,33 +146,41 @@ const handleDownloadFile = (e: Event) => {

<div
v-else-if="contract"
class="flex w-full items-center justify-between truncate"
class="flex w-full flex-col truncate"
>
<div data-testid="contract-file" class="truncate font-semibold">
{{ contract.name }}
<div class="flex w-full items-center justify-between truncate">
<div data-testid="contract-file" class="truncate font-semibold">
{{ contract.name }}
</div>

<div class="hidden flex-row gap-1 group-hover:flex">
<button @click.stop="handleEditFile" v-tooltip="'Edit Name'">
<PencilSquareIcon
class="h-[16px] w-[16px] p-[2px] text-gray-400 transition-all hover:text-gray-800 active:scale-90 dark:hover:text-white"
/>
</button>

<button @click.stop="handleDownloadFile" v-tooltip="'Download file'">
<ArrowDownOnSquareIcon
class="h-[16px] w-[16px] p-[2px] text-gray-400 transition-all hover:text-gray-800 active:scale-90 dark:hover:text-white"
/>
</button>

<button
@click.stop="deleteModalOpen = true"
v-tooltip="'Delete file'"
>
<TrashIcon
class="h-[16px] w-[16px] p-[2px] text-gray-400 transition-all hover:text-gray-800 active:scale-90 dark:hover:text-white"
/>
</button>
</div>
</div>

<div class="hidden flex-row gap-1 group-hover:flex">
<button @click.stop="handleEditFile" v-tooltip="'Edit Name'">
<PencilSquareIcon
class="h-[16px] w-[16px] p-[2px] text-gray-400 transition-all hover:text-gray-800 active:scale-90 dark:hover:text-white"
/>
</button>

<button @click.stop="handleDownloadFile" v-tooltip="'Download file'">
<ArrowDownOnSquareIcon
class="h-[16px] w-[16px] p-[2px] text-gray-400 transition-all hover:text-gray-800 active:scale-90 dark:hover:text-white"
/>
</button>

<button
@click.stop="deleteModalOpen = true"
v-tooltip="'Delete file'"
>
<TrashIcon
class="h-[16px] w-[16px] p-[2px] text-gray-400 transition-all hover:text-gray-800 active:scale-90 dark:hover:text-white"
/>
</button>
<div
v-if="deployedContract"
class="truncate text-[10px] text-gray-400 dark:text-gray-500"
>
{{ shorten(deployedContract.address) }}
</div>
</div>

Expand Down
76 changes: 76 additions & 0 deletions frontend/src/components/Simulator/DeploymentSuccessPanel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<script setup lang="ts">
import { computed } from 'vue';
import { getExplorerUrl } from '@/utils/explorerUrl';
import CopyTextButton from '@/components/global/CopyTextButton.vue';
import { ExternalLink, CheckCircle2, Zap, X } from 'lucide-vue-next';

const props = defineProps<{
contractAddress: string;
txHash: string;
}>();

const emit = defineEmits(['close', 'interact']);

const explorerUrl = computed(() => getExplorerUrl());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm explorer URL helper has no local-network gating and inspect any network store/util that could drive the resolution.
fd -t f 'explorerUrl.ts' frontend/src -x cat {}
echo '---'
rg -nP '(VITE_GENLAYER_NETWORK|asimov|bradbury|studionet)' frontend/src --type=ts --type=vue -C2

Repository: genlayerlabs/genlayer-studio

Length of output: 917


🏁 Script executed:

cat -n frontend/src/components/Simulator/DeploymentSuccessPanel.vue

Repository: genlayerlabs/genlayer-studio

Length of output: 4149


🏁 Script executed:

fd -t f 'runtimeConfig.ts' frontend/src -x cat {}

Repository: genlayerlabs/genlayer-studio

Length of output: 2093


🏁 Script executed:

rg -n 'network|Network' frontend/src/stores -t ts --max-count=20

Repository: genlayerlabs/genlayer-studio

Length of output: 54


🏁 Script executed:

find frontend/src/stores -name '*[Ss]imulator*' -o -name '*[Nn]etwork*' | head -20

Repository: genlayerlabs/genlayer-studio

Length of output: 54


🏁 Script executed:

rg -n 'SimulatorStore' frontend/src --type ts --type vue -l | head -5

Repository: genlayerlabs/genlayer-studio

Length of output: 100


🏁 Script executed:

rg -n 'VITE_GENLAYER_NETWORK' frontend/src --type ts -C3

Repository: genlayerlabs/genlayer-studio

Length of output: 1204


🏁 Script executed:

rg -n 'local.*network|studionet' frontend/src --type ts --type vue -i -C2

Repository: genlayerlabs/genlayer-studio

Length of output: 100


🏁 Script executed:

cat -n frontend/src/hooks/useGenlayer.ts | head -60

Repository: genlayerlabs/genlayer-studio

Length of output: 2142


🏁 Script executed:

rg -n 'chains\s*=' frontend/src/hooks -A 15

Repository: genlayerlabs/genlayer-studio

Length of output: 54


🏁 Script executed:

rg -n 'localnet|studionet|explorer.*not.*available' frontend/src -i

Repository: genlayerlabs/genlayer-studio

Length of output: 1289


🏁 Script executed:

rg -n 'github.com|gitlab.com|issue|#[0-9]+' frontend/src/components/Simulator/DeploymentSuccessPanel.vue

Repository: genlayerlabs/genlayer-studio

Length of output: 54


🏁 Script executed:

git log --oneline -20 -- frontend/src/components/Simulator/DeploymentSuccessPanel.vue 2>/dev/null | head -10

Repository: genlayerlabs/genlayer-studio

Length of output: 139


🏁 Script executed:

rg -n 'isStudio|is_studio|isstudio' frontend/src --type ts -C2

Repository: genlayerlabs/genlayer-studio

Length of output: 1042


Major: explorer link is shown for local networks, contradicting the intended design.

getExplorerUrl() always returns a non-empty string — it falls back to http://localhost:3001 when neither VITE_EXPLORER_URL nor a *.genlayer.com host is present. Therefore v-if="explorerUrl" is always truthy, and on local/studionet the "View on Explorer" button will appear and link to http://localhost:3001/tx/....

The intended behavior (per component design) requires:

  • Hide the explorer link for local/studionet deployments
  • Show "Explorer not available for local networks" placeholder instead

The current implementation doesn't consult the active network at all. Although VITE_GENLAYER_NETWORK is available (providing 'localnet', 'studionet', 'testnetAsimov'), getExplorerUrl() derives the explorer purely from window.location.hostname, so a Studio session on studio.genlayer.com connected to local/studionet would still expose the wrong link.

Suggested fix:

  • Update getExplorerUrl() to accept or access the active network identifier and return null/'' for local/studionet.
  • Guard the template with v-if="explorerUrl && txHash" to also handle empty txHash cases.
  • Show "Explorer not available for local networks" as a fallback in the template.
Template sketch
         <a
-          v-if="explorerUrl"
+          v-if="explorerUrl && txHash"
           :href="`${explorerUrl}/tx/${txHash}`"
           target="_blank"
           class="..."
         >
           <ExternalLink class="h-3.5 w-3.5" />
           View on Explorer
         </a>
+        <span
+          v-else
+          class="inline-flex items-center text-xs text-slate-500 dark:text-zinc-400"
+        >
+          Explorer not available for local networks
+        </span>

Also applies to: 52-61

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/Simulator/DeploymentSuccessPanel.vue` at line 14, The
explorer link currently always shows because getExplorerUrl() falls back to
localhost; update getExplorerUrl to accept (or read) the active network
identifier (VITE_GENLAYER_NETWORK) and return null/empty for 'localnet' and
'studionet' so it does not produce a usable URL for local networks, then change
the computed property explorerUrl in DeploymentSuccessPanel.vue to call
getExplorerUrl(activeNetwork) (or rely on the updated behavior) and update the
template to guard with v-if="explorerUrl && txHash" and render a fallback text
"Explorer not available for local networks" when explorerUrl is falsy; ensure
references to getExplorerUrl, explorerUrl, txHash and the VITE_GENLAYER_NETWORK
env var are used so the logic only shows the "View on Explorer" button for
supported networks.

</script>

<template>
<div
class="relative mb-4 flex flex-col overflow-hidden rounded-xl border border-emerald-200 bg-gradient-to-b from-emerald-50/50 to-white shadow-sm dark:border-emerald-900/30 dark:from-emerald-950/20 dark:to-zinc-900"
data-testid="deployment-success-panel"
>
<button
@click="emit('close')"
class="absolute right-3 top-3 text-emerald-600/50 hover:text-emerald-600 dark:text-emerald-400/50 dark:hover:text-emerald-400"
aria-label="Close success panel"
>
<X class="h-4 w-4" />
</button>

<div class="flex items-center gap-2 border-b border-emerald-100 bg-emerald-50 px-4 py-3 dark:border-emerald-900/30 dark:bg-emerald-900/10">
<CheckCircle2 class="h-5 w-5 text-emerald-500 dark:text-emerald-400" />
<h3 class="font-semibold text-emerald-800 dark:text-emerald-300">Contract Deployed Successfully</h3>
</div>

<div class="flex flex-col gap-3 p-4">
<div class="flex flex-col gap-1">
<span class="text-[10px] font-medium uppercase tracking-wider text-slate-500 dark:text-zinc-400">Contract Address</span>
<div class="flex items-center justify-between rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 dark:border-zinc-700 dark:bg-zinc-800/50">
<span class="font-mono text-xs text-slate-700 break-all dark:text-zinc-300">{{ contractAddress }}</span>
<CopyTextButton :text="contractAddress" class="ml-2" />
</div>
</div>

<div class="flex flex-col gap-1">
<span class="text-[10px] font-medium uppercase tracking-wider text-slate-500 dark:text-zinc-400">Transaction Hash</span>
<div class="flex items-center justify-between rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 dark:border-zinc-700 dark:bg-zinc-800/50">
<span class="font-mono text-xs text-slate-700 break-all dark:text-zinc-300">{{ txHash }}</span>
<CopyTextButton :text="txHash" class="ml-2" />
</div>
</div>

<div class="mt-2 flex flex-row flex-wrap gap-2">
<a
v-if="explorerUrl"
:href="`${explorerUrl}/tx/${txHash}`"
target="_blank"
class="inline-flex items-center gap-1.5 rounded-lg bg-white px-4 py-2 text-xs font-medium text-slate-700 shadow-sm ring-1 ring-inset ring-slate-300 transition-colors hover:bg-slate-50 dark:bg-zinc-800 dark:text-zinc-200 dark:ring-zinc-600 dark:hover:bg-zinc-700"
>
<ExternalLink class="h-3.5 w-3.5" />
View on Explorer
</a>
Comment on lines +53 to +61
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard the explorer link against an empty txHash.

If txHash is ever empty (which is the current state — see the store/listener comment), the link resolves to ${explorerUrl}/tx/ and silently navigates to a broken page. Add && txHash to the v-if (or fall through to the "not available" hint suggested above).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/Simulator/DeploymentSuccessPanel.vue` around lines 53
- 61, The explorer link currently renders when explorerUrl exists but may create
a broken URL if txHash is empty; update the v-if condition on the <a> that
constructs `${explorerUrl}/tx/${txHash}` to require a non-empty txHash as well
(e.g., change the conditional to check both explorerUrl and txHash) so the link
only renders when both explorerUrl and txHash are present, or alternatively
render the "not available" hint when txHash is falsy; locate the anchor in
DeploymentSuccessPanel.vue (the element using
:href="`${explorerUrl}/tx/${txHash}`") and apply the conditional change.

<button
@click="emit('interact')"
class="inline-flex items-center gap-1.5 rounded-lg bg-emerald-500 px-4 py-2 text-xs font-medium text-white shadow-sm transition-colors hover:bg-emerald-600 dark:bg-emerald-600 dark:hover:bg-emerald-500"
>
<Zap class="h-3.5 w-3.5" />
Interact Now
</button>
</div>

<div class="mt-1 text-[11px] text-slate-500 dark:text-zinc-400">
ℹ Save your contract address — you'll need it to call this contract from outside the Studio.
</div>
</div>
</div>
</template>
1 change: 1 addition & 0 deletions frontend/src/hooks/useContractListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function useContractListener() {
contractId: localDeployTx.localContractId,
address: eventData.data.id,
defaultState: eventData.data.data.state,
deployTxHash: localDeployTx.hash,
});
}
}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/types/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface DeployedContract {
contractId: string;
address: Address;
defaultState: string;
deployTxHash?: `0x${string}`;
}

export interface NodeLog {
Expand Down
38 changes: 38 additions & 0 deletions frontend/src/views/Simulator/RunDebugView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ConstructorParameters from '@/components/Simulator/ConstructorParameters.
import ContractReadMethods from '@/components/Simulator/ContractReadMethods.vue';
import ContractWriteMethods from '@/components/Simulator/ContractWriteMethods.vue';
import TransactionsList from '@/components/Simulator/TransactionsList.vue';
import DeploymentSuccessPanel from '@/components/Simulator/DeploymentSuccessPanel.vue';
import { useContractQueries } from '@/hooks';
import MainTitle from '@/components/Simulator/MainTitle.vue';
import { ref, watch, computed } from 'vue';
Expand Down Expand Up @@ -59,6 +60,33 @@ function isFinalityWindowValid(value: number) {
}

const consensusMaxRotations = computed(() => consensusStore.maxRotations);

const showDeploymentSuccessPanel = ref(false);
const newlyDeployedTxHash = ref('');

watch(
[() => isDeployed.value, () => contract.value?.id],
([newIsDeployed, newId], [oldIsDeployed, oldId]) => {
if (newIsDeployed && !oldIsDeployed && newId === oldId) {
showDeploymentSuccessPanel.value = true;
const deployedContract = contractsStore.deployedContracts.find(
(c) => c.contractId === newId,
);
newlyDeployedTxHash.value = deployedContract?.deployTxHash || '';
} else if (newId !== oldId) {
showDeploymentSuccessPanel.value = false;
}
},
);
Comment on lines +67 to +80
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Watcher swallows the missing-hash case behind an empty-string fallback.

newlyDeployedTxHash.value = deployedContract?.deployTxHash || ''; will silently set an empty hash whenever the lookup fails (which today is always, due to the store dropping deployTxHash — see comment in useContractListener.ts). The panel still opens, leading to an empty Transaction Hash row, an empty-clipboard copy, and a broken /tx/ explorer link.

Consider not opening the panel when no hash is available, or at least logging the lookup miss so it surfaces during QA:

🛡️ Defensive variant
-    if (newIsDeployed && !oldIsDeployed && newId === oldId) {
-      showDeploymentSuccessPanel.value = true;
-      const deployedContract = contractsStore.deployedContracts.find(
-        (c) => c.contractId === newId,
-      );
-      newlyDeployedTxHash.value = deployedContract?.deployTxHash || '';
-    } else if (newId !== oldId) {
+    if (newIsDeployed && !oldIsDeployed && newId === oldId) {
+      const deployedContract = contractsStore.deployedContracts.find(
+        (c) => c.contractId === newId,
+      );
+      if (!deployedContract?.deployTxHash) {
+        // Avoid surfacing a panel with an empty tx hash / broken explorer link.
+        return;
+      }
+      newlyDeployedTxHash.value = deployedContract.deployTxHash;
+      showDeploymentSuccessPanel.value = true;
+    } else if (newId !== oldId) {
       showDeploymentSuccessPanel.value = false;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
watch(
[() => isDeployed.value, () => contract.value?.id],
([newIsDeployed, newId], [oldIsDeployed, oldId]) => {
if (newIsDeployed && !oldIsDeployed && newId === oldId) {
showDeploymentSuccessPanel.value = true;
const deployedContract = contractsStore.deployedContracts.find(
(c) => c.contractId === newId,
);
newlyDeployedTxHash.value = deployedContract?.deployTxHash || '';
} else if (newId !== oldId) {
showDeploymentSuccessPanel.value = false;
}
},
);
watch(
[() => isDeployed.value, () => contract.value?.id],
([newIsDeployed, newId], [oldIsDeployed, oldId]) => {
if (newIsDeployed && !oldIsDeployed && newId === oldId) {
const deployedContract = contractsStore.deployedContracts.find(
(c) => c.contractId === newId,
);
if (!deployedContract?.deployTxHash) {
// Avoid surfacing a panel with an empty tx hash / broken explorer link.
return;
}
newlyDeployedTxHash.value = deployedContract.deployTxHash;
showDeploymentSuccessPanel.value = true;
} else if (newId !== oldId) {
showDeploymentSuccessPanel.value = false;
}
},
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/views/Simulator/RunDebugView.vue` around lines 67 - 80, The
watcher that reacts to isDeployed and contract.id currently opens the success
panel even when the found deployedContract has no deployTxHash (due to the store
dropping it), because newlyDeployedTxHash is set with a silent '' fallback;
change the logic in the watcher (the function watching [() => isDeployed.value,
() => contract.value?.id]) to only set showDeploymentSuccessPanel.value = true
and assign newlyDeployedTxHash.value when a deployedContract is found AND
deployedContract.deployTxHash is a non-empty string; otherwise ensure
showDeploymentSuccessPanel.value = false and newlyDeployedTxHash.value is
cleared, and add a warning log (e.g., console.warn) indicating the lookup miss
(reference contractsStore.deployedContracts and useContractListener note) so QA
can see missing hashes.


const scrollToInteract = () => {
const el =
document.getElementById('tutorial-read-methods') ||
document.getElementById('tutorial-write-methods');
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
};
</script>

<template>
Expand Down Expand Up @@ -113,6 +141,16 @@ const consensusMaxRotations = computed(() => consensusStore.maxRotations);
@openDeployment="isDeploymentOpen = true"
/>

<div class="px-2">
<DeploymentSuccessPanel
v-if="showDeploymentSuccessPanel && isDeployed"
:contractAddress="address"
:txHash="newlyDeployedTxHash"
@close="showDeploymentSuccessPanel = false"
@interact="scrollToInteract"
/>
</div>

<template v-if="nodeStore.hasAtLeastOneValidator || uiStore.showTutorial">
<ConstructorParameters
id="tutorial-how-to-deploy"
Expand Down