Skip to content
Merged
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
254 changes: 254 additions & 0 deletions scripts/error-codes.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
/**
* 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<string>} - 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 {
// Try to stringify the object for better display
try {
message = `Error: ${JSON.stringify(error)}`;
} catch (e) {
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;
}
8 changes: 2 additions & 6 deletions scripts/invoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,11 +31,6 @@ if (!secret || !contractId || !method) {
}

export function parseArg(raw) {
console.error("Usage: node invoke.mjs <secret> <contractId> <method> [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(":");
Expand Down Expand Up @@ -65,6 +61,6 @@ async function main() {
}

main().catch((err) => {
console.error(err);
console.error(formatError(err));
process.exit(1);
});