Overview
src/lib/soroban.ts maintains a table intended to translate on-chain contract error codes into user-legible messages:
const CONTRACT_ERROR_MESSAGES: Record<string, string> = {
'1': 'Assets are still locked',
};
export function getContractErrorMessage(errorCode?: string): string | undefined {
const normalized = normalizeContractErrorCode(errorCode);
return normalized ? CONTRACT_ERROR_MESSAGES[normalized] : undefined;
}
This table has exactly one entry. Every call site that surfaces a failed on-chain transaction to the user falls back to a generic message the moment the error code isn't '1':
// SorobanService.lockAssets / unlockAssets, e.g. soroban.ts:986-991
error:
confirmation.status === 'TIMEOUT'
? 'Transaction confirmation is taking longer than expected.'
: getContractErrorMessage(confirmation.errorCode) ??
`Transaction ${submissionResult.hash} failed on-chain`,
Meanwhile the surrounding infrastructure for extracting a contract error code is substantial and clearly built to support more than one mapped case: findContractErrorCode, extractContractErrorCodeFromXdr, extractContractErrorCode, and normalizeContractErrorCode together implement a recursive, defensive walk through Soroban's XDR result structures — decoding hex, matching contract[_-]?code/error[_-]?code patterns, walking nested union arms — specifically so that confirmation.errorCode is a reliable, normalized string regardless of exactly how the RPC/SDK happens to represent it. All of that machinery currently exists to feed a lookup table with a single entry. Every other realistic on-chain failure a farming-pool/staking contract would define — insufficient balance, pool inactive or closed, amount below the pool's minimum, unauthorized caller, boost percentage out of range, a position that doesn't exist — falls through to Transaction {hash} failed on-chain, a message that gives a user precisely zero information about why their deposit, unlock, or boost-configuration attempt failed, or what they could do differently.
Requirements
- Expand
CONTRACT_ERROR_MESSAGES to cover the actual set of error codes the deployed pool/factory contracts define, once that set can be enumerated (from the contract's own error enum/spec, or empirically from observed errorCode values in testnet transactions).
- Where the specific set of contract error codes isn't yet knowable from within this repository, at minimum make the gap visible and trackable rather than silent: log the unmapped
errorCode distinctly (e.g. console.warn('[SmartDrop] Unmapped contract error code:', errorCode)) whenever getContractErrorMessage returns undefined, so unmapped codes are discoverable from real usage instead of requiring someone to notice the generic message and go spelunking.
- Consider whether the generic fallback message could at least include the raw
errorCode for support/debugging purposes (e.g. Transaction {hash} failed on-chain (code {errorCode})) rather than omitting it entirely when unmapped.
Acceptance Criteria
Additional Notes
More precise references
src/lib/soroban.ts:328-330 (CONTRACT_ERROR_MESSAGES) — confirmed the single '1': 'Assets are still locked' entry.
src/lib/soroban.ts:466-469 (getContractErrorMessage) — confirmed the lookup and undefined fallback.
src/lib/soroban.ts:368-464 (findContractErrorCode/extractContractErrorCodeFromXdr/extractContractErrorCode/normalizeContractErrorCode) — confirmed this is genuinely substantial, carefully-written extraction logic (recursive XDR-union walking with cycle protection via a WeakSet, hex/decimal normalization, multiple XDR decoder attempts) — i.e. real engineering investment went into reliably producing an errorCode string, which the single-entry lookup table doesn't currently make good use of.
src/lib/soroban.ts:986-991 (lockAssets's failure path) and :1127-1132 (unlockAssets's failure path) — confirmed both use the identical getContractErrorMessage(...) ?? generic-message pattern, so this gap affects both deposit and withdrawal failures identically.
src/components/UnlockModal/UnlockModal.tsx:185-190 — confirmed the UI-level consumer: getContractErrorMessage(result.errorCode) ?? result.error ?? "Unlock transaction failed.", i.e. the user-visible text in the modal directly reflects this table's coverage.
Additional edge cases
Implementation sketch
const CONTRACT_ERROR_MESSAGES: Record<string, string> = {
'1': 'Assets are still locked',
// additional entries once the contract's real error codes are known, e.g.:
// '2': 'Insufficient balance for this action',
// '3': 'This pool is not currently active',
// ...
};
export function getContractErrorMessage(errorCode?: string): string | undefined {
const normalized = normalizeContractErrorCode(errorCode);
if (!normalized) return undefined;
const message = CONTRACT_ERROR_MESSAGES[normalized];
if (!message) {
console.warn('[SmartDrop] Unmapped contract error code:', normalized);
}
return message;
}
And update the generic fallbacks (lockAssets/unlockAssets) to include the code when present: `Transaction ${hash} failed on-chain${confirmation.errorCode ? ` (code ${confirmation.errorCode})` : ''}`.
Test/reproduction plan
getContractErrorMessage('1') → 'Assets are still locked' (existing behavior, regression-protected).
getContractErrorMessage('99') (an intentionally-unmapped code) → undefined, and assert console.warn fired with that code.
- Once real codes are added: one test per new entry, asserting the exact mapped string.
Cross-references
- No existing issue in the repo's 75-issue history covers the completeness of
CONTRACT_ERROR_MESSAGES.
Overview
src/lib/soroban.tsmaintains a table intended to translate on-chain contract error codes into user-legible messages:This table has exactly one entry. Every call site that surfaces a failed on-chain transaction to the user falls back to a generic message the moment the error code isn't
'1':Meanwhile the surrounding infrastructure for extracting a contract error code is substantial and clearly built to support more than one mapped case:
findContractErrorCode,extractContractErrorCodeFromXdr,extractContractErrorCode, andnormalizeContractErrorCodetogether implement a recursive, defensive walk through Soroban's XDR result structures — decoding hex, matchingcontract[_-]?code/error[_-]?codepatterns, walking nested union arms — specifically so thatconfirmation.errorCodeis a reliable, normalized string regardless of exactly how the RPC/SDK happens to represent it. All of that machinery currently exists to feed a lookup table with a single entry. Every other realistic on-chain failure a farming-pool/staking contract would define — insufficient balance, pool inactive or closed, amount below the pool's minimum, unauthorized caller, boost percentage out of range, a position that doesn't exist — falls through toTransaction {hash} failed on-chain, a message that gives a user precisely zero information about why their deposit, unlock, or boost-configuration attempt failed, or what they could do differently.Requirements
CONTRACT_ERROR_MESSAGESto cover the actual set of error codes the deployed pool/factory contracts define, once that set can be enumerated (from the contract's own error enum/spec, or empirically from observederrorCodevalues in testnet transactions).errorCodedistinctly (e.g.console.warn('[SmartDrop] Unmapped contract error code:', errorCode)) whenevergetContractErrorMessagereturnsundefined, so unmapped codes are discoverable from real usage instead of requiring someone to notice the generic message and go spelunking.errorCodefor support/debugging purposes (e.g.Transaction {hash} failed on-chain (code {errorCode})) rather than omitting it entirely when unmapped.Acceptance Criteria
CONTRACT_ERROR_MESSAGEScontains more than one entry, covering the pool/factory contracts' actual documented or empirically-observed error codes.getContractErrorMessageand receivesundefinedlogs the unmapped code distinctly, so gaps are discoverable going forward without requiring a user complaint first.CONTRACT_ERROR_MESSAGEStable and asserts each entry produces the expected user-facing string viagetContractErrorMessage.Additional Notes
More precise references
src/lib/soroban.ts:328-330(CONTRACT_ERROR_MESSAGES) — confirmed the single'1': 'Assets are still locked'entry.src/lib/soroban.ts:466-469(getContractErrorMessage) — confirmed the lookup andundefinedfallback.src/lib/soroban.ts:368-464(findContractErrorCode/extractContractErrorCodeFromXdr/extractContractErrorCode/normalizeContractErrorCode) — confirmed this is genuinely substantial, carefully-written extraction logic (recursive XDR-union walking with cycle protection via aWeakSet, hex/decimal normalization, multiple XDR decoder attempts) — i.e. real engineering investment went into reliably producing anerrorCodestring, which the single-entry lookup table doesn't currently make good use of.src/lib/soroban.ts:986-991(lockAssets's failure path) and:1127-1132(unlockAssets's failure path) — confirmed both use the identicalgetContractErrorMessage(...) ?? generic-messagepattern, so this gap affects both deposit and withdrawal failures identically.src/components/UnlockModal/UnlockModal.tsx:185-190— confirmed the UI-level consumer:getContractErrorMessage(result.errorCode) ?? result.error ?? "Unlock transaction failed.", i.e. the user-visible text in the modal directly reflects this table's coverage.Additional edge cases
setBoost's failure path (soroban.ts:1193-1198) does not callgetContractErrorMessageat all — it only ever returns the rawSimulation failed: ${simulation.error}string, meaning boost-related contract errors (once boost UI ships, per Boost allocation is fully wired in the data layer but has no UI — the Boost button is permanently disabled dead code #81/Boost allocation is fully wired in the data layer but has no UI — the Boost button is permanently disabled dead code #111) wouldn't benefit from this table even after it's expanded, unlesssetBoostis also updated to extract and map its error code the same waylockAssets/unlockAssetsdo — worth including in this fix's scope for consistency.Implementation sketch
And update the generic fallbacks (
lockAssets/unlockAssets) to include the code when present:`Transaction ${hash} failed on-chain${confirmation.errorCode ? ` (code ${confirmation.errorCode})` : ''}`.Test/reproduction plan
getContractErrorMessage('1')→'Assets are still locked'(existing behavior, regression-protected).getContractErrorMessage('99')(an intentionally-unmapped code) →undefined, and assertconsole.warnfired with that code.Cross-references
CONTRACT_ERROR_MESSAGES.