Skip to content

fix(contract): correctly handle Soroban void (scvVoid) return values in contract.js#264

Open
jabir-dev788 wants to merge 1 commit into
Stellar-Ecosystem:mainfrom
jabir-dev788:FIX-lodestar
Open

fix(contract): correctly handle Soroban void (scvVoid) return values in contract.js#264
jabir-dev788 wants to merge 1 commit into
Stellar-Ecosystem:mainfrom
jabir-dev788:FIX-lodestar

Conversation

@jabir-dev788

@jabir-dev788 jabir-dev788 commented Jun 30, 2026

Copy link
Copy Markdown

Problem

simulateRead and simulateAndSubmit in backend/src/lib/contract.js
treat ScVal return values with a truthy check (if (!retval) return null),
which fails to catch ScVal.scvVoid() — the encoding for Rust's unit type
(). Since scvVoid() is a truthy object, the guard never triggers,
scValToNative() returns undefined, and any caller wrapping the result
in Number(...) (e.g. ID-returning routes) silently produces NaN.
NaN serializes to null in JSON, so callers see a successful-looking
null response with no error or log trail.

Fix

  • Added parseRetval(retval, expectedType) to explicitly distinguish
    null/undefined, void, and typed return values, throwing
    ReturnValueParseError on type mismatch instead of silently coercing.
  • Added CONTRACT_RETURN_TYPES map documenting the expected return type
    per contract function (also serves as runtime validation).
  • Updated simulateRead / simulateAndSubmit and all call sites to use
    the new helper with explicit expected types.
  • Added unit tests for parseRetval (void, u64, bool, null, undefined,
    type mismatch) and updated affected route/service tests.

Scope

Limited to backend/src/lib/contract.js and its direct callers/tests.
No contract (Rust) changes, no frontend changes, no unrelated refactors.

Testing

  • New unit tests for parseRetval pass
  • Updated route tests pass
  • Full existing test suite passes

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of contract responses so missing values now resolve to sensible defaults instead of producing invalid results.
    • Fixed list, count, boolean, and numeric reads to return consistent fallback values when no data is available.
    • Prevented “not found” cases from being misread as valid responses.
  • Tests

    • Added coverage for response parsing, including empty values, boolean and numeric decoding, and type mismatch errors.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds CONTRACT_RETURN_TYPES and a parseRetval(retval, expectedType) helper to contract.js that validates Soroban XDR return values against expected types, treats scvVoid as null, and throws ReturnValueParseError on mismatches. Updates simulateRead and all read-style contract wrappers to use typed decoding with null-aware defaults. Adds a parseRetval test suite and a Vitest node environment config.

Changes

Typed Soroban Return Value Decoding

Layer / File(s) Summary
CONTRACT_RETURN_TYPES and parseRetval implementation
backend/src/lib/contract.js
Adds CONTRACT_RETURN_TYPES map for all contract functions, implements parseRetval with scvVoidnull handling and ReturnValueParseError on type mismatches, and updates simulateRead signature and return to use it.
Service read wrappers
backend/src/lib/contract.js
Updates listServices, getService, getServiceCount, and deactivateServiceOnChain to pass CONTRACT_RETURN_TYPES and use native == null guards with typed defaults ([], null, 0).
Agent read wrappers
backend/src/lib/contract.js
Updates listAgents, listAgentsPage, getAgent, isAgentRegistered, getAgentPolicy, getAgentScore, getAgentCount, isAgentEligible, and checkSpendingAllowed to typed decoding with null-safe defaults.
Tests and Vitest config
backend/src/lib/contract.test.js, backend/vitest.config.js
Adds parseRetval test suite covering null/undefined, scvVoid, correct native decoding for u64/bool, and ReturnValueParseError throwing; adds Vitest node environment config.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Suggested reviewers

  • ritik4ever

🐇 A rabbit hops through contracts and lines,
Decoding the void with the clearest of signs,
scvVoid returns null — no more NaN surprise,
Each type gets its check, each function its guise.
The stars aligned in typed Soroban skies! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: handling Soroban scvVoid return values correctly in contract.js.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/src/lib/contract.js (1)

581-599: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve ReturnValueParseError instead of defaulting.

These wrappers now call parseRetval, but their catch blocks still convert parse/type failures into null, 0, -1, false, or SERVICE_READ_FAILED. That hides the ABI mismatch the new helper is intended to surface.

Proposed pattern
   } catch (err) {
     logger.error({ err, id }, 'getService failed');
+    if (err instanceof ReturnValueParseError) throw err;
     return null;
   }

Apply the same pattern to the other fallback wrappers; in deactivateServiceOnChain, rethrow ReturnValueParseError before wrapping read failures.

Also applies to: 606-611, 759-768, 1020-1025, 1059-1064, 1075-1080, 1088-1093, 1151-1156, 1168-1173

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/lib/contract.js` around lines 581 - 599, The fallback wrappers
are swallowing parse/type failures from parseRetval by returning defaults like
null, 0, -1, false, or SERVICE_READ_FAILED. Update getService and the other
referenced wrappers to detect ReturnValueParseError in their catch blocks and
rethrow it before applying any fallback, while still logging and handling
genuine read failures; apply the same pattern in deactivateServiceOnChain and
the other affected helper methods.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/src/lib/contract.js`:
- Around line 39-45: The list-return contract entries are still validated as
generic structs, which lets object-shaped responses pass and then get converted
into empty arrays by the wrappers. Update the validation in contract.js for the
list methods, especially list_services_page, list_agents, and list_agents_page,
so they are treated as array returns instead of structs. Adjust the
corresponding wrapper/validator logic used by those methods so malformed
non-array responses fail validation rather than being silently swallowed.

