From 2102f6d54926edc193907dd9f8069e2e692a432a Mon Sep 17 00:00:00 2001 From: Dark-Brain07 <85172976+Dark-Brain07@users.noreply.github.com> Date: Sun, 26 Apr 2026 11:48:35 +0600 Subject: [PATCH 1/2] feat(frontend): replace raw GenVM errors with plain-English messages Resolves #1609 ## Problem When new developers make mistakes (e.g., forgetting the runner comment on line 1), GenLayer Studio shows raw GenVM JSON error dumps that are intimidating and unhelpful. This is the #1 onboarding blocker for new contributors reported in issue #1609. ## Solution Introduced a GenVM error parser layer that intercepts raw error output and translates it into plain-English messages with actionable fix suggestions, while keeping the raw output accessible behind a collapsible 'Technical Details' toggle. ### New files - rontend/src/utils/genvmErrors.ts - Error parser with pattern matching for known GenVM error codes (absent_runner_comment, invalid_contract, VM_ERROR, InsufficientFundsError, etc.) - rontend/src/components/Simulator/GenVMErrorDisplay.vue - Polished error display component with title, reason, fix suggestion, and collapsible raw error with copy-to-clipboard - rontend/test/unit/utils/genvmErrors.test.ts - 18 unit tests covering all known patterns and edge cases ### Modified files - useContractQueries.ts - Deploy, write, and schema errors now surface friendly messages via parseGenvmError() - TransactionItem.vue - Error details in tx modal use GenVMErrorDisplay instead of raw pre blocks - ConstructorParameters.vue - Schema errors show GenVMErrorDisplay with actionable fix guidance --- .../Simulator/ConstructorParameters.vue | 13 +- .../Simulator/GenVMErrorDisplay.vue | 123 ++++++++++++ .../components/Simulator/TransactionItem.vue | 34 ++-- frontend/src/hooks/useContractQueries.ts | 20 +- frontend/src/utils/genvmErrors.ts | 182 ++++++++++++++++++ frontend/test/unit/utils/genvmErrors.test.ts | 112 +++++++++++ 6 files changed, 455 insertions(+), 29 deletions(-) create mode 100644 frontend/src/components/Simulator/GenVMErrorDisplay.vue create mode 100644 frontend/src/utils/genvmErrors.ts create mode 100644 frontend/test/unit/utils/genvmErrors.test.ts diff --git a/frontend/src/components/Simulator/ConstructorParameters.vue b/frontend/src/components/Simulator/ConstructorParameters.vue index 98e5f8f4b..4085ba076 100644 --- a/frontend/src/components/Simulator/ConstructorParameters.vue +++ b/frontend/src/components/Simulator/ConstructorParameters.vue @@ -6,6 +6,7 @@ import { ArrowUpTrayIcon } from '@heroicons/vue/16/solid'; import ContractParams from './ContractParams.vue'; import { type ArgData, unfoldArgsData } from './ContractParams'; import type { ExecutionMode } from '@/types'; +import GenVMErrorDisplay from '@/components/Simulator/GenVMErrorDisplay.vue'; const props = defineProps<{ executionMode: ExecutionMode; @@ -15,12 +16,17 @@ const props = defineProps<{ const { contract, contractSchemaQuery, deployContract, isDeploying } = useContractQueries(); -const { data, isPending, isRefetching, isError } = contractSchemaQuery; +const { data, isPending, isRefetching, isError, error: schemaError } = contractSchemaQuery; const calldataArguments = ref({ args: [], kwargs: {} }); const ctorMethod = computed(() => data.value.ctor); +const schemaErrorMessage = computed(() => { + if (!schemaError.value) return ''; + return (schemaError.value as Error)?.message || String(schemaError.value); +}); + const emit = defineEmits(['deployed-contract']); const handleDeployContract = async () => { @@ -45,6 +51,10 @@ const handleDeployContract = async () => { + Could not load contract schema. + diff --git a/frontend/src/components/Simulator/GenVMErrorDisplay.vue b/frontend/src/components/Simulator/GenVMErrorDisplay.vue new file mode 100644 index 000000000..8360ca0cf --- /dev/null +++ b/frontend/src/components/Simulator/GenVMErrorDisplay.vue @@ -0,0 +1,123 @@ + + + diff --git a/frontend/src/components/Simulator/TransactionItem.vue b/frontend/src/components/Simulator/TransactionItem.vue index fe011a551..428887fa5 100644 --- a/frontend/src/components/Simulator/TransactionItem.vue +++ b/frontend/src/components/Simulator/TransactionItem.vue @@ -30,6 +30,7 @@ import { getRuntimeConfigNumber, } from '@/utils/runtimeConfig'; import { getExplorerUrl } from '@/utils/explorerUrl'; +import GenVMErrorDisplay from '@/components/Simulator/GenVMErrorDisplay.vue'; const explorerUrl = computed(() => getExplorerUrl()); @@ -455,15 +456,10 @@ const badgeColorClass = computed(() => { class="overflow-x-auto whitespace-pre rounded bg-gray-200 p-1 text-xs text-gray-600 dark:bg-zinc-800 dark:text-gray-300" >{{ leaderReceipt?.result?.payload?.readable || 'None' }} -
-
Error Detail
-
{{
-                leaderErrorDetail
-              }}
-
+ :raw-error="leaderErrorDetail" + /> @@ -616,14 +612,11 @@ const badgeColorClass = computed(() => { -
-
{{
-                    extractErrorText(history.leader_result[0])
-                  }}
-
+ :raw-error="extractErrorText(history.leader_result[0])!" + class="ml-5 mt-1" + /> @@ -684,14 +677,11 @@ const badgeColorClass = computed(() => { -
-
{{
-                    extractErrorText(validator)
-                  }}
-
+ :raw-error="extractErrorText(validator)!" + class="ml-5 mt-1" + /> diff --git a/frontend/src/hooks/useContractQueries.ts b/frontend/src/hooks/useContractQueries.ts index f36fe2948..51a01c7e6 100644 --- a/frontend/src/hooks/useContractQueries.ts +++ b/frontend/src/hooks/useContractQueries.ts @@ -15,6 +15,7 @@ import { useWallet, useChainEnforcer, } from '@/hooks'; +import { parseGenvmError } from '@/utils/genvmErrors'; import type { Address, TransactionHash, @@ -113,7 +114,9 @@ export function useContractQueries() { schema.value = result; return schema.value; } catch (error: any) { - throw new Error(error.details || error.message); + const rawMsg = error.details || error.message || String(error); + const friendly = parseGenvmError(rawMsg); + throw new Error(friendly.title + ': ' + friendly.reason); } } @@ -174,14 +177,17 @@ export function useContractQueries() { transactionsStore.addTransaction(tx); contractsStore.removeDeployedContract(contract.value?.id ?? ''); return tx; - } catch (error) { + } catch (error: any) { isDeploying.value = false; + const rawMsg = error?.details || error?.message || String(error); + const friendly = parseGenvmError(rawMsg); notify({ type: 'error', - title: 'Error deploying contract', + title: friendly.title, + text: friendly.reason, }); console.error('Error Deploying the contract', error); - throw new Error('Error Deploying the contract'); + throw new Error(friendly.title + ': ' + friendly.reason); } } @@ -289,9 +295,11 @@ export function useContractQueries() { }, }); return true; - } catch (error) { + } catch (error: any) { + const rawMsg = error?.details || error?.message || String(error); + const friendly = parseGenvmError(rawMsg); console.error(error); - throw new Error('Error writing to contract'); + throw new Error(friendly.title + ': ' + friendly.reason); } } diff --git a/frontend/src/utils/genvmErrors.ts b/frontend/src/utils/genvmErrors.ts new file mode 100644 index 000000000..69fb7c3f2 --- /dev/null +++ b/frontend/src/utils/genvmErrors.ts @@ -0,0 +1,182 @@ +/** + * GenVM Error Parser + * + * Translates raw GenVM JSON error output into plain-English messages + * that tell developers exactly what went wrong and how to fix it. + * + * Resolves: https://github.com/genlayerlabs/genlayer-studio/issues/1609 + */ + +export interface FriendlyError { + /** Short, human-readable title (e.g. "Could not load contract") */ + title: string; + /** Plain-English explanation of what went wrong */ + reason: string; + /** Actionable fix suggestion (may contain code snippets) */ + fix: string; + /** The original raw error string for advanced users / bug reports */ + rawError: string; +} + +/** + * Known GenVM error codes mapped to user-friendly messages. + * + * Each key is a substring that may appear in the raw error message. + * The order matters: more specific patterns should come first. + */ +const ERROR_MAP: Array<{ + pattern: string | RegExp; + title: string; + reason: string; + fix: string; +}> = [ + { + pattern: 'absent_runner_comment', + title: 'Missing runner comment', + reason: + 'Your contract is missing the required runner comment on line 1. GenVM needs this comment to know which runtime to use.', + fix: 'Add the following as the very first line of your contract (no blank lines above it):\n\n# { "Depends": "py-genlayer:test" }', + }, + { + pattern: 'invalid_runner_comment', + title: 'Invalid runner comment', + reason: + 'The runner comment on line 1 of your contract is malformed. It must be valid JSON inside a Python comment.', + fix: 'Replace the first line of your contract with:\n\n# { "Depends": "py-genlayer:test" }', + }, + { + pattern: 'invalid_contract', + title: 'Invalid contract', + reason: + 'GenVM could not parse your contract. The contract file may have syntax errors or an unsupported structure.', + fix: 'Check your contract for Python syntax errors. Make sure it starts with the runner comment and contains a valid class that inherits from gl.Contract.', + }, + { + pattern: 'execution failed', + title: 'Contract execution failed', + reason: + 'The GenVM virtual machine encountered an error while trying to execute your contract code.', + fix: 'Check the technical details below for the specific error. Common causes include:\n• Missing imports (e.g. import gl)\n• Syntax errors in your Python code\n• Runtime exceptions in constructor or method logic', + }, + { + pattern: 'VM_ERROR', + title: 'Virtual Machine error', + reason: + 'The GenVM runtime reported an internal error while processing your contract.', + fix: 'This is usually caused by a problem in your contract code. Check the technical details for the specific VM error message.', + }, + { + pattern: 'InsufficientFundsError', + title: 'Insufficient funds', + reason: + 'The account you are using does not have enough funds to complete this transaction.', + fix: 'Fund your account with more tokens before retrying. You can do this from the Accounts panel in the Studio.', + }, + { + pattern: 'InvalidAddressError', + title: 'Invalid address', + reason: + 'The address provided is not in the correct format.', + fix: 'Make sure you are using a valid Ethereum-style address (0x followed by 40 hexadecimal characters).', + }, + { + pattern: 'contract_not_found', + title: 'Contract not found', + reason: + 'No contract was found at the specified address. It may not have been deployed yet, or the address may be incorrect.', + fix: 'Double-check the contract address. If you just deployed, wait for the transaction to be finalized before interacting.', + }, + { + pattern: 'timeout', + title: 'Request timed out', + reason: + 'The operation took too long to complete. This can happen when the network is under heavy load or validators are slow to respond.', + fix: 'Try again in a few moments. If the issue persists, check that your validators are configured correctly and running.', + }, +]; + +/** + * Attempts to extract the GenVM error code from a raw error string. + * + * Looks for patterns like: "message": "invalid_contract absent_runner_comment" + * inside the raw JSON dump. + */ +function extractErrorCode(rawError: string): string | null { + // Try to find the "message" field inside the result JSON + const messageMatch = rawError.match( + /"message"\s*:\s*"([^"]+)"/, + ); + if (messageMatch) { + return messageMatch[1]; + } + + // Try to find a "kind" field + const kindMatch = rawError.match(/"kind"\s*:\s*"([^"]+)"/); + if (kindMatch) { + return kindMatch[1]; + } + + return null; +} + +/** + * Parse a raw GenVM error message into a user-friendly error object. + * + * If the error matches a known pattern, returns a FriendlyError with + * clear title, reason, and fix. Otherwise returns a generic friendly + * message with the raw error preserved for debugging. + * + * @param rawError - The raw error string from the backend/SDK + * @returns A FriendlyError object with user-friendly messaging + */ +export function parseGenvmError(rawError: string): FriendlyError { + const errorCode = extractErrorCode(rawError); + + // Check the error code first, then fall back to pattern matching on full string + for (const entry of ERROR_MAP) { + const pattern = entry.pattern; + const matches = + typeof pattern === 'string' + ? (errorCode?.includes(pattern) || rawError.includes(pattern)) + : pattern.test(errorCode || '') || pattern.test(rawError); + + if (matches) { + return { + title: entry.title, + reason: entry.reason, + fix: entry.fix, + rawError, + }; + } + } + + // Fallback: unknown error + return { + title: 'Something went wrong', + reason: + 'An unexpected error occurred. Expand "Technical Details" below to see the raw error for bug reporting.', + fix: 'If this keeps happening, please report it on the GenLayer Discord or GitHub with the technical details below.', + rawError, + }; +} + +/** + * Check if a raw error string looks like it contains a GenVM JSON dump + * (i.e., it's a raw technical error that should be translated). + * + * @param error - The error message to check + * @returns true if the error looks like a raw GenVM error dump + */ +export function isRawGenvmError(error: string): boolean { + if (!error || typeof error !== 'string') return false; + + return ( + error.includes('genvm_log') || + error.includes('VM_ERROR') || + error.includes('execution failed') || + error.includes('gen_getContractSchemaForCode') || + error.includes('absent_runner_comment') || + error.includes('invalid_contract') || + (error.includes('Unexpected error in') && error.includes('result')) + ); +} diff --git a/frontend/test/unit/utils/genvmErrors.test.ts b/frontend/test/unit/utils/genvmErrors.test.ts new file mode 100644 index 000000000..f22c435a4 --- /dev/null +++ b/frontend/test/unit/utils/genvmErrors.test.ts @@ -0,0 +1,112 @@ +import { describe, it, expect } from 'vitest'; +import { + parseGenvmError, + isRawGenvmError, + type FriendlyError, +} from '@/utils/genvmErrors'; + +describe('genvmErrors', () => { + describe('parseGenvmError', () => { + it('matches absent_runner_comment pattern', () => { + const raw = + '{"message": "invalid_contract absent_runner_comment", "code": -32000}'; + const result: FriendlyError = parseGenvmError(raw); + + expect(result.title).toBe('Missing runner comment'); + expect(result.reason).toContain('missing the required runner comment'); + expect(result.fix).toContain('# { "Depends": "py-genlayer:test" }'); + expect(result.rawError).toBe(raw); + }); + + it('matches invalid_runner_comment pattern', () => { + const raw = '{"message": "invalid_runner_comment"}'; + const result = parseGenvmError(raw); + expect(result.title).toBe('Invalid runner comment'); + }); + + it('matches invalid_contract pattern', () => { + const raw = 'Error: invalid_contract - syntax error at line 5'; + const result = parseGenvmError(raw); + expect(result.title).toBe('Invalid contract'); + }); + + it('matches execution failed pattern', () => { + const raw = 'execution failed: RuntimeError in __init__'; + const result = parseGenvmError(raw); + expect(result.title).toBe('Contract execution failed'); + }); + + it('matches VM_ERROR pattern', () => { + const raw = '{"kind": "VM_ERROR", "details": "segfault"}'; + const result = parseGenvmError(raw); + expect(result.title).toBe('Virtual Machine error'); + }); + + it('matches InsufficientFundsError pattern', () => { + const raw = 'InsufficientFundsError: account 0x123 has 0 balance'; + const result = parseGenvmError(raw); + expect(result.title).toBe('Insufficient funds'); + }); + + it('matches InvalidAddressError pattern', () => { + const raw = 'InvalidAddressError: 0xZZZ is not valid'; + const result = parseGenvmError(raw); + expect(result.title).toBe('Invalid address'); + }); + + it('matches timeout pattern', () => { + const raw = 'Request timeout after 30s'; + const result = parseGenvmError(raw); + expect(result.title).toBe('Request timed out'); + }); + + it('returns generic fallback for unknown errors', () => { + const raw = 'Some completely unknown error xyz'; + const result = parseGenvmError(raw); + expect(result.title).toBe('Something went wrong'); + expect(result.rawError).toBe(raw); + }); + + it('handles empty string gracefully', () => { + const result = parseGenvmError(''); + expect(result.title).toBe('Something went wrong'); + }); + + it('preserves the raw error for all cases', () => { + const raw = 'InsufficientFundsError: account is broke'; + const result = parseGenvmError(raw); + expect(result.rawError).toBe(raw); + }); + }); + + describe('isRawGenvmError', () => { + it('returns true for genvm_log messages', () => { + expect(isRawGenvmError('genvm_log: some log output')).toBe(true); + }); + + it('returns true for VM_ERROR messages', () => { + expect(isRawGenvmError('{"kind": "VM_ERROR"}')).toBe(true); + }); + + it('returns true for execution failed messages', () => { + expect(isRawGenvmError('execution failed in contract')).toBe(true); + }); + + it('returns true for absent_runner_comment messages', () => { + expect(isRawGenvmError('absent_runner_comment error')).toBe(true); + }); + + it('returns false for normal user-friendly messages', () => { + expect(isRawGenvmError('Contract deployed successfully')).toBe(false); + }); + + it('returns false for empty string', () => { + expect(isRawGenvmError('')).toBe(false); + }); + + it('returns false for null/undefined', () => { + expect(isRawGenvmError(null as any)).toBe(false); + expect(isRawGenvmError(undefined as any)).toBe(false); + }); + }); +}); From 358d92d9e5c81121f7a6dffc38dd0378ab0fe65b Mon Sep 17 00:00:00 2001 From: Dark-Brain07 <85172976+Dark-Brain07@users.noreply.github.com> Date: Sun, 26 Apr 2026 12:13:49 +0600 Subject: [PATCH 2/2] fix: preserve raw GenVM error on thrown Error for downstream parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses CodeRabbit review feedback. The catch blocks in useContractQueries now attach the original raw error string as awGenvmError on the thrown Error, so ConstructorParameters.vue can pass the actual GenVM dump to GenVMErrorDisplay instead of the already-translated friendly text. Without this, GenVMErrorDisplay would re-parse 'Missing runner comment: Your contract is missing...' — none of the ERROR_MAP patterns match that, so every known error fell through to the generic 'Something went wrong' card. Now the raw dump is preserved and the component renders the correct friendly message with the original JSON behind Technical Details. --- .../components/Simulator/ConstructorParameters.vue | 5 +++-- frontend/src/hooks/useContractQueries.ts | 12 +++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/Simulator/ConstructorParameters.vue b/frontend/src/components/Simulator/ConstructorParameters.vue index 4085ba076..7933441c9 100644 --- a/frontend/src/components/Simulator/ConstructorParameters.vue +++ b/frontend/src/components/Simulator/ConstructorParameters.vue @@ -23,8 +23,9 @@ const calldataArguments = ref({ args: [], kwargs: {} }); const ctorMethod = computed(() => data.value.ctor); const schemaErrorMessage = computed(() => { - if (!schemaError.value) return ''; - return (schemaError.value as Error)?.message || String(schemaError.value); + const err = schemaError.value as any; + if (!err) return ''; + return err.rawGenvmError || err.message || String(err); }); const emit = defineEmits(['deployed-contract']); diff --git a/frontend/src/hooks/useContractQueries.ts b/frontend/src/hooks/useContractQueries.ts index 51a01c7e6..1416e40ce 100644 --- a/frontend/src/hooks/useContractQueries.ts +++ b/frontend/src/hooks/useContractQueries.ts @@ -116,7 +116,9 @@ export function useContractQueries() { } catch (error: any) { const rawMsg = error.details || error.message || String(error); const friendly = parseGenvmError(rawMsg); - throw new Error(friendly.title + ': ' + friendly.reason); + const err = new Error(friendly.title + ': ' + friendly.reason); + (err as any).rawGenvmError = rawMsg; + throw err; } } @@ -187,7 +189,9 @@ export function useContractQueries() { text: friendly.reason, }); console.error('Error Deploying the contract', error); - throw new Error(friendly.title + ': ' + friendly.reason); + const err = new Error(friendly.title + ': ' + friendly.reason); + (err as any).rawGenvmError = rawMsg; + throw err; } } @@ -299,7 +303,9 @@ export function useContractQueries() { const rawMsg = error?.details || error?.message || String(error); const friendly = parseGenvmError(rawMsg); console.error(error); - throw new Error(friendly.title + ': ' + friendly.reason); + const err = new Error(friendly.title + ': ' + friendly.reason); + (err as any).rawGenvmError = rawMsg; + throw err; } }