1+ use std:: fmt;
12use std:: sync:: Arc ;
23
34use near_openapi_client:: types:: {
@@ -11,7 +12,7 @@ use near_api_types::{
1112 transaction:: {
1213 PrepopulateTransaction , SignedTransaction ,
1314 delegate_action:: { SignedDelegateAction , SignedDelegateActionAsBase64 } ,
14- result:: ExecutionFinalResult ,
15+ result:: { ExecutionFinalResult , TransactionResult } ,
1516 } ,
1617} ;
1718use reqwest:: Response ;
@@ -32,6 +33,35 @@ use super::META_TRANSACTION_VALID_FOR_DEFAULT;
3233const TX_EXECUTOR_TARGET : & str = "near_api::tx::executor" ;
3334const META_EXECUTOR_TARGET : & str = "near_api::meta::executor" ;
3435
36+ /// Internal enum to distinguish between a full RPC response and a minimal pending response.
37+ enum SendImplResponse {
38+ Full ( Box < RpcTransactionResponse > ) ,
39+ Pending ( TxExecutionStatus ) ,
40+ }
41+
42+ impl fmt:: Debug for SendImplResponse {
43+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
44+ match self {
45+ Self :: Full ( _) => write ! ( f, "Full(...)" ) ,
46+ Self :: Pending ( status) => write ! ( f, "Pending({status:?})" ) ,
47+ }
48+ }
49+ }
50+
51+ /// Minimal JSON-RPC response returned when `wait_until` is `NONE` or `INCLUDED`.
52+ ///
53+ /// The RPC returns only `{"jsonrpc":"2.0","result":{"final_execution_status":"..."},"id":"0"}`
54+ /// which doesn't match the full `RpcTransactionResponse` schema.
55+ #[ derive( serde:: Deserialize ) ]
56+ struct MinimalTransactionResponse {
57+ result : MinimalTransactionResult ,
58+ }
59+
60+ #[ derive( serde:: Deserialize ) ]
61+ struct MinimalTransactionResult {
62+ final_execution_status : TxExecutionStatus ,
63+ }
64+
3565#[ async_trait:: async_trait]
3666pub trait Transactionable : Send + Sync {
3767 fn prepopulated ( & self ) -> Result < PrepopulateTransaction , ArgumentValidationError > ;
@@ -191,10 +221,14 @@ impl ExecuteSignedTransaction {
191221 ///
192222 /// This is useful if you want to send the transaction to a non-default network configuration (e.g, custom RPC URL, sandbox).
193223 /// Please note that if the transaction is not presigned, it will be signed with the network's nonce and block hash.
224+ ///
225+ /// Returns a [`TransactionResult`] which is either:
226+ /// - [`TransactionResult::Pending`] if `wait_until` is `None` or `Included` (no execution data available yet)
227+ /// - [`TransactionResult::Full`] for higher finality levels with full execution results
194228 pub async fn send_to (
195229 mut self ,
196230 network : & NetworkConfig ,
197- ) -> Result < ExecutionFinalResult , ExecuteTransactionError > {
231+ ) -> Result < TransactionResult , ExecuteTransactionError > {
198232 let ( signed, transactionable) = match & mut self . transaction {
199233 TransactionableOrSigned :: Transactionable ( transaction) => {
200234 debug ! ( target: TX_EXECUTOR_TARGET , "Preparing unsigned transaction" ) ;
@@ -243,15 +277,15 @@ impl ExecuteSignedTransaction {
243277 /// Sends the transaction to the default mainnet configuration.
244278 ///
245279 /// Please note that this will sign the transaction with the mainnet's nonce and block hash if it's not presigned yet.
246- pub async fn send_to_mainnet ( self ) -> Result < ExecutionFinalResult , ExecuteTransactionError > {
280+ pub async fn send_to_mainnet ( self ) -> Result < TransactionResult , ExecuteTransactionError > {
247281 let network = NetworkConfig :: mainnet ( ) ;
248282 self . send_to ( & network) . await
249283 }
250284
251285 /// Sends the transaction to the default testnet configuration.
252286 ///
253287 /// Please note that this will sign the transaction with the testnet's nonce and block hash if it's not presigned yet.
254- pub async fn send_to_testnet ( self ) -> Result < ExecutionFinalResult , ExecuteTransactionError > {
288+ pub async fn send_to_testnet ( self ) -> Result < TransactionResult , ExecuteTransactionError > {
255289 let network = NetworkConfig :: testnet ( ) ;
256290 self . send_to ( & network) . await
257291 }
@@ -260,7 +294,7 @@ impl ExecuteSignedTransaction {
260294 network : & NetworkConfig ,
261295 signed_tr : SignedTransaction ,
262296 wait_until : TxExecutionStatus ,
263- ) -> Result < ExecutionFinalResult , ExecuteTransactionError > {
297+ ) -> Result < TransactionResult , ExecuteTransactionError > {
264298 let hash = signed_tr. get_hash ( ) ;
265299 let signed_tx_base64: near_openapi_client:: types:: SignedTransaction = signed_tr. into ( ) ;
266300 let result = retry ( network. clone ( ) , |client| {
@@ -285,7 +319,7 @@ impl ExecuteSignedTransaction {
285319 result,
286320 ..
287321 } ,
288- ) => RetryResponse :: Ok ( result) ,
322+ ) => RetryResponse :: Ok ( SendImplResponse :: Full ( Box :: new ( result) ) ) ,
289323 Ok (
290324 JsonRpcResponseForRpcTransactionResponseAndRpcTransactionError :: Variant1 {
291325 error,
@@ -296,7 +330,35 @@ impl ExecuteSignedTransaction {
296330 SendRequestError :: from ( error) ;
297331 to_retry_error ( error, is_critical_transaction_error)
298332 }
299- Err ( err) => to_retry_error ( err, is_critical_transaction_error) ,
333+ Err ( err) => {
334+ // When wait_until is NONE or INCLUDED, the RPC returns a minimal
335+ // response with only `final_execution_status`. The openapi client
336+ // fails to deserialize this into RpcTransactionResponse (which
337+ // expects full execution data) and returns InvalidResponsePayload.
338+ // We intercept this case and parse the minimal response ourselves.
339+ //
340+ // We only attempt this fallback when we explicitly requested a
341+ // minimal response, so unexpected/buggy RPC responses for higher
342+ // finality levels don't get silently treated as Pending.
343+ if matches ! (
344+ wait_until,
345+ TxExecutionStatus :: None | TxExecutionStatus :: Included
346+ ) {
347+ if let SendRequestError :: TransportError (
348+ near_openapi_client:: Error :: InvalidResponsePayload ( ref bytes, _) ,
349+ ) = err
350+ {
351+ if let Ok ( minimal) =
352+ serde_json:: from_slice :: < MinimalTransactionResponse > ( bytes)
353+ {
354+ return RetryResponse :: Ok ( SendImplResponse :: Pending (
355+ minimal. result . final_execution_status ,
356+ ) ) ;
357+ }
358+ }
359+ }
360+ to_retry_error ( err, is_critical_transaction_error)
361+ }
300362 } ;
301363
302364 tracing:: debug!(
@@ -312,39 +374,43 @@ impl ExecuteSignedTransaction {
312374 . await
313375 . map_err ( ExecuteTransactionError :: TransactionError ) ?;
314376
315- // TODO: check if we need to add support for that final_execution_status
316- let final_execution_outcome_view = match result {
317- // We don't use `experimental_tx`, so we can ignore that, but just to be safe
318- RpcTransactionResponse :: Variant0 {
319- final_execution_status : _,
320- receipts : _,
321- receipts_outcome,
322- status,
323- transaction,
324- transaction_outcome,
325- } => FinalExecutionOutcomeView {
326- receipts_outcome,
327- status,
328- transaction,
329- transaction_outcome,
330- } ,
331- RpcTransactionResponse :: Variant1 {
332- final_execution_status : _,
333- receipts_outcome,
334- status,
335- transaction,
336- transaction_outcome,
337- } => FinalExecutionOutcomeView {
338- receipts_outcome,
339- status,
340- transaction,
341- transaction_outcome,
342- } ,
343- } ;
377+ match result {
378+ SendImplResponse :: Pending ( status) => Ok ( TransactionResult :: Pending { status } ) ,
379+ SendImplResponse :: Full ( rpc_response) => {
380+ let final_execution_outcome_view = match * rpc_response {
381+ // We don't use `experimental_tx`, so we can ignore that, but just to be safe
382+ RpcTransactionResponse :: Variant0 {
383+ final_execution_status : _,
384+ receipts : _,
385+ receipts_outcome,
386+ status,
387+ transaction,
388+ transaction_outcome,
389+ } => FinalExecutionOutcomeView {
390+ receipts_outcome,
391+ status,
392+ transaction,
393+ transaction_outcome,
394+ } ,
395+ RpcTransactionResponse :: Variant1 {
396+ final_execution_status : _,
397+ receipts_outcome,
398+ status,
399+ transaction,
400+ transaction_outcome,
401+ } => FinalExecutionOutcomeView {
402+ receipts_outcome,
403+ status,
404+ transaction,
405+ transaction_outcome,
406+ } ,
407+ } ;
344408
345- Ok ( ExecutionFinalResult :: try_from (
346- final_execution_outcome_view,
347- ) ?)
409+ Ok ( TransactionResult :: Full ( Box :: new (
410+ ExecutionFinalResult :: try_from ( final_execution_outcome_view) ?,
411+ ) ) )
412+ }
413+ }
348414 }
349415}
350416
0 commit comments