use crate::account::AccessKey;
use crate::delegate_action::SignedDelegateAction;
use crate::errors::TxExecutionError;
use crate::hash::{hash, CryptoHash};
use crate::merkle::MerklePath;
use crate::serialize::dec_format;
use crate::types::{AccountId, Balance, Gas, Nonce};
use borsh::{BorshDeserialize, BorshSerialize};
use near_crypto::{PublicKey, Signature};
use near_fmt::{AbbrBytes, Slice};
use near_primitives_core::profile::{ProfileDataV2, ProfileDataV3};
use near_primitives_core::types::Compute;
use serde_with::base64::Base64;
use serde_with::serde_as;
use std::borrow::Borrow;
use std::fmt;
use std::hash::{Hash, Hasher};
pub type LogEntry = String;
#[derive(
BorshSerialize,
BorshDeserialize,
PartialEq,
Eq,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
)]
pub struct Transaction {
pub signer_id: AccountId,
pub public_key: PublicKey,
pub nonce: Nonce,
pub receiver_id: AccountId,
pub block_hash: CryptoHash,
pub actions: Vec<Action>,
}
impl Transaction {
pub fn get_hash_and_size(&self) -> (CryptoHash, u64) {
let bytes = self.try_to_vec().expect("Failed to deserialize");
(hash(&bytes), bytes.len() as u64)
}
}
#[derive(
BorshSerialize,
BorshDeserialize,
PartialEq,
Eq,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
strum::AsRefStr,
)]
pub enum Action {
CreateAccount(CreateAccountAction),
DeployContract(DeployContractAction),
FunctionCall(FunctionCallAction),
Transfer(TransferAction),
Stake(StakeAction),
AddKey(AddKeyAction),
DeleteKey(DeleteKeyAction),
DeleteAccount(DeleteAccountAction),
Delegate(SignedDelegateAction),
}
impl Action {
pub fn get_prepaid_gas(&self) -> Gas {
match self {
Action::FunctionCall(a) => a.gas,
_ => 0,
}
}
pub fn get_deposit_balance(&self) -> Balance {
match self {
Action::FunctionCall(a) => a.deposit,
Action::Transfer(a) => a.deposit,
_ => 0,
}
}
}
#[derive(
BorshSerialize,
BorshDeserialize,
PartialEq,
Eq,
Clone,
Debug,
serde::Serialize,
serde::Deserialize,
)]
pub struct CreateAccountAction {}
impl From<CreateAccountAction> for Action {
fn from(create_account_action: CreateAccountAction) -> Self {
Self::CreateAccount(create_account_action)
}
}
#[serde_as]
#[derive(
BorshSerialize, BorshDeserialize, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone,
)]
pub struct DeployContractAction {
#[serde_as(as = "Base64")]
pub code: Vec<u8>,
}
impl From<DeployContractAction> for Action {
fn from(deploy_contract_action: DeployContractAction) -> Self {
Self::DeployContract(deploy_contract_action)
}
}
impl fmt::Debug for DeployContractAction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DeployContractAction")
.field("code", &format_args!("{}", AbbrBytes(&self.code)))
.finish()
}
}
#[serde_as]
#[derive(
BorshSerialize, BorshDeserialize, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone,
)]
pub struct FunctionCallAction {
pub method_name: String,
#[serde_as(as = "Base64")]
pub args: Vec<u8>,
pub gas: Gas,
#[serde(with = "dec_format")]
pub deposit: Balance,
}
impl From<FunctionCallAction> for Action {
fn from(function_call_action: FunctionCallAction) -> Self {
Self::FunctionCall(function_call_action)
}
}
impl fmt::Debug for FunctionCallAction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FunctionCallAction")
.field("method_name", &format_args!("{}", &self.method_name))
.field("args", &format_args!("{}", AbbrBytes(&self.args)))
.field("gas", &format_args!("{}", &self.gas))
.field("deposit", &format_args!("{}", &self.deposit))
.finish()
}
}
#[derive(
BorshSerialize,
BorshDeserialize,
PartialEq,
Eq,
Clone,
Debug,
serde::Serialize,
serde::Deserialize,
)]
pub struct TransferAction {
#[serde(with = "dec_format")]
pub deposit: Balance,
}
impl From<TransferAction> for Action {
fn from(transfer_action: TransferAction) -> Self {
Self::Transfer(transfer_action)
}
}
#[derive(
BorshSerialize,
BorshDeserialize,
PartialEq,
Eq,
Clone,
Debug,
serde::Serialize,
serde::Deserialize,
)]
pub struct StakeAction {
#[serde(with = "dec_format")]
pub stake: Balance,
pub public_key: PublicKey,
}
impl From<StakeAction> for Action {
fn from(stake_action: StakeAction) -> Self {
Self::Stake(stake_action)
}
}
#[derive(
BorshSerialize,
BorshDeserialize,
PartialEq,
Eq,
Clone,
Debug,
serde::Serialize,
serde::Deserialize,
)]
pub struct AddKeyAction {
pub public_key: PublicKey,
pub access_key: AccessKey,
}
impl From<AddKeyAction> for Action {
fn from(add_key_action: AddKeyAction) -> Self {
Self::AddKey(add_key_action)
}
}
#[derive(
BorshSerialize,
BorshDeserialize,
PartialEq,
Eq,
Clone,
Debug,
serde::Serialize,
serde::Deserialize,
)]
pub struct DeleteKeyAction {
pub public_key: PublicKey,
}
impl From<DeleteKeyAction> for Action {
fn from(delete_key_action: DeleteKeyAction) -> Self {
Self::DeleteKey(delete_key_action)
}
}
#[derive(
BorshSerialize,
BorshDeserialize,
PartialEq,
Eq,
Clone,
Debug,
serde::Serialize,
serde::Deserialize,
)]
pub struct DeleteAccountAction {
pub beneficiary_id: AccountId,
}
impl From<DeleteAccountAction> for Action {
fn from(delete_account_action: DeleteAccountAction) -> Self {
Self::DeleteAccount(delete_account_action)
}
}
#[derive(
BorshSerialize, BorshDeserialize, serde::Serialize, serde::Deserialize, Eq, Debug, Clone,
)]
#[borsh_init(init)]
pub struct SignedTransaction {
pub transaction: Transaction,
pub signature: Signature,
#[borsh_skip]
hash: CryptoHash,
#[borsh_skip]
size: u64,
}
impl SignedTransaction {
pub fn new(signature: Signature, transaction: Transaction) -> Self {
let mut signed_tx =
Self { signature, transaction, hash: CryptoHash::default(), size: u64::default() };
signed_tx.init();
signed_tx
}
pub fn init(&mut self) {
let (hash, size) = self.transaction.get_hash_and_size();
self.hash = hash;
self.size = size;
}
pub fn get_hash(&self) -> CryptoHash {
self.hash
}
pub fn get_size(&self) -> u64 {
self.size
}
}
impl Hash for SignedTransaction {
fn hash<H: Hasher>(&self, state: &mut H) {
self.hash.hash(state)
}
}
impl PartialEq for SignedTransaction {
fn eq(&self, other: &SignedTransaction) -> bool {
self.hash == other.hash && self.signature == other.signature
}
}
impl Borrow<CryptoHash> for SignedTransaction {
fn borrow(&self) -> &CryptoHash {
&self.hash
}
}
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Clone, Default)]
pub enum ExecutionStatus {
#[default]
Unknown,
Failure(TxExecutionError),
SuccessValue(Vec<u8>),
SuccessReceiptId(CryptoHash),
}
impl fmt::Debug for ExecutionStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ExecutionStatus::Unknown => f.write_str("Unknown"),
ExecutionStatus::Failure(e) => f.write_fmt(format_args!("Failure({})", e)),
ExecutionStatus::SuccessValue(v) => {
f.write_fmt(format_args!("SuccessValue({})", AbbrBytes(v)))
}
ExecutionStatus::SuccessReceiptId(receipt_id) => {
f.write_fmt(format_args!("SuccessReceiptId({})", receipt_id))
}
}
}
}
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Clone)]
pub struct PartialExecutionOutcome {
pub receipt_ids: Vec<CryptoHash>,
pub gas_burnt: Gas,
pub tokens_burnt: Balance,
pub executor_id: AccountId,
pub status: PartialExecutionStatus,
}
impl From<&ExecutionOutcome> for PartialExecutionOutcome {
fn from(outcome: &ExecutionOutcome) -> Self {
Self {
receipt_ids: outcome.receipt_ids.clone(),
gas_burnt: outcome.gas_burnt,
tokens_burnt: outcome.tokens_burnt,
executor_id: outcome.executor_id.clone(),
status: outcome.status.clone().into(),
}
}
}
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Clone)]
pub enum PartialExecutionStatus {
Unknown,
Failure,
SuccessValue(Vec<u8>),
SuccessReceiptId(CryptoHash),
}
impl From<ExecutionStatus> for PartialExecutionStatus {
fn from(status: ExecutionStatus) -> PartialExecutionStatus {
match status {
ExecutionStatus::Unknown => PartialExecutionStatus::Unknown,
ExecutionStatus::Failure(_) => PartialExecutionStatus::Failure,
ExecutionStatus::SuccessValue(value) => PartialExecutionStatus::SuccessValue(value),
ExecutionStatus::SuccessReceiptId(id) => PartialExecutionStatus::SuccessReceiptId(id),
}
}
}
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Clone, smart_default::SmartDefault, Eq)]
pub struct ExecutionOutcome {
pub logs: Vec<LogEntry>,
pub receipt_ids: Vec<CryptoHash>,
pub gas_burnt: Gas,
#[borsh_skip]
pub compute_usage: Option<Compute>,
pub tokens_burnt: Balance,
#[default("test".parse().unwrap())]
pub executor_id: AccountId,
pub status: ExecutionStatus,
pub metadata: ExecutionMetadata,
}
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Clone, Eq, Debug, Default)]
pub enum ExecutionMetadata {
#[default]
V1,
V2(ProfileDataV2),
V3(ProfileDataV3),
}
impl fmt::Debug for ExecutionOutcome {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ExecutionOutcome")
.field("logs", &Slice(&self.logs))
.field("receipt_ids", &Slice(&self.receipt_ids))
.field("burnt_gas", &self.gas_burnt)
.field("compute_usage", &self.compute_usage.unwrap_or_default())
.field("tokens_burnt", &self.tokens_burnt)
.field("status", &self.status)
.field("metadata", &self.metadata)
.finish()
}
}
#[derive(PartialEq, Clone, Default, Debug, BorshSerialize, BorshDeserialize, Eq)]
pub struct ExecutionOutcomeWithId {
pub id: CryptoHash,
pub outcome: ExecutionOutcome,
}
impl ExecutionOutcomeWithId {
pub fn to_hashes(&self) -> Vec<CryptoHash> {
let mut result = Vec::with_capacity(2 + self.outcome.logs.len());
result.push(self.id);
result.push(CryptoHash::hash_borsh(PartialExecutionOutcome::from(&self.outcome)));
result.extend(self.outcome.logs.iter().map(|log| hash(log.as_bytes())));
result
}
}
#[derive(PartialEq, Clone, Default, Debug, BorshSerialize, BorshDeserialize, Eq)]
pub struct ExecutionOutcomeWithIdAndProof {
pub proof: MerklePath,
pub block_hash: CryptoHash,
pub outcome_with_id: ExecutionOutcomeWithId,
}
impl ExecutionOutcomeWithIdAndProof {
pub fn id(&self) -> &CryptoHash {
&self.outcome_with_id.id
}
}
pub fn verify_transaction_signature(
transaction: &SignedTransaction,
public_keys: &[PublicKey],
) -> bool {
let hash = transaction.get_hash();
let hash = hash.as_ref();
public_keys.iter().any(|key| transaction.signature.verify(hash, key))
}
#[derive(Clone, BorshSerialize, BorshDeserialize)]
pub struct ExecutionOutcomeWithProof {
pub proof: MerklePath,
pub outcome: ExecutionOutcome,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::account::{AccessKeyPermission, FunctionCallPermission};
use borsh::BorshDeserialize;
use near_crypto::{InMemorySigner, KeyType, Signature, Signer};
#[test]
fn test_verify_transaction() {
let signer = InMemorySigner::from_random("test".parse().unwrap(), KeyType::ED25519);
let transaction = Transaction {
signer_id: "test".parse().unwrap(),
public_key: signer.public_key(),
nonce: 0,
receiver_id: "test".parse().unwrap(),
block_hash: Default::default(),
actions: vec![],
}
.sign(&signer);
let wrong_public_key = PublicKey::from_seed(KeyType::ED25519, "wrong");
let valid_keys = vec![signer.public_key(), wrong_public_key.clone()];
assert!(verify_transaction_signature(&transaction, &valid_keys));
let invalid_keys = vec![wrong_public_key];
assert!(!verify_transaction_signature(&transaction, &invalid_keys));
let bytes = transaction.try_to_vec().unwrap();
let decoded_tx = SignedTransaction::try_from_slice(&bytes).unwrap();
assert!(verify_transaction_signature(&decoded_tx, &valid_keys));
}
#[test]
fn test_serialize_transaction() {
let public_key: PublicKey = "22skMptHjFWNyuEWY22ftn2AbLPSYpmYwGJRGwpNHbTV".parse().unwrap();
let transaction = Transaction {
signer_id: "test.near".parse().unwrap(),
public_key: public_key.clone(),
nonce: 1,
receiver_id: "123".parse().unwrap(),
block_hash: Default::default(),
actions: vec![
Action::CreateAccount(CreateAccountAction {}),
Action::DeployContract(DeployContractAction { code: vec![1, 2, 3] }),
Action::FunctionCall(FunctionCallAction {
method_name: "qqq".to_string(),
args: vec![1, 2, 3],
gas: 1_000,
deposit: 1_000_000,
}),
Action::Transfer(TransferAction { deposit: 123 }),
Action::Stake(StakeAction { public_key: public_key.clone(), stake: 1_000_000 }),
Action::AddKey(AddKeyAction {
public_key: public_key.clone(),
access_key: AccessKey {
nonce: 0,
permission: AccessKeyPermission::FunctionCall(FunctionCallPermission {
allowance: None,
receiver_id: "zzz".parse().unwrap(),
method_names: vec!["www".to_string()],
}),
},
}),
Action::DeleteKey(DeleteKeyAction { public_key }),
Action::DeleteAccount(DeleteAccountAction {
beneficiary_id: "123".parse().unwrap(),
}),
],
};
let signed_tx = SignedTransaction::new(Signature::empty(KeyType::ED25519), transaction);
let new_signed_tx =
SignedTransaction::try_from_slice(&signed_tx.try_to_vec().unwrap()).unwrap();
assert_eq!(
new_signed_tx.get_hash().to_string(),
"4GXvjMFN6wSxnU9jEVT8HbXP5Yk6yELX9faRSKp6n9fX"
);
}
#[test]
fn test_outcome_to_hashes() {
let outcome = ExecutionOutcome {
status: ExecutionStatus::SuccessValue(vec![123]),
logs: vec!["123".to_string(), "321".to_string()],
receipt_ids: vec![],
gas_burnt: 123,
compute_usage: Some(456),
tokens_burnt: 1234000,
executor_id: "alice".parse().unwrap(),
metadata: ExecutionMetadata::V1,
};
let id = CryptoHash([42u8; 32]);
let outcome = ExecutionOutcomeWithId { id, outcome };
assert_eq!(
vec![
id,
"5JQs5ekQqKudMmYejuccbtEu1bzhQPXa92Zm4HdV64dQ".parse().unwrap(),
hash("123".as_bytes()),
hash("321".as_bytes()),
],
outcome.to_hashes()
);
}
}