Skip to main content

near_kit/types/
rpc.rs

1//! RPC response types.
2
3use std::collections::BTreeMap;
4
5use base64::{Engine as _, engine::general_purpose::STANDARD};
6use serde::Deserialize;
7use serde_with::{base64::Base64, serde_as};
8
9use super::block_reference::TxExecutionStatus;
10use super::error::{ActionError, TxExecutionError};
11use super::{AccountId, CryptoHash, Gas, NearToken, PublicKey, Signature};
12
13// ============================================================================
14// Constants
15// ============================================================================
16
17/// Cost per byte of storage in yoctoNEAR.
18///
19/// This is a protocol constant (10^19 yoctoNEAR per byte = 0.00001 NEAR/byte).
20/// It has remained unchanged since NEAR genesis and would require a hard fork
21/// to modify. Used for calculating available balance.
22///
23/// See: <https://docs.near.org/concepts/storage/storage-staking>
24pub const STORAGE_AMOUNT_PER_BYTE: u128 = 10_000_000_000_000_000_000; // 10^19 yoctoNEAR
25
26// ============================================================================
27// Account types
28// ============================================================================
29
30/// Account information from view_account RPC.
31#[derive(Debug, Clone, Deserialize)]
32pub struct AccountView {
33    /// Total balance including locked.
34    pub amount: NearToken,
35    /// Locked balance (staked).
36    pub locked: NearToken,
37    /// Hash of deployed contract code (or zeros if none).
38    pub code_hash: CryptoHash,
39    /// Storage used in bytes.
40    pub storage_usage: u64,
41    /// Storage paid at block height (deprecated, always 0).
42    #[serde(default)]
43    pub storage_paid_at: u64,
44    /// Global contract code hash (if using a global contract).
45    #[serde(default)]
46    pub global_contract_hash: Option<CryptoHash>,
47    /// Global contract account ID (if using a global contract by account).
48    #[serde(default)]
49    pub global_contract_account_id: Option<AccountId>,
50    /// Block height of the query.
51    pub block_height: u64,
52    /// Block hash of the query.
53    pub block_hash: CryptoHash,
54}
55
56impl AccountView {
57    /// Calculate the total NEAR required for storage.
58    fn storage_required(&self) -> NearToken {
59        let yocto = STORAGE_AMOUNT_PER_BYTE.saturating_mul(self.storage_usage as u128);
60        NearToken::from_yoctonear(yocto)
61    }
62
63    /// Get available (spendable) balance.
64    ///
65    /// This accounts for the protocol rule that staked tokens count towards
66    /// the storage requirement:
67    /// - available = amount - max(0, storage_required - locked)
68    ///
69    /// If staked >= storage cost, all liquid balance is available.
70    /// If staked < storage cost, some liquid balance is reserved for storage.
71    pub fn available(&self) -> NearToken {
72        let storage_required = self.storage_required();
73
74        // If staked covers storage, all liquid is available
75        if self.locked >= storage_required {
76            return self.amount;
77        }
78
79        // Otherwise, reserve the difference from liquid balance
80        let reserved_for_storage = storage_required.saturating_sub(self.locked);
81        self.amount.saturating_sub(reserved_for_storage)
82    }
83
84    /// Get the amount of NEAR reserved for storage costs.
85    ///
86    /// This is calculated as: max(0, storage_required - locked)
87    pub fn storage_cost(&self) -> NearToken {
88        let storage_required = self.storage_required();
89
90        if self.locked >= storage_required {
91            NearToken::ZERO
92        } else {
93            storage_required.saturating_sub(self.locked)
94        }
95    }
96
97    /// Check if this account has a deployed contract.
98    pub fn has_contract(&self) -> bool {
99        !self.code_hash.is_zero()
100    }
101}
102
103/// Simplified balance info.
104#[derive(Debug, Clone)]
105pub struct AccountBalance {
106    /// Total balance (available + locked).
107    pub total: NearToken,
108    /// Available balance (spendable, accounting for storage).
109    pub available: NearToken,
110    /// Locked balance (staked).
111    pub locked: NearToken,
112    /// Amount reserved for storage costs.
113    pub storage_cost: NearToken,
114    /// Storage used in bytes.
115    pub storage_usage: u64,
116}
117
118impl std::fmt::Display for AccountBalance {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        write!(f, "{}", self.available)
121    }
122}
123
124impl From<AccountView> for AccountBalance {
125    fn from(view: AccountView) -> Self {
126        Self {
127            total: view.amount,
128            available: view.available(),
129            locked: view.locked,
130            storage_cost: view.storage_cost(),
131            storage_usage: view.storage_usage,
132        }
133    }
134}
135
136/// Access key information from view_access_key RPC.
137#[derive(Debug, Clone, Deserialize)]
138pub struct AccessKeyView {
139    /// Nonce for replay protection.
140    pub nonce: u64,
141    /// Permission level.
142    pub permission: AccessKeyPermissionView,
143    /// Block height of the query.
144    pub block_height: u64,
145    /// Block hash of the query.
146    pub block_hash: CryptoHash,
147}
148
149/// Access key details (without block info, used in lists).
150#[derive(Debug, Clone, Deserialize)]
151pub struct AccessKeyDetails {
152    /// Nonce for replay protection.
153    pub nonce: u64,
154    /// Permission level.
155    pub permission: AccessKeyPermissionView,
156}
157
158/// Access key permission from RPC.
159#[derive(Debug, Clone, Deserialize)]
160#[serde(rename_all = "PascalCase")]
161pub enum AccessKeyPermissionView {
162    /// Full access.
163    FullAccess,
164    /// Function call access with restrictions.
165    FunctionCall {
166        /// Maximum amount this key can spend.
167        allowance: Option<NearToken>,
168        /// Contract that can be called.
169        receiver_id: AccountId,
170        /// Methods that can be called (empty = all).
171        method_names: Vec<String>,
172    },
173    /// Gas key with function call access.
174    GasKeyFunctionCall {
175        /// Gas key balance.
176        balance: NearToken,
177        /// Number of nonces.
178        num_nonces: u16,
179        /// Maximum amount this key can spend.
180        allowance: Option<NearToken>,
181        /// Contract that can be called.
182        receiver_id: AccountId,
183        /// Methods that can be called (empty = all).
184        method_names: Vec<String>,
185    },
186    /// Gas key with full access.
187    GasKeyFullAccess {
188        /// Gas key balance.
189        balance: NearToken,
190        /// Number of nonces.
191        num_nonces: u16,
192    },
193}
194
195/// Access key list from view_access_key_list RPC.
196#[derive(Debug, Clone, Deserialize)]
197pub struct AccessKeyListView {
198    /// List of access keys.
199    pub keys: Vec<AccessKeyInfoView>,
200    /// Block height of the query.
201    pub block_height: u64,
202    /// Block hash of the query.
203    pub block_hash: CryptoHash,
204}
205
206/// Single access key info in list.
207#[derive(Debug, Clone, Deserialize)]
208pub struct AccessKeyInfoView {
209    /// Public key.
210    pub public_key: PublicKey,
211    /// Access key details.
212    pub access_key: AccessKeyDetails,
213}
214
215// ============================================================================
216// Block types
217// ============================================================================
218
219/// Block information from block RPC.
220#[derive(Debug, Clone, Deserialize)]
221pub struct BlockView {
222    /// Block author (validator account ID).
223    pub author: AccountId,
224    /// Block header.
225    pub header: BlockHeaderView,
226    /// List of chunks in the block.
227    pub chunks: Vec<ChunkHeaderView>,
228}
229
230/// Block header with full details.
231#[derive(Debug, Clone, Deserialize)]
232pub struct BlockHeaderView {
233    /// Block height.
234    pub height: u64,
235    /// Previous block height (may be None for genesis).
236    #[serde(default)]
237    pub prev_height: Option<u64>,
238    /// Block hash.
239    pub hash: CryptoHash,
240    /// Previous block hash.
241    pub prev_hash: CryptoHash,
242    /// Previous state root.
243    pub prev_state_root: CryptoHash,
244    /// Chunk receipts root.
245    pub chunk_receipts_root: CryptoHash,
246    /// Chunk headers root.
247    pub chunk_headers_root: CryptoHash,
248    /// Chunk transaction root.
249    pub chunk_tx_root: CryptoHash,
250    /// Outcome root.
251    pub outcome_root: CryptoHash,
252    /// Number of chunks included.
253    pub chunks_included: u64,
254    /// Challenges root.
255    pub challenges_root: CryptoHash,
256    /// Timestamp in nanoseconds (as u64).
257    pub timestamp: u64,
258    /// Timestamp in nanoseconds (as string for precision).
259    pub timestamp_nanosec: String,
260    /// Random value for the block.
261    pub random_value: CryptoHash,
262    /// Validator proposals.
263    #[serde(default)]
264    pub validator_proposals: Vec<ValidatorStakeView>,
265    /// Chunk mask (which shards have chunks).
266    #[serde(default)]
267    pub chunk_mask: Vec<bool>,
268    /// Gas price for this block.
269    pub gas_price: NearToken,
270    /// Block ordinal (may be None).
271    #[serde(default)]
272    pub block_ordinal: Option<u64>,
273    /// Total supply of NEAR tokens.
274    pub total_supply: NearToken,
275    /// Challenges result.
276    #[serde(default)]
277    pub challenges_result: Vec<SlashedValidator>,
278    /// Last final block hash.
279    pub last_final_block: CryptoHash,
280    /// Last DS final block hash.
281    pub last_ds_final_block: CryptoHash,
282    /// Epoch ID.
283    pub epoch_id: CryptoHash,
284    /// Next epoch ID.
285    pub next_epoch_id: CryptoHash,
286    /// Next block producer hash.
287    pub next_bp_hash: CryptoHash,
288    /// Block merkle root.
289    pub block_merkle_root: CryptoHash,
290    /// Epoch sync data hash (optional).
291    #[serde(default)]
292    pub epoch_sync_data_hash: Option<CryptoHash>,
293    /// Block body hash (optional, added in later protocol versions).
294    #[serde(default)]
295    pub block_body_hash: Option<CryptoHash>,
296    /// Block approvals (nullable signatures).
297    #[serde(default)]
298    pub approvals: Vec<Option<Signature>>,
299    /// Block signature.
300    pub signature: Signature,
301    /// Latest protocol version.
302    pub latest_protocol_version: u32,
303    /// Rent paid (deprecated; when present, always 0).
304    #[serde(default)]
305    pub rent_paid: Option<NearToken>,
306    /// Validator reward (deprecated; when present, always 0).
307    #[serde(default)]
308    pub validator_reward: Option<NearToken>,
309    /// Chunk endorsements (optional).
310    #[serde(default)]
311    pub chunk_endorsements: Option<Vec<Vec<u8>>>,
312    /// Shard split info (optional).
313    #[serde(default)]
314    pub shard_split: Option<(u64, AccountId)>,
315}
316
317/// Validator stake (versioned).
318///
319/// Used for validator proposals in block/chunk headers.
320#[derive(Debug, Clone, Deserialize)]
321#[serde(untagged)]
322pub enum ValidatorStakeView {
323    /// Version 1 (current).
324    V1(ValidatorStakeViewV1),
325}
326
327/// Validator stake data.
328#[derive(Debug, Clone, Deserialize)]
329pub struct ValidatorStakeViewV1 {
330    /// Validator account ID.
331    pub account_id: AccountId,
332    /// Public key.
333    pub public_key: PublicKey,
334    /// Stake amount.
335    pub stake: NearToken,
336}
337
338impl ValidatorStakeView {
339    /// Get the inner V1 data.
340    pub fn into_v1(self) -> ValidatorStakeViewV1 {
341        match self {
342            Self::V1(v) => v,
343        }
344    }
345
346    /// Get the account ID.
347    pub fn account_id(&self) -> &AccountId {
348        match self {
349            Self::V1(v) => &v.account_id,
350        }
351    }
352
353    /// Get the stake amount.
354    pub fn stake(&self) -> NearToken {
355        match self {
356            Self::V1(v) => v.stake,
357        }
358    }
359}
360
361/// Slashed validator from challenge results.
362#[derive(Debug, Clone, Deserialize)]
363pub struct SlashedValidator {
364    /// Validator account ID.
365    pub account_id: AccountId,
366    /// Whether this was a double sign.
367    pub is_double_sign: bool,
368}
369
370/// Chunk header with full details.
371#[derive(Debug, Clone, Deserialize)]
372pub struct ChunkHeaderView {
373    /// Chunk hash.
374    pub chunk_hash: CryptoHash,
375    /// Previous block hash.
376    pub prev_block_hash: CryptoHash,
377    /// Outcome root.
378    pub outcome_root: CryptoHash,
379    /// Previous state root.
380    pub prev_state_root: CryptoHash,
381    /// Encoded merkle root.
382    pub encoded_merkle_root: CryptoHash,
383    /// Encoded length.
384    pub encoded_length: u64,
385    /// Height when chunk was created.
386    pub height_created: u64,
387    /// Height when chunk was included.
388    pub height_included: u64,
389    /// Shard ID.
390    pub shard_id: u64,
391    /// Gas used in this chunk.
392    pub gas_used: u64,
393    /// Gas limit for this chunk.
394    pub gas_limit: u64,
395    /// Validator reward.
396    pub validator_reward: NearToken,
397    /// Balance burnt.
398    pub balance_burnt: NearToken,
399    /// Outgoing receipts root.
400    pub outgoing_receipts_root: CryptoHash,
401    /// Transaction root.
402    pub tx_root: CryptoHash,
403    /// Validator proposals.
404    #[serde(default)]
405    pub validator_proposals: Vec<ValidatorStakeView>,
406    /// Congestion info (optional, added in later protocol versions).
407    #[serde(default)]
408    pub congestion_info: Option<CongestionInfoView>,
409    /// Bandwidth requests (optional, added in later protocol versions).
410    #[serde(default)]
411    pub bandwidth_requests: Option<BandwidthRequests>,
412    /// Rent paid (deprecated; when present, always 0).
413    #[serde(default)]
414    pub rent_paid: Option<NearToken>,
415    /// Proposed trie split for resharding.
416    ///
417    /// - `None` — field absent (older protocol versions)
418    /// - `Some(None)` — field present as JSON `null` (no split proposed)
419    /// - `Some(Some(split))` — active split proposal
420    #[serde(default)]
421    pub proposed_split: Option<Option<TrieSplit>>,
422    /// Chunk signature.
423    pub signature: Signature,
424}
425
426/// Bandwidth requests for a chunk (versioned).
427#[derive(Debug, Clone, Deserialize)]
428pub enum BandwidthRequests {
429    /// Version 1.
430    V1(BandwidthRequestsV1),
431}
432
433/// Bandwidth requests data (V1).
434#[derive(Debug, Clone, Deserialize)]
435pub struct BandwidthRequestsV1 {
436    /// List of bandwidth requests.
437    pub requests: Vec<BandwidthRequest>,
438}
439
440/// A single bandwidth request to a target shard.
441#[derive(Debug, Clone, Deserialize)]
442pub struct BandwidthRequest {
443    /// Target shard index.
444    pub to_shard: u16,
445    /// Bitmap of requested values.
446    pub requested_values_bitmap: BandwidthRequestBitmap,
447}
448
449/// Bitmap for bandwidth request values.
450#[derive(Debug, Clone, Deserialize)]
451pub struct BandwidthRequestBitmap {
452    /// Raw bitmap data.
453    pub data: [u8; 5],
454}
455
456/// Trie split information for resharding.
457#[derive(Debug, Clone, Deserialize)]
458pub struct TrieSplit {
459    /// Account boundary for the split.
460    pub boundary_account: AccountId,
461    /// Memory usage of the left child.
462    pub left_memory: u64,
463    /// Memory usage of the right child.
464    pub right_memory: u64,
465}
466
467/// Congestion information for a shard.
468#[derive(Debug, Clone, Deserialize)]
469pub struct CongestionInfoView {
470    /// Gas used by delayed receipts.
471    #[serde(default, deserialize_with = "dec_format")]
472    pub delayed_receipts_gas: u128,
473    /// Gas used by buffered receipts.
474    #[serde(default, deserialize_with = "dec_format")]
475    pub buffered_receipts_gas: u128,
476    /// Bytes used by receipts.
477    #[serde(default)]
478    pub receipt_bytes: u64,
479    /// Allowed shard.
480    #[serde(default)]
481    pub allowed_shard: u16,
482}
483
484/// Deserialize a u128 from a decimal string (NEAR RPC sends u128 as strings).
485fn dec_format<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<u128, D::Error> {
486    #[derive(Deserialize)]
487    #[serde(untagged)]
488    enum StringOrNum {
489        String(String),
490        Num(u128),
491    }
492    match StringOrNum::deserialize(deserializer)? {
493        StringOrNum::String(s) => s.parse().map_err(serde::de::Error::custom),
494        StringOrNum::Num(n) => Ok(n),
495    }
496}
497
498/// Deserialize a Gas (u64) from a decimal string or number.
499fn gas_dec_format<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Gas, D::Error> {
500    #[derive(Deserialize)]
501    #[serde(untagged)]
502    enum StringOrNum {
503        String(String),
504        Num(u64),
505    }
506    let raw = match StringOrNum::deserialize(deserializer)? {
507        StringOrNum::String(s) => s.parse::<u64>().map_err(serde::de::Error::custom)?,
508        StringOrNum::Num(n) => n,
509    };
510    Ok(Gas::from_gas(raw))
511}
512
513/// Gas price response.
514#[derive(Debug, Clone, Deserialize)]
515pub struct GasPrice {
516    /// Gas price in yoctoNEAR.
517    pub gas_price: NearToken,
518}
519
520impl GasPrice {
521    /// Get gas price as u128.
522    pub fn as_u128(&self) -> u128 {
523        self.gas_price.as_yoctonear()
524    }
525}
526
527// ============================================================================
528// Transaction outcome types
529// ============================================================================
530
531/// Overall transaction execution status.
532///
533/// Represents the final result of a transaction, matching nearcore's `FinalExecutionStatus`.
534/// This is the `status` field in `FinalExecutionOutcome`.
535#[derive(Debug, Clone, Default, Deserialize)]
536pub enum FinalExecutionStatus {
537    /// The transaction has not yet started execution.
538    #[default]
539    NotStarted,
540    /// The transaction has started but the first receipt hasn't completed.
541    Started,
542    /// The transaction execution failed.
543    Failure(TxExecutionError),
544    /// The transaction execution succeeded (base64-encoded return value).
545    SuccessValue(String),
546}
547
548/// Response returned for non-executed wait levels.
549///
550/// When you use a non-executed wait level ([`Submitted`](crate::types::Submitted),
551/// [`Included`](crate::types::Included), [`IncludedFinal`](crate::types::IncludedFinal)),
552/// the transaction hasn't been executed yet so there's no outcome to return.
553/// This type gives you the information needed to poll for the result later
554/// via [`Near::tx_status`](crate::Near::tx_status).
555///
556/// # Example
557///
558/// ```rust,no_run
559/// # use near_kit::*;
560/// # async fn example(near: &Near) -> Result<(), Error> {
561/// let response = near.transfer("bob.testnet", NearToken::from_near(1))
562///     .wait_until(Included)
563///     .await?;
564///
565/// // Later, poll for the full outcome:
566/// let outcome = near.tx_status(
567///     &response.transaction_hash,
568///     &response.sender_id,
569///     Final,
570/// ).await?;
571/// # Ok(())
572/// # }
573/// ```
574#[derive(Debug, Clone)]
575pub struct SendTxResponse {
576    /// Hash of the submitted transaction.
577    pub transaction_hash: CryptoHash,
578    /// Account ID of the transaction signer.
579    pub sender_id: AccountId,
580}
581
582/// Raw RPC response for transaction submission/status (internal).
583///
584/// Used internally to deserialize responses from `send_tx` and
585/// `EXPERIMENTAL_tx_status` before converting to user-facing types
586/// via [`WaitLevel::convert`](crate::types::WaitLevel::convert).
587#[doc(hidden)]
588#[derive(Debug, Clone, Deserialize)]
589pub struct RawTransactionResponse {
590    /// Transaction hash — populated after deserialization from the signed tx
591    /// (for `send_tx`) or from the outcome (for `tx_status`).
592    #[serde(skip)]
593    pub transaction_hash: CryptoHash,
594    /// The wait level that was reached.
595    pub final_execution_status: TxExecutionStatus,
596    /// The execution outcome, present when the transaction has been executed.
597    #[serde(flatten)]
598    pub outcome: Option<FinalExecutionOutcome>,
599}
600
601/// Final execution outcome from a NEAR transaction.
602///
603/// Returned by both `send_tx` and `EXPERIMENTAL_tx_status` RPC methods.
604/// All fields are required — this type only appears when a transaction has
605/// actually been executed (not for non-executed wait levels).
606///
607/// The `receipts` field contains full receipt details when the outcome comes
608/// from `EXPERIMENTAL_tx_status` (i.e. `near.tx_status()`). When the outcome
609/// comes from `send_tx` (i.e. `near.send()`), this field is an empty `Vec`.
610#[derive(Debug, Clone, Deserialize)]
611pub struct FinalExecutionOutcome {
612    /// Overall transaction execution result.
613    pub status: FinalExecutionStatus,
614    /// The transaction that was executed.
615    pub transaction: TransactionView,
616    /// Outcome of the transaction itself.
617    pub transaction_outcome: ExecutionOutcomeWithId,
618    /// Outcomes of all receipts spawned by the transaction.
619    pub receipts_outcome: Vec<ExecutionOutcomeWithId>,
620    /// Full receipt details (populated by `tx_status`, empty from `send`).
621    #[serde(default)]
622    pub receipts: Vec<Receipt>,
623}
624
625impl FinalExecutionOutcome {
626    /// Check if the transaction succeeded.
627    pub fn is_success(&self) -> bool {
628        matches!(&self.status, FinalExecutionStatus::SuccessValue(_))
629    }
630
631    /// Check if the transaction failed.
632    pub fn is_failure(&self) -> bool {
633        matches!(&self.status, FinalExecutionStatus::Failure(_))
634    }
635
636    /// Get the failure message if present.
637    pub fn failure_message(&self) -> Option<String> {
638        match &self.status {
639            FinalExecutionStatus::Failure(err) => Some(err.to_string()),
640            _ => None,
641        }
642    }
643
644    /// Get the typed execution error if present.
645    pub fn failure_error(&self) -> Option<&TxExecutionError> {
646        match &self.status {
647            FinalExecutionStatus::Failure(err) => Some(err),
648            _ => None,
649        }
650    }
651
652    /// Get the transaction hash.
653    pub fn transaction_hash(&self) -> &CryptoHash {
654        &self.transaction_outcome.id
655    }
656
657    /// Get total gas used across all receipts.
658    pub fn total_gas_used(&self) -> Gas {
659        let tx_gas = self.transaction_outcome.outcome.gas_burnt.as_gas();
660        let receipt_gas: u64 = self
661            .receipts_outcome
662            .iter()
663            .map(|r| r.outcome.gas_burnt.as_gas())
664            .sum();
665        Gas::from_gas(tx_gas + receipt_gas)
666    }
667
668    /// Get the return value as raw bytes (base64-decoded).
669    ///
670    /// Returns `Err` if the execution status is not `SuccessValue` (including
671    /// transaction validation or action failures), or if the value is not
672    /// valid base64.
673    ///
674    /// Note: When using the high-level transaction API, only transaction
675    /// *validation* failures return `Err(Error::InvalidTx(..))` from the
676    /// send call. On-chain action failures are returned as `Ok(outcome)`
677    /// with `outcome.is_failure() == true`. This method is primarily useful
678    /// when working with the low-level `rpc().send_tx()` API or when you
679    /// already have an outcome and want to decode its return value.
680    pub fn result(&self) -> Result<Vec<u8>, crate::error::Error> {
681        match &self.status {
682            FinalExecutionStatus::Failure(TxExecutionError::InvalidTxError(e)) => {
683                Err(crate::error::Error::InvalidTx(Box::new(e.clone())))
684            }
685            FinalExecutionStatus::Failure(TxExecutionError::ActionError(e)) => Err(
686                crate::error::Error::InvalidTransaction(format!("Action error: {e}")),
687            ),
688            FinalExecutionStatus::SuccessValue(s) => STANDARD.decode(s).map_err(|e| {
689                crate::error::Error::InvalidTransaction(format!(
690                    "Failed to decode base64 SuccessValue: {e}"
691                ))
692            }),
693            other => Err(crate::error::Error::InvalidTransaction(format!(
694                "Transaction status is {:?}, expected SuccessValue",
695                other,
696            ))),
697        }
698    }
699
700    /// Deserialize the return value as JSON.
701    ///
702    /// Returns `Err` if `result()` fails (on-chain failure, unexpected status,
703    /// or invalid base64) or if JSON deserialization fails.
704    pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, crate::error::Error> {
705        let bytes = self.result()?;
706        serde_json::from_slice(&bytes).map_err(crate::error::Error::from)
707    }
708}
709
710/// Per-receipt execution status.
711///
712/// Matches nearcore's `ExecutionStatusView`. Used in [`ExecutionOutcome`].
713///
714/// The `Failure` variant contains an [`ActionError`] rather than
715/// [`TxExecutionError`] because receipt execution outcomes can only fail with
716/// action errors. Transaction-validation errors (`InvalidTxError`) are caught
717/// earlier in the send path and surfaced as [`crate::error::Error::InvalidTx`].
718///
719/// The NEAR RPC serialises the failure as `{"Failure": {"ActionError": {…}}}`.
720/// A custom [`Deserialize`] impl unwraps the outer `TxExecutionError` envelope.
721#[derive(Debug, Clone)]
722pub enum ExecutionStatus {
723    /// The execution is pending or unknown.
724    Unknown,
725    /// Execution failed with an action error.
726    Failure(ActionError),
727    /// Execution succeeded with a return value.
728    SuccessValue(Vec<u8>),
729    /// Execution succeeded, producing a receipt.
730    SuccessReceiptId(CryptoHash),
731}
732
733impl<'de> serde::Deserialize<'de> for ExecutionStatus {
734    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
735        /// Mirror of the on-wire format that serde can derive.
736        #[serde_as]
737        #[derive(Deserialize)]
738        enum Raw {
739            Unknown,
740            Failure(TxExecutionError),
741            SuccessValue(#[serde_as(as = "Base64")] Vec<u8>),
742            SuccessReceiptId(CryptoHash),
743        }
744
745        match Raw::deserialize(deserializer)? {
746            Raw::Unknown => Ok(Self::Unknown),
747            Raw::Failure(TxExecutionError::ActionError(e)) => Ok(Self::Failure(e)),
748            Raw::Failure(TxExecutionError::InvalidTxError(e)) => Err(serde::de::Error::custom(
749                format!("unexpected InvalidTxError in receipt execution status: {e}"),
750            )),
751            Raw::SuccessValue(v) => Ok(Self::SuccessValue(v)),
752            Raw::SuccessReceiptId(h) => Ok(Self::SuccessReceiptId(h)),
753        }
754    }
755}
756
757/// Transaction view in outcome.
758#[derive(Debug, Clone, Deserialize)]
759pub struct TransactionView {
760    /// Signer account.
761    pub signer_id: AccountId,
762    /// Signer public key.
763    pub public_key: PublicKey,
764    /// Transaction nonce.
765    pub nonce: u64,
766    /// Receiver account.
767    pub receiver_id: AccountId,
768    /// Transaction hash.
769    pub hash: CryptoHash,
770    /// Actions in the transaction.
771    #[serde(default)]
772    pub actions: Vec<ActionView>,
773    /// Transaction signature.
774    pub signature: Signature,
775    /// Priority fee (optional, for congestion pricing).
776    #[serde(default)]
777    pub priority_fee: Option<u64>,
778    /// Nonce index (for gas key multi-nonce support).
779    #[serde(default)]
780    pub nonce_index: Option<u16>,
781}
782
783// ============================================================================
784// Global contract identifier view
785// ============================================================================
786
787/// Backward-compatible deserialization helper for `GlobalContractIdentifierView`.
788///
789/// Handles both the new format (`{"hash": "<base58>"}` / `{"account_id": "alice.near"}`)
790/// and the deprecated format (bare string `"<base58>"` / `"alice.near"`).
791#[derive(Deserialize)]
792#[serde(untagged)]
793enum GlobalContractIdCompat {
794    CodeHash { hash: CryptoHash },
795    AccountId { account_id: AccountId },
796    DeprecatedCodeHash(CryptoHash),
797    DeprecatedAccountId(AccountId),
798}
799
800/// Global contract identifier in RPC view responses.
801///
802/// Identifies a global contract either by its code hash (immutable) or by the
803/// publishing account ID (updatable). Supports both the current and deprecated
804/// JSON serialization formats from nearcore.
805#[derive(Debug, Clone, Deserialize)]
806#[serde(from = "GlobalContractIdCompat")]
807pub enum GlobalContractIdentifierView {
808    /// Referenced by code hash.
809    CodeHash(CryptoHash),
810    /// Referenced by publisher account ID.
811    AccountId(AccountId),
812}
813
814impl From<GlobalContractIdCompat> for GlobalContractIdentifierView {
815    fn from(compat: GlobalContractIdCompat) -> Self {
816        match compat {
817            GlobalContractIdCompat::CodeHash { hash }
818            | GlobalContractIdCompat::DeprecatedCodeHash(hash) => Self::CodeHash(hash),
819            GlobalContractIdCompat::AccountId { account_id }
820            | GlobalContractIdCompat::DeprecatedAccountId(account_id) => {
821                Self::AccountId(account_id)
822            }
823        }
824    }
825}
826
827impl std::fmt::Display for GlobalContractIdentifierView {
828    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
829        match self {
830            Self::CodeHash(hash) => write!(f, "hash({hash})"),
831            Self::AccountId(account_id) => write!(f, "{account_id}"),
832        }
833    }
834}
835
836// ============================================================================
837// Action view
838// ============================================================================
839
840/// View of a delegate action in RPC responses.
841#[derive(Debug, Clone, Deserialize)]
842pub struct DelegateActionView {
843    /// The account that signed the delegate action.
844    pub sender_id: AccountId,
845    /// The intended receiver of the inner actions.
846    pub receiver_id: AccountId,
847    /// The actions to execute.
848    pub actions: Vec<ActionView>,
849    /// Nonce for replay protection.
850    pub nonce: u64,
851    /// Maximum block height before this delegate action expires.
852    pub max_block_height: u64,
853    /// Public key of the signer.
854    pub public_key: PublicKey,
855}
856
857/// Action view in transaction.
858#[derive(Debug, Clone, Deserialize)]
859#[serde(rename_all = "PascalCase")]
860pub enum ActionView {
861    CreateAccount,
862    DeployContract {
863        code: String, // base64
864    },
865    FunctionCall {
866        method_name: String,
867        args: String, // base64
868        gas: Gas,
869        deposit: NearToken,
870    },
871    Transfer {
872        deposit: NearToken,
873    },
874    Stake {
875        stake: NearToken,
876        public_key: PublicKey,
877    },
878    AddKey {
879        public_key: PublicKey,
880        access_key: AccessKeyDetails,
881    },
882    DeleteKey {
883        public_key: PublicKey,
884    },
885    DeleteAccount {
886        beneficiary_id: AccountId,
887    },
888    Delegate {
889        delegate_action: DelegateActionView,
890        signature: Signature,
891    },
892    #[serde(rename = "DeployGlobalContract")]
893    DeployGlobalContract {
894        code: String,
895    },
896    #[serde(rename = "DeployGlobalContractByAccountId")]
897    DeployGlobalContractByAccountId {
898        code: String,
899    },
900    #[serde(rename = "UseGlobalContract")]
901    UseGlobalContract {
902        code_hash: CryptoHash,
903    },
904    #[serde(rename = "UseGlobalContractByAccountId")]
905    UseGlobalContractByAccountId {
906        account_id: AccountId,
907    },
908    #[serde(rename = "DeterministicStateInit")]
909    DeterministicStateInit {
910        code: GlobalContractIdentifierView,
911        #[serde(default)]
912        data: BTreeMap<String, String>,
913        deposit: NearToken,
914    },
915    TransferToGasKey {
916        public_key: PublicKey,
917        deposit: NearToken,
918    },
919    WithdrawFromGasKey {
920        public_key: PublicKey,
921        amount: NearToken,
922    },
923}
924
925/// Merkle path item for cryptographic proofs.
926#[derive(Debug, Clone, Deserialize)]
927pub struct MerklePathItem {
928    /// Hash at this node.
929    pub hash: CryptoHash,
930    /// Direction of the path.
931    pub direction: MerkleDirection,
932}
933
934/// Direction in merkle path.
935#[derive(Debug, Clone, Deserialize)]
936pub enum MerkleDirection {
937    Left,
938    Right,
939}
940
941/// Execution outcome with ID.
942#[derive(Debug, Clone, Deserialize)]
943pub struct ExecutionOutcomeWithId {
944    /// Receipt or transaction ID.
945    pub id: CryptoHash,
946    /// Outcome details.
947    pub outcome: ExecutionOutcome,
948    /// Proof of execution.
949    #[serde(default)]
950    pub proof: Vec<MerklePathItem>,
951    /// Block hash where this was executed.
952    pub block_hash: CryptoHash,
953}
954
955/// Execution outcome details.
956#[derive(Debug, Clone, Deserialize)]
957pub struct ExecutionOutcome {
958    /// Executor account.
959    pub executor_id: AccountId,
960    /// Gas burnt during execution.
961    pub gas_burnt: Gas,
962    /// Tokens burnt for gas.
963    pub tokens_burnt: NearToken,
964    /// Logs emitted.
965    pub logs: Vec<String>,
966    /// Receipt IDs generated.
967    pub receipt_ids: Vec<CryptoHash>,
968    /// Execution status.
969    pub status: ExecutionStatus,
970    /// Execution metadata (gas profiling).
971    #[serde(default)]
972    pub metadata: Option<ExecutionMetadata>,
973}
974
975/// Execution metadata with gas profiling.
976#[derive(Debug, Clone, Deserialize)]
977pub struct ExecutionMetadata {
978    /// Metadata version.
979    pub version: u32,
980    /// Gas profile entries.
981    #[serde(default)]
982    pub gas_profile: Option<Vec<GasProfileEntry>>,
983}
984
985/// Gas profile entry for detailed gas accounting.
986#[derive(Debug, Clone, Deserialize)]
987pub struct GasProfileEntry {
988    /// Cost category (ACTION_COST or WASM_HOST_COST).
989    pub cost_category: String,
990    /// Cost name.
991    pub cost: String,
992    /// Gas used for this cost.
993    #[serde(deserialize_with = "gas_dec_format")]
994    pub gas_used: Gas,
995}
996
997/// View function result from call_function RPC.
998#[derive(Debug, Clone, Deserialize)]
999pub struct ViewFunctionResult {
1000    /// Result bytes (often JSON).
1001    pub result: Vec<u8>,
1002    /// Logs emitted during view call.
1003    pub logs: Vec<String>,
1004    /// Block height of the query.
1005    pub block_height: u64,
1006    /// Block hash of the query.
1007    pub block_hash: CryptoHash,
1008}
1009
1010impl ViewFunctionResult {
1011    /// Get the result as raw bytes.
1012    pub fn bytes(&self) -> &[u8] {
1013        &self.result
1014    }
1015
1016    /// Get the result as a string.
1017    pub fn as_string(&self) -> Result<String, std::string::FromUtf8Error> {
1018        String::from_utf8(self.result.clone())
1019    }
1020
1021    /// Deserialize the result as JSON.
1022    ///
1023    /// # Example
1024    ///
1025    /// ```rust,ignore
1026    /// let result = rpc.view_function(&contract, "get_data", &[], block).await?;
1027    /// let data: MyData = result.json()?;
1028    /// ```
1029    pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
1030        serde_json::from_slice(&self.result)
1031    }
1032
1033    /// Deserialize the result as Borsh.
1034    ///
1035    /// Use this for contracts that return Borsh-encoded data instead of JSON.
1036    ///
1037    /// # Example
1038    ///
1039    /// ```rust,ignore
1040    /// let result = rpc.view_function(&contract, "get_state", &args, block).await?;
1041    /// let state: ContractState = result.borsh()?;
1042    /// ```
1043    pub fn borsh<T: borsh::BorshDeserialize>(&self) -> Result<T, borsh::io::Error> {
1044        borsh::from_slice(&self.result)
1045    }
1046}
1047
1048// ============================================================================
1049// Receipt types (for EXPERIMENTAL_tx_status)
1050// ============================================================================
1051
1052/// Receipt from EXPERIMENTAL_tx_status.
1053#[derive(Debug, Clone, Deserialize)]
1054pub struct Receipt {
1055    /// Predecessor account that created this receipt.
1056    pub predecessor_id: AccountId,
1057    /// Receiver account for this receipt.
1058    pub receiver_id: AccountId,
1059    /// Receipt ID.
1060    pub receipt_id: CryptoHash,
1061    /// Receipt content (action or data).
1062    pub receipt: ReceiptContent,
1063    /// Priority (optional, for congestion pricing).
1064    #[serde(default)]
1065    pub priority: Option<u64>,
1066}
1067
1068/// Receipt content - action, data, or global contract distribution.
1069#[derive(Debug, Clone, Deserialize)]
1070pub enum ReceiptContent {
1071    /// Action receipt.
1072    Action(ActionReceiptData),
1073    /// Data receipt.
1074    Data(DataReceiptData),
1075    /// Global contract distribution receipt.
1076    GlobalContractDistribution {
1077        /// Global contract identifier.
1078        id: GlobalContractIdentifierView,
1079        /// Target shard ID.
1080        target_shard: u64,
1081        /// Shards that have already received this contract.
1082        #[serde(default)]
1083        already_delivered_shards: Vec<u64>,
1084        /// Code bytes (base64).
1085        code: String,
1086        /// Nonce (present in v2 receipts).
1087        #[serde(default)]
1088        nonce: Option<u64>,
1089    },
1090}
1091
1092/// Data receiver for output data in action receipts.
1093#[derive(Debug, Clone, Deserialize)]
1094pub struct DataReceiverView {
1095    /// Data ID.
1096    pub data_id: CryptoHash,
1097    /// Receiver account ID.
1098    pub receiver_id: AccountId,
1099}
1100
1101/// Action receipt data.
1102#[derive(Debug, Clone, Deserialize)]
1103pub struct ActionReceiptData {
1104    /// Signer account ID.
1105    pub signer_id: AccountId,
1106    /// Signer public key.
1107    pub signer_public_key: PublicKey,
1108    /// Gas price for this receipt.
1109    pub gas_price: NearToken,
1110    /// Output data receivers.
1111    #[serde(default)]
1112    pub output_data_receivers: Vec<DataReceiverView>,
1113    /// Input data IDs.
1114    #[serde(default)]
1115    pub input_data_ids: Vec<CryptoHash>,
1116    /// Actions in this receipt.
1117    pub actions: Vec<ActionView>,
1118    /// Whether this is a promise yield.
1119    #[serde(default)]
1120    pub is_promise_yield: Option<bool>,
1121}
1122
1123/// Data receipt data.
1124#[derive(Debug, Clone, Deserialize)]
1125pub struct DataReceiptData {
1126    /// Data ID.
1127    pub data_id: CryptoHash,
1128    /// Data content (optional).
1129    #[serde(default)]
1130    pub data: Option<String>,
1131}
1132
1133// ============================================================================
1134// Node status types
1135// ============================================================================
1136
1137/// Node status response.
1138#[derive(Debug, Clone, Deserialize)]
1139pub struct StatusResponse {
1140    /// Protocol version.
1141    pub protocol_version: u32,
1142    /// Latest protocol version supported.
1143    pub latest_protocol_version: u32,
1144    /// Chain ID.
1145    pub chain_id: String,
1146    /// Genesis hash.
1147    pub genesis_hash: CryptoHash,
1148    /// RPC address.
1149    #[serde(default)]
1150    pub rpc_addr: Option<String>,
1151    /// Node public key.
1152    #[serde(default)]
1153    pub node_public_key: Option<String>,
1154    /// Node key (deprecated).
1155    #[serde(default)]
1156    pub node_key: Option<String>,
1157    /// Validator account ID (if validating).
1158    #[serde(default)]
1159    pub validator_account_id: Option<AccountId>,
1160    /// Validator public key (if validating).
1161    #[serde(default)]
1162    pub validator_public_key: Option<PublicKey>,
1163    /// List of current validators.
1164    #[serde(default)]
1165    pub validators: Vec<ValidatorInfo>,
1166    /// Sync information.
1167    pub sync_info: SyncInfo,
1168    /// Node version.
1169    pub version: NodeVersion,
1170    /// Uptime in seconds.
1171    #[serde(default)]
1172    pub uptime_sec: Option<u64>,
1173}
1174
1175/// Validator information.
1176#[derive(Debug, Clone, Deserialize)]
1177pub struct ValidatorInfo {
1178    /// Validator account ID.
1179    pub account_id: AccountId,
1180}
1181
1182/// Sync information.
1183#[derive(Debug, Clone, Deserialize)]
1184pub struct SyncInfo {
1185    /// Latest block hash.
1186    pub latest_block_hash: CryptoHash,
1187    /// Latest block height.
1188    pub latest_block_height: u64,
1189    /// Latest state root.
1190    #[serde(default)]
1191    pub latest_state_root: Option<CryptoHash>,
1192    /// Latest block timestamp.
1193    pub latest_block_time: String,
1194    /// Whether the node is syncing.
1195    pub syncing: bool,
1196    /// Earliest block hash (if available).
1197    #[serde(default)]
1198    pub earliest_block_hash: Option<CryptoHash>,
1199    /// Earliest block height (if available).
1200    #[serde(default)]
1201    pub earliest_block_height: Option<u64>,
1202    /// Earliest block time (if available).
1203    #[serde(default)]
1204    pub earliest_block_time: Option<String>,
1205    /// Current epoch ID.
1206    #[serde(default)]
1207    pub epoch_id: Option<CryptoHash>,
1208    /// Epoch start height.
1209    #[serde(default)]
1210    pub epoch_start_height: Option<u64>,
1211}
1212
1213/// Node version information.
1214#[derive(Debug, Clone, Deserialize)]
1215pub struct NodeVersion {
1216    /// Version string.
1217    pub version: String,
1218    /// Build string.
1219    pub build: String,
1220    /// Git commit hash.
1221    #[serde(default)]
1222    pub commit: Option<String>,
1223    /// Rust compiler version.
1224    #[serde(default)]
1225    pub rustc_version: Option<String>,
1226}
1227
1228#[cfg(test)]
1229mod tests {
1230    use super::*;
1231
1232    fn make_account_view(amount: u128, locked: u128, storage_usage: u64) -> AccountView {
1233        AccountView {
1234            amount: NearToken::from_yoctonear(amount),
1235            locked: NearToken::from_yoctonear(locked),
1236            code_hash: CryptoHash::default(),
1237            storage_usage,
1238            storage_paid_at: 0,
1239            global_contract_hash: None,
1240            global_contract_account_id: None,
1241            block_height: 0,
1242            block_hash: CryptoHash::default(),
1243        }
1244    }
1245
1246    #[test]
1247    fn test_available_balance_no_stake_no_storage() {
1248        // No storage, no stake -> all balance is available
1249        let view = make_account_view(1_000_000_000_000_000_000_000_000, 0, 0); // 1 NEAR
1250        assert_eq!(view.available(), view.amount);
1251    }
1252
1253    #[test]
1254    fn test_available_balance_with_storage_no_stake() {
1255        // 1000 bytes storage (= 0.00001 NEAR * 1000 = 0.01 NEAR = 10^22 yocto)
1256        // Amount: 1 NEAR = 10^24 yocto
1257        // Available should be: 1 NEAR - 0.01 NEAR = 0.99 NEAR
1258        let amount = 1_000_000_000_000_000_000_000_000u128; // 1 NEAR
1259        let storage_usage = 1000u64;
1260        let storage_cost = STORAGE_AMOUNT_PER_BYTE * storage_usage as u128; // 10^22
1261
1262        let view = make_account_view(amount, 0, storage_usage);
1263        let expected = NearToken::from_yoctonear(amount - storage_cost);
1264        assert_eq!(view.available(), expected);
1265    }
1266
1267    #[test]
1268    fn test_available_balance_stake_covers_storage() {
1269        // Staked amount >= storage cost -> all liquid balance is available
1270        // 1000 bytes storage = 10^22 yocto cost
1271        // 1 NEAR staked = 10^24 yocto (more than storage cost)
1272        let amount = 1_000_000_000_000_000_000_000_000u128; // 1 NEAR liquid
1273        let locked = 1_000_000_000_000_000_000_000_000u128; // 1 NEAR staked
1274        let storage_usage = 1000u64;
1275
1276        let view = make_account_view(amount, locked, storage_usage);
1277        // All liquid balance should be available since stake covers storage
1278        assert_eq!(view.available(), view.amount);
1279    }
1280
1281    #[test]
1282    fn test_available_balance_stake_partially_covers_storage() {
1283        // Staked = 0.005 NEAR = 5 * 10^21 yocto
1284        // Storage = 1000 bytes = 0.01 NEAR = 10^22 yocto
1285        // Reserved = 0.01 - 0.005 = 0.005 NEAR = 5 * 10^21 yocto
1286        // Amount = 1 NEAR
1287        // Available = 1 NEAR - 0.005 NEAR = 0.995 NEAR
1288        let amount = 1_000_000_000_000_000_000_000_000u128; // 1 NEAR
1289        let locked = 5_000_000_000_000_000_000_000u128; // 0.005 NEAR
1290        let storage_usage = 1000u64;
1291        let storage_cost = STORAGE_AMOUNT_PER_BYTE * storage_usage as u128; // 10^22
1292        let reserved = storage_cost - locked; // 5 * 10^21
1293
1294        let view = make_account_view(amount, locked, storage_usage);
1295        let expected = NearToken::from_yoctonear(amount - reserved);
1296        assert_eq!(view.available(), expected);
1297    }
1298
1299    #[test]
1300    fn test_storage_cost_calculation() {
1301        let storage_usage = 1000u64;
1302        let view = make_account_view(1_000_000_000_000_000_000_000_000, 0, storage_usage);
1303
1304        let expected_cost = STORAGE_AMOUNT_PER_BYTE * storage_usage as u128;
1305        assert_eq!(
1306            view.storage_cost(),
1307            NearToken::from_yoctonear(expected_cost)
1308        );
1309    }
1310
1311    #[test]
1312    fn test_storage_cost_zero_when_stake_covers() {
1313        // Staked > storage cost -> storage_cost returns 0
1314        let locked = 1_000_000_000_000_000_000_000_000u128; // 1 NEAR
1315        let view = make_account_view(1_000_000_000_000_000_000_000_000, locked, 1000);
1316
1317        assert_eq!(view.storage_cost(), NearToken::ZERO);
1318    }
1319
1320    #[test]
1321    fn test_account_balance_from_view() {
1322        let amount = 1_000_000_000_000_000_000_000_000u128; // 1 NEAR
1323        let locked = 500_000_000_000_000_000_000_000u128; // 0.5 NEAR
1324        let storage_usage = 1000u64;
1325
1326        let view = make_account_view(amount, locked, storage_usage);
1327        let balance = AccountBalance::from(view.clone());
1328
1329        assert_eq!(balance.total, view.amount);
1330        assert_eq!(balance.available, view.available());
1331        assert_eq!(balance.locked, view.locked);
1332        assert_eq!(balance.storage_cost, view.storage_cost());
1333        assert_eq!(balance.storage_usage, storage_usage);
1334    }
1335
1336    // ========================================================================
1337    // ViewFunctionResult tests
1338    // ========================================================================
1339
1340    fn make_view_result(result: Vec<u8>) -> ViewFunctionResult {
1341        ViewFunctionResult {
1342            result,
1343            logs: vec![],
1344            block_height: 12345,
1345            block_hash: CryptoHash::default(),
1346        }
1347    }
1348
1349    #[test]
1350    fn test_view_function_result_bytes() {
1351        let data = vec![1, 2, 3, 4, 5];
1352        let result = make_view_result(data.clone());
1353        assert_eq!(result.bytes(), &data[..]);
1354    }
1355
1356    #[test]
1357    fn test_view_function_result_as_string() {
1358        let result = make_view_result(b"hello world".to_vec());
1359        assert_eq!(result.as_string().unwrap(), "hello world");
1360    }
1361
1362    #[test]
1363    fn test_view_function_result_json() {
1364        let result = make_view_result(b"42".to_vec());
1365        let value: u64 = result.json().unwrap();
1366        assert_eq!(value, 42);
1367    }
1368
1369    #[test]
1370    fn test_view_function_result_json_object() {
1371        let result = make_view_result(b"{\"count\":123}".to_vec());
1372        let value: serde_json::Value = result.json().unwrap();
1373        assert_eq!(value["count"], 123);
1374    }
1375
1376    #[test]
1377    fn test_view_function_result_borsh() {
1378        // Borsh-encode a u64 value
1379        let original: u64 = 42;
1380        let encoded = borsh::to_vec(&original).unwrap();
1381        let result = make_view_result(encoded);
1382
1383        let decoded: u64 = result.borsh().unwrap();
1384        assert_eq!(decoded, original);
1385    }
1386
1387    #[test]
1388    fn test_view_function_result_borsh_struct() {
1389        #[derive(borsh::BorshSerialize, borsh::BorshDeserialize, PartialEq, Debug)]
1390        struct TestStruct {
1391            value: u64,
1392            name: String,
1393        }
1394
1395        let original = TestStruct {
1396            value: 123,
1397            name: "test".to_string(),
1398        };
1399        let encoded = borsh::to_vec(&original).unwrap();
1400        let result = make_view_result(encoded);
1401
1402        let decoded: TestStruct = result.borsh().unwrap();
1403        assert_eq!(decoded, original);
1404    }
1405
1406    #[test]
1407    fn test_view_function_result_borsh_error() {
1408        // Invalid Borsh data for a u64 (too short)
1409        let result = make_view_result(vec![1, 2, 3]);
1410        let decoded: Result<u64, _> = result.borsh();
1411        assert!(decoded.is_err());
1412    }
1413
1414    // ========================================================================
1415    // GasProfileEntry tests
1416    // ========================================================================
1417
1418    #[test]
1419    fn test_gas_profile_entry_string_gas_used() {
1420        let json = serde_json::json!({
1421            "cost_category": "WASM_HOST_COST",
1422            "cost": "BASE",
1423            "gas_used": "123456789"
1424        });
1425        let entry: GasProfileEntry = serde_json::from_value(json).unwrap();
1426        assert_eq!(entry.gas_used.as_gas(), 123456789);
1427    }
1428
1429    #[test]
1430    fn test_gas_profile_entry_numeric_gas_used() {
1431        let json = serde_json::json!({
1432            "cost_category": "ACTION_COST",
1433            "cost": "FUNCTION_CALL",
1434            "gas_used": 999000000
1435        });
1436        let entry: GasProfileEntry = serde_json::from_value(json).unwrap();
1437        assert_eq!(entry.gas_used.as_gas(), 999000000);
1438    }
1439
1440    #[test]
1441    fn test_gas_key_function_call_deserialization() {
1442        let json = serde_json::json!({
1443            "GasKeyFunctionCall": {
1444                "balance": "1000000000000000000000000",
1445                "num_nonces": 5,
1446                "allowance": "500000000000000000000000",
1447                "receiver_id": "app.near",
1448                "method_names": ["call_method"]
1449            }
1450        });
1451        let perm: AccessKeyPermissionView = serde_json::from_value(json).unwrap();
1452        assert!(matches!(
1453            perm,
1454            AccessKeyPermissionView::GasKeyFunctionCall { .. }
1455        ));
1456    }
1457
1458    #[test]
1459    fn test_gas_key_full_access_deserialization() {
1460        let json = serde_json::json!({
1461            "GasKeyFullAccess": {
1462                "balance": "1000000000000000000000000",
1463                "num_nonces": 10
1464            }
1465        });
1466        let perm: AccessKeyPermissionView = serde_json::from_value(json).unwrap();
1467        assert!(matches!(
1468            perm,
1469            AccessKeyPermissionView::GasKeyFullAccess { .. }
1470        ));
1471    }
1472
1473    #[test]
1474    fn test_transfer_to_gas_key_action_view_deserialization() {
1475        let json = serde_json::json!({
1476            "TransferToGasKey": {
1477                "public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
1478                "deposit": "1000000000000000000000000"
1479            }
1480        });
1481        let action: ActionView = serde_json::from_value(json).unwrap();
1482        assert!(matches!(action, ActionView::TransferToGasKey { .. }));
1483    }
1484
1485    #[test]
1486    fn test_delegate_action_view_deserialization() {
1487        let json = serde_json::json!({
1488            "Delegate": {
1489                "delegate_action": {
1490                    "sender_id": "alice.near",
1491                    "receiver_id": "contract.near",
1492                    "actions": [
1493                        {"FunctionCall": {
1494                            "method_name": "do_something",
1495                            "args": "e30=",
1496                            "gas": 30000000000000_u64,
1497                            "deposit": "0"
1498                        }}
1499                    ],
1500                    "nonce": 42,
1501                    "max_block_height": 100000,
1502                    "public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp"
1503                },
1504                "signature": "ed25519:3s1dvMqNDCByoMnDnkhB4GPjTSXCRt4nt3Af5n1RX8W7aJ2FC6MfRf5BNXZ52EBifNJnNVBsGvke6GRYuaEYJXt5"
1505            }
1506        });
1507        let action: ActionView = serde_json::from_value(json).unwrap();
1508        match action {
1509            ActionView::Delegate {
1510                delegate_action,
1511                signature,
1512            } => {
1513                assert_eq!(delegate_action.sender_id.as_str(), "alice.near");
1514                assert_eq!(delegate_action.receiver_id.as_str(), "contract.near");
1515                assert_eq!(delegate_action.nonce, 42);
1516                assert_eq!(delegate_action.max_block_height, 100000);
1517                assert_eq!(delegate_action.actions.len(), 1);
1518                assert!(signature.to_string().starts_with("ed25519:"));
1519            }
1520            _ => panic!("Expected Delegate action"),
1521        }
1522    }
1523
1524    #[test]
1525    fn test_withdraw_from_gas_key_action_view_deserialization() {
1526        let json = serde_json::json!({
1527            "WithdrawFromGasKey": {
1528                "public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
1529                "amount": "500000000000000000000000"
1530            }
1531        });
1532        let action: ActionView = serde_json::from_value(json).unwrap();
1533        assert!(matches!(action, ActionView::WithdrawFromGasKey { .. }));
1534    }
1535
1536    // ========================================================================
1537    // FinalExecutionStatus tests
1538    // ========================================================================
1539
1540    #[test]
1541    fn test_final_execution_status_default() {
1542        let status = FinalExecutionStatus::default();
1543        assert!(matches!(status, FinalExecutionStatus::NotStarted));
1544    }
1545
1546    #[test]
1547    fn test_final_execution_status_not_started() {
1548        let json = serde_json::json!("NotStarted");
1549        let status: FinalExecutionStatus = serde_json::from_value(json).unwrap();
1550        assert!(matches!(status, FinalExecutionStatus::NotStarted));
1551    }
1552
1553    #[test]
1554    fn test_final_execution_status_started() {
1555        let json = serde_json::json!("Started");
1556        let status: FinalExecutionStatus = serde_json::from_value(json).unwrap();
1557        assert!(matches!(status, FinalExecutionStatus::Started));
1558    }
1559
1560    #[test]
1561    fn test_final_execution_status_success_value() {
1562        let json = serde_json::json!({"SuccessValue": "aGVsbG8="});
1563        let status: FinalExecutionStatus = serde_json::from_value(json).unwrap();
1564        assert!(matches!(status, FinalExecutionStatus::SuccessValue(ref s) if s == "aGVsbG8="));
1565    }
1566
1567    #[test]
1568    fn test_final_execution_status_failure() {
1569        let json = serde_json::json!({
1570            "Failure": {
1571                "ActionError": {
1572                    "index": 0,
1573                    "kind": {
1574                        "FunctionCallError": {
1575                            "ExecutionError": "Smart contract panicked"
1576                        }
1577                    }
1578                }
1579            }
1580        });
1581        let status: FinalExecutionStatus = serde_json::from_value(json).unwrap();
1582        assert!(matches!(status, FinalExecutionStatus::Failure(_)));
1583    }
1584
1585    // ========================================================================
1586    // FinalExecutionOutcome helper tests
1587    // ========================================================================
1588
1589    #[test]
1590    fn test_send_tx_response_with_outcome() {
1591        let json = serde_json::json!({
1592            "final_execution_status": "FINAL",
1593            "status": {"SuccessValue": ""},
1594            "transaction": {
1595                "signer_id": "alice.near",
1596                "public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
1597                "nonce": 1,
1598                "receiver_id": "bob.near",
1599                "actions": [{"Transfer": {"deposit": "1000000000000000000000000"}}],
1600                "signature": "ed25519:3s1dvMqNDCByoMnDnkhB4GPjTSXCRt4nt3Af5n1RX8W7aJ2FC6MfRf5BNXZ52EBifNJnNVBsGvke6GRYuaEYJXt5",
1601                "hash": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U"
1602            },
1603            "transaction_outcome": {
1604                "id": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U",
1605                "outcome": {
1606                    "executor_id": "alice.near",
1607                    "gas_burnt": 223182562500_i64,
1608                    "tokens_burnt": "22318256250000000000",
1609                    "logs": [],
1610                    "receipt_ids": ["3GTGoiN3FEoJenSw5ob4YMmFEV2Fbiichj3FDBnM78xK"],
1611                    "status": {"SuccessReceiptId": "3GTGoiN3FEoJenSw5ob4YMmFEV2Fbiichj3FDBnM78xK"}
1612                },
1613                "block_hash": "A6DJpKBhmAMmBuQXtY3dWbo8dGVSQ9yH7BQSJBfn8rBo",
1614                "proof": []
1615            },
1616            "receipts_outcome": []
1617        });
1618        let response: RawTransactionResponse = serde_json::from_value(json).unwrap();
1619        assert_eq!(response.final_execution_status, TxExecutionStatus::Final);
1620        let outcome = response.outcome.unwrap();
1621        assert!(outcome.is_success());
1622        assert!(!outcome.is_failure());
1623    }
1624
1625    #[test]
1626    fn test_send_tx_response_pending_none() {
1627        let json = serde_json::json!({
1628            "final_execution_status": "NONE"
1629        });
1630        let response: RawTransactionResponse = serde_json::from_value(json).unwrap();
1631        assert_eq!(response.final_execution_status, TxExecutionStatus::None);
1632        assert!(response.outcome.is_none());
1633        // transaction_hash is serde(skip) — populated by rpc methods, not deserialization
1634        assert!(response.transaction_hash.is_zero());
1635    }
1636
1637    #[test]
1638    fn test_final_execution_outcome_failure() {
1639        let json = serde_json::json!({
1640            "final_execution_status": "EXECUTED_OPTIMISTIC",
1641            "status": {
1642                "Failure": {
1643                    "ActionError": {
1644                        "index": 0,
1645                        "kind": {
1646                            "FunctionCallError": {
1647                                "ExecutionError": "Smart contract panicked"
1648                            }
1649                        }
1650                    }
1651                }
1652            },
1653            "transaction": {
1654                "signer_id": "alice.near",
1655                "public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
1656                "nonce": 1,
1657                "receiver_id": "bob.near",
1658                "actions": [],
1659                "signature": "ed25519:3s1dvMqNDCByoMnDnkhB4GPjTSXCRt4nt3Af5n1RX8W7aJ2FC6MfRf5BNXZ52EBifNJnNVBsGvke6GRYuaEYJXt5",
1660                "hash": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U"
1661            },
1662            "transaction_outcome": {
1663                "id": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U",
1664                "outcome": {
1665                    "executor_id": "alice.near",
1666                    "gas_burnt": 0,
1667                    "tokens_burnt": "0",
1668                    "logs": [],
1669                    "receipt_ids": [],
1670                    "status": "Unknown"
1671                },
1672                "block_hash": "A6DJpKBhmAMmBuQXtY3dWbo8dGVSQ9yH7BQSJBfn8rBo",
1673                "proof": []
1674            },
1675            "receipts_outcome": []
1676        });
1677        let response: RawTransactionResponse = serde_json::from_value(json).unwrap();
1678        let outcome = response.outcome.unwrap();
1679        assert!(outcome.is_failure());
1680        assert!(!outcome.is_success());
1681        assert!(outcome.failure_message().is_some());
1682        assert!(outcome.failure_error().is_some());
1683    }
1684
1685    // ========================================================================
1686    // FinalExecutionOutcome result/json tests
1687    // ========================================================================
1688
1689    fn make_success_outcome(base64_value: &str) -> FinalExecutionOutcome {
1690        let json = serde_json::json!({
1691            "final_execution_status": "FINAL",
1692            "status": {"SuccessValue": base64_value},
1693            "transaction": {
1694                "signer_id": "alice.near",
1695                "public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
1696                "nonce": 1,
1697                "receiver_id": "bob.near",
1698                "actions": [],
1699                "signature": "ed25519:3s1dvMqNDCByoMnDnkhB4GPjTSXCRt4nt3Af5n1RX8W7aJ2FC6MfRf5BNXZ52EBifNJnNVBsGvke6GRYuaEYJXt5",
1700                "hash": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U"
1701            },
1702            "transaction_outcome": {
1703                "id": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U",
1704                "outcome": {
1705                    "executor_id": "alice.near",
1706                    "gas_burnt": 223182562500_i64,
1707                    "tokens_burnt": "22318256250000000000",
1708                    "logs": [],
1709                    "receipt_ids": [],
1710                    "status": {"SuccessReceiptId": "3GTGoiN3FEoJenSw5ob4YMmFEV2Fbiichj3FDBnM78xK"}
1711                },
1712                "block_hash": "A6DJpKBhmAMmBuQXtY3dWbo8dGVSQ9yH7BQSJBfn8rBo",
1713                "proof": []
1714            },
1715            "receipts_outcome": []
1716        });
1717        let response: RawTransactionResponse = serde_json::from_value(json).unwrap();
1718        response.outcome.unwrap()
1719    }
1720
1721    #[test]
1722    fn test_outcome_result() {
1723        // "hello" in base64
1724        let outcome = make_success_outcome("aGVsbG8=");
1725        assert_eq!(outcome.result().unwrap(), b"hello");
1726    }
1727
1728    #[test]
1729    fn test_outcome_result_empty() {
1730        let outcome = make_success_outcome("");
1731        assert_eq!(outcome.result().unwrap(), b"");
1732    }
1733
1734    #[test]
1735    fn test_outcome_json() {
1736        // JSON `42` in base64
1737        let outcome = make_success_outcome("NDI=");
1738        let val: u64 = outcome.json().unwrap();
1739        assert_eq!(val, 42);
1740    }
1741
1742    #[test]
1743    fn test_outcome_json_bad_data() {
1744        // "hello" is not valid JSON
1745        let outcome = make_success_outcome("aGVsbG8=");
1746        let result: Result<u64, _> = outcome.json();
1747        assert!(result.is_err());
1748    }
1749
1750    #[test]
1751    fn test_outcome_result_invalid_base64_returns_err() {
1752        // If the RPC somehow returns invalid base64 (protocol bug),
1753        // result() returns an error so callers can distinguish it from empty values.
1754        let outcome = make_success_outcome("not-valid-base64!!!");
1755        let err = outcome.result().unwrap_err();
1756        assert!(
1757            err.to_string().contains("base64"),
1758            "Error should mention base64 decode failure, got: {err}"
1759        );
1760    }
1761
1762    #[test]
1763    fn test_outcome_failure_result_returns_err() {
1764        let json = serde_json::json!({
1765            "final_execution_status": "FINAL",
1766            "status": {"Failure": {"ActionError": {"index": 0, "kind": {"FunctionCallError": {"ExecutionError": "test error"}}}}},
1767            "transaction": {
1768                "signer_id": "alice.near",
1769                "public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
1770                "nonce": 1,
1771                "receiver_id": "bob.near",
1772                "actions": [],
1773                "signature": "ed25519:3s1dvMqNDCByoMnDnkhB4GPjTSXCRt4nt3Af5n1RX8W7aJ2FC6MfRf5BNXZ52EBifNJnNVBsGvke6GRYuaEYJXt5",
1774                "hash": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U"
1775            },
1776            "transaction_outcome": {
1777                "id": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U",
1778                "outcome": {
1779                    "executor_id": "alice.near",
1780                    "gas_burnt": 223182562500_i64,
1781                    "tokens_burnt": "22318256250000000000",
1782                    "logs": [],
1783                    "receipt_ids": [],
1784                    "status": {"SuccessReceiptId": "3GTGoiN3FEoJenSw5ob4YMmFEV2Fbiichj3FDBnM78xK"}
1785                },
1786                "block_hash": "A6DJpKBhmAMmBuQXtY3dWbo8dGVSQ9yH7BQSJBfn8rBo",
1787                "proof": []
1788            },
1789            "receipts_outcome": []
1790        });
1791        let outcome: FinalExecutionOutcome = serde_json::from_value(json).unwrap();
1792
1793        assert!(outcome.is_failure());
1794        assert!(!outcome.is_success());
1795        assert!(outcome.result().is_err());
1796        assert!(outcome.json::<u64>().is_err());
1797        // Metadata is still accessible
1798        assert!(!outcome.transaction_hash().is_zero());
1799        assert!(outcome.total_gas_used().as_gas() > 0);
1800    }
1801
1802    // ========================================================================
1803    // ExecutionStatus tests (per-receipt)
1804    // ========================================================================
1805
1806    #[test]
1807    fn test_execution_status_unknown() {
1808        let json = serde_json::json!("Unknown");
1809        let status: ExecutionStatus = serde_json::from_value(json).unwrap();
1810        assert!(matches!(status, ExecutionStatus::Unknown));
1811    }
1812
1813    #[test]
1814    fn test_execution_status_success_value() {
1815        let json = serde_json::json!({"SuccessValue": "aGVsbG8="});
1816        let status: ExecutionStatus = serde_json::from_value(json).unwrap();
1817        assert!(matches!(status, ExecutionStatus::SuccessValue(_)));
1818    }
1819
1820    #[test]
1821    fn test_execution_status_success_receipt_id() {
1822        let json =
1823            serde_json::json!({"SuccessReceiptId": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U"});
1824        let status: ExecutionStatus = serde_json::from_value(json).unwrap();
1825        assert!(matches!(status, ExecutionStatus::SuccessReceiptId(_)));
1826    }
1827
1828    #[test]
1829    fn test_execution_status_failure_action_error() {
1830        let json = serde_json::json!({
1831            "Failure": {
1832                "ActionError": {
1833                    "index": 0,
1834                    "kind": {
1835                        "FunctionCallError": {
1836                            "ExecutionError": "Smart contract panicked"
1837                        }
1838                    }
1839                }
1840            }
1841        });
1842        let status: ExecutionStatus = serde_json::from_value(json).unwrap();
1843        match status {
1844            ExecutionStatus::Failure(ae) => {
1845                assert_eq!(ae.index, Some(0));
1846            }
1847            other => panic!("expected Failure, got: {other:?}"),
1848        }
1849    }
1850
1851    #[test]
1852    fn test_execution_status_failure_invalid_tx_error_rejected() {
1853        let json = serde_json::json!({
1854            "Failure": {
1855                "InvalidTxError": "InvalidSignature"
1856            }
1857        });
1858        let err = serde_json::from_value::<ExecutionStatus>(json)
1859            .expect_err("InvalidTxError should be rejected in receipt execution status");
1860        let msg = err.to_string();
1861        assert!(
1862            msg.contains("unexpected InvalidTxError"),
1863            "Expected descriptive error containing \"unexpected InvalidTxError\", got: {msg}"
1864        );
1865    }
1866
1867    // ========================================================================
1868    // GlobalContractIdentifierView tests
1869    // ========================================================================
1870
1871    #[test]
1872    fn test_global_contract_id_view_new_format_hash() {
1873        let json = serde_json::json!({"hash": "9SP8Y3sVADWNN5QoEB5CsvPUE5HT4o8YfBaCnhLss87K"});
1874        let id: GlobalContractIdentifierView = serde_json::from_value(json).unwrap();
1875        assert!(matches!(id, GlobalContractIdentifierView::CodeHash(_)));
1876    }
1877
1878    #[test]
1879    fn test_global_contract_id_view_new_format_account() {
1880        let json = serde_json::json!({"account_id": "alice.near"});
1881        let id: GlobalContractIdentifierView = serde_json::from_value(json).unwrap();
1882        assert!(matches!(id, GlobalContractIdentifierView::AccountId(_)));
1883    }
1884
1885    #[test]
1886    fn test_global_contract_id_view_deprecated_hash() {
1887        let json = serde_json::json!("9SP8Y3sVADWNN5QoEB5CsvPUE5HT4o8YfBaCnhLss87K");
1888        let id: GlobalContractIdentifierView = serde_json::from_value(json).unwrap();
1889        assert!(matches!(id, GlobalContractIdentifierView::CodeHash(_)));
1890    }
1891
1892    #[test]
1893    fn test_global_contract_id_view_deprecated_account() {
1894        let json = serde_json::json!("alice.near");
1895        let id: GlobalContractIdentifierView = serde_json::from_value(json).unwrap();
1896        assert!(matches!(id, GlobalContractIdentifierView::AccountId(_)));
1897    }
1898
1899    // ========================================================================
1900    // DeterministicStateInit ActionView tests
1901    // ========================================================================
1902
1903    #[test]
1904    fn test_action_view_deterministic_state_init() {
1905        let json = serde_json::json!({
1906            "DeterministicStateInit": {
1907                "code": {"hash": "9SP8Y3sVADWNN5QoEB5CsvPUE5HT4o8YfBaCnhLss87K"},
1908                "data": {"a2V5": "dmFsdWU="},
1909                "deposit": "1000000000000000000000000"
1910            }
1911        });
1912        let action: ActionView = serde_json::from_value(json).unwrap();
1913        match action {
1914            ActionView::DeterministicStateInit {
1915                code,
1916                data,
1917                deposit,
1918            } => {
1919                assert!(matches!(code, GlobalContractIdentifierView::CodeHash(_)));
1920                assert_eq!(data.len(), 1);
1921                assert_eq!(data.get("a2V5").unwrap(), "dmFsdWU=");
1922                assert_eq!(deposit, NearToken::from_near(1));
1923            }
1924            _ => panic!("Expected DeterministicStateInit"),
1925        }
1926    }
1927
1928    #[test]
1929    fn test_action_view_deterministic_state_init_empty_data() {
1930        let json = serde_json::json!({
1931            "DeterministicStateInit": {
1932                "code": {"account_id": "publisher.near"},
1933                "deposit": "0"
1934            }
1935        });
1936        let action: ActionView = serde_json::from_value(json).unwrap();
1937        match action {
1938            ActionView::DeterministicStateInit { code, data, .. } => {
1939                assert!(matches!(code, GlobalContractIdentifierView::AccountId(_)));
1940                assert!(data.is_empty());
1941            }
1942            _ => panic!("Expected DeterministicStateInit"),
1943        }
1944    }
1945
1946    // ========================================================================
1947    // GlobalContractDistribution receipt tests
1948    // ========================================================================
1949
1950    #[test]
1951    fn test_receipt_global_contract_distribution() {
1952        let json = serde_json::json!({
1953            "GlobalContractDistribution": {
1954                "id": {"hash": "9SP8Y3sVADWNN5QoEB5CsvPUE5HT4o8YfBaCnhLss87K"},
1955                "target_shard": 3,
1956                "already_delivered_shards": [0, 1, 2],
1957                "code": "AGFzbQ==",
1958                "nonce": 42
1959            }
1960        });
1961        let content: ReceiptContent = serde_json::from_value(json).unwrap();
1962        match content {
1963            ReceiptContent::GlobalContractDistribution {
1964                id,
1965                target_shard,
1966                already_delivered_shards,
1967                code,
1968                nonce,
1969            } => {
1970                assert!(matches!(id, GlobalContractIdentifierView::CodeHash(_)));
1971                assert_eq!(target_shard, 3);
1972                assert_eq!(already_delivered_shards, vec![0, 1, 2]);
1973                assert_eq!(code, "AGFzbQ==");
1974                assert_eq!(nonce, Some(42));
1975            }
1976            _ => panic!("Expected GlobalContractDistribution"),
1977        }
1978    }
1979
1980    #[test]
1981    fn test_receipt_global_contract_distribution_without_nonce() {
1982        let json = serde_json::json!({
1983            "GlobalContractDistribution": {
1984                "id": {"account_id": "publisher.near"},
1985                "target_shard": 0,
1986                "already_delivered_shards": [],
1987                "code": "AGFzbQ=="
1988            }
1989        });
1990        let content: ReceiptContent = serde_json::from_value(json).unwrap();
1991        match content {
1992            ReceiptContent::GlobalContractDistribution { nonce, .. } => {
1993                assert_eq!(nonce, None);
1994            }
1995            _ => panic!("Expected GlobalContractDistribution"),
1996        }
1997    }
1998
1999    #[test]
2000    fn test_gas_profile_entry_deserialization() {
2001        let json = serde_json::json!({
2002            "cost_category": "WASM_HOST_COST",
2003            "cost": "BASE",
2004            "gas_used": "2646228750"
2005        });
2006        let entry: GasProfileEntry = serde_json::from_value(json).unwrap();
2007        assert_eq!(entry.cost_category, "WASM_HOST_COST");
2008        assert_eq!(entry.cost, "BASE");
2009        assert_eq!(entry.gas_used, Gas::from_gas(2646228750));
2010    }
2011
2012    #[test]
2013    fn test_transaction_view_with_signature() {
2014        let json = serde_json::json!({
2015            "signer_id": "alice.near",
2016            "public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
2017            "nonce": 1,
2018            "receiver_id": "bob.near",
2019            "hash": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U",
2020            "actions": [{"Transfer": {"deposit": "1000000000000000000000000"}}],
2021            "signature": "ed25519:3s1dvMqNDCByoMnDnkhB4GPjTSXCRt4nt3Af5n1RX8W7aJ2FC6MfRf5BNXZ52EBifNJnNVBsGvke6GRYuaEYJXt5"
2022        });
2023        let tx: TransactionView = serde_json::from_value(json).unwrap();
2024        assert_eq!(tx.signer_id.as_str(), "alice.near");
2025        assert!(tx.signature.to_string().starts_with("ed25519:"));
2026    }
2027}