Skip to content

CONTRACT_ERROR_MESSAGES maps exactly one on-chain error code — every other lock/unlock/boost failure surfaces a generic failed on-chain message with no actionable detail #146

Description

@prodbycorne

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

  • CONTRACT_ERROR_MESSAGES contains more than one entry, covering the pool/factory contracts' actual documented or empirically-observed error codes.
  • Every code path that calls getContractErrorMessage and receives undefined logs the unmapped code distinctly, so gaps are discoverable going forward without requiring a user complaint first.
  • The generic fallback message includes the raw error code (when one was extracted) so a user or support agent has something concrete to reference, even for a still-unmapped code.
  • A test enumerates the (updated) CONTRACT_ERROR_MESSAGES table and asserts each entry produces the expected user-facing string via getContractErrorMessage.

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.

Metadata

Metadata

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial CampaignCampaign: Official CampaignOfficial Campaign | FWC26Campaign: Official Campaign | FWC26Third CampaignCampaign: Third CampaignsorobanSoroban smart-contract integration (XDR, RPC, transaction building)uxUser experience, interaction design, loading statesvery hardExtremely hard — deep expertise, careful design, and significant time required

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions