use std::collections::BTreeMap;
use base64::{Engine as _, engine::general_purpose::STANDARD};
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
use serde_with::base64::Base64;
use serde_with::serde_as;
use sha3::{Digest, Keccak256};
use super::{AccountId, CryptoHash, Gas, NearToken, PublicKey, Signature, TryIntoAccountId};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PublishMode {
Updatable,
Immutable,
}
pub trait GlobalContractRef {
fn into_identifier(self) -> GlobalContractIdentifier;
}
impl GlobalContractRef for CryptoHash {
fn into_identifier(self) -> GlobalContractIdentifier {
GlobalContractIdentifier::CodeHash(self)
}
}
impl GlobalContractRef for AccountId {
fn into_identifier(self) -> GlobalContractIdentifier {
GlobalContractIdentifier::AccountId(self)
}
}
impl GlobalContractRef for &AccountId {
fn into_identifier(self) -> GlobalContractIdentifier {
GlobalContractIdentifier::AccountId(self.clone())
}
}
impl GlobalContractRef for &str {
fn into_identifier(self) -> GlobalContractIdentifier {
let account_id: AccountId = self.try_into_account_id().expect("invalid account ID");
GlobalContractIdentifier::AccountId(account_id)
}
}
impl GlobalContractRef for String {
fn into_identifier(self) -> GlobalContractIdentifier {
let account_id: AccountId = self.try_into_account_id().expect("invalid account ID");
GlobalContractIdentifier::AccountId(account_id)
}
}
impl GlobalContractRef for &String {
fn into_identifier(self) -> GlobalContractIdentifier {
let account_id: AccountId = self
.as_str()
.try_into_account_id()
.expect("invalid account ID");
GlobalContractIdentifier::AccountId(account_id)
}
}
pub const DELEGATE_ACTION_PREFIX: u32 = 1_073_742_190;
#[derive(
Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize,
)]
pub struct GasKeyInfo {
pub balance: NearToken,
pub num_nonces: u16,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub enum AccessKeyPermission {
FunctionCall(FunctionCallPermission),
FullAccess,
GasKeyFunctionCall(GasKeyInfo, FunctionCallPermission),
GasKeyFullAccess(GasKeyInfo),
}
impl AccessKeyPermission {
pub fn function_call(
receiver_id: AccountId,
method_names: Vec<String>,
allowance: Option<NearToken>,
) -> Self {
Self::FunctionCall(FunctionCallPermission {
allowance,
receiver_id,
method_names,
})
}
pub fn full_access() -> Self {
Self::FullAccess
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct FunctionCallPermission {
pub allowance: Option<NearToken>,
pub receiver_id: AccountId,
pub method_names: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct AccessKey {
pub nonce: u64,
pub permission: AccessKeyPermission,
}
impl AccessKey {
pub fn full_access() -> Self {
Self {
nonce: 0,
permission: AccessKeyPermission::FullAccess,
}
}
pub fn function_call(
receiver_id: AccountId,
method_names: Vec<String>,
allowance: Option<NearToken>,
) -> Self {
Self {
nonce: 0,
permission: AccessKeyPermission::function_call(receiver_id, method_names, allowance),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub enum Action {
CreateAccount(CreateAccountAction),
DeployContract(DeployContractAction),
FunctionCall(FunctionCallAction),
Transfer(TransferAction),
Stake(StakeAction),
AddKey(AddKeyAction),
DeleteKey(DeleteKeyAction),
DeleteAccount(DeleteAccountAction),
Delegate(Box<SignedDelegateAction>),
DeployGlobalContract(DeployGlobalContractAction),
UseGlobalContract(UseGlobalContractAction),
DeterministicStateInit(DeterministicStateInitAction),
TransferToGasKey(TransferToGasKeyAction),
WithdrawFromGasKey(WithdrawFromGasKeyAction),
}
#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct CreateAccountAction;
#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct DeployContractAction {
pub code: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct FunctionCallAction {
pub method_name: String,
pub args: Vec<u8>,
pub gas: Gas,
pub deposit: NearToken,
}
#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct TransferAction {
pub deposit: NearToken,
}
#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct StakeAction {
pub stake: NearToken,
pub public_key: PublicKey,
}
#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct AddKeyAction {
pub public_key: PublicKey,
pub access_key: AccessKey,
}
#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct DeleteKeyAction {
pub public_key: PublicKey,
}
#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct DeleteAccountAction {
pub beneficiary_id: AccountId,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub enum GlobalContractIdentifier {
#[serde(rename = "hash")]
CodeHash(CryptoHash),
#[serde(rename = "account_id")]
AccountId(AccountId),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
#[repr(u8)]
pub enum GlobalContractDeployMode {
CodeHash,
AccountId,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct DeployGlobalContractAction {
pub code: Vec<u8>,
pub deploy_mode: GlobalContractDeployMode,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct UseGlobalContractAction {
pub contract_identifier: GlobalContractIdentifier,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
#[repr(u8)]
pub enum DeterministicAccountStateInit {
V1(DeterministicAccountStateInitV1),
}
#[serde_as]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct DeterministicAccountStateInitV1 {
pub code: GlobalContractIdentifier,
#[serde_as(as = "BTreeMap<Base64, Base64>")]
pub data: BTreeMap<Vec<u8>, Vec<u8>>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct DeterministicStateInitAction {
pub state_init: DeterministicAccountStateInit,
pub deposit: NearToken,
}
impl DeterministicAccountStateInit {
pub fn by_hash(code_hash: CryptoHash, data: BTreeMap<Vec<u8>, Vec<u8>>) -> Self {
Self::V1(DeterministicAccountStateInitV1 {
code: GlobalContractIdentifier::CodeHash(code_hash),
data,
})
}
pub fn by_publisher(publisher_id: AccountId, data: BTreeMap<Vec<u8>, Vec<u8>>) -> Self {
Self::V1(DeterministicAccountStateInitV1 {
code: GlobalContractIdentifier::AccountId(publisher_id),
data,
})
}
pub fn derive_account_id(&self) -> AccountId {
let serialized = borsh::to_vec(self).expect("StateInit serialization should not fail");
let hash = Keccak256::digest(&serialized);
let suffix = &hash[12..32];
let account_str = format!("0s{}", hex::encode(suffix));
account_str
.parse()
.expect("deterministic account ID should always be valid")
}
}
impl DeterministicStateInitAction {
pub fn derive_account_id(&self) -> AccountId {
self.state_init.derive_account_id()
}
}
#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct TransferToGasKeyAction {
pub public_key: PublicKey,
pub deposit: NearToken,
}
#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct WithdrawFromGasKeyAction {
pub public_key: PublicKey,
pub amount: NearToken,
}
#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct DelegateAction {
pub sender_id: AccountId,
pub receiver_id: AccountId,
pub actions: Vec<NonDelegateAction>,
pub nonce: u64,
pub max_block_height: u64,
pub public_key: PublicKey,
}
#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct SignedDelegateAction {
pub delegate_action: DelegateAction,
pub signature: super::Signature,
}
#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct NonDelegateAction(Action);
impl Action {
pub fn create_account() -> Self {
Self::CreateAccount(CreateAccountAction)
}
pub fn deploy_contract(code: Vec<u8>) -> Self {
Self::DeployContract(DeployContractAction { code })
}
pub fn function_call(
method_name: impl Into<String>,
args: Vec<u8>,
gas: Gas,
deposit: NearToken,
) -> Self {
Self::FunctionCall(FunctionCallAction {
method_name: method_name.into(),
args,
gas,
deposit,
})
}
pub fn transfer(deposit: NearToken) -> Self {
Self::Transfer(TransferAction { deposit })
}
pub fn stake(stake: NearToken, public_key: PublicKey) -> Self {
Self::Stake(StakeAction { stake, public_key })
}
pub fn add_full_access_key(public_key: PublicKey) -> Self {
Self::AddKey(AddKeyAction {
public_key,
access_key: AccessKey::full_access(),
})
}
pub fn add_function_call_key(
public_key: PublicKey,
receiver_id: AccountId,
method_names: Vec<String>,
allowance: Option<NearToken>,
) -> Self {
Self::AddKey(AddKeyAction {
public_key,
access_key: AccessKey::function_call(receiver_id, method_names, allowance),
})
}
pub fn delete_key(public_key: PublicKey) -> Self {
Self::DeleteKey(DeleteKeyAction { public_key })
}
pub fn delete_account(beneficiary_id: AccountId) -> Self {
Self::DeleteAccount(DeleteAccountAction { beneficiary_id })
}
pub fn delegate(signed_delegate: SignedDelegateAction) -> Self {
Self::Delegate(Box::new(signed_delegate))
}
pub fn publish(code: Vec<u8>, mode: PublishMode) -> Self {
Self::DeployGlobalContract(DeployGlobalContractAction {
code,
deploy_mode: match mode {
PublishMode::Updatable => GlobalContractDeployMode::AccountId,
PublishMode::Immutable => GlobalContractDeployMode::CodeHash,
},
})
}
pub fn deploy_from_hash(code_hash: CryptoHash) -> Self {
Self::UseGlobalContract(UseGlobalContractAction {
contract_identifier: GlobalContractIdentifier::CodeHash(code_hash),
})
}
pub fn deploy_from_account(account_id: AccountId) -> Self {
Self::UseGlobalContract(UseGlobalContractAction {
contract_identifier: GlobalContractIdentifier::AccountId(account_id),
})
}
pub fn state_init(state_init: DeterministicAccountStateInit, deposit: NearToken) -> Self {
Self::DeterministicStateInit(DeterministicStateInitAction {
state_init,
deposit,
})
}
pub fn transfer_to_gas_key(public_key: PublicKey, deposit: NearToken) -> Self {
Self::TransferToGasKey(TransferToGasKeyAction {
public_key,
deposit,
})
}
pub fn withdraw_from_gas_key(public_key: PublicKey, amount: NearToken) -> Self {
Self::WithdrawFromGasKey(WithdrawFromGasKeyAction { public_key, amount })
}
}
impl DelegateAction {
pub fn serialize_for_signing(&self) -> Vec<u8> {
let prefix_bytes = DELEGATE_ACTION_PREFIX.to_le_bytes();
let action_bytes =
borsh::to_vec(self).expect("delegate action serialization should never fail");
let mut result = Vec::with_capacity(prefix_bytes.len() + action_bytes.len());
result.extend_from_slice(&prefix_bytes);
result.extend_from_slice(&action_bytes);
result
}
pub fn get_hash(&self) -> CryptoHash {
let bytes = self.serialize_for_signing();
CryptoHash::hash(&bytes)
}
pub fn sign(self, signature: Signature) -> SignedDelegateAction {
SignedDelegateAction {
delegate_action: self,
signature,
}
}
}
impl SignedDelegateAction {
pub fn to_bytes(&self) -> Vec<u8> {
borsh::to_vec(self).expect("signed delegate action serialization should never fail")
}
pub fn to_base64(&self) -> String {
STANDARD.encode(self.to_bytes())
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, borsh::io::Error> {
borsh::from_slice(bytes)
}
pub fn from_base64(s: &str) -> Result<Self, DecodeError> {
let bytes = STANDARD.decode(s).map_err(DecodeError::Base64)?;
Self::from_bytes(&bytes).map_err(DecodeError::Borsh)
}
pub fn sender_id(&self) -> &AccountId {
&self.delegate_action.sender_id
}
pub fn receiver_id(&self) -> &AccountId {
&self.delegate_action.receiver_id
}
}
#[derive(Debug, thiserror::Error)]
pub enum DecodeError {
#[error("base64 decode error: {0}")]
Base64(#[from] base64::DecodeError),
#[error("borsh decode error: {0}")]
Borsh(#[from] borsh::io::Error),
}
impl NonDelegateAction {
pub fn from_action(action: Action) -> Option<Self> {
if matches!(action, Action::Delegate(_)) {
None
} else {
Some(Self(action))
}
}
pub fn inner(&self) -> &Action {
&self.0
}
pub fn into_inner(self) -> Action {
self.0
}
}
impl From<NonDelegateAction> for Action {
fn from(action: NonDelegateAction) -> Self {
action.0
}
}
impl TryFrom<Action> for NonDelegateAction {
type Error = ();
fn try_from(action: Action) -> Result<Self, Self::Error> {
Self::from_action(action).ok_or(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Gas, NearToken, SecretKey};
fn create_test_delegate_action() -> DelegateAction {
let sender_id: AccountId = "alice.testnet".parse().unwrap();
let receiver_id: AccountId = "bob.testnet".parse().unwrap();
let public_key: PublicKey = "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp"
.parse()
.unwrap();
DelegateAction {
sender_id,
receiver_id,
actions: vec![
NonDelegateAction::from_action(Action::Transfer(TransferAction {
deposit: NearToken::from_near(1),
}))
.unwrap(),
],
nonce: 1,
max_block_height: 1000,
public_key,
}
}
#[test]
fn test_delegate_action_prefix() {
assert_eq!(DELEGATE_ACTION_PREFIX, 1073742190);
assert_eq!(DELEGATE_ACTION_PREFIX, (1 << 30) + 366);
}
#[test]
fn test_delegate_action_serialize_for_signing() {
let delegate_action = create_test_delegate_action();
let bytes = delegate_action.serialize_for_signing();
let prefix_bytes = &bytes[0..4];
let prefix = u32::from_le_bytes(prefix_bytes.try_into().unwrap());
assert_eq!(prefix, DELEGATE_ACTION_PREFIX);
let action_bytes = &bytes[4..];
let expected_action_bytes = borsh::to_vec(&delegate_action).unwrap();
assert_eq!(action_bytes, expected_action_bytes.as_slice());
}
#[test]
fn test_delegate_action_get_hash() {
let delegate_action = create_test_delegate_action();
let hash = delegate_action.get_hash();
let bytes = delegate_action.serialize_for_signing();
let expected_hash = CryptoHash::hash(&bytes);
assert_eq!(hash, expected_hash);
}
#[test]
fn test_signed_delegate_action_roundtrip_bytes() {
let delegate_action = create_test_delegate_action();
let secret_key = SecretKey::generate_ed25519();
let hash = delegate_action.get_hash();
let signature = secret_key.sign(hash.as_bytes());
let signed = delegate_action.sign(signature);
let bytes = signed.to_bytes();
let decoded = SignedDelegateAction::from_bytes(&bytes).unwrap();
assert_eq!(decoded.sender_id().as_str(), signed.sender_id().as_str());
assert_eq!(
decoded.receiver_id().as_str(),
signed.receiver_id().as_str()
);
assert_eq!(decoded.delegate_action.nonce, signed.delegate_action.nonce);
assert_eq!(
decoded.delegate_action.max_block_height,
signed.delegate_action.max_block_height
);
}
#[test]
fn test_signed_delegate_action_roundtrip_base64() {
let delegate_action = create_test_delegate_action();
let secret_key = SecretKey::generate_ed25519();
let hash = delegate_action.get_hash();
let signature = secret_key.sign(hash.as_bytes());
let signed = delegate_action.sign(signature);
let base64 = signed.to_base64();
let decoded = SignedDelegateAction::from_base64(&base64).unwrap();
assert_eq!(decoded.sender_id().as_str(), signed.sender_id().as_str());
assert_eq!(
decoded.receiver_id().as_str(),
signed.receiver_id().as_str()
);
}
#[test]
fn test_signed_delegate_action_accessors() {
let delegate_action = create_test_delegate_action();
let secret_key = SecretKey::generate_ed25519();
let hash = delegate_action.get_hash();
let signature = secret_key.sign(hash.as_bytes());
let signed = delegate_action.sign(signature);
assert_eq!(signed.sender_id().as_str(), "alice.testnet");
assert_eq!(signed.receiver_id().as_str(), "bob.testnet");
}
#[test]
fn test_non_delegate_action_from_action() {
let transfer = Action::Transfer(TransferAction {
deposit: NearToken::from_near(1),
});
assert!(NonDelegateAction::from_action(transfer).is_some());
let call = Action::FunctionCall(FunctionCallAction {
method_name: "test".to_string(),
args: vec![],
gas: Gas::default(),
deposit: NearToken::ZERO,
});
assert!(NonDelegateAction::from_action(call).is_some());
let delegate_action = create_test_delegate_action();
let secret_key = SecretKey::generate_ed25519();
let hash = delegate_action.get_hash();
let signature = secret_key.sign(hash.as_bytes());
let signed = delegate_action.sign(signature);
let delegate = Action::delegate(signed);
assert!(NonDelegateAction::from_action(delegate).is_none());
}
#[test]
fn test_decode_error_display() {
let base64_err = DecodeError::Base64(base64::DecodeError::InvalidLength(5));
assert!(format!("{}", base64_err).contains("base64"));
}
#[test]
fn test_action_discriminants() {
let create_account = Action::create_account();
let bytes = borsh::to_vec(&create_account).unwrap();
assert_eq!(bytes[0], 0, "CreateAccount should have discriminant 0");
let deploy = Action::deploy_contract(vec![1, 2, 3]);
let bytes = borsh::to_vec(&deploy).unwrap();
assert_eq!(bytes[0], 1, "DeployContract should have discriminant 1");
let transfer = Action::transfer(NearToken::from_near(1));
let bytes = borsh::to_vec(&transfer).unwrap();
assert_eq!(bytes[0], 3, "Transfer should have discriminant 3");
let publish = Action::publish(vec![1, 2, 3], PublishMode::Updatable);
let bytes = borsh::to_vec(&publish).unwrap();
assert_eq!(
bytes[0], 9,
"DeployGlobalContract should have discriminant 9"
);
let code_hash = CryptoHash::hash(&[1, 2, 3]);
let use_global = Action::deploy_from_hash(code_hash);
let bytes = borsh::to_vec(&use_global).unwrap();
assert_eq!(
bytes[0], 10,
"UseGlobalContract should have discriminant 10"
);
let state_init = Action::state_init(
DeterministicAccountStateInit::by_hash(code_hash, BTreeMap::new()),
NearToken::from_near(1),
);
let bytes = borsh::to_vec(&state_init).unwrap();
assert_eq!(
bytes[0], 11,
"DeterministicStateInit should have discriminant 11"
);
let pk: PublicKey = "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp"
.parse()
.unwrap();
let transfer_gas = Action::transfer_to_gas_key(pk.clone(), NearToken::from_near(1));
let bytes = borsh::to_vec(&transfer_gas).unwrap();
assert_eq!(bytes[0], 12, "TransferToGasKey should have discriminant 12");
let withdraw_gas = Action::withdraw_from_gas_key(pk, NearToken::from_near(1));
let bytes = borsh::to_vec(&withdraw_gas).unwrap();
assert_eq!(
bytes[0], 13,
"WithdrawFromGasKey should have discriminant 13"
);
}
#[test]
fn test_global_contract_deploy_mode_serialization() {
let by_hash = GlobalContractDeployMode::CodeHash;
let bytes = borsh::to_vec(&by_hash).unwrap();
assert_eq!(bytes, vec![0], "CodeHash mode should serialize to 0");
let by_account = GlobalContractDeployMode::AccountId;
let bytes = borsh::to_vec(&by_account).unwrap();
assert_eq!(bytes, vec![1], "AccountId mode should serialize to 1");
}
#[test]
fn test_global_contract_identifier_serialization() {
let hash = CryptoHash::hash(&[1, 2, 3]);
let by_hash = GlobalContractIdentifier::CodeHash(hash);
let bytes = borsh::to_vec(&by_hash).unwrap();
assert_eq!(
bytes[0], 0,
"CodeHash identifier should have discriminant 0"
);
assert_eq!(
bytes.len(),
1 + 32,
"Should be 1 byte discriminant + 32 byte hash"
);
let account_id: AccountId = "test.near".parse().unwrap();
let by_account = GlobalContractIdentifier::AccountId(account_id);
let bytes = borsh::to_vec(&by_account).unwrap();
assert_eq!(
bytes[0], 1,
"AccountId identifier should have discriminant 1"
);
}
#[test]
fn test_deploy_global_contract_action_roundtrip() {
let code = vec![0, 97, 115, 109]; let action = DeployGlobalContractAction {
code: code.clone(),
deploy_mode: GlobalContractDeployMode::CodeHash,
};
let bytes = borsh::to_vec(&action).unwrap();
let decoded: DeployGlobalContractAction = borsh::from_slice(&bytes).unwrap();
assert_eq!(decoded.code, code);
assert_eq!(decoded.deploy_mode, GlobalContractDeployMode::CodeHash);
}
#[test]
fn test_use_global_contract_action_roundtrip() {
let hash = CryptoHash::hash(&[1, 2, 3, 4]);
let action = UseGlobalContractAction {
contract_identifier: GlobalContractIdentifier::CodeHash(hash),
};
let bytes = borsh::to_vec(&action).unwrap();
let decoded: UseGlobalContractAction = borsh::from_slice(&bytes).unwrap();
assert_eq!(
decoded.contract_identifier,
GlobalContractIdentifier::CodeHash(hash)
);
}
#[test]
fn test_deterministic_state_init_roundtrip() {
let hash = CryptoHash::hash(&[1, 2, 3, 4]);
let mut data = BTreeMap::new();
data.insert(b"key1".to_vec(), b"value1".to_vec());
data.insert(b"key2".to_vec(), b"value2".to_vec());
let action = DeterministicStateInitAction {
state_init: DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
code: GlobalContractIdentifier::CodeHash(hash),
data: data.clone(),
}),
deposit: NearToken::from_near(5),
};
let bytes = borsh::to_vec(&action).unwrap();
let decoded: DeterministicStateInitAction = borsh::from_slice(&bytes).unwrap();
assert_eq!(decoded.deposit, NearToken::from_near(5));
let DeterministicAccountStateInit::V1(v1) = decoded.state_init;
assert_eq!(v1.code, GlobalContractIdentifier::CodeHash(hash));
assert_eq!(v1.data, data);
}
#[test]
fn test_action_helper_constructors() {
let code = vec![1, 2, 3];
let action = Action::publish(code.clone(), PublishMode::Immutable);
if let Action::DeployGlobalContract(inner) = action {
assert_eq!(inner.code, code);
assert_eq!(inner.deploy_mode, GlobalContractDeployMode::CodeHash);
} else {
panic!("Expected DeployGlobalContract");
}
let action = Action::publish(code.clone(), PublishMode::Updatable);
if let Action::DeployGlobalContract(inner) = action {
assert_eq!(inner.deploy_mode, GlobalContractDeployMode::AccountId);
} else {
panic!("Expected DeployGlobalContract");
}
let hash = CryptoHash::hash(&code);
let action = Action::deploy_from_hash(hash);
if let Action::UseGlobalContract(inner) = action {
assert_eq!(
inner.contract_identifier,
GlobalContractIdentifier::CodeHash(hash)
);
} else {
panic!("Expected UseGlobalContract");
}
let account_id: AccountId = "publisher.near".parse().unwrap();
let action = Action::deploy_from_account(account_id.clone());
if let Action::UseGlobalContract(inner) = action {
assert_eq!(
inner.contract_identifier,
GlobalContractIdentifier::AccountId(account_id)
);
} else {
panic!("Expected UseGlobalContract");
}
}
#[test]
fn test_derive_account_id_format() {
let state_init = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
code: GlobalContractIdentifier::CodeHash(CryptoHash::default()),
data: BTreeMap::new(),
});
let account_id = state_init.derive_account_id();
let account_str = account_id.as_str();
assert!(
account_str.starts_with("0s"),
"Derived account should start with '0s', got: {}",
account_str
);
assert_eq!(
account_str.len(),
42,
"Derived account should be 42 chars, got: {}",
account_str.len()
);
let hex_part = &account_str[2..];
assert!(
hex_part
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()),
"Hex part should be lowercase hex, got: {}",
hex_part
);
}
#[test]
fn test_derive_account_id_deterministic() {
let state_init1 = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
code: GlobalContractIdentifier::AccountId("publisher.near".parse().unwrap()),
data: BTreeMap::new(),
});
let state_init2 = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
code: GlobalContractIdentifier::AccountId("publisher.near".parse().unwrap()),
data: BTreeMap::new(),
});
assert_eq!(
state_init1.derive_account_id(),
state_init2.derive_account_id(),
"Same input should produce same account ID"
);
}
#[test]
fn test_derive_account_id_different_inputs() {
let state_init1 = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
code: GlobalContractIdentifier::AccountId("publisher1.near".parse().unwrap()),
data: BTreeMap::new(),
});
let state_init2 = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
code: GlobalContractIdentifier::AccountId("publisher2.near".parse().unwrap()),
data: BTreeMap::new(),
});
assert_ne!(
state_init1.derive_account_id(),
state_init2.derive_account_id(),
"Different code references should produce different account IDs"
);
}
#[test]
fn test_access_key_permission_discriminants() {
let fc = AccessKeyPermission::FunctionCall(FunctionCallPermission {
allowance: None,
receiver_id: "test.near".parse().unwrap(),
method_names: vec![],
});
let bytes = borsh::to_vec(&fc).unwrap();
assert_eq!(bytes[0], 0, "FunctionCall should have discriminant 0");
let fa = AccessKeyPermission::FullAccess;
let bytes = borsh::to_vec(&fa).unwrap();
assert_eq!(bytes[0], 1, "FullAccess should have discriminant 1");
let gkfc = AccessKeyPermission::GasKeyFunctionCall(
GasKeyInfo {
balance: NearToken::from_near(1),
num_nonces: 5,
},
FunctionCallPermission {
allowance: None,
receiver_id: "test.near".parse().unwrap(),
method_names: vec![],
},
);
let bytes = borsh::to_vec(&gkfc).unwrap();
assert_eq!(bytes[0], 2, "GasKeyFunctionCall should have discriminant 2");
let gkfa = AccessKeyPermission::GasKeyFullAccess(GasKeyInfo {
balance: NearToken::from_near(1),
num_nonces: 5,
});
let bytes = borsh::to_vec(&gkfa).unwrap();
assert_eq!(bytes[0], 3, "GasKeyFullAccess should have discriminant 3");
}
#[test]
fn test_derive_account_id_different_data() {
let mut data = BTreeMap::new();
data.insert(b"key".to_vec(), b"value".to_vec());
let state_init1 = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
code: GlobalContractIdentifier::AccountId("publisher.near".parse().unwrap()),
data: BTreeMap::new(),
});
let state_init2 = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
code: GlobalContractIdentifier::AccountId("publisher.near".parse().unwrap()),
data,
});
assert_ne!(
state_init1.derive_account_id(),
state_init2.derive_account_id(),
"Different data should produce different account IDs"
);
}
#[test]
fn test_deterministic_state_init_json_roundtrip() {
let hash = CryptoHash::hash(&[1, 2, 3, 4]);
let mut data = BTreeMap::new();
data.insert(b"key1".to_vec(), b"value1".to_vec());
data.insert(b"key2".to_vec(), b"value2".to_vec());
let state_init = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
code: GlobalContractIdentifier::CodeHash(hash),
data: data.clone(),
});
let json = serde_json::to_value(&state_init).unwrap();
assert!(
json.get("V1").is_some(),
"Expected externally-tagged 'V1' key, got: {json}"
);
let v1 = json.get("V1").unwrap();
assert!(v1.get("code").is_some(), "Expected 'code' field in V1");
assert!(v1.get("data").is_some(), "Expected 'data' field in V1");
let data_obj = v1.get("data").unwrap().as_object().unwrap();
assert!(
data_obj.contains_key("a2V5MQ=="),
"Expected base64-encoded key 'a2V5MQ==', got keys: {:?}",
data_obj.keys().collect::<Vec<_>>()
);
let deserialized: DeterministicAccountStateInit = serde_json::from_value(json).unwrap();
let DeterministicAccountStateInit::V1(v1_decoded) = deserialized;
assert_eq!(v1_decoded.code, GlobalContractIdentifier::CodeHash(hash));
assert_eq!(v1_decoded.data, data);
}
#[test]
fn test_global_contract_identifier_json_roundtrip() {
let hash = CryptoHash::hash(&[1, 2, 3]);
let id = GlobalContractIdentifier::CodeHash(hash);
let json = serde_json::to_string(&id).unwrap();
let decoded: GlobalContractIdentifier = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, id);
let account_id: AccountId = "test.near".parse().unwrap();
let id = GlobalContractIdentifier::AccountId(account_id);
let json = serde_json::to_string(&id).unwrap();
let decoded: GlobalContractIdentifier = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, id);
}
#[test]
fn test_deterministic_state_init_action_json_roundtrip() {
let action = DeterministicStateInitAction {
state_init: DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
code: GlobalContractIdentifier::AccountId("publisher.near".parse().unwrap()),
data: BTreeMap::new(),
}),
deposit: NearToken::from_near(5),
};
let json = serde_json::to_string(&action).unwrap();
let decoded: DeterministicStateInitAction = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, action);
}
}