mod addressable_entity_identifier;
mod approval;
mod approvals_hash;
mod deploy;
mod error;
mod execution_info;
mod initiator_addr;
#[cfg(any(feature = "std", test, feature = "testing"))]
mod initiator_addr_and_secret_key;
mod package_identifier;
mod pricing_mode;
mod runtime_args;
mod serialization;
mod transaction_entry_point;
mod transaction_hash;
mod transaction_id;
mod transaction_invocation_target;
mod transaction_runtime;
mod transaction_scheduling;
mod transaction_target;
mod transaction_v1;
mod transfer_target;
use alloc::{
collections::BTreeSet,
string::{String, ToString},
vec::Vec,
};
use core::fmt::{self, Debug, Display, Formatter};
#[cfg(any(feature = "std", test))]
use serde::{de, ser, Deserializer, Serializer};
#[cfg(any(feature = "std", test))]
use serde_bytes::ByteBuf;
#[cfg(any(feature = "std", test))]
use std::hash::Hash;
#[cfg(any(feature = "std", test))]
use thiserror::Error;
#[cfg(any(feature = "std", test))]
use transaction_v1::TransactionV1Json;
#[cfg(feature = "json-schema")]
use crate::URef;
#[cfg(feature = "datasize")]
use datasize::DataSize;
#[cfg(feature = "json-schema")]
use once_cell::sync::Lazy;
#[cfg(any(all(feature = "std", feature = "testing"), test))]
use rand::Rng;
#[cfg(feature = "json-schema")]
use schemars::JsonSchema;
#[cfg(any(feature = "std", test))]
use serde::{Deserialize, Serialize};
use tracing::error;
#[cfg(any(all(feature = "std", feature = "testing"), test))]
use crate::testing::TestRng;
use crate::{
account::AccountHash,
bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH},
Digest, Phase, SecretKey, TimeDiff, Timestamp,
};
#[cfg(any(feature = "std", test))]
use crate::{Chainspec, Gas, Motes};
pub use addressable_entity_identifier::AddressableEntityIdentifier;
pub use approval::Approval;
pub use approvals_hash::ApprovalsHash;
pub use deploy::{
Deploy, DeployDecodeFromJsonError, DeployError, DeployExcessiveSizeError, DeployHash,
DeployHeader, DeployId, ExecutableDeployItem, ExecutableDeployItemIdentifier, InvalidDeploy,
};
#[cfg(any(feature = "std", test))]
pub use deploy::{DeployBuilder, DeployBuilderError};
pub use error::InvalidTransaction;
pub use execution_info::ExecutionInfo;
pub use initiator_addr::InitiatorAddr;
#[cfg(any(feature = "std", feature = "testing", test))]
pub(crate) use initiator_addr_and_secret_key::InitiatorAddrAndSecretKey;
pub use package_identifier::PackageIdentifier;
pub use pricing_mode::{PricingMode, PricingModeError};
pub use runtime_args::{NamedArg, RuntimeArgs};
pub use transaction_entry_point::TransactionEntryPoint;
pub use transaction_hash::TransactionHash;
pub use transaction_id::TransactionId;
pub use transaction_invocation_target::TransactionInvocationTarget;
pub use transaction_runtime::TransactionRuntime;
pub use transaction_scheduling::TransactionScheduling;
pub use transaction_target::TransactionTarget;
#[cfg(any(feature = "std", feature = "testing", feature = "gens", test))]
pub(crate) use transaction_v1::fields_container::FieldsContainer;
#[cfg(any(feature = "testing", feature = "gens", test))]
pub use transaction_v1::TransactionV1Payload;
pub use transaction_v1::{
arg_handling, InvalidTransactionV1, TransactionArgs, TransactionV1,
TransactionV1DecodeFromJsonError, TransactionV1Error, TransactionV1ExcessiveSizeError,
TransactionV1Hash,
};
#[cfg(any(feature = "std", test))]
pub use transaction_v1::{TransactionV1Builder, TransactionV1BuilderError};
pub use transfer_target::TransferTarget;
const DEPLOY_TAG: u8 = 0;
const V1_TAG: u8 = 1;
#[cfg(feature = "json-schema")]
pub(super) static TRANSACTION: Lazy<Transaction> = Lazy::new(|| {
let secret_key = SecretKey::example();
let source = URef::from_formatted_str(
"uref-0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a-007",
)
.unwrap();
let target = URef::from_formatted_str(
"uref-1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b-000",
)
.unwrap();
let id = Some(999);
let v1_txn = TransactionV1Builder::new_transfer(30_000_000_000_u64, Some(source), target, id)
.unwrap()
.with_chain_name("casper-example")
.with_timestamp(*Timestamp::example())
.with_ttl(TimeDiff::from_seconds(3_600))
.with_secret_key(secret_key)
.build()
.unwrap();
Transaction::V1(v1_txn)
});
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
#[cfg_attr(feature = "datasize", derive(DataSize))]
pub enum Transaction {
Deploy(Deploy),
#[cfg_attr(
feature = "json-schema",
serde(rename = "Version1"),
schemars(with = "TransactionV1Json")
)]
V1(TransactionV1),
}
impl Transaction {
pub fn hash(&self) -> TransactionHash {
match self {
Transaction::Deploy(deploy) => TransactionHash::from(*deploy.hash()),
Transaction::V1(txn) => TransactionHash::from(*txn.hash()),
}
}
pub fn size_estimate(&self) -> usize {
match self {
Transaction::Deploy(deploy) => deploy.serialized_length(),
Transaction::V1(v1) => v1.serialized_length(),
}
}
pub fn timestamp(&self) -> Timestamp {
match self {
Transaction::Deploy(deploy) => deploy.header().timestamp(),
Transaction::V1(v1) => v1.payload().timestamp(),
}
}
pub fn ttl(&self) -> TimeDiff {
match self {
Transaction::Deploy(deploy) => deploy.header().ttl(),
Transaction::V1(v1) => v1.payload().ttl(),
}
}
pub fn verify(&self) -> Result<(), InvalidTransaction> {
match self {
Transaction::Deploy(deploy) => deploy.is_valid().map_err(Into::into),
Transaction::V1(v1) => v1.verify().map_err(Into::into),
}
}
pub fn sign(&mut self, secret_key: &SecretKey) {
match self {
Transaction::Deploy(deploy) => deploy.sign(secret_key),
Transaction::V1(v1) => v1.sign(secret_key),
}
}
pub fn approvals(&self) -> BTreeSet<Approval> {
match self {
Transaction::Deploy(deploy) => deploy.approvals().clone(),
Transaction::V1(v1) => v1.approvals().clone(),
}
}
pub fn compute_approvals_hash(&self) -> Result<ApprovalsHash, bytesrepr::Error> {
let approvals_hash = match self {
Transaction::Deploy(deploy) => deploy.compute_approvals_hash()?,
Transaction::V1(txn) => txn.compute_approvals_hash()?,
};
Ok(approvals_hash)
}
pub fn chain_name(&self) -> String {
match self {
Transaction::Deploy(txn) => txn.chain_name().to_string(),
Transaction::V1(txn) => txn.chain_name().to_string(),
}
}
pub fn is_standard_payment(&self) -> bool {
match self {
Transaction::Deploy(txn) => txn.session().is_standard_payment(Phase::Payment),
Transaction::V1(txn) => match txn.pricing_mode() {
PricingMode::PaymentLimited {
standard_payment, ..
} => *standard_payment,
_ => true,
},
}
}
pub fn compute_id(&self) -> TransactionId {
match self {
Transaction::Deploy(deploy) => {
let deploy_hash = *deploy.hash();
let approvals_hash = deploy.compute_approvals_hash().unwrap_or_else(|error| {
error!(%error, "failed to serialize deploy approvals");
ApprovalsHash::from(Digest::default())
});
TransactionId::new(TransactionHash::Deploy(deploy_hash), approvals_hash)
}
Transaction::V1(txn) => {
let txn_hash = *txn.hash();
let approvals_hash = txn.compute_approvals_hash().unwrap_or_else(|error| {
error!(%error, "failed to serialize transaction approvals");
ApprovalsHash::from(Digest::default())
});
TransactionId::new(TransactionHash::V1(txn_hash), approvals_hash)
}
}
}
pub fn initiator_addr(&self) -> InitiatorAddr {
match self {
Transaction::Deploy(deploy) => InitiatorAddr::PublicKey(deploy.account().clone()),
Transaction::V1(txn) => txn.initiator_addr().clone(),
}
}
pub fn expired(&self, current_instant: Timestamp) -> bool {
match self {
Transaction::Deploy(deploy) => deploy.expired(current_instant),
Transaction::V1(txn) => txn.expired(current_instant),
}
}
pub fn expires(&self) -> Timestamp {
match self {
Transaction::Deploy(deploy) => deploy.header().expires(),
Transaction::V1(txn) => txn.payload().expires(),
}
}
pub fn signers(&self) -> BTreeSet<AccountHash> {
match self {
Transaction::Deploy(deploy) => deploy
.approvals()
.iter()
.map(|approval| approval.signer().to_account_hash())
.collect(),
Transaction::V1(txn) => txn
.approvals()
.iter()
.map(|approval| approval.signer().to_account_hash())
.collect(),
}
}
#[doc(hidden)]
pub fn with_approvals(self, approvals: BTreeSet<Approval>) -> Self {
match self {
Transaction::Deploy(deploy) => Transaction::Deploy(deploy.with_approvals(approvals)),
Transaction::V1(transaction_v1) => {
Transaction::V1(transaction_v1.with_approvals(approvals))
}
}
}
pub fn as_transaction_v1(&self) -> Option<&TransactionV1> {
match self {
Transaction::Deploy(_) => None,
Transaction::V1(v1) => Some(v1),
}
}
pub fn authorization_keys(&self) -> BTreeSet<AccountHash> {
match self {
Transaction::Deploy(deploy) => deploy
.approvals()
.iter()
.map(|approval| approval.signer().to_account_hash())
.collect(),
Transaction::V1(transaction_v1) => transaction_v1
.approvals()
.iter()
.map(|approval| approval.signer().to_account_hash())
.collect(),
}
}
pub fn is_legacy_transaction(&self) -> bool {
match self {
Transaction::Deploy(_) => true,
Transaction::V1(_) => false,
}
}
#[cfg(any(all(feature = "std", feature = "testing"), test))]
pub fn gas_limit(&self, chainspec: &Chainspec, lane_id: u8) -> Result<Gas, InvalidTransaction> {
match self {
Transaction::Deploy(deploy) => deploy
.gas_limit(chainspec)
.map_err(InvalidTransaction::from),
Transaction::V1(v1) => {
let pricing_mode = v1.pricing_mode();
let entry_point = v1
.get_transaction_entry_point()
.map_err(InvalidTransaction::from)?;
pricing_mode
.gas_limit(chainspec, &entry_point, lane_id)
.map_err(InvalidTransaction::from)
}
}
}
#[cfg(any(all(feature = "std", feature = "testing"), test))]
pub fn gas_cost(
&self,
chainspec: &Chainspec,
lane_id: u8,
gas_price: u8,
) -> Result<Motes, InvalidTransaction> {
match self {
Transaction::Deploy(deploy) => deploy
.gas_cost(chainspec, gas_price)
.map_err(InvalidTransaction::from),
Transaction::V1(v1) => {
let pricing_mode = v1.pricing_mode();
let entry_point = v1
.get_transaction_entry_point()
.map_err(InvalidTransaction::from)?;
pricing_mode
.gas_cost(chainspec, &entry_point, lane_id, gas_price)
.map_err(InvalidTransaction::from)
}
}
}
#[doc(hidden)]
#[cfg(feature = "json-schema")]
pub fn example() -> &'static Self {
&TRANSACTION
}
#[cfg(any(all(feature = "std", feature = "testing"), test))]
pub fn random(rng: &mut TestRng) -> Self {
if rng.gen() {
Transaction::Deploy(Deploy::random_valid_native_transfer(rng))
} else {
Transaction::V1(TransactionV1::random(rng))
}
}
}
#[cfg(any(feature = "std", test))]
impl Serialize for Transaction {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
if serializer.is_human_readable() {
TransactionJson::try_from(self.clone())
.map_err(|error| ser::Error::custom(format!("{:?}", error)))?
.serialize(serializer)
} else {
let bytes = self
.to_bytes()
.map_err(|error| ser::Error::custom(format!("{:?}", error)))?;
ByteBuf::from(bytes).serialize(serializer)
}
}
}
#[cfg(any(feature = "std", test))]
impl<'de> Deserialize<'de> for Transaction {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
if deserializer.is_human_readable() {
let json_helper = TransactionJson::deserialize(deserializer)?;
Transaction::try_from(json_helper)
.map_err(|error| de::Error::custom(format!("{:?}", error)))
} else {
let bytes = ByteBuf::deserialize(deserializer)?.into_vec();
bytesrepr::deserialize::<Transaction>(bytes)
.map_err(|error| de::Error::custom(format!("{:?}", error)))
}
}
}
#[cfg(any(feature = "std", test))]
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
enum TransactionJson {
Deploy(Deploy),
#[serde(rename = "Version1")]
V1(TransactionV1Json),
}
#[cfg(any(feature = "std", test))]
#[derive(Error, Debug)]
enum TransactionJsonError {
#[error("{0}")]
FailedToMap(String),
}
#[cfg(any(feature = "std", test))]
impl TryFrom<TransactionJson> for Transaction {
type Error = TransactionJsonError;
fn try_from(transaction: TransactionJson) -> Result<Self, Self::Error> {
match transaction {
TransactionJson::Deploy(deploy) => Ok(Transaction::Deploy(deploy)),
TransactionJson::V1(v1) => {
TransactionV1::try_from(v1)
.map(Transaction::V1)
.map_err(|error| {
TransactionJsonError::FailedToMap(format!(
"Failed to map TransactionJson::V1 to Transaction::V1, err: {}",
error
))
})
}
}
}
}
#[cfg(any(feature = "std", test))]
impl TryFrom<Transaction> for TransactionJson {
type Error = TransactionJsonError;
fn try_from(transaction: Transaction) -> Result<Self, Self::Error> {
match transaction {
Transaction::Deploy(deploy) => Ok(TransactionJson::Deploy(deploy)),
Transaction::V1(v1) => TransactionV1Json::try_from(v1)
.map(TransactionJson::V1)
.map_err(|error| {
TransactionJsonError::FailedToMap(format!(
"Failed to map Transaction::V1 to TransactionJson::V1, err: {}",
error
))
}),
}
}
}
#[cfg(any(feature = "std", test))]
pub trait GasLimited {
type Error;
const GAS_PRICE_FLOOR: u8 = 1;
fn gas_cost(&self, chainspec: &Chainspec, gas_price: u8) -> Result<Motes, Self::Error>;
fn gas_limit(&self, chainspec: &Chainspec) -> Result<Gas, Self::Error>;
fn gas_price_tolerance(&self) -> Result<u8, Self::Error>;
}
impl From<Deploy> for Transaction {
fn from(deploy: Deploy) -> Self {
Self::Deploy(deploy)
}
}
impl From<TransactionV1> for Transaction {
fn from(txn: TransactionV1) -> Self {
Self::V1(txn)
}
}
impl ToBytes for Transaction {
fn to_bytes(&self) -> Result<Vec<u8>, bytesrepr::Error> {
let mut buffer = bytesrepr::allocate_buffer(self)?;
self.write_bytes(&mut buffer)?;
Ok(buffer)
}
fn serialized_length(&self) -> usize {
U8_SERIALIZED_LENGTH
+ match self {
Transaction::Deploy(deploy) => deploy.serialized_length(),
Transaction::V1(txn) => txn.serialized_length(),
}
}
fn write_bytes(&self, writer: &mut Vec<u8>) -> Result<(), bytesrepr::Error> {
match self {
Transaction::Deploy(deploy) => {
DEPLOY_TAG.write_bytes(writer)?;
deploy.write_bytes(writer)
}
Transaction::V1(txn) => {
V1_TAG.write_bytes(writer)?;
txn.write_bytes(writer)
}
}
}
}
impl FromBytes for Transaction {
fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> {
let (tag, remainder) = u8::from_bytes(bytes)?;
match tag {
DEPLOY_TAG => {
let (deploy, remainder) = Deploy::from_bytes(remainder)?;
Ok((Transaction::Deploy(deploy), remainder))
}
V1_TAG => {
let (txn, remainder) = TransactionV1::from_bytes(remainder)?;
Ok((Transaction::V1(txn), remainder))
}
_ => Err(bytesrepr::Error::Formatting),
}
}
}
impl Display for Transaction {
fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
match self {
Transaction::Deploy(deploy) => Display::fmt(deploy, formatter),
Transaction::V1(txn) => Display::fmt(txn, formatter),
}
}
}
#[cfg(any(feature = "testing", feature = "gens", test))]
pub mod gens {
use super::*;
use proptest::{
array,
prelude::{Arbitrary, Strategy},
};
pub fn deploy_hash_arb() -> impl Strategy<Value = DeployHash> {
array::uniform32(<u8>::arbitrary()).prop_map(DeployHash::from_raw)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::TestRng;
#[test]
fn json_roundtrip() {
let rng = &mut TestRng::new();
let transaction = Transaction::from(Deploy::random(rng));
let json_string = serde_json::to_string_pretty(&transaction).unwrap();
let decoded = serde_json::from_str(&json_string).unwrap();
assert_eq!(transaction, decoded);
let transaction = Transaction::from(TransactionV1::random(rng));
let json_string = serde_json::to_string_pretty(&transaction).unwrap();
let decoded = serde_json::from_str(&json_string).unwrap();
assert_eq!(transaction, decoded);
}
#[test]
fn bincode_roundtrip() {
let rng = &mut TestRng::new();
let transaction = Transaction::from(Deploy::random(rng));
let serialized = bincode::serialize(&transaction).unwrap();
let deserialized = bincode::deserialize(&serialized).unwrap();
assert_eq!(transaction, deserialized);
let transaction = Transaction::from(TransactionV1::random(rng));
let serialized = bincode::serialize(&transaction).unwrap();
let deserialized = bincode::deserialize(&serialized).unwrap();
assert_eq!(transaction, deserialized);
}
#[test]
fn bytesrepr_roundtrip() {
let rng = &mut TestRng::new();
let transaction = Transaction::from(Deploy::random(rng));
bytesrepr::test_serialization_roundtrip(&transaction);
let transaction = Transaction::from(TransactionV1::random(rng));
bytesrepr::test_serialization_roundtrip(&transaction);
}
}
#[cfg(test)]
mod proptests {
use super::*;
use crate::{
bytesrepr,
gens::{legal_transaction_arb, transaction_arb},
};
use proptest::prelude::*;
proptest! {
#[test]
fn bytesrepr_roundtrip(transaction in transaction_arb()) {
bytesrepr::test_serialization_roundtrip(&transaction);
}
#[test]
fn json_roundtrip(transaction in legal_transaction_arb()) {
let json_string = serde_json::to_string_pretty(&transaction).unwrap();
let decoded = serde_json::from_str::<Transaction>(&json_string).unwrap();
assert_eq!(transaction, decoded);
}
}
}