feat: handle minimal tx response for NONE/INCLUDED wait_until#128
feat: handle minimal tx response for NONE/INCLUDED wait_until#128
Conversation
When `wait_until` is set to `NONE` or `INCLUDED`, the NEAR RPC returns a
minimal response containing only `final_execution_status` without full
execution data. The openapi client fails to deserialize this into
`RpcTransactionResponse` and returns `InvalidResponsePayload`.
This change intercepts that error in `send_impl`, parses the minimal
response, and returns `TransactionResult::Pending { status }` instead of
propagating a transport error. Full responses are wrapped in
`TransactionResult::Final(...)`.
Also adds `is_failure()`, `is_success()`, and `assert_failure()` delegate
methods to `TransactionResult` for test compatibility.
Refs: near/near-openapi-client-rs#40
There was a problem hiding this comment.
Pull request overview
This PR updates the transaction sending flow to correctly handle NEAR RPC’s minimal transaction response returned for wait_until = NONE/INCLUDED, avoiding a surfaced transport error and instead returning a structured pending result.
Changes:
- Introduces
TransactionResult(Pending { status }vsFinal(ExecutionFinalResult)) plus helper/delegate methods intypes. - Updates
ExecuteSignedTransaction::send_to*to returnTransactionResultand parses minimal RPC responses fromInvalidResponsePayload. - Wraps full RPC execution responses into
TransactionResult::Final(...)while returningPendingfor minimal responses.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| types/src/transaction/result.rs | Adds TransactionResult and TransactionResultError plus helper methods (is_*, assert_*, into_result). |
| api/src/common/send.rs | Changes send APIs to return TransactionResult and adds minimal-response parsing fallback for NONE/INCLUDED. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Fix cargo-fmt formatting issues - Box large enum variants (TransactionResult::Final, TransactionResultError::Failure) to satisfy clippy::large_enum_variant - Make pending_status() const per clippy::missing_const_for_fn - Gate minimal response fallback on wait_until being NONE/INCLUDED per review feedback - Add transaction() and logs() delegation methods to TransactionResult
Fixes MSRV CI failures caused by deflate64 v0.1.11 using `unbounded_shr` (stabilized in Rust 1.87). near-sandbox 0.3.7 pins deflate64 < 0.1.11.
This reverts commit b1fde20.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #128 +/- ##
==========================================
- Coverage 51.58% 50.90% -0.69%
==========================================
Files 40 40
Lines 5172 5265 +93
==========================================
+ Hits 2668 2680 +12
- Misses 2504 2585 +81 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
vsavchyn-dev
left a comment
There was a problem hiding this comment.
Few questions regarding naming, but it works with all TxExecutionStatus-es from what I checked without giving me error on 'None', 'Included', and SemiFinal ones
types/src/transaction/result.rs
Outdated
| /// - Higher finality levels (`ExecutedOptimistic`, `Final`, etc.) will return | ||
| /// [`TransactionResult::Final`] with the full execution outcome. | ||
| #[derive(Clone, Debug)] | ||
| #[must_use] |
types/src/transaction/result.rs
Outdated
| /// The `status` field indicates how far the transaction has progressed. | ||
| Pending { status: TxExecutionStatus }, | ||
| /// Full execution result is available. | ||
| Final(Box<ExecutionFinalResult>), |
There was a problem hiding this comment.
How about we call it 'Full'?
It might be only me, but I get confused between 'TxExecutionStatus::Final' and 'TxResult::Final', as when we wait for 'TxExecutionStatus::ExecutedOptimistic', it will provide us with valid result, but it won't be "Final" per say in some cases.
Also, I might be wrong about naming conventions from nearcore RPC types so correct me if I'm wrong
There was a problem hiding this comment.
Good idea, renamed it here: e1c727e (this PR)
| /// Returns the pending status, if the transaction is still pending. | ||
| pub const fn pending_status(&self) -> Option<&TxExecutionStatus> { | ||
| match self { | ||
| Self::Pending { status } => Some(status), | ||
| Self::Final(_) => None, | ||
| } | ||
| } |
There was a problem hiding this comment.
| /// Returns the pending status, if the transaction is still pending. | |
| pub const fn pending_status(&self) -> Option<&TxExecutionStatus> { | |
| match self { | |
| Self::Pending { status } => Some(status), | |
| Self::Final(_) => None, | |
| } | |
| } | |
| /// Returns the pending status, if the transaction is still pending. | |
| pub const fn pending_status(self) -> Option<TxExecutionStatus> { | |
| match self { | |
| Self::Pending { status } => Some(status), | |
| Self::Final(_) => None, | |
| } | |
| } |
I think this function should work without ref to TxExecutionStatus? Or maybe there is some other reason to have ref here?
There was a problem hiding this comment.
You're right. Fixed it: 2b6b76a (this PR)
The bare #[must_use] attribute produces a confusing compiler warning. Add an explicit message guiding users to `into_result()` for handling both execution errors and pending transaction cases.
Avoids confusion with TxExecutionStatus::Final which refers to NEAR finality level. TransactionResult::Full means "full execution data is available" regardless of which finality level was requested.
TxExecutionStatus is Copy, so returning Option<&TxExecutionStatus> via &self is unnecessary. Take self by value and return Option<TxExecutionStatus> directly.

Summary
InvalidResponsePayloaderrors insend_implwhen the RPC returns a minimal response (onlyfinal_execution_status) forwait_untilset toNONEorINCLUDEDTransactionResult::Pending { status }instead of propagating a transport errorTransactionResult::Final(...)is_failure(),is_success(), andassert_failure()delegate methods toTransactionResultContext
When
wait_untilisNONEorINCLUDED, the NEAR RPC returns:{"jsonrpc":"2.0","result":{"final_execution_status":"INCLUDED"},"id":"0"}The openapi client can't deserialize this into
RpcTransactionResponse(which expects full execution data likereceipts_outcome,status, etc.), so it returnsInvalidResponsePayload. Previously this surfaced as aTransportErrorto callers.Refs: near/near-openapi-client-rs#40
Test plan
cargo checkandcargo check --testspasswait_until(TxExecutionStatus::None)andwait_until(TxExecutionStatus::Included)against testnetwait_until(TxExecutionStatus::Final)(default)