Summary
The Sui, Ethereum, Solana, and Soroban JSON-RPC adapters never check HTTP status before parsing a response. A rate-limit/gateway error whose body doesn't happen to be JSON-RPC-shaped (no error key) is silently converted into a "successful" null result, printed with exit code 0.
Background
call_rpc/call_rpc_internal in each of the four adapters call response.json().await? unconditionally and only check whether the parsed body has a top-level "error" key. reqwest::Response::json() succeeds regardless of the HTTP status code. A rate-limiting proxy or gateway in front of a public RPC endpoint commonly returns 429/5xx with a body that is not JSON-RPC-shaped (e.g. {"message":"rate limited"}). Because neither "error" nor "result" is present in that shape, the code falls through to Value::Null and reports success.
All five supported chains default to unauthenticated public nodes (api.mainnet-beta.solana.com, eth.llamarpc.com, fullnode.mainnet.sui.io, soroban-rpc.mainnet.stellar.org) — rate limiting under this pattern is routine, not an edge case.
Evidence
src/chains/sui.rs:60-93 (call_rpc_internal) — status never read; result defaults to Value::Null at line 92.
src/chains/ethereum.rs:55-81 (call_rpc) — same pattern, result at line 80.
src/chains/solana.rs:48-74 (call_rpc) — same pattern, result at line 73.
src/chains/soroban.rs:62-88 (call_rpc) — same pattern, result at line 87.
- Downstream,
src/cli/handlers.rs (Balance/Gas/Block/Tx/Call handlers, ~lines 444-691) take Ok(Value::Null) at face value — for Sui, result.as_array() on Null is None, which falls through to printing the literal null with exit code 0.
This is a distinct failure mechanism from open issue #2 (Aptos/Soroban REST calls ignoring HTTP status and displaying error bodies as data): #2 covers the two REST-based adapters and their message/error_code shape specifically. This issue covers the four JSON-RPC-over-HTTP adapters, whose failure mode is worse — a real error becomes an indistinguishable null "success" rather than visibly wrong data.
Proposed Solution
In each call_rpc/call_rpc_internal, capture response.status() before parsing. Treat any non-2xx response that doesn't parse into a recognizable JSON-RPC error object as a hard anyhow! error including the HTTP status code and a truncated raw body. Keep the existing 200-with-JSON-RPC-error-field handling exactly as is (spec-compliant case).
Technical Scope
src/chains/sui.rs — call_rpc_internal
src/chains/ethereum.rs — call_rpc
src/chains/solana.rs — call_rpc
src/chains/soroban.rs — call_rpc
- Tests for each adapter
Acceptance Criteria
Edge Cases
- 200 response with a JSON-RPC
error object (existing, correct behavior — must not regress)
- 200 response with a legitimate
null result field
- Non-2xx response with an empty body
- Non-2xx response with a JSON-RPC-shaped error object (some gateways forward these with a non-2xx status)
Risks
- Low regression risk — the change only tightens what currently falls through to
Null; existing passing tests for the 200-with-error case should continue to pass unmodified
Deliverables
- Status-aware error handling in all four adapters
- Mock-server tests per chain (extending the existing pattern in
src/chains/sui.rs, e.g. unresolvable_name_is_left_as_literal) simulating a 429 with a non-JSON-RPC body and asserting call_rpc returns Err
Priority
High — affects the primary command surface (balance, tx, gas, block, raw call) on four of five supported chains, silently breaks scripting/automation that checks exit codes, and is trivial to trigger against the default public endpoints.
GrantFox Evaluation
Impact Score: 78/100
Difficulty Score: 35/100
Priority Score: 64
Confidence: 82%
Category: Reliability / Bug
Estimated Reward Tier: B
AI Rationale: A well-evidenced correctness bug in the shared RPC layer, reachable via routine rate-limiting on default public endpoints, that silently returns a fake successful result rather than an error. Fix is well-scoped to four adapter files with a clear, testable acceptance boundary.
Estimated Completion: 96 hours
Telegram: https://t.me/txioCommunity
Summary
The Sui, Ethereum, Solana, and Soroban JSON-RPC adapters never check HTTP status before parsing a response. A rate-limit/gateway error whose body doesn't happen to be JSON-RPC-shaped (no
errorkey) is silently converted into a "successful"nullresult, printed with exit code 0.Background
call_rpc/call_rpc_internalin each of the four adapters callresponse.json().await?unconditionally and only check whether the parsed body has a top-level"error"key.reqwest::Response::json()succeeds regardless of the HTTP status code. A rate-limiting proxy or gateway in front of a public RPC endpoint commonly returns 429/5xx with a body that is not JSON-RPC-shaped (e.g.{"message":"rate limited"}). Because neither"error"nor"result"is present in that shape, the code falls through toValue::Nulland reports success.All five supported chains default to unauthenticated public nodes (
api.mainnet-beta.solana.com,eth.llamarpc.com,fullnode.mainnet.sui.io,soroban-rpc.mainnet.stellar.org) — rate limiting under this pattern is routine, not an edge case.Evidence
src/chains/sui.rs:60-93(call_rpc_internal) — status never read; result defaults toValue::Nullat line 92.src/chains/ethereum.rs:55-81(call_rpc) — same pattern, result at line 80.src/chains/solana.rs:48-74(call_rpc) — same pattern, result at line 73.src/chains/soroban.rs:62-88(call_rpc) — same pattern, result at line 87.src/cli/handlers.rs(Balance/Gas/Block/Tx/Callhandlers, ~lines 444-691) takeOk(Value::Null)at face value — for Sui,result.as_array()onNullisNone, which falls through to printing the literalnullwith exit code 0.This is a distinct failure mechanism from open issue #2 (Aptos/Soroban REST calls ignoring HTTP status and displaying error bodies as data): #2 covers the two REST-based adapters and their
message/error_codeshape specifically. This issue covers the four JSON-RPC-over-HTTP adapters, whose failure mode is worse — a real error becomes an indistinguishablenull"success" rather than visibly wrong data.Proposed Solution
In each
call_rpc/call_rpc_internal, captureresponse.status()before parsing. Treat any non-2xx response that doesn't parse into a recognizable JSON-RPC error object as a hardanyhow!error including the HTTP status code and a truncated raw body. Keep the existing 200-with-JSON-RPC-error-field handling exactly as is (spec-compliant case).Technical Scope
src/chains/sui.rs—call_rpc_internalsrc/chains/ethereum.rs—call_rpcsrc/chains/solana.rs—call_rpcsrc/chains/soroban.rs—call_rpcAcceptance Criteria
Ok(Value::Null)in any of the four adapterserrorbehavior is unchanged"result": nullon a 200 response (e.g.eth_getTransactionByHashfor an unknown hash) still returnsOk(Value::Null)Edge Cases
errorobject (existing, correct behavior — must not regress)nullresult fieldRisks
Null; existing passing tests for the 200-with-error case should continue to pass unmodifiedDeliverables
src/chains/sui.rs, e.g.unresolvable_name_is_left_as_literal) simulating a 429 with a non-JSON-RPC body and assertingcall_rpcreturnsErrPriority
High — affects the primary command surface (balance, tx, gas, block, raw call) on four of five supported chains, silently breaks scripting/automation that checks exit codes, and is trivial to trigger against the default public endpoints.
GrantFox Evaluation
Impact Score: 78/100
Difficulty Score: 35/100
Priority Score: 64
Confidence: 82%
Category: Reliability / Bug
Estimated Reward Tier: B
AI Rationale: A well-evidenced correctness bug in the shared RPC layer, reachable via routine rate-limiting on default public endpoints, that silently returns a fake successful result rather than an error. Fix is well-scoped to four adapter files with a clear, testable acceptance boundary.
Estimated Completion: 96 hours
Telegram: https://t.me/txioCommunity