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::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 success value if present (base64 decoded).
594    pub fn success_value(&self) -> Option<Vec<u8>> {
595        match &self.status {
596            FinalExecutionStatus::SuccessValue(s) => STANDARD.decode(s).ok(),
597            _ => None,
598        }
599    }
600
601    /// Get the success value as a string if present.
602    pub fn success_value_string(&self) -> Option<String> {
603        self.success_value().and_then(|v| String::from_utf8(v).ok())
604    }
605
606    /// Get the success value deserialized as JSON.
607    pub fn success_value_json<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
608        self.success_value()
609            .and_then(|v| serde_json::from_slice(&v).ok())
610    }
611
612    /// Get the failure message if present.
613    pub fn failure_message(&self) -> Option<String> {
614        match &self.status {
615            FinalExecutionStatus::Failure(err) => Some(err.to_string()),
616            _ => None,
617        }
618    }
619
620    /// Get the typed execution error if present.
621    pub fn failure_error(&self) -> Option<&TxExecutionError> {
622        match &self.status {
623            FinalExecutionStatus::Failure(err) => Some(err),
624            _ => None,
625        }
626    }
627
628    /// Get the transaction hash.
629    pub fn transaction_hash(&self) -> &CryptoHash {
630        &self.transaction_outcome.id
631    }
632
633    /// Get total gas used across all receipts.
634    pub fn total_gas_used(&self) -> Gas {
635        let tx_gas = self.transaction_outcome.outcome.gas_burnt.as_gas();
636        let receipt_gas: u64 = self
637            .receipts_outcome
638            .iter()
639            .map(|r| r.outcome.gas_burnt.as_gas())
640            .sum();
641        Gas::from_gas(tx_gas + receipt_gas)
642    }
643}
644
645/// Per-receipt execution status.
646///
647/// Matches nearcore's `ExecutionStatusView`. Used in [`ExecutionOutcome`].
648#[derive(Debug, Clone, Deserialize)]
649pub enum ExecutionStatus {
650    /// The execution is pending or unknown.
651    Unknown,
652    /// Execution failed.
653    Failure(TxExecutionError),
654    /// Execution succeeded with a return value (base64 encoded).
655    SuccessValue(String),
656    /// Execution succeeded, producing a receipt.
657    SuccessReceiptId(CryptoHash),
658}
659
660/// Transaction view in outcome.
661#[derive(Debug, Clone, Deserialize)]
662pub struct TransactionView {
663    /// Signer account.
664    pub signer_id: AccountId,
665    /// Signer public key.
666    pub public_key: PublicKey,
667    /// Transaction nonce.
668    pub nonce: u64,
669    /// Receiver account.
670    pub receiver_id: AccountId,
671    /// Transaction hash.
672    pub hash: CryptoHash,
673    /// Actions in the transaction.
674    #[serde(default)]
675    pub actions: Vec<ActionView>,
676    /// Transaction signature.
677    pub signature: Signature,
678    /// Priority fee (optional, for congestion pricing).
679    #[serde(default)]
680    pub priority_fee: Option<u64>,
681    /// Nonce index (for gas key multi-nonce support).
682    #[serde(default)]
683    pub nonce_index: Option<u16>,
684}
685
686// ============================================================================
687// Global contract identifier view
688// ============================================================================
689
690/// Backward-compatible deserialization helper for `GlobalContractIdentifierView`.
691///
692/// Handles both the new format (`{"hash": "<base58>"}` / `{"account_id": "alice.near"}`)
693/// and the deprecated format (bare string `"<base58>"` / `"alice.near"`).
694#[derive(Deserialize)]
695#[serde(untagged)]
696enum GlobalContractIdCompat {
697    CodeHash { hash: CryptoHash },
698    AccountId { account_id: AccountId },
699    DeprecatedCodeHash(CryptoHash),
700    DeprecatedAccountId(AccountId),
701}
702
703/// Global contract identifier in RPC view responses.
704///
705/// Identifies a global contract either by its code hash (immutable) or by the
706/// publishing account ID (updatable). Supports both the current and deprecated
707/// JSON serialization formats from nearcore.
708#[derive(Debug, Clone, Deserialize)]
709#[serde(from = "GlobalContractIdCompat")]
710pub enum GlobalContractIdentifierView {
711    /// Referenced by code hash.
712    CodeHash(CryptoHash),
713    /// Referenced by publisher account ID.
714    AccountId(AccountId),
715}
716
717impl From<GlobalContractIdCompat> for GlobalContractIdentifierView {
718    fn from(compat: GlobalContractIdCompat) -> Self {
719        match compat {
720            GlobalContractIdCompat::CodeHash { hash }
721            | GlobalContractIdCompat::DeprecatedCodeHash(hash) => Self::CodeHash(hash),
722            GlobalContractIdCompat::AccountId { account_id }
723            | GlobalContractIdCompat::DeprecatedAccountId(account_id) => {
724                Self::AccountId(account_id)
725            }
726        }
727    }
728}
729
730// ============================================================================
731// Action view
732// ============================================================================
733
734/// View of a delegate action in RPC responses.
735#[derive(Debug, Clone, Deserialize)]
736pub struct DelegateActionView {
737    /// The account that signed the delegate action.
738    pub sender_id: AccountId,
739    /// The intended receiver of the inner actions.
740    pub receiver_id: AccountId,
741    /// The actions to execute.
742    pub actions: Vec<ActionView>,
743    /// Nonce for replay protection.
744    pub nonce: u64,
745    /// Maximum block height before this delegate action expires.
746    pub max_block_height: u64,
747    /// Public key of the signer.
748    pub public_key: PublicKey,
749}
750
751/// Action view in transaction.
752#[derive(Debug, Clone, Deserialize)]
753#[serde(rename_all = "PascalCase")]
754pub enum ActionView {
755    CreateAccount,
756    DeployContract {
757        code: String, // base64
758    },
759    FunctionCall {
760        method_name: String,
761        args: String, // base64
762        gas: Gas,
763        deposit: NearToken,
764    },
765    Transfer {
766        deposit: NearToken,
767    },
768    Stake {
769        stake: NearToken,
770        public_key: PublicKey,
771    },
772    AddKey {
773        public_key: PublicKey,
774        access_key: AccessKeyDetails,
775    },
776    DeleteKey {
777        public_key: PublicKey,
778    },
779    DeleteAccount {
780        beneficiary_id: AccountId,
781    },
782    Delegate {
783        delegate_action: DelegateActionView,
784        signature: Signature,
785    },
786    #[serde(rename = "DeployGlobalContract")]
787    DeployGlobalContract {
788        code: String,
789    },
790    #[serde(rename = "DeployGlobalContractByAccountId")]
791    DeployGlobalContractByAccountId {
792        code: String,
793    },
794    #[serde(rename = "UseGlobalContract")]
795    UseGlobalContract {
796        code_hash: CryptoHash,
797    },
798    #[serde(rename = "UseGlobalContractByAccountId")]
799    UseGlobalContractByAccountId {
800        account_id: AccountId,
801    },
802    #[serde(rename = "DeterministicStateInit")]
803    DeterministicStateInit {
804        code: GlobalContractIdentifierView,
805        #[serde(default)]
806        data: BTreeMap<String, String>,
807        deposit: NearToken,
808    },
809    TransferToGasKey {
810        public_key: PublicKey,
811        deposit: NearToken,
812    },
813    WithdrawFromGasKey {
814        public_key: PublicKey,
815        amount: NearToken,
816    },
817}
818
819/// Merkle path item for cryptographic proofs.
820#[derive(Debug, Clone, Deserialize)]
821pub struct MerklePathItem {
822    /// Hash at this node.
823    pub hash: CryptoHash,
824    /// Direction of the path.
825    pub direction: MerkleDirection,
826}
827
828/// Direction in merkle path.
829#[derive(Debug, Clone, Deserialize)]
830pub enum MerkleDirection {
831    Left,
832    Right,
833}
834
835/// Execution outcome with ID.
836#[derive(Debug, Clone, Deserialize)]
837pub struct ExecutionOutcomeWithId {
838    /// Receipt or transaction ID.
839    pub id: CryptoHash,
840    /// Outcome details.
841    pub outcome: ExecutionOutcome,
842    /// Proof of execution.
843    #[serde(default)]
844    pub proof: Vec<MerklePathItem>,
845    /// Block hash where this was executed.
846    pub block_hash: CryptoHash,
847}
848
849/// Execution outcome details.
850#[derive(Debug, Clone, Deserialize)]
851pub struct ExecutionOutcome {
852    /// Executor account.
853    pub executor_id: AccountId,
854    /// Gas burnt during execution.
855    pub gas_burnt: Gas,
856    /// Tokens burnt for gas.
857    pub tokens_burnt: NearToken,
858    /// Logs emitted.
859    pub logs: Vec<String>,
860    /// Receipt IDs generated.
861    pub receipt_ids: Vec<CryptoHash>,
862    /// Execution status.
863    pub status: ExecutionStatus,
864    /// Execution metadata (gas profiling).
865    #[serde(default)]
866    pub metadata: Option<ExecutionMetadata>,
867}
868
869/// Execution metadata with gas profiling.
870#[derive(Debug, Clone, Deserialize)]
871pub struct ExecutionMetadata {
872    /// Metadata version.
873    pub version: u32,
874    /// Gas profile entries.
875    #[serde(default)]
876    pub gas_profile: Option<Vec<GasProfileEntry>>,
877}
878
879/// Gas profile entry for detailed gas accounting.
880#[derive(Debug, Clone, Deserialize)]
881pub struct GasProfileEntry {
882    /// Cost category (ACTION_COST or WASM_HOST_COST).
883    pub cost_category: String,
884    /// Cost name.
885    pub cost: String,
886    /// Gas used for this cost.
887    #[serde(deserialize_with = "gas_dec_format")]
888    pub gas_used: Gas,
889}
890
891/// View function result from call_function RPC.
892#[derive(Debug, Clone, Deserialize)]
893pub struct ViewFunctionResult {
894    /// Result bytes (often JSON).
895    pub result: Vec<u8>,
896    /// Logs emitted during view call.
897    pub logs: Vec<String>,
898    /// Block height of the query.
899    pub block_height: u64,
900    /// Block hash of the query.
901    pub block_hash: CryptoHash,
902}
903
904impl ViewFunctionResult {
905    /// Get the result as raw bytes.
906    pub fn bytes(&self) -> &[u8] {
907        &self.result
908    }
909
910    /// Get the result as a string.
911    pub fn as_string(&self) -> Result<String, std::string::FromUtf8Error> {
912        String::from_utf8(self.result.clone())
913    }
914
915    /// Deserialize the result as JSON.
916    ///
917    /// # Example
918    ///
919    /// ```rust,ignore
920    /// let result = rpc.view_function(&contract, "get_data", &[], block).await?;
921    /// let data: MyData = result.json()?;
922    /// ```
923    pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
924        serde_json::from_slice(&self.result)
925    }
926
927    /// Deserialize the result as Borsh.
928    ///
929    /// Use this for contracts that return Borsh-encoded data instead of JSON.
930    ///
931    /// # Example
932    ///
933    /// ```rust,ignore
934    /// let result = rpc.view_function(&contract, "get_state", &args, block).await?;
935    /// let state: ContractState = result.borsh()?;
936    /// ```
937    pub fn borsh<T: borsh::BorshDeserialize>(&self) -> Result<T, borsh::io::Error> {
938        borsh::from_slice(&self.result)
939    }
940}
941
942// ============================================================================
943// Receipt types (for EXPERIMENTAL_tx_status)
944// ============================================================================
945
946/// Receipt from EXPERIMENTAL_tx_status.
947#[derive(Debug, Clone, Deserialize)]
948pub struct Receipt {
949    /// Predecessor account that created this receipt.
950    pub predecessor_id: AccountId,
951    /// Receiver account for this receipt.
952    pub receiver_id: AccountId,
953    /// Receipt ID.
954    pub receipt_id: CryptoHash,
955    /// Receipt content (action or data).
956    pub receipt: ReceiptContent,
957    /// Priority (optional, for congestion pricing).
958    #[serde(default)]
959    pub priority: Option<u64>,
960}
961
962/// Receipt content - action, data, or global contract distribution.
963#[derive(Debug, Clone, Deserialize)]
964pub enum ReceiptContent {
965    /// Action receipt.
966    Action(ActionReceiptData),
967    /// Data receipt.
968    Data(DataReceiptData),
969    /// Global contract distribution receipt.
970    GlobalContractDistribution {
971        /// Global contract identifier.
972        id: GlobalContractIdentifierView,
973        /// Target shard ID.
974        target_shard: u64,
975        /// Shards that have already received this contract.
976        #[serde(default)]
977        already_delivered_shards: Vec<u64>,
978        /// Code bytes (base64).
979        code: String,
980        /// Nonce (present in v2 receipts).
981        #[serde(default)]
982        nonce: Option<u64>,
983    },
984}
985
986/// Data receiver for output data in action receipts.
987#[derive(Debug, Clone, Deserialize)]
988pub struct DataReceiverView {
989    /// Data ID.
990    pub data_id: CryptoHash,
991    /// Receiver account ID.
992    pub receiver_id: AccountId,
993}
994
995/// Action receipt data.
996#[derive(Debug, Clone, Deserialize)]
997pub struct ActionReceiptData {
998    /// Signer account ID.
999    pub signer_id: AccountId,
1000    /// Signer public key.
1001    pub signer_public_key: PublicKey,
1002    /// Gas price for this receipt.
1003    pub gas_price: NearToken,
1004    /// Output data receivers.
1005    #[serde(default)]
1006    pub output_data_receivers: Vec<DataReceiverView>,
1007    /// Input data IDs.
1008    #[serde(default)]
1009    pub input_data_ids: Vec<CryptoHash>,
1010    /// Actions in this receipt.
1011    pub actions: Vec<ActionView>,
1012    /// Whether this is a promise yield.
1013    #[serde(default)]
1014    pub is_promise_yield: Option<bool>,
1015}
1016
1017/// Data receipt data.
1018#[derive(Debug, Clone, Deserialize)]
1019pub struct DataReceiptData {
1020    /// Data ID.
1021    pub data_id: CryptoHash,
1022    /// Data content (optional).
1023    #[serde(default)]
1024    pub data: Option<String>,
1025}
1026
1027/// Response from `EXPERIMENTAL_tx_status` RPC.
1028///
1029/// Same pattern as [`SendTxResponse`] but includes full receipt details.
1030#[derive(Debug, Clone, Deserialize)]
1031pub struct SendTxWithReceiptsResponse {
1032    /// The wait level that was reached.
1033    pub final_execution_status: TxExecutionStatus,
1034    /// The execution outcome, present when the transaction has been executed.
1035    #[serde(flatten)]
1036    pub outcome: Option<FinalExecutionOutcomeWithReceipts>,
1037}
1038
1039/// Final execution outcome with receipts (from EXPERIMENTAL_tx_status).
1040///
1041/// All fields are required — this type only appears when a transaction has actually
1042/// been executed.
1043#[derive(Debug, Clone, Deserialize)]
1044pub struct FinalExecutionOutcomeWithReceipts {
1045    /// Overall transaction execution result.
1046    pub status: FinalExecutionStatus,
1047    /// The transaction that was executed.
1048    pub transaction: TransactionView,
1049    /// Outcome of the transaction itself.
1050    pub transaction_outcome: ExecutionOutcomeWithId,
1051    /// Outcomes of all receipts spawned by the transaction.
1052    pub receipts_outcome: Vec<ExecutionOutcomeWithId>,
1053    /// Full receipt details.
1054    #[serde(default)]
1055    pub receipts: Vec<Receipt>,
1056}
1057
1058impl FinalExecutionOutcomeWithReceipts {
1059    /// Check if the transaction succeeded.
1060    pub fn is_success(&self) -> bool {
1061        matches!(&self.status, FinalExecutionStatus::SuccessValue(_))
1062    }
1063
1064    /// Check if the transaction failed.
1065    pub fn is_failure(&self) -> bool {
1066        matches!(&self.status, FinalExecutionStatus::Failure(_))
1067    }
1068
1069    /// Get the transaction hash.
1070    pub fn transaction_hash(&self) -> &CryptoHash {
1071        &self.transaction_outcome.id
1072    }
1073}
1074
1075// ============================================================================
1076// Node status types
1077// ============================================================================
1078
1079/// Node status response.
1080#[derive(Debug, Clone, Deserialize)]
1081pub struct StatusResponse {
1082    /// Protocol version.
1083    pub protocol_version: u32,
1084    /// Latest protocol version supported.
1085    pub latest_protocol_version: u32,
1086    /// Chain ID.
1087    pub chain_id: String,
1088    /// Genesis hash.
1089    pub genesis_hash: CryptoHash,
1090    /// RPC address.
1091    #[serde(default)]
1092    pub rpc_addr: Option<String>,
1093    /// Node public key.
1094    #[serde(default)]
1095    pub node_public_key: Option<String>,
1096    /// Node key (deprecated).
1097    #[serde(default)]
1098    pub node_key: Option<String>,
1099    /// Validator account ID (if validating).
1100    #[serde(default)]
1101    pub validator_account_id: Option<AccountId>,
1102    /// Validator public key (if validating).
1103    #[serde(default)]
1104    pub validator_public_key: Option<PublicKey>,
1105    /// List of current validators.
1106    #[serde(default)]
1107    pub validators: Vec<ValidatorInfo>,
1108    /// Sync information.
1109    pub sync_info: SyncInfo,
1110    /// Node version.
1111    pub version: NodeVersion,
1112    /// Uptime in seconds.
1113    #[serde(default)]
1114    pub uptime_sec: Option<u64>,
1115}
1116
1117/// Validator information.
1118#[derive(Debug, Clone, Deserialize)]
1119pub struct ValidatorInfo {
1120    /// Validator account ID.
1121    pub account_id: AccountId,
1122}
1123
1124/// Sync information.
1125#[derive(Debug, Clone, Deserialize)]
1126pub struct SyncInfo {
1127    /// Latest block hash.
1128    pub latest_block_hash: CryptoHash,
1129    /// Latest block height.
1130    pub latest_block_height: u64,
1131    /// Latest state root.
1132    #[serde(default)]
1133    pub latest_state_root: Option<CryptoHash>,
1134    /// Latest block timestamp.
1135    pub latest_block_time: String,
1136    /// Whether the node is syncing.
1137    pub syncing: bool,
1138    /// Earliest block hash (if available).
1139    #[serde(default)]
1140    pub earliest_block_hash: Option<CryptoHash>,
1141    /// Earliest block height (if available).
1142    #[serde(default)]
1143    pub earliest_block_height: Option<u64>,
1144    /// Earliest block time (if available).
1145    #[serde(default)]
1146    pub earliest_block_time: Option<String>,
1147    /// Current epoch ID.
1148    #[serde(default)]
1149    pub epoch_id: Option<CryptoHash>,
1150    /// Epoch start height.
1151    #[serde(default)]
1152    pub epoch_start_height: Option<u64>,
1153}
1154
1155/// Node version information.
1156#[derive(Debug, Clone, Deserialize)]
1157pub struct NodeVersion {
1158    /// Version string.
1159    pub version: String,
1160    /// Build string.
1161    pub build: String,
1162    /// Git commit hash.
1163    #[serde(default)]
1164    pub commit: Option<String>,
1165    /// Rust compiler version.
1166    #[serde(default)]
1167    pub rustc_version: Option<String>,
1168}
1169
1170#[cfg(test)]
1171mod tests {
1172    use super::*;
1173
1174    fn make_account_view(amount: u128, locked: u128, storage_usage: u64) -> AccountView {
1175        AccountView {
1176            amount: NearToken::from_yoctonear(amount),
1177            locked: NearToken::from_yoctonear(locked),
1178            code_hash: CryptoHash::default(),
1179            storage_usage,
1180            storage_paid_at: 0,
1181            global_contract_hash: None,
1182            global_contract_account_id: None,
1183            block_height: 0,
1184            block_hash: CryptoHash::default(),
1185        }
1186    }
1187
1188    #[test]
1189    fn test_available_balance_no_stake_no_storage() {
1190        // No storage, no stake -> all balance is available
1191        let view = make_account_view(1_000_000_000_000_000_000_000_000, 0, 0); // 1 NEAR
1192        assert_eq!(view.available(), view.amount);
1193    }
1194
1195    #[test]
1196    fn test_available_balance_with_storage_no_stake() {
1197        // 1000 bytes storage (= 0.00001 NEAR * 1000 = 0.01 NEAR = 10^22 yocto)
1198        // Amount: 1 NEAR = 10^24 yocto
1199        // Available should be: 1 NEAR - 0.01 NEAR = 0.99 NEAR
1200        let amount = 1_000_000_000_000_000_000_000_000u128; // 1 NEAR
1201        let storage_usage = 1000u64;
1202        let storage_cost = STORAGE_AMOUNT_PER_BYTE * storage_usage as u128; // 10^22
1203
1204        let view = make_account_view(amount, 0, storage_usage);
1205        let expected = NearToken::from_yoctonear(amount - storage_cost);
1206        assert_eq!(view.available(), expected);
1207    }
1208
1209    #[test]
1210    fn test_available_balance_stake_covers_storage() {
1211        // Staked amount >= storage cost -> all liquid balance is available
1212        // 1000 bytes storage = 10^22 yocto cost
1213        // 1 NEAR staked = 10^24 yocto (more than storage cost)
1214        let amount = 1_000_000_000_000_000_000_000_000u128; // 1 NEAR liquid
1215        let locked = 1_000_000_000_000_000_000_000_000u128; // 1 NEAR staked
1216        let storage_usage = 1000u64;
1217
1218        let view = make_account_view(amount, locked, storage_usage);
1219        // All liquid balance should be available since stake covers storage
1220        assert_eq!(view.available(), view.amount);
1221    }
1222
1223    #[test]
1224    fn test_available_balance_stake_partially_covers_storage() {
1225        // Staked = 0.005 NEAR = 5 * 10^21 yocto
1226        // Storage = 1000 bytes = 0.01 NEAR = 10^22 yocto
1227        // Reserved = 0.01 - 0.005 = 0.005 NEAR = 5 * 10^21 yocto
1228        // Amount = 1 NEAR
1229        // Available = 1 NEAR - 0.005 NEAR = 0.995 NEAR
1230        let amount = 1_000_000_000_000_000_000_000_000u128; // 1 NEAR
1231        let locked = 5_000_000_000_000_000_000_000u128; // 0.005 NEAR
1232        let storage_usage = 1000u64;
1233        let storage_cost = STORAGE_AMOUNT_PER_BYTE * storage_usage as u128; // 10^22
1234        let reserved = storage_cost - locked; // 5 * 10^21
1235
1236        let view = make_account_view(amount, locked, storage_usage);
1237        let expected = NearToken::from_yoctonear(amount - reserved);
1238        assert_eq!(view.available(), expected);
1239    }
1240
1241    #[test]
1242    fn test_storage_cost_calculation() {
1243        let storage_usage = 1000u64;
1244        let view = make_account_view(1_000_000_000_000_000_000_000_000, 0, storage_usage);
1245
1246        let expected_cost = STORAGE_AMOUNT_PER_BYTE * storage_usage as u128;
1247        assert_eq!(
1248            view.storage_cost(),
1249            NearToken::from_yoctonear(expected_cost)
1250        );
1251    }
1252
1253    #[test]
1254    fn test_storage_cost_zero_when_stake_covers() {
1255        // Staked > storage cost -> storage_cost returns 0
1256        let locked = 1_000_000_000_000_000_000_000_000u128; // 1 NEAR
1257        let view = make_account_view(1_000_000_000_000_000_000_000_000, locked, 1000);
1258
1259        assert_eq!(view.storage_cost(), NearToken::ZERO);
1260    }
1261
1262    #[test]
1263    fn test_account_balance_from_view() {
1264        let amount = 1_000_000_000_000_000_000_000_000u128; // 1 NEAR
1265        let locked = 500_000_000_000_000_000_000_000u128; // 0.5 NEAR
1266        let storage_usage = 1000u64;
1267
1268        let view = make_account_view(amount, locked, storage_usage);
1269        let balance = AccountBalance::from(view.clone());
1270
1271        assert_eq!(balance.total, view.amount);
1272        assert_eq!(balance.available, view.available());
1273        assert_eq!(balance.locked, view.locked);
1274        assert_eq!(balance.storage_cost, view.storage_cost());
1275        assert_eq!(balance.storage_usage, storage_usage);
1276    }
1277
1278    // ========================================================================
1279    // ViewFunctionResult tests
1280    // ========================================================================
1281
1282    fn make_view_result(result: Vec<u8>) -> ViewFunctionResult {
1283        ViewFunctionResult {
1284            result,
1285            logs: vec![],
1286            block_height: 12345,
1287            block_hash: CryptoHash::default(),
1288        }
1289    }
1290
1291    #[test]
1292    fn test_view_function_result_bytes() {
1293        let data = vec![1, 2, 3, 4, 5];
1294        let result = make_view_result(data.clone());
1295        assert_eq!(result.bytes(), &data[..]);
1296    }
1297
1298    #[test]
1299    fn test_view_function_result_as_string() {
1300        let result = make_view_result(b"hello world".to_vec());
1301        assert_eq!(result.as_string().unwrap(), "hello world");
1302    }
1303
1304    #[test]
1305    fn test_view_function_result_json() {
1306        let result = make_view_result(b"42".to_vec());
1307        let value: u64 = result.json().unwrap();
1308        assert_eq!(value, 42);
1309    }
1310
1311    #[test]
1312    fn test_view_function_result_json_object() {
1313        let result = make_view_result(b"{\"count\":123}".to_vec());
1314        let value: serde_json::Value = result.json().unwrap();
1315        assert_eq!(value["count"], 123);
1316    }
1317
1318    #[test]
1319    fn test_view_function_result_borsh() {
1320        // Borsh-encode a u64 value
1321        let original: u64 = 42;
1322        let encoded = borsh::to_vec(&original).unwrap();
1323        let result = make_view_result(encoded);
1324
1325        let decoded: u64 = result.borsh().unwrap();
1326        assert_eq!(decoded, original);
1327    }
1328
1329    #[test]
1330    fn test_view_function_result_borsh_struct() {
1331        #[derive(borsh::BorshSerialize, borsh::BorshDeserialize, PartialEq, Debug)]
1332        struct TestStruct {
1333            value: u64,
1334            name: String,
1335        }
1336
1337        let original = TestStruct {
1338            value: 123,
1339            name: "test".to_string(),
1340        };
1341        let encoded = borsh::to_vec(&original).unwrap();
1342        let result = make_view_result(encoded);
1343
1344        let decoded: TestStruct = result.borsh().unwrap();
1345        assert_eq!(decoded, original);
1346    }
1347
1348    #[test]
1349    fn test_view_function_result_borsh_error() {
1350        // Invalid Borsh data for a u64 (too short)
1351        let result = make_view_result(vec![1, 2, 3]);
1352        let decoded: Result<u64, _> = result.borsh();
1353        assert!(decoded.is_err());
1354    }
1355
1356    // ========================================================================
1357    // GasProfileEntry tests
1358    // ========================================================================
1359
1360    #[test]
1361    fn test_gas_profile_entry_string_gas_used() {
1362        let json = serde_json::json!({
1363            "cost_category": "WASM_HOST_COST",
1364            "cost": "BASE",
1365            "gas_used": "123456789"
1366        });
1367        let entry: GasProfileEntry = serde_json::from_value(json).unwrap();
1368        assert_eq!(entry.gas_used.as_gas(), 123456789);
1369    }
1370
1371    #[test]
1372    fn test_gas_profile_entry_numeric_gas_used() {
1373        let json = serde_json::json!({
1374            "cost_category": "ACTION_COST",
1375            "cost": "FUNCTION_CALL",
1376            "gas_used": 999000000
1377        });
1378        let entry: GasProfileEntry = serde_json::from_value(json).unwrap();
1379        assert_eq!(entry.gas_used.as_gas(), 999000000);
1380    }
1381
1382    #[test]
1383    fn test_gas_key_function_call_deserialization() {
1384        let json = serde_json::json!({
1385            "GasKeyFunctionCall": {
1386                "balance": "1000000000000000000000000",
1387                "num_nonces": 5,
1388                "allowance": "500000000000000000000000",
1389                "receiver_id": "app.near",
1390                "method_names": ["call_method"]
1391            }
1392        });
1393        let perm: AccessKeyPermissionView = serde_json::from_value(json).unwrap();
1394        assert!(matches!(
1395            perm,
1396            AccessKeyPermissionView::GasKeyFunctionCall { .. }
1397        ));
1398    }
1399
1400    #[test]
1401    fn test_gas_key_full_access_deserialization() {
1402        let json = serde_json::json!({
1403            "GasKeyFullAccess": {
1404                "balance": "1000000000000000000000000",
1405                "num_nonces": 10
1406            }
1407        });
1408        let perm: AccessKeyPermissionView = serde_json::from_value(json).unwrap();
1409        assert!(matches!(
1410            perm,
1411            AccessKeyPermissionView::GasKeyFullAccess { .. }
1412        ));
1413    }
1414
1415    #[test]
1416    fn test_transfer_to_gas_key_action_view_deserialization() {
1417        let json = serde_json::json!({
1418            "TransferToGasKey": {
1419                "public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
1420                "deposit": "1000000000000000000000000"
1421            }
1422        });
1423        let action: ActionView = serde_json::from_value(json).unwrap();
1424        assert!(matches!(action, ActionView::TransferToGasKey { .. }));
1425    }
1426
1427    #[test]
1428    fn test_delegate_action_view_deserialization() {
1429        let json = serde_json::json!({
1430            "Delegate": {
1431                "delegate_action": {
1432                    "sender_id": "alice.near",
1433                    "receiver_id": "contract.near",
1434                    "actions": [
1435                        {"FunctionCall": {
1436                            "method_name": "do_something",
1437                            "args": "e30=",
1438                            "gas": 30000000000000_u64,
1439                            "deposit": "0"
1440                        }}
1441                    ],
1442                    "nonce": 42,
1443                    "max_block_height": 100000,
1444                    "public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp"
1445                },
1446                "signature": "ed25519:3s1dvMqNDCByoMnDnkhB4GPjTSXCRt4nt3Af5n1RX8W7aJ2FC6MfRf5BNXZ52EBifNJnNVBsGvke6GRYuaEYJXt5"
1447            }
1448        });
1449        let action: ActionView = serde_json::from_value(json).unwrap();
1450        match action {
1451            ActionView::Delegate {
1452                delegate_action,
1453                signature,
1454            } => {
1455                assert_eq!(delegate_action.sender_id.as_ref(), "alice.near");
1456                assert_eq!(delegate_action.receiver_id.as_ref(), "contract.near");
1457                assert_eq!(delegate_action.nonce, 42);
1458                assert_eq!(delegate_action.max_block_height, 100000);
1459                assert_eq!(delegate_action.actions.len(), 1);
1460                assert!(signature.to_string().starts_with("ed25519:"));
1461            }
1462            _ => panic!("Expected Delegate action"),
1463        }
1464    }
1465
1466    #[test]
1467    fn test_withdraw_from_gas_key_action_view_deserialization() {
1468        let json = serde_json::json!({
1469            "WithdrawFromGasKey": {
1470                "public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
1471                "amount": "500000000000000000000000"
1472            }
1473        });
1474        let action: ActionView = serde_json::from_value(json).unwrap();
1475        assert!(matches!(action, ActionView::WithdrawFromGasKey { .. }));
1476    }
1477
1478    // ========================================================================
1479    // FinalExecutionStatus tests
1480    // ========================================================================
1481
1482    #[test]
1483    fn test_final_execution_status_default() {
1484        let status = FinalExecutionStatus::default();
1485        assert!(matches!(status, FinalExecutionStatus::NotStarted));
1486    }
1487
1488    #[test]
1489    fn test_final_execution_status_not_started() {
1490        let json = serde_json::json!("NotStarted");
1491        let status: FinalExecutionStatus = serde_json::from_value(json).unwrap();
1492        assert!(matches!(status, FinalExecutionStatus::NotStarted));
1493    }
1494
1495    #[test]
1496    fn test_final_execution_status_started() {
1497        let json = serde_json::json!("Started");
1498        let status: FinalExecutionStatus = serde_json::from_value(json).unwrap();
1499        assert!(matches!(status, FinalExecutionStatus::Started));
1500    }
1501
1502    #[test]
1503    fn test_final_execution_status_success_value() {
1504        let json = serde_json::json!({"SuccessValue": "aGVsbG8="});
1505        let status: FinalExecutionStatus = serde_json::from_value(json).unwrap();
1506        assert!(matches!(status, FinalExecutionStatus::SuccessValue(ref s) if s == "aGVsbG8="));
1507    }
1508
1509    #[test]
1510    fn test_final_execution_status_failure() {
1511        let json = serde_json::json!({
1512            "Failure": {
1513                "ActionError": {
1514                    "index": 0,
1515                    "kind": {
1516                        "FunctionCallError": {
1517                            "ExecutionError": "Smart contract panicked"
1518                        }
1519                    }
1520                }
1521            }
1522        });
1523        let status: FinalExecutionStatus = serde_json::from_value(json).unwrap();
1524        assert!(matches!(status, FinalExecutionStatus::Failure(_)));
1525    }
1526
1527    // ========================================================================
1528    // FinalExecutionOutcome helper tests
1529    // ========================================================================
1530
1531    #[test]
1532    fn test_send_tx_response_with_outcome() {
1533        let json = serde_json::json!({
1534            "final_execution_status": "FINAL",
1535            "status": {"SuccessValue": ""},
1536            "transaction": {
1537                "signer_id": "alice.near",
1538                "public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
1539                "nonce": 1,
1540                "receiver_id": "bob.near",
1541                "actions": [{"Transfer": {"deposit": "1000000000000000000000000"}}],
1542                "signature": "ed25519:3s1dvMqNDCByoMnDnkhB4GPjTSXCRt4nt3Af5n1RX8W7aJ2FC6MfRf5BNXZ52EBifNJnNVBsGvke6GRYuaEYJXt5",
1543                "hash": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U"
1544            },
1545            "transaction_outcome": {
1546                "id": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U",
1547                "outcome": {
1548                    "executor_id": "alice.near",
1549                    "gas_burnt": 223182562500_i64,
1550                    "tokens_burnt": "22318256250000000000",
1551                    "logs": [],
1552                    "receipt_ids": ["3GTGoiN3FEoJenSw5ob4YMmFEV2Fbiichj3FDBnM78xK"],
1553                    "status": {"SuccessReceiptId": "3GTGoiN3FEoJenSw5ob4YMmFEV2Fbiichj3FDBnM78xK"}
1554                },
1555                "block_hash": "A6DJpKBhmAMmBuQXtY3dWbo8dGVSQ9yH7BQSJBfn8rBo",
1556                "proof": []
1557            },
1558            "receipts_outcome": []
1559        });
1560        let response: SendTxResponse = serde_json::from_value(json).unwrap();
1561        assert_eq!(response.final_execution_status, TxExecutionStatus::Final);
1562        let outcome = response.outcome.unwrap();
1563        assert!(outcome.is_success());
1564        assert!(!outcome.is_failure());
1565    }
1566
1567    #[test]
1568    fn test_send_tx_response_pending_none() {
1569        let json = serde_json::json!({
1570            "final_execution_status": "NONE"
1571        });
1572        let response: SendTxResponse = serde_json::from_value(json).unwrap();
1573        assert_eq!(response.final_execution_status, TxExecutionStatus::None);
1574        assert!(response.outcome.is_none());
1575        // transaction_hash is serde(skip) — populated by rpc.send_tx(), not deserialization
1576        assert!(response.transaction_hash.is_zero());
1577    }
1578
1579    #[test]
1580    fn test_final_execution_outcome_failure() {
1581        let json = serde_json::json!({
1582            "final_execution_status": "EXECUTED_OPTIMISTIC",
1583            "status": {
1584                "Failure": {
1585                    "ActionError": {
1586                        "index": 0,
1587                        "kind": {
1588                            "FunctionCallError": {
1589                                "ExecutionError": "Smart contract panicked"
1590                            }
1591                        }
1592                    }
1593                }
1594            },
1595            "transaction": {
1596                "signer_id": "alice.near",
1597                "public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
1598                "nonce": 1,
1599                "receiver_id": "bob.near",
1600                "actions": [],
1601                "signature": "ed25519:3s1dvMqNDCByoMnDnkhB4GPjTSXCRt4nt3Af5n1RX8W7aJ2FC6MfRf5BNXZ52EBifNJnNVBsGvke6GRYuaEYJXt5",
1602                "hash": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U"
1603            },
1604            "transaction_outcome": {
1605                "id": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U",
1606                "outcome": {
1607                    "executor_id": "alice.near",
1608                    "gas_burnt": 0,
1609                    "tokens_burnt": "0",
1610                    "logs": [],
1611                    "receipt_ids": [],
1612                    "status": "Unknown"
1613                },
1614                "block_hash": "A6DJpKBhmAMmBuQXtY3dWbo8dGVSQ9yH7BQSJBfn8rBo",
1615                "proof": []
1616            },
1617            "receipts_outcome": []
1618        });
1619        let response: SendTxResponse = serde_json::from_value(json).unwrap();
1620        let outcome = response.outcome.unwrap();
1621        assert!(outcome.is_failure());
1622        assert!(!outcome.is_success());
1623        assert!(outcome.failure_message().is_some());
1624        assert!(outcome.failure_error().is_some());
1625    }
1626
1627    // ========================================================================
1628    // ExecutionStatus tests (per-receipt)
1629    // ========================================================================
1630
1631    #[test]
1632    fn test_execution_status_unknown() {
1633        let json = serde_json::json!("Unknown");
1634        let status: ExecutionStatus = serde_json::from_value(json).unwrap();
1635        assert!(matches!(status, ExecutionStatus::Unknown));
1636    }
1637
1638    #[test]
1639    fn test_execution_status_success_value() {
1640        let json = serde_json::json!({"SuccessValue": "aGVsbG8="});
1641        let status: ExecutionStatus = serde_json::from_value(json).unwrap();
1642        assert!(matches!(status, ExecutionStatus::SuccessValue(_)));
1643    }
1644
1645    #[test]
1646    fn test_execution_status_success_receipt_id() {
1647        let json =
1648            serde_json::json!({"SuccessReceiptId": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U"});
1649        let status: ExecutionStatus = serde_json::from_value(json).unwrap();
1650        assert!(matches!(status, ExecutionStatus::SuccessReceiptId(_)));
1651    }
1652
1653    // ========================================================================
1654    // GlobalContractIdentifierView tests
1655    // ========================================================================
1656
1657    #[test]
1658    fn test_global_contract_id_view_new_format_hash() {
1659        let json = serde_json::json!({"hash": "9SP8Y3sVADWNN5QoEB5CsvPUE5HT4o8YfBaCnhLss87K"});
1660        let id: GlobalContractIdentifierView = serde_json::from_value(json).unwrap();
1661        assert!(matches!(id, GlobalContractIdentifierView::CodeHash(_)));
1662    }
1663
1664    #[test]
1665    fn test_global_contract_id_view_new_format_account() {
1666        let json = serde_json::json!({"account_id": "alice.near"});
1667        let id: GlobalContractIdentifierView = serde_json::from_value(json).unwrap();
1668        assert!(matches!(id, GlobalContractIdentifierView::AccountId(_)));
1669    }
1670
1671    #[test]
1672    fn test_global_contract_id_view_deprecated_hash() {
1673        let json = serde_json::json!("9SP8Y3sVADWNN5QoEB5CsvPUE5HT4o8YfBaCnhLss87K");
1674        let id: GlobalContractIdentifierView = serde_json::from_value(json).unwrap();
1675        assert!(matches!(id, GlobalContractIdentifierView::CodeHash(_)));
1676    }
1677
1678    #[test]
1679    fn test_global_contract_id_view_deprecated_account() {
1680        let json = serde_json::json!("alice.near");
1681        let id: GlobalContractIdentifierView = serde_json::from_value(json).unwrap();
1682        assert!(matches!(id, GlobalContractIdentifierView::AccountId(_)));
1683    }
1684
1685    // ========================================================================
1686    // DeterministicStateInit ActionView tests
1687    // ========================================================================
1688
1689    #[test]
1690    fn test_action_view_deterministic_state_init() {
1691        let json = serde_json::json!({
1692            "DeterministicStateInit": {
1693                "code": {"hash": "9SP8Y3sVADWNN5QoEB5CsvPUE5HT4o8YfBaCnhLss87K"},
1694                "data": {"a2V5": "dmFsdWU="},
1695                "deposit": "1000000000000000000000000"
1696            }
1697        });
1698        let action: ActionView = serde_json::from_value(json).unwrap();
1699        match action {
1700            ActionView::DeterministicStateInit {
1701                code,
1702                data,
1703                deposit,
1704            } => {
1705                assert!(matches!(code, GlobalContractIdentifierView::CodeHash(_)));
1706                assert_eq!(data.len(), 1);
1707                assert_eq!(data.get("a2V5").unwrap(), "dmFsdWU=");
1708                assert_eq!(deposit, NearToken::from_near(1));
1709            }
1710            _ => panic!("Expected DeterministicStateInit"),
1711        }
1712    }
1713
1714    #[test]
1715    fn test_action_view_deterministic_state_init_empty_data() {
1716        let json = serde_json::json!({
1717            "DeterministicStateInit": {
1718                "code": {"account_id": "publisher.near"},
1719                "deposit": "0"
1720            }
1721        });
1722        let action: ActionView = serde_json::from_value(json).unwrap();
1723        match action {
1724            ActionView::DeterministicStateInit { code, data, .. } => {
1725                assert!(matches!(code, GlobalContractIdentifierView::AccountId(_)));
1726                assert!(data.is_empty());
1727            }
1728            _ => panic!("Expected DeterministicStateInit"),
1729        }
1730    }
1731
1732    // ========================================================================
1733    // GlobalContractDistribution receipt tests
1734    // ========================================================================
1735
1736    #[test]
1737    fn test_receipt_global_contract_distribution() {
1738        let json = serde_json::json!({
1739            "GlobalContractDistribution": {
1740                "id": {"hash": "9SP8Y3sVADWNN5QoEB5CsvPUE5HT4o8YfBaCnhLss87K"},
1741                "target_shard": 3,
1742                "already_delivered_shards": [0, 1, 2],
1743                "code": "AGFzbQ==",
1744                "nonce": 42
1745            }
1746        });
1747        let content: ReceiptContent = serde_json::from_value(json).unwrap();
1748        match content {
1749            ReceiptContent::GlobalContractDistribution {
1750                id,
1751                target_shard,
1752                already_delivered_shards,
1753                code,
1754                nonce,
1755            } => {
1756                assert!(matches!(id, GlobalContractIdentifierView::CodeHash(_)));
1757                assert_eq!(target_shard, 3);
1758                assert_eq!(already_delivered_shards, vec![0, 1, 2]);
1759                assert_eq!(code, "AGFzbQ==");
1760                assert_eq!(nonce, Some(42));
1761            }
1762            _ => panic!("Expected GlobalContractDistribution"),
1763        }
1764    }
1765
1766    #[test]
1767    fn test_receipt_global_contract_distribution_without_nonce() {
1768        let json = serde_json::json!({
1769            "GlobalContractDistribution": {
1770                "id": {"account_id": "publisher.near"},
1771                "target_shard": 0,
1772                "already_delivered_shards": [],
1773                "code": "AGFzbQ=="
1774            }
1775        });
1776        let content: ReceiptContent = serde_json::from_value(json).unwrap();
1777        match content {
1778            ReceiptContent::GlobalContractDistribution { nonce, .. } => {
1779                assert_eq!(nonce, None);
1780            }
1781            _ => panic!("Expected GlobalContractDistribution"),
1782        }
1783    }
1784
1785    #[test]
1786    fn test_gas_profile_entry_deserialization() {
1787        let json = serde_json::json!({
1788            "cost_category": "WASM_HOST_COST",
1789            "cost": "BASE",
1790            "gas_used": "2646228750"
1791        });
1792        let entry: GasProfileEntry = serde_json::from_value(json).unwrap();
1793        assert_eq!(entry.cost_category, "WASM_HOST_COST");
1794        assert_eq!(entry.cost, "BASE");
1795        assert_eq!(entry.gas_used, Gas::from_gas(2646228750));
1796    }
1797
1798    #[test]
1799    fn test_transaction_view_with_signature() {
1800        let json = serde_json::json!({
1801            "signer_id": "alice.near",
1802            "public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
1803            "nonce": 1,
1804            "receiver_id": "bob.near",
1805            "hash": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U",
1806            "actions": [{"Transfer": {"deposit": "1000000000000000000000000"}}],
1807            "signature": "ed25519:3s1dvMqNDCByoMnDnkhB4GPjTSXCRt4nt3Af5n1RX8W7aJ2FC6MfRf5BNXZ52EBifNJnNVBsGvke6GRYuaEYJXt5"
1808        });
1809        let tx: TransactionView = serde_json::from_value(json).unwrap();
1810        assert_eq!(tx.signer_id.as_str(), "alice.near");
1811        assert!(tx.signature.to_string().starts_with("ed25519:"));
1812    }
1813}