In `@backend/src/lib/contract.test.js`:
- Around line 929-931: The parseRetval() coverage in contract.test.js only
checks the happy path for scvVoid() with expectedType set to void; add a
negative test around parseRetval() that passes a non-void ScVal while
expectedType is 'void' and assert it throws instead of returning null. Use the
existing contractLib.parseRetval and sdkPkg.xdr.ScVal helpers so the test
targets the same coercion path, and keep it focused on the void-return mismatch
case.

---

Outside diff comments:
In `@backend/src/lib/contract.js`:
- Around line 581-599: The fallback wrappers are swallowing parse/type failures
from parseRetval by returning defaults like null, 0, -1, false, or
SERVICE_READ_FAILED. Update getService and the other referenced wrappers to
detect ReturnValueParseError in their catch blocks and rethrow it before
applying any fallback, while still logging and handling genuine read failures;
apply the same pattern in deactivateServiceOnChain and the other affected helper
methods.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1242b3d0-56a4-4e3f-a81b-3ba7df511fd9

📥 Commits

Reviewing files that changed from the base of the PR and between a3fd824 and 3c03d36.

📒 Files selected for processing (3)
  • backend/src/lib/contract.js
  • backend/src/lib/contract.test.js
  • backend/vitest.config.js

Comment on lines +39 to +45
list_services_page: 'struct',
get_service: 'struct',
get_service_count: 'u64',
// Agents contract (read)
list_agents: 'struct',
list_agents_page: 'struct',
get_agent: 'struct',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and locate the referenced symbols/lines.
git ls-files backend/src/lib/contract.js backend/src | sed -n '1,120p'
printf '\n--- outline contract.js ---\n'
ast-grep outline backend/src/lib/contract.js --view expanded || true
printf '\n--- targeted search ---\n'
rg -n "list_services_page|list_agents_page|list_agents|get_service|get_agent|expectedType|ReturnValueParseError|Array.isArray|struct" backend/src/lib/contract.js backend/src -g '*.js' -g '*.ts' || true

Repository: Stellar-Ecosystem/lodestar

Length of output: 17597


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read only the relevant slices of backend/src/lib/contract.js around the reported lines
# and the parser/validator logic around expectedType handling.
sed -n '1,140p' backend/src/lib/contract.js
printf '\n--- later section ---\n'
sed -n '520,620p' backend/src/lib/contract.js
printf '\n--- even later section ---\n'
sed -n '960,1025p' backend/src/lib/contract.js

Repository: Stellar-Ecosystem/lodestar

Length of output: 10139


Treat list returns as arrays, not generic structs. list_services_page, list_agents, and list_agents_page still use struct validation, so any object-shaped return can slip through and then get collapsed to [] by the wrappers. Add array-specific validation for these list methods so malformed responses fail instead of being silently swallowed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/lib/contract.js` around lines 39 - 45, The list-return contract
entries are still validated as generic structs, which lets object-shaped
responses pass and then get converted into empty arrays by the wrappers. Update
the validation in contract.js for the list methods, especially
list_services_page, list_agents, and list_agents_page, so they are treated as
array returns instead of structs. Adjust the corresponding wrapper/validator
logic used by those methods so malformed non-array responses fail validation
rather than being silently swallowed.

Comment on lines +929 to +931
it('returns null for scvVoid() when expectedType is void', () => {
const scvVoid = sdkPkg.xdr.ScVal.scvVoid();
expect(contractLib.parseRetval(scvVoid, 'void')).toBeNull();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add a negative test for unexpected non-void void returns.

This suite still misses the case that exposes the current silent-coercion bug: parseRetval() returns null whenever expectedType === 'void', even if the contract actually returns a value. That contradicts the PR goal of throwing on type mismatches, so a non-void retval for mutating methods would be silently accepted.

Suggested test
   it('returns null for scvVoid() when expectedType is void', () => {
     const scvVoid = sdkPkg.xdr.ScVal.scvVoid();
     expect(contractLib.parseRetval(scvVoid, 'void')).toBeNull();
   });
+
+  it('throws ReturnValueParseError when a non-void retval is returned for expectedType void', () => {
+    const scvU64 = sdkPkg.nativeToScVal(5n, { type: 'u64' });
+    expect(() => contractLib.parseRetval(scvU64, 'void')).toThrow(
+      expect.objectContaining({ name: 'ReturnValueParseError', code: 'RETURN_VALUE_PARSE_FAILED' })
+    );
+  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('returns null for scvVoid() when expectedType is void', () => {
const scvVoid = sdkPkg.xdr.ScVal.scvVoid();
expect(contractLib.parseRetval(scvVoid, 'void')).toBeNull();
it('returns null for scvVoid() when expectedType is void', () => {
const scvVoid = sdkPkg.xdr.ScVal.scvVoid();
expect(contractLib.parseRetval(scvVoid, 'void')).toBeNull();
});
it('throws ReturnValueParseError when a non-void retval is returned for expectedType void', () => {
const scvU64 = sdkPkg.nativeToScVal(5n, { type: 'u64' });
expect(() => contractLib.parseRetval(scvU64, 'void')).toThrow(
expect.objectContaining({ name: 'ReturnValueParseError', code: 'RETURN_VALUE_PARSE_FAILED' })
);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/lib/contract.test.js` around lines 929 - 931, The parseRetval()
coverage in contract.test.js only checks the happy path for scvVoid() with
expectedType set to void; add a negative test around parseRetval() that passes a
non-void ScVal while expectedType is 'void' and assert it throws instead of
returning null. Use the existing contractLib.parseRetval and sdkPkg.xdr.ScVal
helpers so the test targets the same coercion path, and keep it focused on the
void-return mismatch case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant