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