diff --git a/Cargo.lock b/Cargo.lock index 9dee6dd1c0..beb9c895dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5818,9 +5818,9 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.16" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" dependencies = [ "dyn-clone", "indexmap 1.9.3", @@ -5831,14 +5831,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.16" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", "serde_derive_internals", - "syn 1.0.109", + "syn 2.0.87", ] [[package]] @@ -5981,13 +5981,13 @@ dependencies = [ [[package]] name = "serde_derive_internals" -version = "0.26.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", - "syn 1.0.109", + "syn 2.0.87", ] [[package]] diff --git a/node/src/types/transaction/meta_transaction/meta_transaction_v1.rs b/node/src/types/transaction/meta_transaction/meta_transaction_v1.rs index a43e1cfe57..88616e924f 100644 --- a/node/src/types/transaction/meta_transaction/meta_transaction_v1.rs +++ b/node/src/types/transaction/meta_transaction/meta_transaction_v1.rs @@ -17,9 +17,7 @@ const ARGS_MAP_KEY: u16 = 0; const TARGET_MAP_KEY: u16 = 1; const ENTRY_POINT_MAP_KEY: u16 = 2; const SCHEDULING_MAP_KEY: u16 = 3; -const TRANSFERRED_VALUE_MAP_KEY: u16 = 4; -const SEED_MAP_KEY: u16 = 5; -const EXPECTED_NUMBER_OF_FIELDS: usize = 6; +const EXPECTED_NUMBER_OF_FIELDS: usize = 4; #[derive(Clone, Debug, Serialize, DataSize)] pub struct MetaTransactionV1 { @@ -37,8 +35,6 @@ pub struct MetaTransactionV1 { approvals: BTreeSet, serialized_length: usize, payload_hash: Digest, - transferred_value: u64, - seed: Option<[u8; 32]>, has_valid_hash: Result<(), InvalidTransactionV1>, #[serde(skip)] #[data_size(skip)] @@ -64,14 +60,6 @@ impl MetaTransactionV1 { v1.deserialize_field(SCHEDULING_MAP_KEY).map_err(|error| { InvalidTransaction::V1(InvalidTransactionV1::CouldNotDeserializeField { error }) })?; - let transferred_value = - v1.deserialize_field(TRANSFERRED_VALUE_MAP_KEY) - .map_err(|error| { - InvalidTransaction::V1(InvalidTransactionV1::CouldNotDeserializeField { error }) - })?; - let seed = v1.deserialize_field(SEED_MAP_KEY).map_err(|error| { - InvalidTransaction::V1(InvalidTransactionV1::CouldNotDeserializeField { error }) - })?; if v1.number_of_fields() != EXPECTED_NUMBER_OF_FIELDS { return Err(InvalidTransaction::V1( @@ -108,8 +96,6 @@ impl MetaTransactionV1 { serialized_length, payload_hash, approvals, - transferred_value, - seed, has_valid_hash, )) } @@ -149,8 +135,6 @@ impl MetaTransactionV1 { serialized_length: usize, payload_hash: Digest, approvals: BTreeSet, - transferred_value: u64, - seed: Option<[u8; 32]>, has_valid_hash: Result<(), InvalidTransactionV1>, ) -> Self { Self { @@ -169,8 +153,6 @@ impl MetaTransactionV1 { serialized_length, payload_hash, has_valid_hash, - transferred_value, - seed, is_verified: OnceCell::new(), } } @@ -654,12 +636,40 @@ impl MetaTransactionV1 { /// Returns the seed of the transaction. pub(crate) fn seed(&self) -> Option<[u8; 32]> { - self.seed + match &self.target { + TransactionTarget::Native => None, + TransactionTarget::Stored { + id: _, + runtime: _, + transferred_value: _, + } => None, + TransactionTarget::Session { + is_install_upgrade: _, + runtime: _, + module_bytes: _, + transferred_value: _, + seed, + } => *seed, + } } /// Returns the transferred value of the transaction. pub fn transferred_value(&self) -> u64 { - self.transferred_value + match &self.target { + TransactionTarget::Native => 0, + TransactionTarget::Stored { + id: _, + runtime: _, + transferred_value, + } => *transferred_value, + TransactionTarget::Session { + is_install_upgrade: _, + runtime: _, + module_bytes: _, + transferred_value, + seed: _, + } => *transferred_value, + } } } diff --git a/resources/test/sse_data_schema.json b/resources/test/sse_data_schema.json index 15713e3203..8e494a1b03 100644 --- a/resources/test/sse_data_schema.json +++ b/resources/test/sse_data_schema.json @@ -1577,11 +1577,10 @@ }, "uniqueItems": true } - }, - "additionalProperties": false + } }, "TransactionV1Payload": { - "description": "A unit of work sent by a client to the network, which when executed can cause global state to be altered.", + "description": "Internal payload of the transaction. The actual data over which the signing is done.", "type": "object", "required": [ "chain_name", @@ -1609,9 +1608,7 @@ }, "fields": { "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Bytes" - } + "additionalProperties": true } }, "additionalProperties": false diff --git a/types/Cargo.toml b/types/Cargo.toml index 1db7598bb8..23af9075e4 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -37,7 +37,7 @@ proptest = { version = "1.0.0", optional = true } proptest-derive = { version = "0.3.0", optional = true } rand = { version = "0.8.3", default-features = false, features = ["small_rng"] } rand_pcg = { version = "0.3.0", optional = true } -schemars = { version = "0.8.16", features = ["preserve_order"], optional = true } +schemars = { version = "0.8.21", features = ["preserve_order"], optional = true } serde-map-to-array = "1.1.0" serde = { version = "1", default-features = false, features = ["alloc", "derive"] } serde_bytes = { version = "0.11.5", default-features = false, features = ["alloc"] } diff --git a/types/src/cl_value.rs b/types/src/cl_value.rs index 864bb771fa..059e2f7b46 100644 --- a/types/src/cl_value.rs +++ b/types/src/cl_value.rs @@ -1,19 +1,15 @@ -use alloc::vec::Vec; +use alloc::{string::String, vec::Vec}; use core::fmt::{self, Display, Formatter}; -#[cfg(feature = "json-schema")] -use crate::checksummed_hex; use crate::{ bytesrepr::{self, Bytes, FromBytes, ToBytes, U32_SERIALIZED_LENGTH}, - CLType, CLTyped, + checksummed_hex, CLType, CLTyped, }; #[cfg(feature = "datasize")] use datasize::DataSize; #[cfg(feature = "json-schema")] use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema}; -#[cfg(feature = "json-schema")] -use serde::de::Error as SerdeError; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde::{de::Error as SerdeError, Deserialize, Deserializer, Serialize, Serializer}; #[cfg(feature = "json-schema")] use serde_json::Value; @@ -219,20 +215,20 @@ impl JsonSchema for CLValue { #[serde(deny_unknown_fields)] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] #[cfg_attr(feature = "json-schema", schemars(rename = "CLValue"))] -#[cfg(feature = "json-schema")] struct CLValueJson { cl_type: CLType, bytes: String, + #[cfg(feature = "json-schema")] parsed: Option, } -#[cfg(feature = "json-schema")] impl Serialize for CLValue { fn serialize(&self, serializer: S) -> Result { if serializer.is_human_readable() { CLValueJson { cl_type: self.cl_type.clone(), bytes: base16::encode_lower(&self.bytes), + #[cfg(feature = "json-schema")] parsed: jsonrepr::cl_value_to_json(self), } .serialize(serializer) @@ -242,14 +238,6 @@ impl Serialize for CLValue { } } -#[cfg(not(feature = "json-schema"))] -impl Serialize for CLValue { - fn serialize(&self, serializer: S) -> Result { - (&self.cl_type, &self.bytes).serialize(serializer) - } -} - -#[cfg(feature = "json-schema")] impl<'de> Deserialize<'de> for CLValue { fn deserialize>(deserializer: D) -> Result { let (cl_type, bytes) = if deserializer.is_human_readable() { @@ -268,17 +256,6 @@ impl<'de> Deserialize<'de> for CLValue { } } -#[cfg(not(feature = "json-schema"))] -impl<'de> Deserialize<'de> for CLValue { - fn deserialize>(deserializer: D) -> Result { - let (cl_type, bytes) = <(CLType, Vec)>::deserialize(deserializer)?; - Ok(CLValue { - cl_type, - bytes: bytes.into(), - }) - } -} - #[cfg(test)] mod tests { use alloc::string::ToString; diff --git a/types/src/gens.rs b/types/src/gens.rs index 6a55098ad0..11c6d06da4 100644 --- a/types/src/gens.rs +++ b/types/src/gens.rs @@ -965,6 +965,14 @@ pub fn transaction_scheduling_arb() -> impl Strategy impl Strategy { + prop_oneof![ + Just(TransactionScheduling::Standard), + era_id_arb().prop_map(TransactionScheduling::FutureEra), + timestamp_arb().prop_map(TransactionScheduling::FutureTimestamp), + ] +} + pub fn transaction_invocation_target_arb() -> impl Strategy { prop_oneof![ addressable_entity_hash_arb().prop_map(TransactionInvocationTarget::new_invocable_entity), @@ -1171,7 +1179,7 @@ pub fn initiator_addr_arb() -> impl Strategy { pub fn timestamp_arb() -> impl Strategy { //The weird u64 value is the max milliseconds that are bofeore year 10000. 5 digit years are - // not rfc3339 compliant and will cause an error + // not rfc3339 compliant and will cause an error when trying to serialize to json. prop_oneof![Just(0_u64), Just(1_u64), Just(253_402_300_799_999_u64)].prop_map(Timestamp::from) } @@ -1183,7 +1191,7 @@ pub fn legal_v1_transaction_arb() -> impl Strategy { pricing_mode_arb(), secret_key_arb_no_system(), transaction_args_arb(), - transaction_scheduling_arb(), + json_compliant_transaction_scheduling_arb(), legal_target_entry_point_calls_arb(), ) .prop_map( diff --git a/types/src/transaction.rs b/types/src/transaction.rs index 893160a8f6..acf250ff86 100644 --- a/types/src/transaction.rs +++ b/types/src/transaction.rs @@ -28,7 +28,15 @@ use alloc::{ }; 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; @@ -119,18 +127,17 @@ pub(super) static TRANSACTION: Lazy = Lazy::new(|| { /// A versioned wrapper for a transaction or deploy. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -#[cfg_attr( - any(feature = "std", test), - derive(Serialize, Deserialize), - serde(deny_unknown_fields) -)] -#[cfg_attr(feature = "datasize", derive(DataSize))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] +#[cfg_attr(feature = "datasize", derive(DataSize))] pub enum Transaction { /// A deploy. Deploy(Deploy), /// A version 1 transaction. - #[cfg_attr(any(feature = "std", test), serde(rename = "Version1"))] + #[cfg_attr( + feature = "json-schema", + serde(rename = "Version1"), + schemars(with = "TransactionV1Json") + )] V1(TransactionV1), } @@ -397,6 +404,94 @@ impl Transaction { } } +#[cfg(any(feature = "std", test))] +impl Serialize for Transaction { + fn serialize(&self, serializer: S) -> Result { + 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>(deserializer: D) -> Result { + 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::(bytes) + .map_err(|error| de::Error::custom(format!("{:?}", error))) + } + } +} + +/// A util structure to json-serialize a transaction. +#[cfg(any(feature = "std", test))] +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +#[serde(deny_unknown_fields)] +enum TransactionJson { + /// A deploy. + Deploy(Deploy), + /// A version 1 transaction. + #[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 for Transaction { + type Error = TransactionJsonError; + fn try_from(transaction: TransactionJson) -> Result { + 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 for TransactionJson { + type Error = TransactionJsonError; + fn try_from(transaction: Transaction) -> Result { + 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 + )) + }), + } + } +} /// Calculates gas limit. #[cfg(any(feature = "std", test))] pub trait GasLimited { @@ -553,7 +648,10 @@ mod tests { #[cfg(test)] mod proptests { use super::*; - use crate::{bytesrepr, gens::transaction_arb}; + use crate::{ + bytesrepr, + gens::{legal_transaction_arb, transaction_arb}, + }; use proptest::prelude::*; proptest! { @@ -563,7 +661,7 @@ mod proptests { } #[test] - fn json_roundtrip(transaction in transaction_arb()) { + fn json_roundtrip(transaction in legal_transaction_arb()) { let json_string = serde_json::to_string_pretty(&transaction).unwrap(); let decoded = serde_json::from_str::(&json_string).unwrap(); assert_eq!(transaction, decoded); diff --git a/types/src/transaction/transaction_v1.rs b/types/src/transaction/transaction_v1.rs index 7054ef8575..b455f959c3 100644 --- a/types/src/transaction/transaction_v1.rs +++ b/types/src/transaction/transaction_v1.rs @@ -7,6 +7,10 @@ mod transaction_v1_builder; mod transaction_v1_hash; pub mod transaction_v1_payload; +#[cfg(any(feature = "std", feature = "testing", test))] +use super::InitiatorAddrAndSecretKey; +#[cfg(any(all(feature = "std", feature = "testing"), test))] +use super::{TransactionEntryPoint, TransactionTarget}; use crate::{ bytesrepr::{self, Error, FromBytes, ToBytes}, crypto, @@ -18,24 +22,23 @@ use crate::{ #[cfg(any(feature = "std", test, feature = "testing"))] use alloc::collections::BTreeMap; use alloc::{collections::BTreeSet, vec::Vec}; +#[cfg(feature = "datasize")] +use datasize::DataSize; use errors_v1::FieldDeserializationError; #[cfg(any(all(feature = "std", feature = "testing"), test))] use fields_container::{ENTRY_POINT_MAP_KEY, TARGET_MAP_KEY}; -use tracing::debug; -pub use transaction_v1_payload::TransactionV1Payload; - -#[cfg(any(feature = "std", feature = "testing", test))] -use super::InitiatorAddrAndSecretKey; -#[cfg(any(all(feature = "std", feature = "testing"), test))] -use super::{TransactionEntryPoint, TransactionTarget}; -#[cfg(feature = "datasize")] -use datasize::DataSize; #[cfg(any(feature = "once_cell", test))] use once_cell::sync::OnceCell; #[cfg(feature = "json-schema")] use schemars::JsonSchema; #[cfg(any(feature = "std", test))] use serde::{Deserialize, Serialize}; +#[cfg(any(feature = "std", test))] +use thiserror::Error; +use tracing::debug; +pub use transaction_v1_payload::TransactionV1Payload; +#[cfg(any(feature = "std", test))] +use transaction_v1_payload::TransactionV1PayloadJson; use super::{ serialization::{CalltableSerializationEnvelope, CalltableSerializationEnvelopeBuilder}, @@ -68,19 +71,12 @@ const APPROVALS_FIELD_INDEX: u16 = 2; /// A unit of work sent by a client to the network, which when executed can cause global state to /// be altered. #[derive(Clone, Eq, Debug)] -#[cfg_attr( - any(feature = "std", test), - derive(Serialize, Deserialize), - serde(deny_unknown_fields) -)] +#[cfg_attr(any(feature = "std", test), derive(Serialize, Deserialize))] #[cfg_attr(feature = "datasize", derive(DataSize))] #[cfg_attr( feature = "json-schema", derive(JsonSchema), - schemars( - description = "A unit of work sent by a client to the network, which when executed can \ - cause global state to be altered." - ) + schemars(with = "TransactionV1Json") )] pub struct TransactionV1 { hash: TransactionV1Hash, @@ -95,6 +91,67 @@ pub struct TransactionV1 { is_verified: OnceCell>, } +#[cfg(any(feature = "std", test))] +impl TryFrom for TransactionV1 { + type Error = TransactionV1JsonError; + fn try_from(transaction_v1_json: TransactionV1Json) -> Result { + Ok(TransactionV1 { + hash: transaction_v1_json.hash, + payload: transaction_v1_json.payload.try_into().map_err(|error| { + TransactionV1JsonError::FailedToMap(format!( + "Failed to map TransactionJson::V1 to Transaction::V1, err: {}", + error + )) + })?, + approvals: transaction_v1_json.approvals, + #[cfg(any(feature = "once_cell", test))] + is_verified: OnceCell::new(), + }) + } +} + +/// A helper struct to represent the transaction as json. +#[cfg(any(feature = "std", test))] +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr( + feature = "json-schema", + derive(JsonSchema), + schemars( + description = "A unit of work sent by a client to the network, which when executed can \ + cause global state to be altered.", + rename = "TransactionV1", + ) +)] +pub(super) struct TransactionV1Json { + hash: TransactionV1Hash, + payload: TransactionV1PayloadJson, + approvals: BTreeSet, +} + +#[cfg(any(feature = "std", test))] +#[derive(Error, Debug)] +pub(super) enum TransactionV1JsonError { + #[error("{0}")] + FailedToMap(String), +} + +#[cfg(any(feature = "std", test))] +impl TryFrom for TransactionV1Json { + type Error = TransactionV1JsonError; + fn try_from(transaction: TransactionV1) -> Result { + Ok(TransactionV1Json { + hash: transaction.hash, + payload: transaction.payload.try_into().map_err(|error| { + TransactionV1JsonError::FailedToMap(format!( + "Failed to map Transaction::V1 to TransactionJson::V1, err: {}", + error + )) + })?, + approvals: transaction.approvals, + }) + } +} + impl TransactionV1 { #[cfg(any(feature = "std", test, feature = "testing"))] pub(crate) fn build( diff --git a/types/src/transaction/transaction_v1/fields_container.rs b/types/src/transaction/transaction_v1/fields_container.rs index fbdf38117a..78a4a6a54a 100644 --- a/types/src/transaction/transaction_v1/fields_container.rs +++ b/types/src/transaction/transaction_v1/fields_container.rs @@ -24,10 +24,6 @@ pub(crate) const TARGET_MAP_KEY: u16 = 1; pub(crate) const ENTRY_POINT_MAP_KEY: u16 = 2; #[cfg(any(feature = "std", feature = "testing", feature = "gens", test))] pub(crate) const SCHEDULING_MAP_KEY: u16 = 3; -#[cfg(any(feature = "std", feature = "testing", feature = "gens", test))] -pub(crate) const TRANSFERRED_VALUE_MAP_KEY: u16 = 4; -#[cfg(any(feature = "std", feature = "testing", feature = "gens", test))] -pub(crate) const SEED_MAP_KEY: u16 = 5; #[cfg(any(feature = "std", feature = "testing", feature = "gens", test))] #[derive(Clone, Eq, PartialEq, Debug)] @@ -93,49 +89,6 @@ impl FieldsContainer { } })?, ); - - let transferred_value; - let seed; - - match self.target { - TransactionTarget::Session { - transferred_value: value, - seed: maybe_seed, - .. - } => { - transferred_value = value; - seed = maybe_seed; - } - TransactionTarget::Stored { - transferred_value: value, - .. - } => { - transferred_value = value; - seed = None; - } - TransactionTarget::Native => { - transferred_value = 0; - seed = None; - } - } - - map.insert( - TRANSFERRED_VALUE_MAP_KEY, - transferred_value.to_bytes().map(Into::into).map_err(|_| { - FieldsContainerError::CouldNotSerializeField { - field_index: TRANSFERRED_VALUE_MAP_KEY, - } - })?, - ); - map.insert( - SEED_MAP_KEY, - seed.to_bytes().map(Into::into).map_err(|_| { - FieldsContainerError::CouldNotSerializeField { - field_index: SEED_MAP_KEY, - } - })?, - ); - Ok(map) } diff --git a/types/src/transaction/transaction_v1/transaction_v1_payload.rs b/types/src/transaction/transaction_v1/transaction_v1_payload.rs index eb89a33303..7a60807939 100644 --- a/types/src/transaction/transaction_v1/transaction_v1_payload.rs +++ b/types/src/transaction/transaction_v1/transaction_v1_payload.rs @@ -12,13 +12,19 @@ use crate::{ }, DisplayIter, InitiatorAddr, TimeDiff, Timestamp, }; +#[cfg(any(feature = "std", test))] +use crate::{TransactionArgs, TransactionEntryPoint, TransactionScheduling, TransactionTarget}; use alloc::{collections::BTreeMap, string::String, vec::Vec}; #[cfg(feature = "datasize")] use datasize::DataSize; #[cfg(feature = "json-schema")] use schemars::JsonSchema; #[cfg(any(feature = "std", test))] -use serde::{Deserialize, Serialize}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +#[cfg(any(feature = "std", test))] +use serde_json::Value; +#[cfg(any(feature = "std", test))] +use thiserror::Error; const INITIATOR_ADDR_FIELD_INDEX: u16 = 0; const TIMESTAMP_FIELD_INDEX: u16 = 1; @@ -31,30 +37,33 @@ const ARGS_MAP_KEY: u16 = 0; const TARGET_MAP_KEY: u16 = 1; const ENTRY_POINT_MAP_KEY: u16 = 2; const SCHEDULING_MAP_KEY: u16 = 3; +#[cfg(any(feature = "std", test))] +const ARGS_MAP_HUMAN_READABLE_KEY: &str = "args"; +#[cfg(any(feature = "std", test))] +const TARGET_MAP_HUMAN_READABLE_KEY: &str = "target"; +#[cfg(any(feature = "std", test))] +const ENTRY_POINT_MAP_HUMAN_READABLE_KEY: &str = "entry_point"; +#[cfg(any(feature = "std", test))] +const SCHEDULING_MAP_HUMAN_READABLE_KEY: &str = "scheduling"; -const EXPECTED_FIELD_KEYS: [u16; 6] = [ +const EXPECTED_FIELD_KEYS: [u16; 4] = [ ARGS_MAP_KEY, TARGET_MAP_KEY, ENTRY_POINT_MAP_KEY, SCHEDULING_MAP_KEY, - 4, - 5, ]; #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)] +#[cfg_attr(feature = "datasize", derive(DataSize))] #[cfg_attr( any(feature = "std", test), derive(Serialize, Deserialize), serde(deny_unknown_fields) )] -#[cfg_attr(feature = "datasize", derive(DataSize))] #[cfg_attr( feature = "json-schema", derive(JsonSchema), - schemars( - description = "A unit of work sent by a client to the network, which when executed can \ - cause global state to be altered." - ) + schemars(with = "TransactionV1PayloadJson") )] pub struct TransactionV1Payload { initiator_addr: InitiatorAddr, @@ -155,6 +164,164 @@ impl TransactionV1Payload { } } +#[cfg(any(feature = "std", test))] +impl TryFrom for TransactionV1Payload { + type Error = TransactionV1PayloadJsonError; + fn try_from(transaction_v1_json: TransactionV1PayloadJson) -> Result { + Ok(TransactionV1Payload { + initiator_addr: transaction_v1_json.initiator_addr, + timestamp: transaction_v1_json.timestamp, + ttl: transaction_v1_json.ttl, + chain_name: transaction_v1_json.chain_name, + pricing_mode: transaction_v1_json.pricing_mode, + fields: from_human_readable_fields(&transaction_v1_json.fields)?, + }) + } +} + +#[cfg(any(feature = "std", test))] +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +#[cfg_attr( + feature = "json-schema", + derive(JsonSchema), + schemars( + description = "Internal payload of the transaction. The actual data over which the signing is done.", + rename = "TransactionV1Payload", + ) +)] +pub(super) struct TransactionV1PayloadJson { + initiator_addr: InitiatorAddr, + timestamp: Timestamp, + ttl: TimeDiff, + chain_name: String, + pricing_mode: PricingMode, + fields: BTreeMap, +} + +#[cfg(any(feature = "std", test))] +#[derive(Error, Debug)] + +pub(super) enum TransactionV1PayloadJsonError { + #[error("{0}")] + FailedToMap(String), +} + +#[cfg(any(feature = "std", test))] +impl TryFrom for TransactionV1PayloadJson { + type Error = TransactionV1PayloadJsonError; + + fn try_from(value: TransactionV1Payload) -> Result { + Ok(TransactionV1PayloadJson { + initiator_addr: value.initiator_addr, + timestamp: value.timestamp, + ttl: value.ttl, + chain_name: value.chain_name, + pricing_mode: value.pricing_mode, + fields: to_human_readable_fields(&value.fields)?, + }) + } +} + +#[cfg(any(feature = "std", test))] +fn from_human_readable_fields( + fields: &BTreeMap, +) -> Result, TransactionV1PayloadJsonError> { + let number_of_expected_fields = EXPECTED_FIELD_KEYS.len(); + if fields.len() != number_of_expected_fields { + return Err(TransactionV1PayloadJsonError::FailedToMap(format!( + "Expected exactly {} fields", + number_of_expected_fields + ))); + } + let args_bytes = to_bytesrepr::(fields, ARGS_MAP_HUMAN_READABLE_KEY)?; + let target_bytes = to_bytesrepr::(fields, TARGET_MAP_HUMAN_READABLE_KEY)?; + let entry_point_bytes = + to_bytesrepr::(fields, ENTRY_POINT_MAP_HUMAN_READABLE_KEY)?; + let schedule_bytes = + to_bytesrepr::(fields, SCHEDULING_MAP_HUMAN_READABLE_KEY)?; + Ok(BTreeMap::from_iter(vec![ + (ARGS_MAP_KEY, args_bytes), + (TARGET_MAP_KEY, target_bytes), + (ENTRY_POINT_MAP_KEY, entry_point_bytes), + (SCHEDULING_MAP_KEY, schedule_bytes), + ])) +} + +#[cfg(any(feature = "std", test))] +fn to_human_readable_fields( + fields: &BTreeMap, +) -> Result, TransactionV1PayloadJsonError> { + let args_value = + extract_and_deserialize_field::(fields, ARGS_MAP_KEY, "args")?; + let target_value = + extract_and_deserialize_field::(fields, TARGET_MAP_KEY, "target")?; + let entry_point_value = extract_and_deserialize_field::( + fields, + ENTRY_POINT_MAP_KEY, + "entry_point", + )?; + let scheduling_value = extract_and_deserialize_field::( + fields, + SCHEDULING_MAP_KEY, + "scheduling", + )?; + + Ok(BTreeMap::from_iter(vec![ + (ARGS_MAP_HUMAN_READABLE_KEY.to_string(), args_value), + (TARGET_MAP_HUMAN_READABLE_KEY.to_string(), target_value), + ( + ENTRY_POINT_MAP_HUMAN_READABLE_KEY.to_string(), + entry_point_value, + ), + ( + SCHEDULING_MAP_HUMAN_READABLE_KEY.to_string(), + scheduling_value, + ), + ])) +} + +#[cfg(any(feature = "std", test))] +fn to_bytesrepr( + fields: &BTreeMap, + field_name: &str, +) -> Result { + let value_json = fields + .get(field_name) + .ok_or(TransactionV1PayloadJsonError::FailedToMap(format!( + "Could not find {field_name} field" + )))?; + let deserialized = serde_json::from_value::(value_json.clone()) + .map_err(|e| TransactionV1PayloadJsonError::FailedToMap(format!("{:?}", e)))?; + deserialized + .to_bytes() + .map(|bytes| bytes.into()) + .map_err(|e| TransactionV1PayloadJsonError::FailedToMap(format!("{:?}", e))) +} + +#[cfg(any(feature = "std", test))] +fn extract_and_deserialize_field( + fields: &BTreeMap, + key: u16, + field_name: &str, +) -> Result { + let value_bytes = fields + .get(&key) + .ok_or(TransactionV1PayloadJsonError::FailedToMap(format!( + "Could not find {field_name} field" + )))?; + let (from_bytes, remainder) = T::from_bytes(value_bytes) + .map_err(|e| TransactionV1PayloadJsonError::FailedToMap(format!("{:?}", e)))?; + if !remainder.is_empty() { + return Err(TransactionV1PayloadJsonError::FailedToMap(format!( + "Unexpexcted bytes in {field_name} field" + ))); + } + let value = serde_json::to_value(from_bytes) + .map_err(|e| TransactionV1PayloadJsonError::FailedToMap(format!("{:?}", e)))?; + Ok(value) +} + impl ToBytes for TransactionV1Payload { fn to_bytes(&self) -> Result, crate::bytesrepr::Error> { let expected_payload_sizes = self.serialized_field_lengths();