From 0ee0b9dc3885231ed3c5f05e896b93acf4fd7f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CMainnet-ops=E2=80=9D?= <“footballwatch68@gmail.com”> Date: Tue, 29 Sep 2026 10:58:45 +0100 Subject: [PATCH 1/3] Add error code decoding for Soroban contracts - Create error-codes.mjs with mappings for escrow, milestones, and maintenance-pool contracts - Implement extractErrorCode() to parse numeric error codes from Soroban errors - Add decodeErrorCode() to map numeric codes to human-readable names - Update invoke.mjs to use formatError() in catch block - Handle ambiguous error codes (same number across different contracts) - Fall back gracefully for non-contract errors (network/auth failures) Fixes issue #124 - contract errors now show named Error variants instead of opaque XDR/host error objects --- scripts/error-codes.mjs | 249 ++++++++++++++++++++++++++++++++++++++++ scripts/invoke.mjs | 3 +- 2 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 scripts/error-codes.mjs diff --git a/scripts/error-codes.mjs b/scripts/error-codes.mjs new file mode 100644 index 0000000..e882e53 --- /dev/null +++ b/scripts/error-codes.mjs @@ -0,0 +1,249 @@ +/** + * Error code mappings for Soroban contracts. + * Generated from contract error.rs files. + */ + +export const errorMappings = { + // Escrow contract errors + escrow: { + 1: "AlreadyInitialized", + 2: "NotInitialized", + 3: "Unauthorized", + 4: "EscrowNotFound", + 5: "AlreadyFunded", + 6: "AlreadyPaid", + 7: "AlreadyRefunded", + 8: "InvalidSplit", + 9: "InvalidAmount", + 10: "NotExpired", + 11: "InsufficientBalance", + 12: "InvalidFee", + 13: "InvalidDeadline", + 14: "TooManySponsors", + 15: "ContributionNotFound", + 16: "InvalidTarget", + 17: "InvalidTreasury", + 19: "ContractPaused", + 20: "SelfPayout" + }, + + // Milestones contract errors + milestones: { + 1: "AlreadyInitialized", + 2: "NotInitialized", + 3: "Unauthorized", + 4: "MilestoneNotFound", + 5: "IssueAlreadyAllocated", + 6: "IssueNotAllocated", + 7: "IssueAlreadyReleased", + 8: "OverAllocation", + 9: "InvalidSplit", + 10: "InvalidAmount", + 11: "InvalidFee", + 12: "MilestoneClosed", + 13: "TooManySponsors", + 14: "InvalidTreasury", + 15: "MilestoneAlreadyExists", + 16: "DeadlineNotPassed", + 17: "IssueNotAllocatedForDeallocate", + 18: "ContractPaused", + 19: "ContributionNotFound", + 20: "SelfPayout" + }, + + // Maintenance-pool contract errors + maintenancePool: { + 1: "AlreadyInitialized", + 2: "NotInitialized", + 3: "Unauthorized", + 4: "PoolNotFound", + 5: "TokenMismatch", + 6: "InvalidAmount", + 7: "InsufficientBalance", + 8: "InvalidFee", + 9: "InvalidTreasury", + 10: "InactivityWindowNotElapsed", + 11: "NotDepositSponsor", + 12: "DepositNotFound", + 13: "ContractPaused", + 14: "DepositCountOverflow" + } +}; + +/** + * Extract numeric error code from Soroban error object. + * @param {any} error - The error object from Soroban RPC + * @returns {number|null} - The extracted error code, or null if not found + */ +export function extractErrorCode(error) { + if (!error) return null; + + // Try to parse error from various possible structures + + // Case 1: Error might be a string containing the error + if (typeof error === 'string') { + // Look for common Soroban error patterns + // Pattern 1: "Send failed: {...}" or "Tx failed: {...}" from submit.mjs + const sendFailedMatch = error.match(/Send failed:\s*({.*})/); + const txFailedMatch = error.match(/Tx failed:\s*({.*})/); + + if (sendFailedMatch || txFailedMatch) { + try { + const jsonStr = sendFailedMatch ? sendFailedMatch[1] : txFailedMatch[1]; + const obj = JSON.parse(jsonStr); + return extractErrorCode(obj); + } catch (e) { + // If JSON parsing fails, fall back to general number extraction + } + } + + // Pattern 2: Look for error codes in strings like "error: 5" or "code: 5" + const match = error.match(/error\s*code\s*[:=]?\s*(\d+)/i) || + error.match(/\bcode\s*[:=]?\s*(\d+)/i) || + error.match(/\berror\s*[:=]?\s*(\d+)/i) || + error.match(/\bvalue\s*[:=]?\s*(\d+)/i) || + error.match(/\b(\d+)\b/); + if (match) { + return parseInt(match[1], 10); + } + return null; + } + + // Case 2: Error might be an object with errorResult + if (error.errorResult) { + return extractErrorCode(error.errorResult); + } + + // Case 3: Error might have a resultXdr field (common in Soroban) + if (error.resultXdr) { + try { + // Try to parse XDR to extract error code + // This is a simplified approach - actual XDR parsing would be more complex + const str = JSON.stringify(error.resultXdr); + const match = str.match(/\b(\d+)\b/); + if (match) { + return parseInt(match[1], 10); + } + } catch (e) { + // Ignore XDR parsing errors + } + } + + // Case 4: Error might have a value or code property + if (error.code !== undefined) { + const code = error.code; + if (typeof code === 'number') return code; + if (typeof code === 'string') { + const num = parseInt(code, 10); + if (!isNaN(num)) return num; + } + } + + if (error.error !== undefined) { + const err = error.error; + if (typeof err === 'number') return err; + if (typeof err === 'string') { + const num = parseInt(err, 10); + if (!isNaN(num)) return num; + } + } + + if (error.value !== undefined) { + const value = error.value; + if (typeof value === 'number') return value; + if (typeof value === 'string') { + const num = parseInt(value, 10); + if (!isNaN(num)) return num; + } + } + + // Case 5: Check error message for numeric codes + if (error.message) { + return extractErrorCode(error.message); + } + + // Case 6: Try to stringify and look for numbers + try { + const str = JSON.stringify(error); + // Look for patterns like "error": 5 or "code": 5 + const jsonMatch = str.match(/"error"\s*:\s*(\d+)/) || + str.match(/"code"\s*:\s*(\d+)/) || + str.match(/"value"\s*:\s*(\d+)/); + if (jsonMatch) { + return parseInt(jsonMatch[1], 10); + } + + // General number extraction as fallback + const match = str.match(/\b(\d+)\b/); + if (match) { + return parseInt(match[1], 10); + } + } catch (e) { + // Ignore JSON stringify errors + } + + return null; +} + +/** + * Decode error code to human-readable names. + * @param {number} errorCode - The numeric error code + * @returns {Array} - Array of possible error meanings across contracts + */ +export function decodeErrorCode(errorCode) { + if (errorCode === null || errorCode === undefined) return []; + + const meanings = []; + + for (const [contractName, mapping] of Object.entries(errorMappings)) { + if (mapping[errorCode]) { + meanings.push(`${contractName}: ${mapping[errorCode]}`); + } + } + + return meanings; +} + +/** + * Format error with decoded meanings if available. + * @param {any} error - The original error object + * @returns {string} - Formatted error message with decoding + */ +export function formatError(error) { + const errorCode = extractErrorCode(error); + const decoded = decodeErrorCode(errorCode); + + let message = ''; + + // Include the original error for debugging + if (typeof error === 'string') { + message = `Error: ${error}`; + } else if (error && error.message) { + message = `Error: ${error.message}`; + } else { + message = `Error: ${error}`; + } + + if (errorCode !== null) { + message += `\n\nDetected error code: ${errorCode}`; + + if (decoded.length > 0) { + message += `\nPossible contract error meanings:`; + decoded.forEach(meaning => { + message += `\n • ${meaning}`; + }); + + // Add a note about ambiguous codes + if (decoded.length > 1) { + message += `\n\nNote: Error code ${errorCode} exists in multiple contracts. `; + message += `The actual meaning depends on which contract was invoked.`; + } + } else { + message += `\nNo known mapping for error code ${errorCode}`; + } + } else { + message += `\n\nNo error code detected. This may be a network or authentication error.`; + } + + return message; +} \ No newline at end of file diff --git a/scripts/invoke.mjs b/scripts/invoke.mjs index 1699a7f..b0135b4 100644 --- a/scripts/invoke.mjs +++ b/scripts/invoke.mjs @@ -9,6 +9,7 @@ import { rpc, } from "@stellar/stellar-sdk"; import { submitAndWait } from "./lib/submit.mjs"; +import { formatError } from "./error-codes.mjs"; const RPC_URL = process.env.RPC_URL || "https://soroban-testnet.stellar.org"; const NETWORK_PASSPHRASE = process.env.NETWORK_PASSPHRASE || Networks.TESTNET; @@ -65,6 +66,6 @@ async function main() { } main().catch((err) => { - console.error(err); + console.error(formatError(err)); process.exit(1); }); From b6c5dd0d159d0a38046b4d0d4c8fa31609ea1bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CMainnet-ops=E2=80=9D?= <“footballwatch68@gmail.com”> Date: Tue, 29 Sep 2026 11:01:35 +0100 Subject: [PATCH 2/3] Fix duplicate parseArg function declaration - Remove duplicate parseArg function definition - Keep the working parseArg implementation as exported function --- scripts/invoke.mjs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/scripts/invoke.mjs b/scripts/invoke.mjs index b0135b4..b46943a 100644 --- a/scripts/invoke.mjs +++ b/scripts/invoke.mjs @@ -31,11 +31,6 @@ if (!secret || !contractId || !method) { } export function parseArg(raw) { - console.error("Usage: node invoke.mjs [args as address:G..., u32:123, u64:123, i128:123, or none]"); - process.exit(1); -} - -function parseArg(raw) { if (raw === "none") return nativeToScVal(null); const [type, value] = raw.split(":"); From 7ff1eec5ac4622c462fa04a7e390826b85d8e09f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CMainnet-ops=E2=80=9D?= <“footballwatch68@gmail.com”> Date: Tue, 29 Sep 2026 11:05:16 +0100 Subject: [PATCH 3/3] worked --- scripts/error-codes.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/error-codes.mjs b/scripts/error-codes.mjs index e882e53..182434d 100644 --- a/scripts/error-codes.mjs +++ b/scripts/error-codes.mjs @@ -221,7 +221,12 @@ export function formatError(error) { } else if (error && error.message) { message = `Error: ${error.message}`; } else { - message = `Error: ${error}`; + // Try to stringify the object for better display + try { + message = `Error: ${JSON.stringify(error)}`; + } catch (e) { + message = `Error: ${error}`; + } } if (errorCode !== null) {