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
106 changes: 105 additions & 1 deletion examples/core-integration/fetch-taxonomy/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,105 @@
// Dummy implementation for Fetch Taxonomy
#!/usr/bin/env node

const fs = require('fs').promises;
const path = require('path');
const toml = require('@iarna/toml');

function sanitizeText(value) {
if (typeof value !== 'string') {
return value;
}

return value
.replace(/\r\n?/g, '\n')
.replace(/[ \t]+$/gm, '')
.replace(/[ \t]+(?=\n)/g, '')
.replace(/\n{3,}/g, '\n\n')
.trim();
}

function normalizeErrorCode(value) {
if (typeof value === 'number') {
return Number.isSafeInteger(value) ? value : String(value);
}

if (typeof value === 'string') {
const trimmed = value.trim();
if (/^-?\d+$/.test(trimmed)) {
const parsed = BigInt(trimmed);
return parsed > BigInt(Number.MAX_SAFE_INTEGER) || parsed < BigInt(-Number.MAX_SAFE_INTEGER)
? trimmed
: Number(parsed);
}

return trimmed;
}

if (typeof value === 'bigint') {
return value.toString();
}

return value;
}

function sanitizeAndNormalize(value, key = '') {
if (Array.isArray(value)) {
return value.map((item) => sanitizeAndNormalize(item, key));
}

if (value && typeof value === 'object') {
return Object.entries(value).reduce((acc, [entryKey, entryValue]) => {
const nextKey = key ? `${key}.${entryKey}` : entryKey;
let normalizedValue = entryValue;

if (entryKey === 'code' || entryKey === 'error_code') {
normalizedValue = normalizeErrorCode(entryValue);
} else if (typeof entryValue === 'string' && /description|summary|explanation|name|severity|category|difficulty|likelihood|source|id/i.test(entryKey)) {
normalizedValue = sanitizeText(entryValue);
} else if (entryValue && typeof entryValue === 'object') {
normalizedValue = sanitizeAndNormalize(entryValue, nextKey);
}

acc[entryKey] = normalizedValue;
return acc;
}, {});
}

if (typeof value === 'string' && /description|summary|explanation|name|severity|category|difficulty|likelihood|source|id/i.test(key)) {
return sanitizeText(value);
}

return value;
}

async function buildTaxonomyJson({ inputPath, outputPath }) {
const input = await fs.readFile(inputPath, 'utf8');
const parsed = toml.parse(input);
const sanitized = sanitizeAndNormalize(parsed);
const serialized = JSON.stringify(sanitized, null, 2) + '\n';
await fs.writeFile(outputPath, serialized, 'utf8');
return serialized;
}

async function main() {
const workspaceRoot = path.resolve(__dirname, '..', '..', '..');
const inputPath = path.join(workspaceRoot, 'crates', 'core', 'src', 'taxonomy', 'data', 'contract.toml');
const outputPath = path.join(__dirname, 'taxonomy.json');

const serialized = await buildTaxonomyJson({ inputPath, outputPath });
console.log(`Wrote sanitized taxonomy JSON to ${path.relative(workspaceRoot, outputPath)}`);
return serialized;
}

if (require.main === module) {
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
}

module.exports = {
sanitizeText,
normalizeErrorCode,
sanitizeAndNormalize,
buildTaxonomyJson,
};
14 changes: 14 additions & 0 deletions examples/core-integration/fetch-taxonomy/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const { sanitizeText, normalizeErrorCode } = require('./index');

test('sanitizeText trims and normalizes pesky whitespace', () => {
const value = ' Contract error\r\nwith trailing spaces \r\n';
assert.equal(sanitizeText(value), 'Contract error\nwith trailing spaces');
});

