use std::collections::BTreeMap;
use base64::{Engine as _, engine::general_purpose::STANDARD};
use serde::Deserialize;
use serde_with::{base64::Base64, serde_as};
use super::block_reference::TxExecutionStatus;
use super::error::{ActionError, TxExecutionError};
use super::{AccountId, CryptoHash, Gas, NearToken, PublicKey, Signature};
pub const STORAGE_AMOUNT_PER_BYTE: u128 = 10_000_000_000_000_000_000;
#[derive(Debug, Clone, Deserialize)]
pub struct AccountView {
pub amount: NearToken,
pub locked: NearToken,
pub code_hash: CryptoHash,
pub storage_usage: u64,
#[serde(default)]
pub storage_paid_at: u64,
#[serde(default)]
pub global_contract_hash: Option<CryptoHash>,
#[serde(default)]
pub global_contract_account_id: Option<AccountId>,
pub block_height: u64,
pub block_hash: CryptoHash,
}
impl AccountView {
fn storage_required(&self) -> NearToken {
let yocto = STORAGE_AMOUNT_PER_BYTE.saturating_mul(self.storage_usage as u128);
NearToken::from_yoctonear(yocto)
}
pub fn available(&self) -> NearToken {
let storage_required = self.storage_required();
if self.locked >= storage_required {
return self.amount;
}
let reserved_for_storage = storage_required.saturating_sub(self.locked);
self.amount.saturating_sub(reserved_for_storage)
}
pub fn storage_cost(&self) -> NearToken {
let storage_required = self.storage_required();
if self.locked >= storage_required {
NearToken::ZERO
} else {
storage_required.saturating_sub(self.locked)
}
}
pub fn has_contract(&self) -> bool {
!self.code_hash.is_zero()
}
}
#[derive(Debug, Clone)]
pub struct AccountBalance {
pub total: NearToken,
pub available: NearToken,
pub locked: NearToken,
pub storage_cost: NearToken,
pub storage_usage: u64,
}
impl std::fmt::Display for AccountBalance {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.available)
}
}
impl From<AccountView> for AccountBalance {
fn from(view: AccountView) -> Self {
Self {
total: view.amount,
available: view.available(),
locked: view.locked,
storage_cost: view.storage_cost(),
storage_usage: view.storage_usage,
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct AccessKeyView {
pub nonce: u64,
pub permission: AccessKeyPermissionView,
pub block_height: u64,
pub block_hash: CryptoHash,
}
#[derive(Debug, Clone, Deserialize)]
pub struct AccessKeyDetails {
pub nonce: u64,
pub permission: AccessKeyPermissionView,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum AccessKeyPermissionView {
FullAccess,
FunctionCall {
allowance: Option<NearToken>,
receiver_id: AccountId,
method_names: Vec<String>,
},
GasKeyFunctionCall {
balance: NearToken,
num_nonces: u16,
allowance: Option<NearToken>,
receiver_id: AccountId,
method_names: Vec<String>,
},
GasKeyFullAccess {
balance: NearToken,
num_nonces: u16,
},
}
#[derive(Debug, Clone, Deserialize)]
pub struct AccessKeyListView {
pub keys: Vec<AccessKeyInfoView>,
pub block_height: u64,
pub block_hash: CryptoHash,
}
#[derive(Debug, Clone, Deserialize)]
pub struct AccessKeyInfoView {
pub public_key: PublicKey,
pub access_key: AccessKeyDetails,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BlockView {
pub author: AccountId,
pub header: BlockHeaderView,
pub chunks: Vec<ChunkHeaderView>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BlockHeaderView {
pub height: u64,
#[serde(default)]
pub prev_height: Option<u64>,
pub hash: CryptoHash,
pub prev_hash: CryptoHash,
pub prev_state_root: CryptoHash,
pub chunk_receipts_root: CryptoHash,
pub chunk_headers_root: CryptoHash,
pub chunk_tx_root: CryptoHash,
pub outcome_root: CryptoHash,
pub chunks_included: u64,
pub challenges_root: CryptoHash,
pub timestamp: u64,
pub timestamp_nanosec: String,
pub random_value: CryptoHash,
#[serde(default)]
pub validator_proposals: Vec<ValidatorStakeView>,
#[serde(default)]
pub chunk_mask: Vec<bool>,
pub gas_price: NearToken,
#[serde(default)]
pub block_ordinal: Option<u64>,
pub total_supply: NearToken,
#[serde(default)]
pub challenges_result: Vec<SlashedValidator>,
pub last_final_block: CryptoHash,
pub last_ds_final_block: CryptoHash,
pub epoch_id: CryptoHash,
pub next_epoch_id: CryptoHash,
pub next_bp_hash: CryptoHash,
pub block_merkle_root: CryptoHash,
#[serde(default)]
pub epoch_sync_data_hash: Option<CryptoHash>,
#[serde(default)]
pub block_body_hash: Option<CryptoHash>,
#[serde(default)]
pub approvals: Vec<Option<Signature>>,
pub signature: Signature,
pub latest_protocol_version: u32,
#[serde(default)]
pub rent_paid: Option<NearToken>,
#[serde(default)]
pub validator_reward: Option<NearToken>,
#[serde(default)]
pub chunk_endorsements: Option<Vec<Vec<u8>>>,
#[serde(default)]
pub shard_split: Option<(u64, AccountId)>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum ValidatorStakeView {
V1(ValidatorStakeViewV1),
}
#[derive(Debug, Clone, Deserialize)]
pub struct ValidatorStakeViewV1 {
pub account_id: AccountId,
pub public_key: PublicKey,
pub stake: NearToken,
}
impl ValidatorStakeView {
pub fn into_v1(self) -> ValidatorStakeViewV1 {
match self {
Self::V1(v) => v,
}
}
pub fn account_id(&self) -> &AccountId {
match self {
Self::V1(v) => &v.account_id,
}
}
pub fn stake(&self) -> NearToken {
match self {
Self::V1(v) => v.stake,
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct SlashedValidator {
pub account_id: AccountId,
pub is_double_sign: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ChunkHeaderView {
pub chunk_hash: CryptoHash,
pub prev_block_hash: CryptoHash,
pub outcome_root: CryptoHash,
pub prev_state_root: CryptoHash,
pub encoded_merkle_root: CryptoHash,
pub encoded_length: u64,
pub height_created: u64,
pub height_included: u64,
pub shard_id: u64,
pub gas_used: u64,
pub gas_limit: u64,
pub validator_reward: NearToken,
pub balance_burnt: NearToken,
pub outgoing_receipts_root: CryptoHash,
pub tx_root: CryptoHash,
#[serde(default)]
pub validator_proposals: Vec<ValidatorStakeView>,
#[serde(default)]
pub congestion_info: Option<CongestionInfoView>,
#[serde(default)]
pub bandwidth_requests: Option<BandwidthRequests>,
#[serde(default)]
pub rent_paid: Option<NearToken>,
#[serde(default)]
pub proposed_split: Option<Option<TrieSplit>>,
pub signature: Signature,
}
#[derive(Debug, Clone, Deserialize)]
pub enum BandwidthRequests {
V1(BandwidthRequestsV1),
}
#[derive(Debug, Clone, Deserialize)]
pub struct BandwidthRequestsV1 {
pub requests: Vec<BandwidthRequest>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BandwidthRequest {
pub to_shard: u16,
pub requested_values_bitmap: BandwidthRequestBitmap,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BandwidthRequestBitmap {
pub data: [u8; 5],
}
#[derive(Debug, Clone, Deserialize)]
pub struct TrieSplit {
pub boundary_account: AccountId,
pub left_memory: u64,
pub right_memory: u64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CongestionInfoView {
#[serde(default, deserialize_with = "dec_format")]
pub delayed_receipts_gas: u128,
#[serde(default, deserialize_with = "dec_format")]
pub buffered_receipts_gas: u128,
#[serde(default)]
pub receipt_bytes: u64,
#[serde(default)]
pub allowed_shard: u16,
}
fn dec_format<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<u128, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrNum {
String(String),
Num(u128),
}
match StringOrNum::deserialize(deserializer)? {
StringOrNum::String(s) => s.parse().map_err(serde::de::Error::custom),
StringOrNum::Num(n) => Ok(n),
}
}
fn gas_dec_format<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Gas, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrNum {
String(String),
Num(u64),
}
let raw = match StringOrNum::deserialize(deserializer)? {
StringOrNum::String(s) => s.parse::<u64>().map_err(serde::de::Error::custom)?,
StringOrNum::Num(n) => n,
};
Ok(Gas::from_gas(raw))
}
#[derive(Debug, Clone, Deserialize)]
pub struct GasPrice {
pub gas_price: NearToken,
}
impl GasPrice {
pub fn as_u128(&self) -> u128 {
self.gas_price.as_yoctonear()
}
}
#[derive(Debug, Clone, Default, Deserialize)]
pub enum FinalExecutionStatus {
#[default]
NotStarted,
Started,
Failure(TxExecutionError),
SuccessValue(String),
}
#[derive(Debug, Clone)]
pub struct SendTxResponse {
pub transaction_hash: CryptoHash,
pub sender_id: AccountId,
}
#[doc(hidden)]
#[derive(Debug, Clone, Deserialize)]
pub struct RawTransactionResponse {
#[serde(skip)]
pub transaction_hash: CryptoHash,
pub final_execution_status: TxExecutionStatus,
#[serde(flatten)]
pub outcome: Option<FinalExecutionOutcome>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct FinalExecutionOutcome {
pub status: FinalExecutionStatus,
pub transaction: TransactionView,
pub transaction_outcome: ExecutionOutcomeWithId,
pub receipts_outcome: Vec<ExecutionOutcomeWithId>,
#[serde(default)]
pub receipts: Vec<Receipt>,
}
impl FinalExecutionOutcome {
pub fn is_success(&self) -> bool {
matches!(&self.status, FinalExecutionStatus::SuccessValue(_))
}
pub fn is_failure(&self) -> bool {
matches!(&self.status, FinalExecutionStatus::Failure(_))
}
pub fn failure_message(&self) -> Option<String> {
match &self.status {
FinalExecutionStatus::Failure(err) => Some(err.to_string()),
_ => None,
}
}
pub fn failure_error(&self) -> Option<&TxExecutionError> {
match &self.status {
FinalExecutionStatus::Failure(err) => Some(err),
_ => None,
}
}
pub fn transaction_hash(&self) -> &CryptoHash {
&self.transaction_outcome.id
}
pub fn total_gas_used(&self) -> Gas {
let tx_gas = self.transaction_outcome.outcome.gas_burnt.as_gas();
let receipt_gas: u64 = self
.receipts_outcome
.iter()
.map(|r| r.outcome.gas_burnt.as_gas())
.sum();
Gas::from_gas(tx_gas + receipt_gas)
}
pub fn result(&self) -> Result<Vec<u8>, crate::error::Error> {
match &self.status {
FinalExecutionStatus::Failure(TxExecutionError::InvalidTxError(e)) => {
Err(crate::error::Error::InvalidTx(Box::new(e.clone())))
}
FinalExecutionStatus::Failure(TxExecutionError::ActionError(e)) => Err(
crate::error::Error::InvalidTransaction(format!("Action error: {e}")),
),
FinalExecutionStatus::SuccessValue(s) => STANDARD.decode(s).map_err(|e| {
crate::error::Error::InvalidTransaction(format!(
"Failed to decode base64 SuccessValue: {e}"
))
}),
other => Err(crate::error::Error::InvalidTransaction(format!(
"Transaction status is {:?}, expected SuccessValue",
other,
))),
}
}
pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, crate::error::Error> {
let bytes = self.result()?;
serde_json::from_slice(&bytes).map_err(crate::error::Error::from)
}
}
#[derive(Debug, Clone)]
pub enum ExecutionStatus {
Unknown,
Failure(ActionError),
SuccessValue(Vec<u8>),
SuccessReceiptId(CryptoHash),
}
impl<'de> serde::Deserialize<'de> for ExecutionStatus {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[serde_as]
#[derive(Deserialize)]
enum Raw {
Unknown,
Failure(TxExecutionError),
SuccessValue(#[serde_as(as = "Base64")] Vec<u8>),
SuccessReceiptId(CryptoHash),
}
match Raw::deserialize(deserializer)? {
Raw::Unknown => Ok(Self::Unknown),
Raw::Failure(TxExecutionError::ActionError(e)) => Ok(Self::Failure(e)),
Raw::Failure(TxExecutionError::InvalidTxError(e)) => Err(serde::de::Error::custom(
format!("unexpected InvalidTxError in receipt execution status: {e}"),
)),
Raw::SuccessValue(v) => Ok(Self::SuccessValue(v)),
Raw::SuccessReceiptId(h) => Ok(Self::SuccessReceiptId(h)),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct TransactionView {
pub signer_id: AccountId,
pub public_key: PublicKey,
pub nonce: u64,
pub receiver_id: AccountId,
pub hash: CryptoHash,
#[serde(default)]
pub actions: Vec<ActionView>,
pub signature: Signature,
#[serde(default)]
pub priority_fee: Option<u64>,
#[serde(default)]
pub nonce_index: Option<u16>,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum GlobalContractIdCompat {
CodeHash { hash: CryptoHash },
AccountId { account_id: AccountId },
DeprecatedCodeHash(CryptoHash),
DeprecatedAccountId(AccountId),
}
#[derive(Debug, Clone, Deserialize)]
#[serde(from = "GlobalContractIdCompat")]
pub enum GlobalContractIdentifierView {
CodeHash(CryptoHash),
AccountId(AccountId),
}
impl From<GlobalContractIdCompat> for GlobalContractIdentifierView {
fn from(compat: GlobalContractIdCompat) -> Self {
match compat {
GlobalContractIdCompat::CodeHash { hash }
| GlobalContractIdCompat::DeprecatedCodeHash(hash) => Self::CodeHash(hash),
GlobalContractIdCompat::AccountId { account_id }
| GlobalContractIdCompat::DeprecatedAccountId(account_id) => {
Self::AccountId(account_id)
}
}
}
}
impl std::fmt::Display for GlobalContractIdentifierView {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CodeHash(hash) => write!(f, "hash({hash})"),
Self::AccountId(account_id) => write!(f, "{account_id}"),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct DelegateActionView {
pub sender_id: AccountId,
pub receiver_id: AccountId,
pub actions: Vec<ActionView>,
pub nonce: u64,
pub max_block_height: u64,
pub public_key: PublicKey,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ActionView {
CreateAccount,
DeployContract {
code: String, },
FunctionCall {
method_name: String,
args: String, gas: Gas,
deposit: NearToken,
},
Transfer {
deposit: NearToken,
},
Stake {
stake: NearToken,
public_key: PublicKey,
},
AddKey {
public_key: PublicKey,
access_key: AccessKeyDetails,
},
DeleteKey {
public_key: PublicKey,
},
DeleteAccount {
beneficiary_id: AccountId,
},
Delegate {
delegate_action: DelegateActionView,
signature: Signature,
},
#[serde(rename = "DeployGlobalContract")]
DeployGlobalContract {
code: String,
},
#[serde(rename = "DeployGlobalContractByAccountId")]
DeployGlobalContractByAccountId {
code: String,
},
#[serde(rename = "UseGlobalContract")]
UseGlobalContract {
code_hash: CryptoHash,
},
#[serde(rename = "UseGlobalContractByAccountId")]
UseGlobalContractByAccountId {
account_id: AccountId,
},
#[serde(rename = "DeterministicStateInit")]
DeterministicStateInit {
code: GlobalContractIdentifierView,
#[serde(default)]
data: BTreeMap<String, String>,
deposit: NearToken,
},
TransferToGasKey {
public_key: PublicKey,
deposit: NearToken,
},
WithdrawFromGasKey {
public_key: PublicKey,
amount: NearToken,
},
}
#[derive(Debug, Clone, Deserialize)]
pub struct MerklePathItem {
pub hash: CryptoHash,
pub direction: MerkleDirection,
}
#[derive(Debug, Clone, Deserialize)]
pub enum MerkleDirection {
Left,
Right,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ExecutionOutcomeWithId {
pub id: CryptoHash,
pub outcome: ExecutionOutcome,
#[serde(default)]
pub proof: Vec<MerklePathItem>,
pub block_hash: CryptoHash,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ExecutionOutcome {
pub executor_id: AccountId,
pub gas_burnt: Gas,
pub tokens_burnt: NearToken,
pub logs: Vec<String>,
pub receipt_ids: Vec<CryptoHash>,
pub status: ExecutionStatus,
#[serde(default)]
pub metadata: Option<ExecutionMetadata>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ExecutionMetadata {
pub version: u32,
#[serde(default)]
pub gas_profile: Option<Vec<GasProfileEntry>>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct GasProfileEntry {
pub cost_category: String,
pub cost: String,
#[serde(deserialize_with = "gas_dec_format")]
pub gas_used: Gas,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ViewFunctionResult {
pub result: Vec<u8>,
pub logs: Vec<String>,
pub block_height: u64,
pub block_hash: CryptoHash,
}
impl ViewFunctionResult {
pub fn bytes(&self) -> &[u8] {
&self.result
}
pub fn as_string(&self) -> Result<String, std::string::FromUtf8Error> {
String::from_utf8(self.result.clone())
}
pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
serde_json::from_slice(&self.result)
}
pub fn borsh<T: borsh::BorshDeserialize>(&self) -> Result<T, borsh::io::Error> {
borsh::from_slice(&self.result)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct Receipt {
pub predecessor_id: AccountId,
pub receiver_id: AccountId,
pub receipt_id: CryptoHash,
pub receipt: ReceiptContent,
#[serde(default)]
pub priority: Option<u64>,
}
#[derive(Debug, Clone, Deserialize)]
pub enum ReceiptContent {
Action(ActionReceiptData),
Data(DataReceiptData),
GlobalContractDistribution {
id: GlobalContractIdentifierView,
target_shard: u64,
#[serde(default)]
already_delivered_shards: Vec<u64>,
code: String,
#[serde(default)]
nonce: Option<u64>,
},
}
#[derive(Debug, Clone, Deserialize)]
pub struct DataReceiverView {
pub data_id: CryptoHash,
pub receiver_id: AccountId,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ActionReceiptData {
pub signer_id: AccountId,
pub signer_public_key: PublicKey,
pub gas_price: NearToken,
#[serde(default)]
pub output_data_receivers: Vec<DataReceiverView>,
#[serde(default)]
pub input_data_ids: Vec<CryptoHash>,
pub actions: Vec<ActionView>,
#[serde(default)]
pub is_promise_yield: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct DataReceiptData {
pub data_id: CryptoHash,
#[serde(default)]
pub data: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct StatusResponse {
pub protocol_version: u32,
pub latest_protocol_version: u32,
pub chain_id: String,
pub genesis_hash: CryptoHash,
#[serde(default)]
pub rpc_addr: Option<String>,
#[serde(default)]
pub node_public_key: Option<String>,
#[serde(default)]
pub node_key: Option<String>,
#[serde(default)]
pub validator_account_id: Option<AccountId>,
#[serde(default)]
pub validator_public_key: Option<PublicKey>,
#[serde(default)]
pub validators: Vec<ValidatorInfo>,
pub sync_info: SyncInfo,
pub version: NodeVersion,
#[serde(default)]
pub uptime_sec: Option<u64>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ValidatorInfo {
pub account_id: AccountId,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SyncInfo {
pub latest_block_hash: CryptoHash,
pub latest_block_height: u64,
#[serde(default)]
pub latest_state_root: Option<CryptoHash>,
pub latest_block_time: String,
pub syncing: bool,
#[serde(default)]
pub earliest_block_hash: Option<CryptoHash>,
#[serde(default)]
pub earliest_block_height: Option<u64>,
#[serde(default)]
pub earliest_block_time: Option<String>,
#[serde(default)]
pub epoch_id: Option<CryptoHash>,
#[serde(default)]
pub epoch_start_height: Option<u64>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct NodeVersion {
pub version: String,
pub build: String,
#[serde(default)]
pub commit: Option<String>,
#[serde(default)]
pub rustc_version: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
fn make_account_view(amount: u128, locked: u128, storage_usage: u64) -> AccountView {
AccountView {
amount: NearToken::from_yoctonear(amount),
locked: NearToken::from_yoctonear(locked),
code_hash: CryptoHash::default(),
storage_usage,
storage_paid_at: 0,
global_contract_hash: None,
global_contract_account_id: None,
block_height: 0,
block_hash: CryptoHash::default(),
}
}
#[test]
fn test_available_balance_no_stake_no_storage() {
let view = make_account_view(1_000_000_000_000_000_000_000_000, 0, 0); assert_eq!(view.available(), view.amount);
}
#[test]
fn test_available_balance_with_storage_no_stake() {
let amount = 1_000_000_000_000_000_000_000_000u128; let storage_usage = 1000u64;
let storage_cost = STORAGE_AMOUNT_PER_BYTE * storage_usage as u128;
let view = make_account_view(amount, 0, storage_usage);
let expected = NearToken::from_yoctonear(amount - storage_cost);
assert_eq!(view.available(), expected);
}
#[test]
fn test_available_balance_stake_covers_storage() {
let amount = 1_000_000_000_000_000_000_000_000u128; let locked = 1_000_000_000_000_000_000_000_000u128; let storage_usage = 1000u64;
let view = make_account_view(amount, locked, storage_usage);
assert_eq!(view.available(), view.amount);
}
#[test]
fn test_available_balance_stake_partially_covers_storage() {
let amount = 1_000_000_000_000_000_000_000_000u128; let locked = 5_000_000_000_000_000_000_000u128; let storage_usage = 1000u64;
let storage_cost = STORAGE_AMOUNT_PER_BYTE * storage_usage as u128; let reserved = storage_cost - locked;
let view = make_account_view(amount, locked, storage_usage);
let expected = NearToken::from_yoctonear(amount - reserved);
assert_eq!(view.available(), expected);
}
#[test]
fn test_storage_cost_calculation() {
let storage_usage = 1000u64;
let view = make_account_view(1_000_000_000_000_000_000_000_000, 0, storage_usage);
let expected_cost = STORAGE_AMOUNT_PER_BYTE * storage_usage as u128;
assert_eq!(
view.storage_cost(),
NearToken::from_yoctonear(expected_cost)
);
}
#[test]
fn test_storage_cost_zero_when_stake_covers() {
let locked = 1_000_000_000_000_000_000_000_000u128; let view = make_account_view(1_000_000_000_000_000_000_000_000, locked, 1000);
assert_eq!(view.storage_cost(), NearToken::ZERO);
}
#[test]
fn test_account_balance_from_view() {
let amount = 1_000_000_000_000_000_000_000_000u128; let locked = 500_000_000_000_000_000_000_000u128; let storage_usage = 1000u64;
let view = make_account_view(amount, locked, storage_usage);
let balance = AccountBalance::from(view.clone());
assert_eq!(balance.total, view.amount);
assert_eq!(balance.available, view.available());
assert_eq!(balance.locked, view.locked);
assert_eq!(balance.storage_cost, view.storage_cost());
assert_eq!(balance.storage_usage, storage_usage);
}
fn make_view_result(result: Vec<u8>) -> ViewFunctionResult {
ViewFunctionResult {
result,
logs: vec![],
block_height: 12345,
block_hash: CryptoHash::default(),
}
}
#[test]
fn test_view_function_result_bytes() {
let data = vec![1, 2, 3, 4, 5];
let result = make_view_result(data.clone());
assert_eq!(result.bytes(), &data[..]);
}
#[test]
fn test_view_function_result_as_string() {
let result = make_view_result(b"hello world".to_vec());
assert_eq!(result.as_string().unwrap(), "hello world");
}
#[test]
fn test_view_function_result_json() {
let result = make_view_result(b"42".to_vec());
let value: u64 = result.json().unwrap();
assert_eq!(value, 42);
}
#[test]
fn test_view_function_result_json_object() {
let result = make_view_result(b"{\"count\":123}".to_vec());
let value: serde_json::Value = result.json().unwrap();
assert_eq!(value["count"], 123);
}
#[test]
fn test_view_function_result_borsh() {
let original: u64 = 42;
let encoded = borsh::to_vec(&original).unwrap();
let result = make_view_result(encoded);
let decoded: u64 = result.borsh().unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_view_function_result_borsh_struct() {
#[derive(borsh::BorshSerialize, borsh::BorshDeserialize, PartialEq, Debug)]
struct TestStruct {
value: u64,
name: String,
}
let original = TestStruct {
value: 123,
name: "test".to_string(),
};
let encoded = borsh::to_vec(&original).unwrap();
let result = make_view_result(encoded);
let decoded: TestStruct = result.borsh().unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_view_function_result_borsh_error() {
let result = make_view_result(vec![1, 2, 3]);
let decoded: Result<u64, _> = result.borsh();
assert!(decoded.is_err());
}
#[test]
fn test_gas_profile_entry_string_gas_used() {
let json = serde_json::json!({
"cost_category": "WASM_HOST_COST",
"cost": "BASE",
"gas_used": "123456789"
});
let entry: GasProfileEntry = serde_json::from_value(json).unwrap();
assert_eq!(entry.gas_used.as_gas(), 123456789);
}
#[test]
fn test_gas_profile_entry_numeric_gas_used() {
let json = serde_json::json!({
"cost_category": "ACTION_COST",
"cost": "FUNCTION_CALL",
"gas_used": 999000000
});
let entry: GasProfileEntry = serde_json::from_value(json).unwrap();
assert_eq!(entry.gas_used.as_gas(), 999000000);
}
#[test]
fn test_gas_key_function_call_deserialization() {
let json = serde_json::json!({
"GasKeyFunctionCall": {
"balance": "1000000000000000000000000",
"num_nonces": 5,
"allowance": "500000000000000000000000",
"receiver_id": "app.near",
"method_names": ["call_method"]
}
});
let perm: AccessKeyPermissionView = serde_json::from_value(json).unwrap();
assert!(matches!(
perm,
AccessKeyPermissionView::GasKeyFunctionCall { .. }
));
}
#[test]
fn test_gas_key_full_access_deserialization() {
let json = serde_json::json!({
"GasKeyFullAccess": {
"balance": "1000000000000000000000000",
"num_nonces": 10
}
});
let perm: AccessKeyPermissionView = serde_json::from_value(json).unwrap();
assert!(matches!(
perm,
AccessKeyPermissionView::GasKeyFullAccess { .. }
));
}
#[test]
fn test_transfer_to_gas_key_action_view_deserialization() {
let json = serde_json::json!({
"TransferToGasKey": {
"public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
"deposit": "1000000000000000000000000"
}
});
let action: ActionView = serde_json::from_value(json).unwrap();
assert!(matches!(action, ActionView::TransferToGasKey { .. }));
}
#[test]
fn test_delegate_action_view_deserialization() {
let json = serde_json::json!({
"Delegate": {
"delegate_action": {
"sender_id": "alice.near",
"receiver_id": "contract.near",
"actions": [
{"FunctionCall": {
"method_name": "do_something",
"args": "e30=",
"gas": 30000000000000_u64,
"deposit": "0"
}}
],
"nonce": 42,
"max_block_height": 100000,
"public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp"
},
"signature": "ed25519:3s1dvMqNDCByoMnDnkhB4GPjTSXCRt4nt3Af5n1RX8W7aJ2FC6MfRf5BNXZ52EBifNJnNVBsGvke6GRYuaEYJXt5"
}
});
let action: ActionView = serde_json::from_value(json).unwrap();
match action {
ActionView::Delegate {
delegate_action,
signature,
} => {
assert_eq!(delegate_action.sender_id.as_str(), "alice.near");
assert_eq!(delegate_action.receiver_id.as_str(), "contract.near");
assert_eq!(delegate_action.nonce, 42);
assert_eq!(delegate_action.max_block_height, 100000);
assert_eq!(delegate_action.actions.len(), 1);
assert!(signature.to_string().starts_with("ed25519:"));
}
_ => panic!("Expected Delegate action"),
}
}
#[test]
fn test_withdraw_from_gas_key_action_view_deserialization() {
let json = serde_json::json!({
"WithdrawFromGasKey": {
"public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
"amount": "500000000000000000000000"
}
});
let action: ActionView = serde_json::from_value(json).unwrap();
assert!(matches!(action, ActionView::WithdrawFromGasKey { .. }));
}
#[test]
fn test_final_execution_status_default() {
let status = FinalExecutionStatus::default();
assert!(matches!(status, FinalExecutionStatus::NotStarted));
}
#[test]
fn test_final_execution_status_not_started() {
let json = serde_json::json!("NotStarted");
let status: FinalExecutionStatus = serde_json::from_value(json).unwrap();
assert!(matches!(status, FinalExecutionStatus::NotStarted));
}
#[test]
fn test_final_execution_status_started() {
let json = serde_json::json!("Started");
let status: FinalExecutionStatus = serde_json::from_value(json).unwrap();
assert!(matches!(status, FinalExecutionStatus::Started));
}
#[test]
fn test_final_execution_status_success_value() {
let json = serde_json::json!({"SuccessValue": "aGVsbG8="});
let status: FinalExecutionStatus = serde_json::from_value(json).unwrap();
assert!(matches!(status, FinalExecutionStatus::SuccessValue(ref s) if s == "aGVsbG8="));
}
#[test]
fn test_final_execution_status_failure() {
let json = serde_json::json!({
"Failure": {
"ActionError": {
"index": 0,
"kind": {
"FunctionCallError": {
"ExecutionError": "Smart contract panicked"
}
}
}
}
});
let status: FinalExecutionStatus = serde_json::from_value(json).unwrap();
assert!(matches!(status, FinalExecutionStatus::Failure(_)));
}
#[test]
fn test_send_tx_response_with_outcome() {
let json = serde_json::json!({
"final_execution_status": "FINAL",
"status": {"SuccessValue": ""},
"transaction": {
"signer_id": "alice.near",
"public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
"nonce": 1,
"receiver_id": "bob.near",
"actions": [{"Transfer": {"deposit": "1000000000000000000000000"}}],
"signature": "ed25519:3s1dvMqNDCByoMnDnkhB4GPjTSXCRt4nt3Af5n1RX8W7aJ2FC6MfRf5BNXZ52EBifNJnNVBsGvke6GRYuaEYJXt5",
"hash": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U"
},
"transaction_outcome": {
"id": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U",
"outcome": {
"executor_id": "alice.near",
"gas_burnt": 223182562500_i64,
"tokens_burnt": "22318256250000000000",
"logs": [],
"receipt_ids": ["3GTGoiN3FEoJenSw5ob4YMmFEV2Fbiichj3FDBnM78xK"],
"status": {"SuccessReceiptId": "3GTGoiN3FEoJenSw5ob4YMmFEV2Fbiichj3FDBnM78xK"}
},
"block_hash": "A6DJpKBhmAMmBuQXtY3dWbo8dGVSQ9yH7BQSJBfn8rBo",
"proof": []
},
"receipts_outcome": []
});
let response: RawTransactionResponse = serde_json::from_value(json).unwrap();
assert_eq!(response.final_execution_status, TxExecutionStatus::Final);
let outcome = response.outcome.unwrap();
assert!(outcome.is_success());
assert!(!outcome.is_failure());
}
#[test]
fn test_send_tx_response_pending_none() {
let json = serde_json::json!({
"final_execution_status": "NONE"
});
let response: RawTransactionResponse = serde_json::from_value(json).unwrap();
assert_eq!(response.final_execution_status, TxExecutionStatus::None);
assert!(response.outcome.is_none());
assert!(response.transaction_hash.is_zero());
}
#[test]
fn test_final_execution_outcome_failure() {
let json = serde_json::json!({
"final_execution_status": "EXECUTED_OPTIMISTIC",
"status": {
"Failure": {
"ActionError": {
"index": 0,
"kind": {
"FunctionCallError": {
"ExecutionError": "Smart contract panicked"
}
}
}
}
},
"transaction": {
"signer_id": "alice.near",
"public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
"nonce": 1,
"receiver_id": "bob.near",
"actions": [],
"signature": "ed25519:3s1dvMqNDCByoMnDnkhB4GPjTSXCRt4nt3Af5n1RX8W7aJ2FC6MfRf5BNXZ52EBifNJnNVBsGvke6GRYuaEYJXt5",
"hash": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U"
},
"transaction_outcome": {
"id": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U",
"outcome": {
"executor_id": "alice.near",
"gas_burnt": 0,
"tokens_burnt": "0",
"logs": [],
"receipt_ids": [],
"status": "Unknown"
},
"block_hash": "A6DJpKBhmAMmBuQXtY3dWbo8dGVSQ9yH7BQSJBfn8rBo",
"proof": []
},
"receipts_outcome": []
});
let response: RawTransactionResponse = serde_json::from_value(json).unwrap();
let outcome = response.outcome.unwrap();
assert!(outcome.is_failure());
assert!(!outcome.is_success());
assert!(outcome.failure_message().is_some());
assert!(outcome.failure_error().is_some());
}
fn make_success_outcome(base64_value: &str) -> FinalExecutionOutcome {
let json = serde_json::json!({
"final_execution_status": "FINAL",
"status": {"SuccessValue": base64_value},
"transaction": {
"signer_id": "alice.near",
"public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
"nonce": 1,
"receiver_id": "bob.near",
"actions": [],
"signature": "ed25519:3s1dvMqNDCByoMnDnkhB4GPjTSXCRt4nt3Af5n1RX8W7aJ2FC6MfRf5BNXZ52EBifNJnNVBsGvke6GRYuaEYJXt5",
"hash": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U"
},
"transaction_outcome": {
"id": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U",
"outcome": {
"executor_id": "alice.near",
"gas_burnt": 223182562500_i64,
"tokens_burnt": "22318256250000000000",
"logs": [],
"receipt_ids": [],
"status": {"SuccessReceiptId": "3GTGoiN3FEoJenSw5ob4YMmFEV2Fbiichj3FDBnM78xK"}
},
"block_hash": "A6DJpKBhmAMmBuQXtY3dWbo8dGVSQ9yH7BQSJBfn8rBo",
"proof": []
},
"receipts_outcome": []
});
let response: RawTransactionResponse = serde_json::from_value(json).unwrap();
response.outcome.unwrap()
}
#[test]
fn test_outcome_result() {
let outcome = make_success_outcome("aGVsbG8=");
assert_eq!(outcome.result().unwrap(), b"hello");
}
#[test]
fn test_outcome_result_empty() {
let outcome = make_success_outcome("");
assert_eq!(outcome.result().unwrap(), b"");
}
#[test]
fn test_outcome_json() {
let outcome = make_success_outcome("NDI=");
let val: u64 = outcome.json().unwrap();
assert_eq!(val, 42);
}
#[test]
fn test_outcome_json_bad_data() {
let outcome = make_success_outcome("aGVsbG8=");
let result: Result<u64, _> = outcome.json();
assert!(result.is_err());
}
#[test]
fn test_outcome_result_invalid_base64_returns_err() {
let outcome = make_success_outcome("not-valid-base64!!!");
let err = outcome.result().unwrap_err();
assert!(
err.to_string().contains("base64"),
"Error should mention base64 decode failure, got: {err}"
);
}
#[test]
fn test_outcome_failure_result_returns_err() {
let json = serde_json::json!({
"final_execution_status": "FINAL",
"status": {"Failure": {"ActionError": {"index": 0, "kind": {"FunctionCallError": {"ExecutionError": "test error"}}}}},
"transaction": {
"signer_id": "alice.near",
"public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
"nonce": 1,
"receiver_id": "bob.near",
"actions": [],
"signature": "ed25519:3s1dvMqNDCByoMnDnkhB4GPjTSXCRt4nt3Af5n1RX8W7aJ2FC6MfRf5BNXZ52EBifNJnNVBsGvke6GRYuaEYJXt5",
"hash": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U"
},
"transaction_outcome": {
"id": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U",
"outcome": {
"executor_id": "alice.near",
"gas_burnt": 223182562500_i64,
"tokens_burnt": "22318256250000000000",
"logs": [],
"receipt_ids": [],
"status": {"SuccessReceiptId": "3GTGoiN3FEoJenSw5ob4YMmFEV2Fbiichj3FDBnM78xK"}
},
"block_hash": "A6DJpKBhmAMmBuQXtY3dWbo8dGVSQ9yH7BQSJBfn8rBo",
"proof": []
},
"receipts_outcome": []
});
let outcome: FinalExecutionOutcome = serde_json::from_value(json).unwrap();
assert!(outcome.is_failure());
assert!(!outcome.is_success());
assert!(outcome.result().is_err());
assert!(outcome.json::<u64>().is_err());
assert!(!outcome.transaction_hash().is_zero());
assert!(outcome.total_gas_used().as_gas() > 0);
}
#[test]
fn test_execution_status_unknown() {
let json = serde_json::json!("Unknown");
let status: ExecutionStatus = serde_json::from_value(json).unwrap();
assert!(matches!(status, ExecutionStatus::Unknown));
}
#[test]
fn test_execution_status_success_value() {
let json = serde_json::json!({"SuccessValue": "aGVsbG8="});
let status: ExecutionStatus = serde_json::from_value(json).unwrap();
assert!(matches!(status, ExecutionStatus::SuccessValue(_)));
}
#[test]
fn test_execution_status_success_receipt_id() {
let json =
serde_json::json!({"SuccessReceiptId": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U"});
let status: ExecutionStatus = serde_json::from_value(json).unwrap();
assert!(matches!(status, ExecutionStatus::SuccessReceiptId(_)));
}
#[test]
fn test_execution_status_failure_action_error() {
let json = serde_json::json!({
"Failure": {
"ActionError": {
"index": 0,
"kind": {
"FunctionCallError": {
"ExecutionError": "Smart contract panicked"
}
}
}
}
});
let status: ExecutionStatus = serde_json::from_value(json).unwrap();
match status {
ExecutionStatus::Failure(ae) => {
assert_eq!(ae.index, Some(0));
}
other => panic!("expected Failure, got: {other:?}"),
}
}
#[test]
fn test_execution_status_failure_invalid_tx_error_rejected() {
let json = serde_json::json!({
"Failure": {
"InvalidTxError": "InvalidSignature"
}
});
let err = serde_json::from_value::<ExecutionStatus>(json)
.expect_err("InvalidTxError should be rejected in receipt execution status");
let msg = err.to_string();
assert!(
msg.contains("unexpected InvalidTxError"),
"Expected descriptive error containing \"unexpected InvalidTxError\", got: {msg}"
);
}
#[test]
fn test_global_contract_id_view_new_format_hash() {
let json = serde_json::json!({"hash": "9SP8Y3sVADWNN5QoEB5CsvPUE5HT4o8YfBaCnhLss87K"});
let id: GlobalContractIdentifierView = serde_json::from_value(json).unwrap();
assert!(matches!(id, GlobalContractIdentifierView::CodeHash(_)));
}
#[test]
fn test_global_contract_id_view_new_format_account() {
let json = serde_json::json!({"account_id": "alice.near"});
let id: GlobalContractIdentifierView = serde_json::from_value(json).unwrap();
assert!(matches!(id, GlobalContractIdentifierView::AccountId(_)));
}
#[test]
fn test_global_contract_id_view_deprecated_hash() {
let json = serde_json::json!("9SP8Y3sVADWNN5QoEB5CsvPUE5HT4o8YfBaCnhLss87K");
let id: GlobalContractIdentifierView = serde_json::from_value(json).unwrap();
assert!(matches!(id, GlobalContractIdentifierView::CodeHash(_)));
}
#[test]
fn test_global_contract_id_view_deprecated_account() {
let json = serde_json::json!("alice.near");
let id: GlobalContractIdentifierView = serde_json::from_value(json).unwrap();
assert!(matches!(id, GlobalContractIdentifierView::AccountId(_)));
}
#[test]
fn test_action_view_deterministic_state_init() {
let json = serde_json::json!({
"DeterministicStateInit": {
"code": {"hash": "9SP8Y3sVADWNN5QoEB5CsvPUE5HT4o8YfBaCnhLss87K"},
"data": {"a2V5": "dmFsdWU="},
"deposit": "1000000000000000000000000"
}
});
let action: ActionView = serde_json::from_value(json).unwrap();
match action {
ActionView::DeterministicStateInit {
code,
data,
deposit,
} => {
assert!(matches!(code, GlobalContractIdentifierView::CodeHash(_)));
assert_eq!(data.len(), 1);
assert_eq!(data.get("a2V5").unwrap(), "dmFsdWU=");
assert_eq!(deposit, NearToken::from_near(1));
}
_ => panic!("Expected DeterministicStateInit"),
}
}
#[test]
fn test_action_view_deterministic_state_init_empty_data() {
let json = serde_json::json!({
"DeterministicStateInit": {
"code": {"account_id": "publisher.near"},
"deposit": "0"
}
});
let action: ActionView = serde_json::from_value(json).unwrap();
match action {
ActionView::DeterministicStateInit { code, data, .. } => {
assert!(matches!(code, GlobalContractIdentifierView::AccountId(_)));
assert!(data.is_empty());
}
_ => panic!("Expected DeterministicStateInit"),
}
}
#[test]
fn test_receipt_global_contract_distribution() {
let json = serde_json::json!({
"GlobalContractDistribution": {
"id": {"hash": "9SP8Y3sVADWNN5QoEB5CsvPUE5HT4o8YfBaCnhLss87K"},
"target_shard": 3,
"already_delivered_shards": [0, 1, 2],
"code": "AGFzbQ==",
"nonce": 42
}
});
let content: ReceiptContent = serde_json::from_value(json).unwrap();
match content {
ReceiptContent::GlobalContractDistribution {
id,
target_shard,
already_delivered_shards,
code,
nonce,
} => {
assert!(matches!(id, GlobalContractIdentifierView::CodeHash(_)));
assert_eq!(target_shard, 3);
assert_eq!(already_delivered_shards, vec![0, 1, 2]);
assert_eq!(code, "AGFzbQ==");
assert_eq!(nonce, Some(42));
}
_ => panic!("Expected GlobalContractDistribution"),
}
}
#[test]
fn test_receipt_global_contract_distribution_without_nonce() {
let json = serde_json::json!({
"GlobalContractDistribution": {
"id": {"account_id": "publisher.near"},
"target_shard": 0,
"already_delivered_shards": [],
"code": "AGFzbQ=="
}
});
let content: ReceiptContent = serde_json::from_value(json).unwrap();
match content {
ReceiptContent::GlobalContractDistribution { nonce, .. } => {
assert_eq!(nonce, None);
}
_ => panic!("Expected GlobalContractDistribution"),
}
}
#[test]
fn test_gas_profile_entry_deserialization() {
let json = serde_json::json!({
"cost_category": "WASM_HOST_COST",
"cost": "BASE",
"gas_used": "2646228750"
});
let entry: GasProfileEntry = serde_json::from_value(json).unwrap();
assert_eq!(entry.cost_category, "WASM_HOST_COST");
assert_eq!(entry.cost, "BASE");
assert_eq!(entry.gas_used, Gas::from_gas(2646228750));
}
#[test]
fn test_transaction_view_with_signature() {
let json = serde_json::json!({
"signer_id": "alice.near",
"public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
"nonce": 1,
"receiver_id": "bob.near",
"hash": "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U",
"actions": [{"Transfer": {"deposit": "1000000000000000000000000"}}],
"signature": "ed25519:3s1dvMqNDCByoMnDnkhB4GPjTSXCRt4nt3Af5n1RX8W7aJ2FC6MfRf5BNXZ52EBifNJnNVBsGvke6GRYuaEYJXt5"
});
let tx: TransactionView = serde_json::from_value(json).unwrap();
assert_eq!(tx.signer_id.as_str(), "alice.near");
assert!(tx.signature.to_string().starts_with("ed25519:"));
}
}