diff --git a/frontend/src/components/Simulator/ConstructorParameters.vue b/frontend/src/components/Simulator/ConstructorParameters.vue index 98e5f8f4b..7933441c9 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,18 @@ 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(() => { + const err = schemaError.value as any; + if (!err) return ''; + return err.rawGenvmError || err.message || String(err); +}); + const emit = defineEmits(['deployed-contract']); const handleDeployContract = async () => { @@ -45,6 +52,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..1416e40ce 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,11 @@ 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); + const err = new Error(friendly.title + ': ' + friendly.reason); + (err as any).rawGenvmError = rawMsg; + throw err; } } @@ -174,14 +179,19 @@ 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'); + const err = new Error(friendly.title + ': ' + friendly.reason); + (err as any).rawGenvmError = rawMsg; + throw err; } } @@ -289,9 +299,13 @@ 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'); + const err = new Error(friendly.title + ': ' + friendly.reason); + (err as any).rawGenvmError = rawMsg; + throw err; } } 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); + }); + }); +});