test('normalizeErrorCode preserves unsafe integers without losing precision', () => {
assert.equal(normalizeErrorCode(42), 42);
assert.equal(normalizeErrorCode('9007199254740993'), '9007199254740993');
assert.equal(normalizeErrorCode(BigInt('9007199254740993')), '9007199254740993');
});
149 changes: 149 additions & 0 deletions examples/core-integration/fetch-taxonomy/taxonomy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
{
"category": {
"name": "Contract",
"description": "Errors raised by contract code itself (not the host). These use numeric codes defined in the contract's error enum.",
"source_module": "soroban-env-host/src/host.rs"
},
"errors": [
{
"id": "host.contract.error_general",
"category": "contract",
"code": 0,
"name": "ContractError",
"severity": "Error",
"since_protocol": 20,
"summary": "Contract error: the contract's own logic rejected this call — run with --resolve to map the code to its name.",
"detailed_explanation": "Unlike host errors, contract errors are defined by the contract author. Each contract can define an error enum with numeric codes and descriptive names. When a contract panics or returns an error value, the host wraps it as a ContractError with the numeric code.\n\nTo understand a contract error, you need the contract's specification (contractspecv0 metadata) which maps the numeric code to a human-readable name and optional documentation.",
"common_causes": [
{
"description": "Business logic assertion failure in the contract (e.g., insufficient balance, invalid state)",
"likelihood": "high"
},
{
"description": "Input validation failure — the contract rejected the provided arguments",
"likelihood": "high"
},
{
"description": "Access control check failed — the caller is not authorized for this operation",
"likelihood": "medium"
}
],
"suggested_fixes": [
{
"description": "Use grat's contract error resolver to map the numeric code to the contract's error enum name",
"difficulty": "easy",
"requires_upgrade": false
},
{
"description": "Review the contract source code for the error enum definition to understand the specific failure",
"difficulty": "medium",
"requires_upgrade": false,
"related_errors": [
"host.auth.not_authorized"
],
"source_file": "soroban-env-host/src/host.rs"
}
]
},
{
"id": "host.contract.internal_error",
"category": "contract",
"code": 1,
"name": "InternalError",
"severity": "Error",
"since_protocol": 20,
"summary": "An internal protocol implementation error occurred (e.g. invalid ledger state).",
"detailed_explanation": "The contract encountered an internal error during execution, indicating a protocol implementation issue or invalid ledger state.",
"common_causes": [
{
"description": "Internal validation or state consistency checks failed in the contract",
"likelihood": "high"
}
],
"suggested_fixes": [
{
"description": "Check the diagnostic logs to see if there are internal contract assertion failures",
"difficulty": "medium",
"requires_upgrade": false,
"related_errors": [],
"source_file": "soroban-env-host/src/host.rs"
}
]
},
{
"id": "host.contract.operation_not_supported",
"category": "contract",
"code": 2,
"name": "OperationNotSupportedError",
"severity": "Error",
"since_protocol": 20,
"summary": "The operation is not supported (e.g. calling clawback on an asset without clawback enabled).",
"detailed_explanation": "The contract attempted an operation that is unsupported by the contract configuration or type (e.g., performing a clawback on an asset when clawback is disabled).",
"common_causes": [
{
"description": "Invoking clawback on a non-clawbackable asset",
"likelihood": "high"
}
],
"suggested_fixes": [
{
"description": "Check the asset flags and ensure the operation is allowed for the target asset",
"difficulty": "easy",
"requires_upgrade": false,
"related_errors": [],
"source_file": "soroban-env-host/src/host.rs"
}
]
},
{
"id": "host.contract.already_initialized",
"category": "contract",
"code": 3,
"name": "AlreadyInitializedError",
"severity": "Error",
"since_protocol": 20,
"summary": "The contract instance has already been initialized and cannot be re-initialized.",
"detailed_explanation": "The contract instance was initialized twice. Soroban contracts typically only allow initialization once.",
"common_causes": [
{
"description": "Calling the initialize function on an already initialized contract instance",
"likelihood": "high"
}
],
"suggested_fixes": [
{
"description": "Ensure that initialization is only called once per contract deployment",
"difficulty": "easy",
"requires_upgrade": false,
"related_errors": [],
"source_file": "soroban-env-host/src/host.rs"
}
]
},
{
"id": "host.contract.account_missing",
"category": "contract",
"code": 6,
"name": "AccountMissingError",
"severity": "Error",
"since_protocol": 20,
"summary": "An account involved in the transaction does not exist on the network.",
"detailed_explanation": "The operation required a specific account to exist on the network, but the account could not be found.",
"common_causes": [
{
"description": "Providing an invalid account address or an account that has not been created/funded",
"likelihood": "high"
}
],
"suggested_fixes": [
{
"description": "Verify that the addresses provided exist and are active on the target network",
"difficulty": "easy",
"requires_upgrade": false,
"related_errors": [],
"source_file": "soroban-env-host/src/host.rs"
}
]
}
]
}
Loading