diff --git a/Cargo.lock b/Cargo.lock index a3e6efcd3..3e03fee45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2865,7 +2865,7 @@ dependencies = [ [[package]] name = "sargon" -version = "1.1.29" +version = "1.1.30" dependencies = [ "actix-rt", "aes-gcm", diff --git a/apple/Sources/Sargon/Extensions/Methods/RET/TransactionManifest+Wrap+Functions.swift b/apple/Sources/Sargon/Extensions/Methods/RET/TransactionManifest+Wrap+Functions.swift index 007b07b30..fbb00692a 100644 --- a/apple/Sources/Sargon/Extensions/Methods/RET/TransactionManifest+Wrap+Functions.swift +++ b/apple/Sources/Sargon/Extensions/Methods/RET/TransactionManifest+Wrap+Functions.swift @@ -38,15 +38,4 @@ extension TransactionManifest { public var summary: ManifestSummary { transactionManifestSummary(manifest: self) } - - /// Creates the `ExecutionSummary` based on the `engineToolkitReceipt` data. - /// - /// Such value should be obtained from the Gateway `/transaction/preview` endpoint, under the `radix_engine_toolkit_receipt` field. - /// Its content will be parsed into a `String` representation and used as parameter here. - public func executionSummary(engineToolkitReceipt: String) throws -> ExecutionSummary { - try transactionManifestExecutionSummary( - manifest: self, - engineToolkitReceipt: engineToolkitReceipt - ) - } } diff --git a/apple/Tests/TestCases/RET/TransactionManifestTests.swift b/apple/Tests/TestCases/RET/TransactionManifestTests.swift index 36adf8572..5bf97581c 100644 --- a/apple/Tests/TestCases/RET/TransactionManifestTests.swift +++ b/apple/Tests/TestCases/RET/TransactionManifestTests.swift @@ -34,16 +34,6 @@ final class TransactionManifestTests: Test { func test_manifest_summary() { XCTAssertNoDifference(SUT.sample.summary.addressesOfAccountsWithdrawnFrom, [AccountAddress.sampleMainnet]) } - - func test_execution_summary() throws { - let name = "third_party_deposits_update" - let receipt = try engineToolkitReceipt(name) - let manifest = try rtm(name) - - let summary = try manifest.executionSummary(engineToolkitReceipt: receipt) - - XCTAssertNoDifference(summary.addressesOfAccountsRequiringAuth, ["account_tdx_2_129uv9r46an4hwng8wc97qwpraspvnrc7v2farne4lr6ff7yaevaz2a"]) - } func test_from_instructions_string_with_max_sbor_depth_is_ok() throws { let instructionsString = """ diff --git a/crates/sargon/Cargo.toml b/crates/sargon/Cargo.toml index 68819c3af..e30f5e833 100644 --- a/crates/sargon/Cargo.toml +++ b/crates/sargon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sargon" -version = "1.1.29" +version = "1.1.30" edition = "2021" build = "build.rs" diff --git a/crates/sargon/src/core/error/common_error.rs b/crates/sargon/src/core/error/common_error.rs index 1d9f215d5..bcf66621a 100644 --- a/crates/sargon/src/core/error/common_error.rs +++ b/crates/sargon/src/core/error/common_error.rs @@ -674,6 +674,19 @@ pub enum CommonError { #[error("Invalid security structure. A factor must not be present in both threshold and override list.")] InvalidSecurityStructureFactorInBothThresholdAndOverride = 10188, + + #[error("One of the receiving accounts does not allow deposits")] + OneOfReceivingAccountsDoesNotAllowDeposits = 10189, + + #[error("Failed transaction preview with status: {error_message}")] + FailedTransactionPreview { error_message: String } = 10190, + + #[error("Failed to extract radix engine toolkit receipt bytes")] + FailedToExtractTransactionReceiptBytes = 10191, + + #[error("Transaction Manifest contains forbidden instructions: {reserved_instructions}")] + ReservedInstructionsNotAllowedInManifest { reserved_instructions: String } = + 10192, } #[uniffi::export] diff --git a/crates/sargon/src/core/types/epoch.rs b/crates/sargon/src/core/types/epoch.rs index 49a910c2c..150c900bf 100644 --- a/crates/sargon/src/core/types/epoch.rs +++ b/crates/sargon/src/core/types/epoch.rs @@ -19,9 +19,24 @@ uniffi::custom_newtype!(Epoch, u64); )] pub struct Epoch(pub u64); +impl Epoch { + /// Circa 1 hour, since one epoch is circa 6 minutes. + pub const DEFAULT_EPOCH_WINDOW_SIZE: u64 = 10; +} + +impl Epoch { + pub fn new(value: u64) -> Self { + Self(value) + } + + pub fn window_end_from_start(start: Self) -> Self { + Self::new(start.0 + Self::DEFAULT_EPOCH_WINDOW_SIZE) + } +} + impl From for Epoch { fn from(value: u64) -> Self { - Self(value) + Self::new(value) } } @@ -39,7 +54,7 @@ impl From for ScryptoEpoch { impl From for Epoch { fn from(value: ScryptoEpoch) -> Self { - Self(value.number()) + Self::new(value.number()) } } diff --git a/crates/sargon/src/gateway_api/methods/transaction_methods.rs b/crates/sargon/src/gateway_api/methods/transaction_methods.rs index 205574f2f..85f763c10 100644 --- a/crates/sargon/src/gateway_api/methods/transaction_methods.rs +++ b/crates/sargon/src/gateway_api/methods/transaction_methods.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +use native_radix_engine_toolkit::receipt::SerializableToolkitTransactionReceipt; #[uniffi::export] impl GatewayClient { @@ -9,26 +10,6 @@ impl GatewayClient { .map(|state| Epoch::from(state.epoch)) } - /// Returns the String version of the `radix_engine_toolkit_receipt` by running a "dry run" of a - /// transaction - a preview of the transaction. The `radix_engine_toolkit_receipt`` is - /// required by the [`execution_summary` method](TransactionManifest::execution_summary) - /// on [`TransactionManifest`]. - pub async fn dry_run_transaction( - &self, - intent: TransactionIntent, - signer_public_keys: Vec, - ) -> Result { - let request = - TransactionPreviewRequest::new(intent, signer_public_keys, None); - self.transaction_preview(request) - .await - .map(|r| r.radix_engine_toolkit_receipt) - .and_then(|s| { - serde_json::to_string(&s) - .map_err(|_| CommonError::FailedToSerializeToJSON) - }) - } - /// Submits a signed transaction payload to the network. /// /// Returns `Ok(IntentHash)` if the transaction was submitted and not a duplicate. @@ -60,4 +41,20 @@ impl GatewayClient { let request = TransactionStatusRequest::new(intent_hash.to_string()); self.transaction_status(request).await } + + /// Returns the `radix_engine_toolkit_receipt` by running a "dry run" of a + /// transaction - a preview of the transaction. The `radix_engine_toolkit_receipt`` is + /// required by the [`execution_summary` method](TransactionManifest::execution_summary) + /// on [`TransactionManifest`]. + pub async fn dry_run_transaction( + &self, + intent: TransactionIntent, + signer_public_keys: Vec, + ) -> Result> { + let request = + TransactionPreviewRequest::new(intent, signer_public_keys, None); + self.transaction_preview(request) + .await + .map(|r| r.radix_engine_toolkit_receipt) + } } diff --git a/crates/sargon/src/gateway_api/models/logic/request/transaction/preview/request_flags.rs b/crates/sargon/src/gateway_api/models/logic/request/transaction/preview/request_flags.rs index 728b3ed9c..20f6b374b 100644 --- a/crates/sargon/src/gateway_api/models/logic/request/transaction/preview/request_flags.rs +++ b/crates/sargon/src/gateway_api/models/logic/request/transaction/preview/request_flags.rs @@ -2,9 +2,9 @@ use crate::prelude::*; impl TransactionPreviewRequestFlags { pub fn new( - use_free_credit: bool, - assume_all_signature_proofs: bool, - skip_epoch_check: bool, + use_free_credit: UseFreeCredit, + assume_all_signature_proofs: AssumeAllSignatureProofs, + skip_epoch_check: SkipEpochCheck, ) -> Self { Self { use_free_credit, @@ -16,7 +16,11 @@ impl TransactionPreviewRequestFlags { impl Default for TransactionPreviewRequestFlags { fn default() -> Self { - Self::new(true, false, false) + Self::new( + UseFreeCredit::default(), + AssumeAllSignatureProofs::default(), + SkipEpochCheck::default(), + ) } } @@ -28,10 +32,32 @@ mod tests { type SUT = TransactionPreviewRequestFlags; #[test] - fn default_value() { + fn default_is_use_free_credit() { + assert!(SUT::default().use_free_credit.0); + } + + #[test] + fn default_assume_all_signature_proofs() { + assert!(!SUT::default().assume_all_signature_proofs.0); + } + + #[test] + fn default_skip_epoch_check() { + assert!(!SUT::default().skip_epoch_check.0); + } + + #[test] + fn json_roundtrip() { let sut = SUT::default(); - assert!(sut.use_free_credit); - assert!(!sut.assume_all_signature_proofs); - assert!(!sut.skip_epoch_check); + assert_eq_after_json_roundtrip( + &sut, + r#" + { + "use_free_credit": true, + "assume_all_signature_proofs": false, + "skip_epoch_check": false + } + "#, + ) } } diff --git a/crates/sargon/src/gateway_api/models/logic/request/transaction/preview/transaction_preview.rs b/crates/sargon/src/gateway_api/models/logic/request/transaction/preview/transaction_preview.rs index 401836698..2b296cffb 100644 --- a/crates/sargon/src/gateway_api/models/logic/request/transaction/preview/transaction_preview.rs +++ b/crates/sargon/src/gateway_api/models/logic/request/transaction/preview/transaction_preview.rs @@ -48,8 +48,8 @@ mod tests { let do_test = |intent: TransactionIntent| { let header = intent.header; let keys = vec![PublicKey::sample(), PublicKey::sample_other()]; - let flags = TransactionPreviewRequestFlags::new(false, true, true); - let sut = SUT::new(intent.clone(), keys.clone(), flags); + let flags = TransactionPreviewRequestFlags::default(); + let sut = SUT::new(intent.clone(), keys.clone(), flags.clone()); assert_eq!(sut.flags, flags); assert_eq!( sut.signer_public_keys, diff --git a/crates/sargon/src/gateway_api/models/types/request/transaction/preview/request_flags.rs b/crates/sargon/src/gateway_api/models/types/request/transaction/preview/request_flags.rs index 1e8a16a2c..3ed7e76e6 100644 --- a/crates/sargon/src/gateway_api/models/types/request/transaction/preview/request_flags.rs +++ b/crates/sargon/src/gateway_api/models/types/request/transaction/preview/request_flags.rs @@ -2,7 +2,6 @@ use crate::prelude::*; #[derive( Clone, - Copy, Debug, PartialEq, Eq, @@ -10,7 +9,11 @@ use crate::prelude::*; Deserialize, /* Deserialize so we can test roundtrip of JSON vectors */ )] pub struct TransactionPreviewRequestFlags { - pub(crate) use_free_credit: bool, - pub(crate) assume_all_signature_proofs: bool, - pub(crate) skip_epoch_check: bool, + pub(crate) use_free_credit: UseFreeCredit, + pub(crate) assume_all_signature_proofs: AssumeAllSignatureProofs, + pub(crate) skip_epoch_check: SkipEpochCheck, } + +decl_bool_type!(UseFreeCredit, true); +decl_bool_type!(AssumeAllSignatureProofs, false); +decl_bool_type!(SkipEpochCheck, false); diff --git a/crates/sargon/src/gateway_api/models/types/response/transaction/preview/mod.rs b/crates/sargon/src/gateway_api/models/types/response/transaction/preview/mod.rs index 3b23abab6..d6ab96c0b 100644 --- a/crates/sargon/src/gateway_api/models/types/response/transaction/preview/mod.rs +++ b/crates/sargon/src/gateway_api/models/types/response/transaction/preview/mod.rs @@ -1,5 +1,9 @@ mod logs_inner; mod transaction_preview_response; +mod transaction_receipt; +mod transaction_receipt_status; pub use logs_inner::*; pub use transaction_preview_response::*; +pub use transaction_receipt::*; +pub use transaction_receipt_status::*; diff --git a/crates/sargon/src/gateway_api/models/types/response/transaction/preview/transaction_preview_response.rs b/crates/sargon/src/gateway_api/models/types/response/transaction/preview/transaction_preview_response.rs index 7143e8383..9859dbefb 100644 --- a/crates/sargon/src/gateway_api/models/types/response/transaction/preview/transaction_preview_response.rs +++ b/crates/sargon/src/gateway_api/models/types/response/transaction/preview/transaction_preview_response.rs @@ -12,6 +12,7 @@ pub struct TransactionPreviewResponse { /** Hex-encoded binary blob. */ pub encoded_receipt: String, pub radix_engine_toolkit_receipt: - ScryptoSerializableToolkitTransactionReceipt, + Option, pub logs: Vec, + pub receipt: TransactionReceipt, } diff --git a/crates/sargon/src/gateway_api/models/types/response/transaction/preview/transaction_receipt.rs b/crates/sargon/src/gateway_api/models/types/response/transaction/preview/transaction_receipt.rs new file mode 100644 index 000000000..69cbd099b --- /dev/null +++ b/crates/sargon/src/gateway_api/models/types/response/transaction/preview/transaction_receipt.rs @@ -0,0 +1,54 @@ +use crate::prelude::*; + +/// This is part of the response to a transaction preview request, and contains the status of the transaction. +/// Error message is only present if status is `Failed` or `Rejected`. +#[derive( + Deserialize, + Serialize, + Clone, + PartialEq, + Eq, + Debug, + derive_more::Display, + uniffi::Record, +)] +#[display("{status}")] +pub struct TransactionReceipt { + pub status: TransactionReceiptStatus, + pub error_message: Option, +} + +impl HasSampleValues for TransactionReceipt { + fn sample() -> Self { + Self { + status: TransactionReceiptStatus::Succeeded, + error_message: None, + } + } + + fn sample_other() -> Self { + Self { + status: TransactionReceiptStatus::Failed, + error_message: Some("An error occurred".to_string()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = TransactionReceipt; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } +} diff --git a/crates/sargon/src/gateway_api/models/types/response/transaction/preview/transaction_receipt_status.rs b/crates/sargon/src/gateway_api/models/types/response/transaction/preview/transaction_receipt_status.rs new file mode 100644 index 000000000..24db32632 --- /dev/null +++ b/crates/sargon/src/gateway_api/models/types/response/transaction/preview/transaction_receipt_status.rs @@ -0,0 +1,17 @@ +use crate::prelude::*; + +#[derive( + Deserialize, + Serialize, + Clone, + PartialEq, + Eq, + Debug, + derive_more::Display, + uniffi::Enum, +)] +pub enum TransactionReceiptStatus { + Succeeded, + Failed, + Rejected, +} diff --git a/crates/sargon/src/signing/collector/extractor_of_instances_required_to_sign_transactions.rs b/crates/sargon/src/signing/collector/extractor_of_instances_required_to_sign_transactions.rs new file mode 100644 index 000000000..3707a832f --- /dev/null +++ b/crates/sargon/src/signing/collector/extractor_of_instances_required_to_sign_transactions.rs @@ -0,0 +1,119 @@ +use crate::prelude::*; + +/// Utility to extract factor instances required to sign transactions. +pub struct ExtractorOfInstancesRequiredToSignTransactions; + +impl ExtractorOfInstancesRequiredToSignTransactions { + /// Extracts factor instances required to sign transactions. + /// Returns a set of `HierarchicalDeterministicFactorInstance`. + /// Returns an error if the `SignaturesCollectorPreprocessor` fails to initialize. + pub fn extract( + profile: &Profile, + transactions: Vec, + for_any_securified_entity_select_role: RoleKind, + ) -> Result> { + let preprocessor = + SignaturesCollectorPreprocessor::analyzing_transaction_intents( + profile, + transactions, + )?; + let (petitions, _) = preprocessor.preprocess( + IndexSet::from_iter(profile.factor_sources.iter()), + for_any_securified_entity_select_role, + ); + + let factor_instances = petitions + .txid_to_petition + .borrow() + .values() + .flat_map(|p| { + p.for_entities + .borrow() + .values() + .flat_map(|p| p.all_factor_instances()) + .collect::>() + }) + .map(|p| p.factor_instance().clone()) + .collect::>(); + Ok(factor_instances) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn preprocessor_init_fail() { + let result = ExtractorOfInstancesRequiredToSignTransactions::extract( + &Profile::sample_other(), + vec![TransactionIntent::sample()], + RoleKind::Primary, + ); + + assert!(matches!(result, Err(CommonError::UnknownAccount))); + } + + #[test] + fn success() { + let private_hd_factor_source = + PrivateHierarchicalDeterministicFactorSource::sample(); + let account_creating_factor_instance_1 = private_hd_factor_source + .derive_entity_creation_factor_instance(NetworkID::Mainnet, 0); + let account_creating_factor_instance_2 = private_hd_factor_source + .derive_entity_creation_factor_instance(NetworkID::Mainnet, 1); + + let account_1 = Account::new( + account_creating_factor_instance_1.clone(), + DisplayName::sample(), + AppearanceID::sample(), + ); + let account_2 = Account::new( + account_creating_factor_instance_2.clone(), + DisplayName::sample(), + AppearanceID::sample(), + ); + + let persona_creating_factor_instance = private_hd_factor_source + .derive_entity_creation_factor_instance(NetworkID::Mainnet, 1); + let persona = Persona::new( + persona_creating_factor_instance.clone(), + DisplayName::sample(), + None, + ); + + let intent_1 = TransactionIntent::new_requiring_auth( + vec![account_1.address], + vec![persona.address], + ); + let intent_2 = TransactionIntent::new_requiring_auth( + vec![account_2.address], + vec![], + ); + + let result = ExtractorOfInstancesRequiredToSignTransactions::extract( + &Profile::sample(), + vec![intent_1, intent_2], + RoleKind::Primary, + ); + + let account_fi_1 = HierarchicalDeterministicFactorInstance::from( + account_creating_factor_instance_1, + ); + let account_fi_2 = HierarchicalDeterministicFactorInstance::from( + account_creating_factor_instance_2, + ); + let persona_fi = HierarchicalDeterministicFactorInstance::from( + persona_creating_factor_instance, + ); + + assert_eq!( + result, + Ok(IndexSet::from_iter(vec![ + account_fi_1, + persona_fi, + account_fi_2 + ])) + ); + } +} diff --git a/crates/sargon/src/signing/collector/mod.rs b/crates/sargon/src/signing/collector/mod.rs index 081e29254..9eb142339 100644 --- a/crates/sargon/src/signing/collector/mod.rs +++ b/crates/sargon/src/signing/collector/mod.rs @@ -1,3 +1,4 @@ +mod extractor_of_instances_required_to_sign_transactions; mod signatures_collecting_continuation; mod signatures_collector; mod signatures_collector_dependencies; @@ -5,6 +6,7 @@ mod signatures_collector_preprocessor; mod signatures_collector_state; mod signing_finish_early_strategy; +pub(crate) use extractor_of_instances_required_to_sign_transactions::*; pub(crate) use signatures_collector_preprocessor::*; pub use signatures_collecting_continuation::*; diff --git a/crates/sargon/src/signing/collector/signatures_collector_preprocessor.rs b/crates/sargon/src/signing/collector/signatures_collector_preprocessor.rs index c1b2ea1c7..e88e3e4da 100644 --- a/crates/sargon/src/signing/collector/signatures_collector_preprocessor.rs +++ b/crates/sargon/src/signing/collector/signatures_collector_preprocessor.rs @@ -30,6 +30,18 @@ pub(crate) fn sort_group_factors( } impl SignaturesCollectorPreprocessor { + pub(super) fn analyzing_transaction_intents( + profile: &Profile, + transactions: Vec, + ) -> Result { + let transactions = transactions + .into_iter() + .map(|i| TXToSign::extracting_from_intent_and_profile(&i, profile)) + .collect::>>()?; + + Ok(Self::new(transactions)) + } + pub(super) fn new(transactions: IndexSet) -> Self { Self { transactions } } diff --git a/crates/sargon/src/signing/extractor_of_entities_requiring_auth.rs b/crates/sargon/src/signing/extractor_of_entities_requiring_auth.rs new file mode 100644 index 000000000..ec4a87d41 --- /dev/null +++ b/crates/sargon/src/signing/extractor_of_entities_requiring_auth.rs @@ -0,0 +1,137 @@ +use crate::prelude::*; + +/// Utility to extract entities requiring auth from a profile and a manifest summary. +pub struct ExtractorOfEntitiesRequiringAuth; +impl ExtractorOfEntitiesRequiringAuth { + /// Matches entities requiring auth from a manifest summary with the entities in the given profile. + /// Returns a set of `AccountOrPersona` or empty if the manifest summary does not require auth. + /// Returns an error if an account or persona is unknown. + pub fn extract( + profile: &Profile, + summary: ManifestSummary, + ) -> Result> { + let mut entities_requiring_auth: IndexSet = + IndexSet::new(); + + let accounts = summary + .addresses_of_accounts_requiring_auth + .iter() + .map(|a| profile.account_by_address(*a)) + .collect::>>()?; + + entities_requiring_auth.extend( + accounts + .into_iter() + .map(AccountOrPersona::from) + .collect_vec(), + ); + + let personas = summary + .addresses_of_personas_requiring_auth + .into_iter() + .map(|a| profile.persona_by_address(a)) + .collect::>>()?; + + entities_requiring_auth.extend( + personas + .into_iter() + .map(AccountOrPersona::from) + .collect_vec(), + ); + Ok(entities_requiring_auth) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use radix_transactions::prelude::ManifestBuilder; + + #[test] + fn extract_when_account_is_unknown() { + let profile = Profile::sample(); + + let manifest_builder = ManifestBuilder::new(); + let mut manifest = TransactionManifest::sargon_built( + manifest_builder, + NetworkID::Mainnet, + ); + manifest = manifest.modify_add_lock_fee( + &AccountAddress::sample_stokenet(), + Some(Decimal192::one()), + ); + let manifest_summary = manifest.summary(); + + let result = ExtractorOfEntitiesRequiringAuth::extract( + &profile, + manifest_summary, + ); + + assert!(matches!(result, Err(CommonError::UnknownAccount))); + } + + #[test] + fn extract_when_persona_is_unknown() { + let profile = Profile::sample(); + + let manifest = TransactionManifest::set_owner_keys_hashes( + &Persona::sample_mainnet_third().address.into(), + vec![PublicKeyHash::sample()], + ); + let manifest_summary = manifest.summary(); + + let result = ExtractorOfEntitiesRequiringAuth::extract( + &profile, + manifest_summary, + ); + + assert!(matches!(result, Err(CommonError::UnknownPersona))); + } + + #[test] + fn extract_when_no_entities_require_auth() { + let profile = Profile::sample(); + + let manifest_builder = ManifestBuilder::new(); + let manifest = TransactionManifest::sargon_built( + manifest_builder, + NetworkID::Mainnet, + ); + let manifest_summary = manifest.summary(); + + let result = ExtractorOfEntitiesRequiringAuth::extract( + &profile, + manifest_summary, + ); + + assert!(result.is_ok()); + assert!(result.unwrap().is_empty()); + } + + #[test] + fn extract_entities_success() { + let profile = Profile::sample(); + let account = Account::sample_mainnet(); + let persona = Persona::sample_mainnet(); + + let manifest = TransactionManifest::set_owner_keys_hashes( + &persona.address.into(), + vec![PublicKeyHash::sample()], + ) + .modify_add_lock_fee(&account.address, Some(Decimal192::one())); + let manifest_summary = manifest.summary(); + + let result = ExtractorOfEntitiesRequiringAuth::extract( + &profile, + manifest_summary, + ); + + assert_eq!( + result, + Ok(IndexSet::from_iter(vec![ + AccountOrPersona::from(account), + AccountOrPersona::from(persona), + ])) + ); + } +} diff --git a/crates/sargon/src/signing/mod.rs b/crates/sargon/src/signing/mod.rs index 9716fba48..c7fdb094a 100644 --- a/crates/sargon/src/signing/mod.rs +++ b/crates/sargon/src/signing/mod.rs @@ -1,4 +1,5 @@ mod collector; +mod extractor_of_entities_requiring_auth; mod host_interaction; mod petition_types; mod signatures_outecome_types; @@ -7,6 +8,7 @@ mod tx_to_sign; #[cfg(test)] mod testing; +pub(crate) use extractor_of_entities_requiring_auth::*; pub(crate) use tx_to_sign::*; pub use collector::*; diff --git a/crates/sargon/src/signing/tx_to_sign.rs b/crates/sargon/src/signing/tx_to_sign.rs index 148699c27..76d7c3aae 100644 --- a/crates/sargon/src/signing/tx_to_sign.rs +++ b/crates/sargon/src/signing/tx_to_sign.rs @@ -31,35 +31,11 @@ impl TXToSign { profile: &Profile, ) -> Result { let intent_hash = intent.intent_hash().clone(); - let summary = intent.manifest_summary(); - let mut entities_requiring_auth: IndexSet = - IndexSet::new(); - - let accounts = summary - .addresses_of_accounts_requiring_auth - .iter() - .map(|a| profile.account_by_address(*a)) - .collect::>>()?; - - entities_requiring_auth.extend( - accounts - .into_iter() - .map(AccountOrPersona::from) - .collect_vec(), - ); - - let personas = summary - .addresses_of_personas_requiring_auth - .into_iter() - .map(|a| profile.persona_by_address(a)) - .collect::>>()?; - - entities_requiring_auth.extend( - personas - .into_iter() - .map(AccountOrPersona::from) - .collect_vec(), - ); + let entities_requiring_auth = + ExtractorOfEntitiesRequiringAuth::extract( + profile, + intent.manifest_summary().clone(), + )?; Ok(Self::with(intent_hash, entities_requiring_auth)) } diff --git a/crates/sargon/src/system/drivers/networking_driver/support/test/mock_networking_driver.rs b/crates/sargon/src/system/drivers/networking_driver/support/test/mock_networking_driver.rs index 6130fb640..8920db351 100644 --- a/crates/sargon/src/system/drivers/networking_driver/support/test/mock_networking_driver.rs +++ b/crates/sargon/src/system/drivers/networking_driver/support/test/mock_networking_driver.rs @@ -25,10 +25,26 @@ impl MockNetworkingDriver { } } + pub fn with_spy_multiple_bodies( + status: u16, + bodies: Vec, + spy: fn(NetworkRequest) -> (), + ) -> Self { + Self { + hard_coded_status: status, + hard_coded_bodies: Mutex::new(bodies), + spy, + } + } + pub fn new(status: u16, body: impl Into) -> Self { Self::with_spy(status, body, |_| {}) } + pub fn new_with_bodies(status: u16, bodies: Vec) -> Self { + Self::with_spy_multiple_bodies(status, bodies, |_| {}) + } + pub fn new_always_failing() -> Self { Self::new(500, BagOfBytes::new()) } diff --git a/crates/sargon/src/system/sargon_os/mod.rs b/crates/sargon/src/system/sargon_os/mod.rs index 03162aa0b..0502050c9 100644 --- a/crates/sargon/src/system/sargon_os/mod.rs +++ b/crates/sargon/src/system/sargon_os/mod.rs @@ -5,7 +5,7 @@ mod sargon_os_factors; mod sargon_os_gateway; mod sargon_os_profile; mod sargon_os_security_structures; -mod sargon_os_transactions; +mod transactions; pub use profile_state_holder::*; pub use sargon_os::*; @@ -14,4 +14,4 @@ pub use sargon_os_factors::*; pub use sargon_os_gateway::*; pub use sargon_os_profile::*; pub use sargon_os_security_structures::*; -pub use sargon_os_transactions::*; +pub use transactions::*; diff --git a/crates/sargon/src/system/sargon_os/transactions/mod.rs b/crates/sargon/src/system/sargon_os/transactions/mod.rs new file mode 100644 index 000000000..58bcdad43 --- /dev/null +++ b/crates/sargon/src/system/sargon_os/transactions/mod.rs @@ -0,0 +1,9 @@ +mod sargon_os_transaction_analysis; +mod sargon_os_transaction_status; +mod sargon_os_transaction_submit; +mod support; + +pub use sargon_os_transaction_analysis::*; +pub use sargon_os_transaction_status::*; +pub use sargon_os_transaction_submit::*; +pub use support::*; diff --git a/crates/sargon/src/system/sargon_os/transactions/sargon_os_transaction_analysis.rs b/crates/sargon/src/system/sargon_os/transactions/sargon_os_transaction_analysis.rs new file mode 100644 index 000000000..83b1a4195 --- /dev/null +++ b/crates/sargon/src/system/sargon_os/transactions/sargon_os_transaction_analysis.rs @@ -0,0 +1,711 @@ +use std::sync::RwLockWriteGuard; + +use crate::prelude::*; + +#[uniffi::export] +impl SargonOS { + /// Performs initial transaction analysis for a given raw manifest, including: + /// 1. Extracting the transaction signers. + /// 2. Executing the transaction preview GW request. + /// 3. Running the execution summary with the manifest and receipt. + /// Maps relevant errors to ensure proper handling by the hosts. + pub async fn analyse_transaction_preview( + &self, + instructions: String, + blobs: Blobs, + message: Message, + are_instructions_originating_from_host: bool, + nonce: Nonce, + notary_public_key: PublicKey, + ) -> Result { + self.perform_transaction_preview_analysis( + instructions, + blobs, + message, + are_instructions_originating_from_host, + nonce, + notary_public_key, + ) + .await + } +} + +/// This is part of an error message returned **by Gateway**, indicating the deposits are denied for the account. +/// We use it part of logic below, matching against this String - we really should upgrade this code to be more +/// structured - we MUST update this value if Gateway where to change this value. +const GW_ERR_ACCOUNT_DEPOSIT_DISALLOWED: &'static str = + "AccountError(DepositIsDisallowed"; +/// This is part of an error message returned **by Gateway**, indicating the deposits are denied for the account. +/// We use it part of logic below, matching against this String - we really should upgrade this code to be more +/// structured - we MUST update this value if Gateway where to change this value. +const GW_ERR_NOT_ALL_COULD_BE_DEPOSITED: &'static str = + "AccountError(NotAllBucketsCouldBeDeposited"; + +impl SargonOS { + /// Performs initial transaction analysis for a given raw manifest, including: + /// 1. Extracting the transaction signers. + /// 2. Executing the transaction preview GW request. + /// 3. Running the execution summary with the manifest and receipt. + /// Maps relevant errors to ensure proper handling by the hosts. + /// + /// This is the internal implementation of `analyse_transaction_preview`, which is the public API. + /// Returns `TransactionToReview`, which includes the manifest and the execution summary. + pub async fn perform_transaction_preview_analysis( + &self, + instructions: String, + blobs: Blobs, + message: Message, + are_instructions_originating_from_host: bool, + nonce: Nonce, + notary_public_key: PublicKey, + ) -> Result { + let network_id = self.profile_state_holder.current_network_id()?; + let gateway_client = GatewayClient::new( + self.clients.http_client.driver.clone(), + network_id, + ); + let transaction_manifest = + TransactionManifest::new(instructions, network_id, blobs)?; + + // Get the transaction preview + let transaction_preview = self + .get_transaction_preview( + gateway_client, + transaction_manifest.clone(), + network_id, + message, + nonce, + notary_public_key, + ) + .await?; + let engine_toolkit_receipt = transaction_preview + .radix_engine_toolkit_receipt + .ok_or(CommonError::FailedToExtractTransactionReceiptBytes)?; + + // Analyze the manifest + let execution_summary = + transaction_manifest.execution_summary(engine_toolkit_receipt)?; + + // Transactions created outside of the Wallet are not allowed to use reserved instructions + if !are_instructions_originating_from_host + && !execution_summary.reserved_instructions.is_empty() + { + return Err( + CommonError::ReservedInstructionsNotAllowedInManifest { + reserved_instructions: execution_summary + .reserved_instructions + .iter() + .map(|i| i.to_string()) + .collect(), + }, + ); + } + + Ok(TransactionToReview { + transaction_manifest, + execution_summary, + }) + } + + async fn get_transaction_preview( + &self, + gateway_client: GatewayClient, + manifest: TransactionManifest, + network_id: NetworkID, + message: Message, + nonce: Nonce, + notary_public_key: PublicKey, + ) -> Result { + // Getting the current ledger epoch + let epoch = gateway_client.current_epoch().await?; + + // Extracting the entities requiring auth to check if the notary is signatory + let profile = self.profile_state_holder.profile()?; + let entities_requiring_auth = + ExtractorOfEntitiesRequiringAuth::extract( + &profile, + manifest.summary(), + )?; + + // Creating the transaction header and intent + let header = TransactionHeader::new( + network_id, + epoch, + Epoch::window_end_from_start(epoch), + nonce, + notary_public_key, + entities_requiring_auth.is_empty(), + 0, + ); + let intent = TransactionIntent::new(header, manifest, message)?; + + // Extracting the signers public keys + let signer_public_keys = + ExtractorOfInstancesRequiredToSignTransactions::extract( + &profile, + vec![intent.clone()], + RoleKind::Primary, + )? + .iter() + .map(|i| i.public_key.public_key) + .collect::>(); + + // Making the transaction preview Gateway request + let request = TransactionPreviewRequest::new( + intent, + signer_public_keys, + TransactionPreviewRequestFlags::default(), + ); + let response = gateway_client.transaction_preview(request).await?; + + // Checking the transaction receipt status and mapping the response + if response.receipt.status != TransactionReceiptStatus::Succeeded { + return Err(Self::map_failed_transaction_preview(response)); + }; + Ok(response) + } + + fn map_failed_transaction_preview( + response: TransactionPreviewResponse, + ) -> CommonError { + let message = response + .receipt + .error_message + .unwrap_or_else(|| "Unknown reason".to_string()); + + // Quite rudimentary, but it is not worth making something smarter, + // as the GW will provide in the future strongly typed errors + let is_failure_due_to_deposit_rules = message + .contains(GW_ERR_ACCOUNT_DEPOSIT_DISALLOWED) + || message.contains(GW_ERR_NOT_ALL_COULD_BE_DEPOSITED); + + if is_failure_due_to_deposit_rules { + CommonError::OneOfReceivingAccountsDoesNotAllowDeposits + } else { + CommonError::FailedTransactionPreview { + error_message: message, + } + } + } +} + +#[cfg(test)] +mod transaction_preview_analysis_tests { + use super::*; + use native_radix_engine_toolkit::receipt::AsStr; + use radix_common::prelude::Decimal; + use std::sync::Mutex; + + #[allow(clippy::upper_case_acronyms)] + type SUT = SargonOS; + + #[actix_rt::test] + async fn manifest_parse_error() { + let os = SUT::fast_boot().await; + + let result = os + .perform_transaction_preview_analysis( + "instructions".to_string(), + Blobs::sample(), + Message::sample(), + false, + Nonce::sample(), + PublicKey::sample(), + ) + .await; + + assert!(matches!( + result, + Err(CommonError::InvalidInstructionsString { .. }) + )); + } + + #[actix_rt::test] + async fn profile_not_loaded_error() { + let os = SUT::fast_boot().await; + os.profile_state_holder + .replace_profile_state_with(ProfileState::None) + .unwrap(); + + let result = os + .perform_transaction_preview_analysis( + TransactionManifest::sample().instructions_string(), + Blobs::sample(), + Message::sample(), + false, + Nonce::sample(), + PublicKey::sample(), + ) + .await; + + assert!(matches!( + result, + Err(CommonError::ProfileStateNotLoaded { .. }) + )); + } + + #[actix_rt::test] + async fn failed_network_response_error() { + let os = prepare_os(MockNetworkingDriver::new_always_failing()).await; + + let result = os + .perform_transaction_preview_analysis( + prepare_manifest_with_account_entity().instructions_string(), + Blobs::sample(), + Message::sample(), + false, + Nonce::sample(), + PublicKey::sample(), + ) + .await; + + assert_eq!(result, Err(CommonError::NetworkResponseBadCode)) + } + + #[actix_rt::test] + async fn failed_preview_response_unknown_error() { + let responses = prepare_responses( + LedgerState { + network: "".to_string(), + state_version: 0, + proposer_round_timestamp: "".to_string(), + epoch: 0, + round: 0, + }, + TransactionPreviewResponse { + encoded_receipt: "".to_string(), + radix_engine_toolkit_receipt: None, + logs: vec![], + receipt: TransactionReceipt { + status: TransactionReceiptStatus::Failed, + error_message: None, + }, + }, + ); + let os = + prepare_os(MockNetworkingDriver::new_with_bodies(200, responses)) + .await; + + let result = os + .perform_transaction_preview_analysis( + prepare_manifest_with_account_entity().instructions_string(), + Blobs::sample(), + Message::sample(), + false, + Nonce::sample(), + PublicKey::sample(), + ) + .await; + + assert_eq!( + result, + Err(CommonError::FailedTransactionPreview { + error_message: "Unknown reason".to_string() + }) + ) + } + + #[actix_rt::test] + async fn failed_preview_response_deposit_rules_error() { + let mut responses: Vec = vec![]; + let mut first_call_responses = prepare_responses( + LedgerState { + network: "".to_string(), + state_version: 0, + proposer_round_timestamp: "".to_string(), + epoch: 0, + round: 0, + }, + TransactionPreviewResponse { + encoded_receipt: "".to_string(), + radix_engine_toolkit_receipt: None, + logs: vec![], + receipt: TransactionReceipt { + status: TransactionReceiptStatus::Failed, + error_message: Some( + "AccountError(DepositIsDisallowed".to_string(), + ), + }, + }, + ); + let mut second_call_responses = prepare_responses( + LedgerState { + network: "".to_string(), + state_version: 0, + proposer_round_timestamp: "".to_string(), + epoch: 0, + round: 0, + }, + TransactionPreviewResponse { + encoded_receipt: "".to_string(), + radix_engine_toolkit_receipt: None, + logs: vec![], + receipt: TransactionReceipt { + status: TransactionReceiptStatus::Failed, + error_message: Some( + "AccountError(NotAllBucketsCouldBeDeposited" + .to_string(), + ), + }, + }, + ); + responses.append(&mut first_call_responses); + responses.append(&mut second_call_responses); + let os = + prepare_os(MockNetworkingDriver::new_with_bodies(200, responses)) + .await; + let manifest = prepare_manifest_with_account_entity(); + + let result = os + .perform_transaction_preview_analysis( + manifest.instructions_string(), + Blobs::sample(), + Message::sample(), + false, + Nonce::sample(), + PublicKey::sample(), + ) + .await; + + assert_eq!( + result, + Err(CommonError::OneOfReceivingAccountsDoesNotAllowDeposits) + ); + + let result = os + .perform_transaction_preview_analysis( + manifest.instructions_string(), + Blobs::sample(), + Message::sample(), + false, + Nonce::sample(), + PublicKey::sample(), + ) + .await; + + assert_eq!( + result, + Err(CommonError::OneOfReceivingAccountsDoesNotAllowDeposits) + ) + } + + #[actix_rt::test] + async fn missing_radix_engine_toolkit_receipt_error() { + let responses = prepare_responses( + LedgerState { + network: "".to_string(), + state_version: 0, + proposer_round_timestamp: "".to_string(), + epoch: 0, + round: 0, + }, + TransactionPreviewResponse { + encoded_receipt: "".to_string(), + radix_engine_toolkit_receipt: None, + logs: vec![], + receipt: TransactionReceipt { + status: TransactionReceiptStatus::Succeeded, + error_message: None, + }, + }, + ); + let os = + prepare_os(MockNetworkingDriver::new_with_bodies(200, responses)) + .await; + let manifest = TransactionManifest::set_owner_keys_hashes( + &IdentityAddress::sample().into(), + vec![PublicKeyHash::sample()], + ); + + let result = os + .perform_transaction_preview_analysis( + manifest.instructions_string(), + Blobs::sample(), + Message::sample(), + false, + Nonce::sample(), + PublicKey::sample(), + ) + .await; + + assert_eq!( + result, + Err(CommonError::FailedToExtractTransactionReceiptBytes) + ) + } + + #[actix_rt::test] + async fn signer_entities_not_found() { + let responses = prepare_responses( + LedgerState { + network: "".to_string(), + state_version: 0, + proposer_round_timestamp: "".to_string(), + epoch: 0, + round: 0, + }, + TransactionPreviewResponse { + encoded_receipt: "".to_string(), + radix_engine_toolkit_receipt: Some( + ScryptoSerializableToolkitTransactionReceipt::Reject { + reason: "Test".to_string(), + }, + ), + logs: vec![], + receipt: TransactionReceipt { + status: TransactionReceiptStatus::Succeeded, + error_message: None, + }, + }, + ); + let os = + prepare_os(MockNetworkingDriver::new_with_bodies(200, responses)) + .await; + + let result = os + .perform_transaction_preview_analysis( + TransactionManifest::sample().instructions_string(), + Blobs::sample(), + Message::sample(), + false, + Nonce::sample(), + PublicKey::sample(), + ) + .await; + + assert_eq!(result, Err(CommonError::UnknownAccount)) + } + + #[actix_rt::test] + async fn execution_summary_parse_error() { + let responses = prepare_responses( + LedgerState { + network: "".to_string(), + state_version: 0, + proposer_round_timestamp: "".to_string(), + epoch: 0, + round: 0, + }, + TransactionPreviewResponse { + encoded_receipt: "".to_string(), + radix_engine_toolkit_receipt: Some( + ScryptoSerializableToolkitTransactionReceipt::Reject { + reason: "Test".to_string(), + }, + ), + logs: vec![], + receipt: TransactionReceipt { + status: TransactionReceiptStatus::Succeeded, + error_message: None, + }, + }, + ); + let os = + prepare_os(MockNetworkingDriver::new_with_bodies(200, responses)) + .await; + + let result = os + .perform_transaction_preview_analysis( + prepare_manifest_with_account_entity().instructions_string(), + Blobs::sample(), + Message::sample(), + false, + Nonce::sample(), + PublicKey::sample(), + ) + .await; + + assert_eq!( + result, + Err(CommonError::ExecutionSummaryFail { + underlying: "InvalidReceipt".to_string() + }) + ) + } + + #[actix_rt::test] + async fn execution_summary_reserved_instructions_error() { + let ret_zero: AsStr = Decimal::ZERO.into(); + let responses = prepare_responses( + LedgerState { + network: "".to_string(), + state_version: 0, + proposer_round_timestamp: "".to_string(), + epoch: 0, + round: 0, + }, + TransactionPreviewResponse { + encoded_receipt: "".to_string(), + radix_engine_toolkit_receipt: Some(ScryptoSerializableToolkitTransactionReceipt::CommitSuccess { + state_updates_summary: native_radix_engine_toolkit::receipt::StateUpdatesSummary { + new_entities: IndexSet::new(), + metadata_updates: IndexMap::new(), + non_fungible_data_updates: IndexMap::new(), + newly_minted_non_fungibles: IndexSet::new(), + }, + worktop_changes: IndexMap::new(), + fee_summary: native_radix_engine_toolkit::receipt::FeeSummary { + execution_fees_in_xrd: ret_zero, + finalization_fees_in_xrd: ret_zero, + storage_fees_in_xrd: ret_zero, + royalty_fees_in_xrd: ret_zero, + }, + locked_fees: native_radix_engine_toolkit::receipt::LockedFees { + contingent: ret_zero, + non_contingent: ret_zero, + }, + }), + logs: vec![], + receipt: TransactionReceipt { + status: TransactionReceiptStatus::Succeeded, + error_message: None, + }, + }, + ); + let os = + prepare_os(MockNetworkingDriver::new_with_bodies(200, responses)) + .await; + + let result = os + .perform_transaction_preview_analysis( + prepare_manifest_with_account_entity().instructions_string(), + Blobs::sample(), + Message::sample(), + false, + Nonce::sample(), + PublicKey::sample(), + ) + .await; + + assert_eq!( + result, + Err(CommonError::ReservedInstructionsNotAllowedInManifest { + reserved_instructions: "AccountLockFee".to_string() + }) + ) + } + + #[actix_rt::test] + async fn success() { + let responses = prepare_responses( + LedgerState { + network: "".to_string(), + state_version: 0, + proposer_round_timestamp: "".to_string(), + epoch: 0, + round: 0, + }, + TransactionPreviewResponse { + encoded_receipt: "".to_string(), + radix_engine_toolkit_receipt: Some(ScryptoSerializableToolkitTransactionReceipt::CommitSuccess { + state_updates_summary: native_radix_engine_toolkit::receipt::StateUpdatesSummary { + new_entities: IndexSet::new(), + metadata_updates: IndexMap::new(), + non_fungible_data_updates: IndexMap::new(), + newly_minted_non_fungibles: IndexSet::new(), + }, + worktop_changes: IndexMap::new(), + fee_summary: native_radix_engine_toolkit::receipt::FeeSummary { + execution_fees_in_xrd: ScryptoDecimal192::zero().into(), + finalization_fees_in_xrd: ScryptoDecimal192::zero().into(), + storage_fees_in_xrd: ScryptoDecimal192::zero().into(), + royalty_fees_in_xrd: ScryptoDecimal192::zero().into(), + }, + locked_fees: native_radix_engine_toolkit::receipt::LockedFees { + contingent: ScryptoDecimal192::zero().into(), + non_contingent: ScryptoDecimal192::zero().into(), + }, + }), + logs: vec![], + receipt: TransactionReceipt { + status: TransactionReceiptStatus::Succeeded, + error_message: None, + }, + }, + ); + let os = + prepare_os(MockNetworkingDriver::new_with_bodies(200, responses)) + .await; + let acc: AccountAddress = Account::sample().address; + let manifest = prepare_manifest_with_account_entity(); + + let result = os + .analyse_transaction_preview( + manifest.instructions_string(), + Blobs::default(), + Message::sample(), + true, + Nonce::sample(), + PublicKey::sample(), + ) + .await; + + assert_eq!( + result, + Ok(TransactionToReview { + transaction_manifest: manifest, + execution_summary: ExecutionSummary::new( + [], + [], + [acc], + [], + [], + [ReservedInstruction::AccountLockFee], + [], + [], + [], + FeeLocks::default(), + FeeSummary::new("0", "0", "0", 0,), + NewEntities::default() + ) + }) + ) + } + + async fn prepare_os( + mock_networking_driver: MockNetworkingDriver, + ) -> Arc { + let req = SUT::boot_test_with_networking_driver(Arc::new( + mock_networking_driver, + )); + let os = + actix_rt::time::timeout(SARGON_OS_TEST_MAX_ASYNC_DURATION, req) + .await + .unwrap() + .unwrap(); + os.profile_state_holder + .update_profile_with(|profile| { + profile.networks.insert(ProfileNetwork::sample_mainnet()); + profile.factor_sources.insert(FactorSource::sample()); + Ok(()) + }) + .unwrap(); + os + } + + fn prepare_manifest_with_account_entity() -> TransactionManifest { + let account = Account::sample_mainnet(); + TransactionManifest::set_owner_keys_hashes( + &account.address.into(), + vec![PublicKeyHash::sample()], + ) + .modify_add_lock_fee(&account.address, Some(Decimal192::zero())) + } + + fn prepare_responses( + ledger_state: LedgerState, + preview_response: TransactionPreviewResponse, + ) -> Vec { + vec![ + to_bag_of_bytes(TransactionConstructionResponse { ledger_state }), + to_bag_of_bytes(preview_response), + ] + } + + fn to_bag_of_bytes(value: T) -> BagOfBytes + where + T: Serialize, + { + BagOfBytes::from(serde_json::to_vec(&value).unwrap()) + } +} diff --git a/crates/sargon/src/system/sargon_os/sargon_os_transactions.rs b/crates/sargon/src/system/sargon_os/transactions/sargon_os_transaction_status.rs similarity index 79% rename from crates/sargon/src/system/sargon_os/sargon_os_transactions.rs rename to crates/sargon/src/system/sargon_os/transactions/sargon_os_transaction_status.rs index 136a8c5ed..4dd7e59c0 100644 --- a/crates/sargon/src/system/sargon_os/sargon_os_transactions.rs +++ b/crates/sargon/src/system/sargon_os/transactions/sargon_os_transaction_status.rs @@ -1,27 +1,6 @@ use crate::prelude::*; use std::time::Duration; -// ================== -// Submit Transaction -// ================== -#[uniffi::export] -impl SargonOS { - /// Submits a notarized transaction payload to the network. - pub async fn submit_transaction( - &self, - notarized_transaction: NotarizedTransaction, - ) -> Result { - let network_id = self.current_network_id()?; - let gateway_client = GatewayClient::new( - self.clients.http_client.driver.clone(), - network_id, - ); - gateway_client - .submit_notarized_transaction(notarized_transaction) - .await - } -} - // ================== // Poll Transaction Status (Public) // ================== @@ -114,74 +93,7 @@ impl SargonOS { } #[cfg(test)] -mod submit_transaction_tests { - use super::*; - use actix_rt::time::timeout; - use std::{future::Future, time::Duration}; - - #[allow(clippy::upper_case_acronyms)] - type SUT = SargonOS; - - #[actix_rt::test] - async fn submit_transaction_success() { - let notarized_transaction = NotarizedTransaction::sample(); - let response = TransactionSubmitResponse { duplicate: false }; - let body = serde_json::to_vec(&response).unwrap(); - - let mock_driver = - MockNetworkingDriver::with_spy(200, body, |request| { - // Verify the body sent matches the expected one - let sent_request = TransactionSubmitRequest::new( - NotarizedTransaction::sample(), - ); - let sent_body = serde_json::to_vec(&sent_request).unwrap(); - - assert_eq!(request.body.bytes, sent_body); - }); - - let req = SUT::boot_test_with_networking_driver(Arc::new(mock_driver)); - - let os = - actix_rt::time::timeout(SARGON_OS_TEST_MAX_ASYNC_DURATION, req) - .await - .unwrap() - .unwrap(); - - let result = os - .submit_transaction(notarized_transaction.clone()) - .await - .unwrap(); - - let expected_result = - notarized_transaction.signed_intent().intent().intent_hash(); - - assert_eq!(result, expected_result); - } - - #[actix_rt::test] - async fn submit_transaction_failure() { - let notarized_transaction = NotarizedTransaction::sample(); - let mock_driver = MockNetworkingDriver::new_always_failing(); - - let req = SUT::boot_test_with_networking_driver(Arc::new(mock_driver)); - - let os = - actix_rt::time::timeout(SARGON_OS_TEST_MAX_ASYNC_DURATION, req) - .await - .unwrap() - .unwrap(); - - let result = os - .submit_transaction(notarized_transaction) - .await - .expect_err("Expected an error"); - - assert_eq!(result, CommonError::NetworkResponseBadCode); - } -} - -#[cfg(test)] -mod poll_status_tests { +mod tests { use super::*; use actix_rt::time::timeout; use std::{future::Future, time::Duration}; diff --git a/crates/sargon/src/system/sargon_os/transactions/sargon_os_transaction_submit.rs b/crates/sargon/src/system/sargon_os/transactions/sargon_os_transaction_submit.rs new file mode 100644 index 000000000..6654b6b3c --- /dev/null +++ b/crates/sargon/src/system/sargon_os/transactions/sargon_os_transaction_submit.rs @@ -0,0 +1,89 @@ +use crate::prelude::*; + +// ================== +// Submit Transaction +// ================== +#[uniffi::export] +impl SargonOS { + /// Submits a notarized transaction payload to the network. + pub async fn submit_transaction( + &self, + notarized_transaction: NotarizedTransaction, + ) -> Result { + let network_id = self.current_network_id()?; + let gateway_client = GatewayClient::new( + self.clients.http_client.driver.clone(), + network_id, + ); + gateway_client + .submit_notarized_transaction(notarized_transaction) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use actix_rt::time::timeout; + use std::{future::Future, time::Duration}; + + #[allow(clippy::upper_case_acronyms)] + type SUT = SargonOS; + + #[actix_rt::test] + async fn submit_transaction_success() { + let notarized_transaction = NotarizedTransaction::sample(); + let response = TransactionSubmitResponse { duplicate: false }; + let body = serde_json::to_vec(&response).unwrap(); + + let mock_driver = + MockNetworkingDriver::with_spy(200, body, |request| { + // Verify the body sent matches the expected one + let sent_request = TransactionSubmitRequest::new( + NotarizedTransaction::sample(), + ); + let sent_body = serde_json::to_vec(&sent_request).unwrap(); + + assert_eq!(request.body.bytes, sent_body); + }); + + let req = SUT::boot_test_with_networking_driver(Arc::new(mock_driver)); + + let os = + actix_rt::time::timeout(SARGON_OS_TEST_MAX_ASYNC_DURATION, req) + .await + .unwrap() + .unwrap(); + + let result = os + .submit_transaction(notarized_transaction.clone()) + .await + .unwrap(); + + let expected_result = + notarized_transaction.signed_intent().intent().intent_hash(); + + assert_eq!(result, expected_result); + } + + #[actix_rt::test] + async fn submit_transaction_failure() { + let notarized_transaction = NotarizedTransaction::sample(); + let mock_driver = MockNetworkingDriver::new_always_failing(); + + let req = SUT::boot_test_with_networking_driver(Arc::new(mock_driver)); + + let os = + actix_rt::time::timeout(SARGON_OS_TEST_MAX_ASYNC_DURATION, req) + .await + .unwrap() + .unwrap(); + + let result = os + .submit_transaction(notarized_transaction) + .await + .expect_err("Expected an error"); + + assert_eq!(result, CommonError::NetworkResponseBadCode); + } +} diff --git a/crates/sargon/src/system/sargon_os/transactions/support/mod.rs b/crates/sargon/src/system/sargon_os/transactions/support/mod.rs new file mode 100644 index 000000000..0657a3654 --- /dev/null +++ b/crates/sargon/src/system/sargon_os/transactions/support/mod.rs @@ -0,0 +1,3 @@ +mod transaction_to_review; + +pub use transaction_to_review::*; diff --git a/crates/sargon/src/system/sargon_os/transactions/support/transaction_to_review.rs b/crates/sargon/src/system/sargon_os/transactions/support/transaction_to_review.rs new file mode 100644 index 000000000..7e6361456 --- /dev/null +++ b/crates/sargon/src/system/sargon_os/transactions/support/transaction_to_review.rs @@ -0,0 +1,9 @@ +use crate::prelude::*; + +/// This is the result of the transaction preview analysis. +/// It contains all the information needed to compute and display the transaction details to the user. +#[derive(Debug, PartialEq, uniffi::Record)] +pub struct TransactionToReview { + pub transaction_manifest: TransactionManifest, + pub execution_summary: ExecutionSummary, +} diff --git a/crates/sargon/src/wrapped_radix_engine_toolkit/low_level/execution_summary/reserved_instruction.rs b/crates/sargon/src/wrapped_radix_engine_toolkit/low_level/execution_summary/reserved_instruction.rs index a4c159695..2971686b8 100644 --- a/crates/sargon/src/wrapped_radix_engine_toolkit/low_level/execution_summary/reserved_instruction.rs +++ b/crates/sargon/src/wrapped_radix_engine_toolkit/low_level/execution_summary/reserved_instruction.rs @@ -2,7 +2,7 @@ use crate::prelude::*; /// The set of instructions that is only allowed in manifests created by the /// wallet itself. -#[derive(Clone, Debug, PartialEq, Eq, uniffi::Enum)] +#[derive(Clone, Debug, PartialEq, Eq, uniffi::Enum, derive_more::Display)] pub enum ReservedInstruction { AccountLockFee, AccountSecurify, diff --git a/crates/sargon/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/execution_summary/transaction_manifest_execution_summary.rs b/crates/sargon/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/execution_summary/transaction_manifest_execution_summary.rs index c3130452e..446259158 100644 --- a/crates/sargon/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/execution_summary/transaction_manifest_execution_summary.rs +++ b/crates/sargon/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/execution_summary/transaction_manifest_execution_summary.rs @@ -3,29 +3,22 @@ use crate::prelude::*; use radix_engine_toolkit::functions::manifest::execution_summary as RET_execution_summary; impl TransactionManifest { - /// Creates the `ExecutionSummary` based on the `engine_toolkit_receipt` data. + /// Creates the `ExecutionSummary` based on the `engine_toolkit_receipt`. /// /// Such value should be obtained from the Gateway `/transaction/preview` endpoint, under the `radix_engine_toolkit_receipt` field. - /// The field will be a JSON that host apps should parse into a . pub fn execution_summary( &self, - engine_toolkit_receipt: impl AsRef, + engine_toolkit_receipt: ScryptoSerializableToolkitTransactionReceipt, ) -> Result { let network_definition = self.network_id().network_definition(); - let receipt = serde_json::from_str::< - ScryptoSerializableToolkitTransactionReceipt, - >(engine_toolkit_receipt.as_ref()) - .ok() - .and_then(|receipt| { - receipt - .into_runtime_receipt(&ScryptoAddressBech32Decoder::new( - &network_definition, - )) - .ok() - }) - .ok_or(CommonError::FailedToDecodeEngineToolkitReceipt)?; - - self.execution_summary_with_receipt(receipt) + let runtime_receipt = engine_toolkit_receipt + .into_runtime_receipt(&ScryptoAddressBech32Decoder::new( + &network_definition, + )) + .ok() + .ok_or(CommonError::FailedToDecodeEngineToolkitReceipt)?; + + self.execution_summary_with_receipt(runtime_receipt) } fn execution_summary_with_receipt( @@ -61,14 +54,6 @@ mod tests { #[allow(clippy::upper_case_acronyms)] type SUT = ExecutionSummary; - #[test] - fn invalid_receipt() { - assert_eq!( - TransactionManifest::sample().execution_summary("dead"), - Err(CommonError::FailedToDecodeEngineToolkitReceipt) - ); - } - #[test] fn failure_if_receipt_result_is_abort() { let wrong_receipt = ScryptoRuntimeToolkitTransactionReceipt::Abort { @@ -96,10 +81,10 @@ mod tests { env!("FIXTURES_TX"), "transfer_1to2_multiple_nf_and_f_tokens.rtm" )); - let receipt = include_str!(concat!( + let receipt = deserialize_receipt(include_str!(concat!( env!("FIXTURES_TX"), "transfer_1to2_multiple_nf_and_f_tokens.dat" - )); + ))); let transaction_manifest = TransactionManifest::new( instructions_string, @@ -204,10 +189,10 @@ mod tests { env!("FIXTURES_TX"), "third_party_deposits_update.rtm" )); - let receipt = include_str!(concat!( + let receipt = deserialize_receipt(include_str!(concat!( env!("FIXTURES_TX"), "third_party_deposits_update.dat" - )); + ))); let transaction_manifest = TransactionManifest::new( instructions_string, @@ -270,10 +255,10 @@ mod tests { env!("FIXTURES_TX"), "create_single_fungible.rtm" )); - let receipt = include_str!(concat!( + let receipt = deserialize_receipt(include_str!(concat!( env!("FIXTURES_TX"), "create_single_fungible.dat" - )); + ))); let transaction_manifest = TransactionManifest::new( instructions_string, @@ -321,10 +306,10 @@ mod tests { env!("FIXTURES_TX"), "create_nft_collection.rtm" )); - let receipt = include_str!(concat!( + let receipt = deserialize_receipt(include_str!(concat!( env!("FIXTURES_TX"), "create_nft_collection.dat" - )); + ))); let transaction_manifest = TransactionManifest::new( instructions_string, @@ -408,10 +393,10 @@ mod tests { env!("FIXTURES_TX"), "mint_nft_gumball_card.rtm" )); - let receipt = include_str!(concat!( + let receipt = deserialize_receipt(include_str!(concat!( env!("FIXTURES_TX"), "mint_nft_gumball_card.dat" - )); + ))); let transaction_manifest = TransactionManifest::new( instructions_string, @@ -481,10 +466,10 @@ mod tests { env!("FIXTURES_TX"), "present_proof_swap_candy.rtm" )); - let receipt = include_str!(concat!( + let receipt = deserialize_receipt(include_str!(concat!( env!("FIXTURES_TX"), "present_proof_swap_candy.dat" - )); + ))); let transaction_manifest = TransactionManifest::new( instructions_string, @@ -538,8 +523,10 @@ mod tests { let instructions_string = include_str!(concat!(env!("FIXTURES_TX"), "create_pool.rtm")); - let receipt = - include_str!(concat!(env!("FIXTURES_TX"), "create_pool.dat")); + let receipt = deserialize_receipt(include_str!(concat!( + env!("FIXTURES_TX"), + "create_pool.dat" + ))); let transaction_manifest = TransactionManifest::new( instructions_string, @@ -578,10 +565,10 @@ mod tests { "contribute_to_bi_pool.rtm" )); - let receipt = include_str!(concat!( + let receipt = deserialize_receipt(include_str!(concat!( env!("FIXTURES_TX"), "contribute_to_bi_pool.dat" - )); + ))); let transaction_manifest = TransactionManifest::new( instructions_string, @@ -634,10 +621,10 @@ mod tests { env!("FIXTURES_TX"), "stake_to_three_validators.rtm" )); - let receipt = include_str!(concat!( + let receipt = deserialize_receipt(include_str!(concat!( env!("FIXTURES_TX"), "stake_to_three_validators.dat" - )); + ))); let transaction_manifest = TransactionManifest::new( instructions_string, @@ -713,10 +700,10 @@ mod tests { env!("FIXTURES_TX"), "redeem_from_bi_pool.rtm" )); - let receipt = include_str!(concat!( + let receipt = deserialize_receipt(include_str!(concat!( env!("FIXTURES_TX"), "redeem_from_bi_pool.dat" - )); + ))); let transaction_manifest = TransactionManifest::new( instructions_string, @@ -770,10 +757,10 @@ mod tests { env!("FIXTURES_TX"), "unstake_partially_from_one_validator.rtm" )); - let receipt = include_str!(concat!( + let receipt = deserialize_receipt(include_str!(concat!( env!("FIXTURES_TX"), "unstake_partially_from_one_validator.dat" - )); + ))); let transaction_manifest = TransactionManifest::new( instructions_string, @@ -853,10 +840,10 @@ mod tests { env!("FIXTURES_TX"), "claim_two_stakes_from_one_validator.rtm" )); - let receipt = include_str!(concat!( + let receipt = deserialize_receipt(include_str!(concat!( env!("FIXTURES_TX"), "claim_two_stakes_from_one_validator.dat" - )); + ))); let transaction_manifest = TransactionManifest::new( instructions_string, @@ -930,10 +917,10 @@ mod tests { "account_locker_claim_fungibles_and_non_fungibles.rtm" )); - let receipt = include_str!(concat!( + let receipt = deserialize_receipt(include_str!(concat!( env!("FIXTURES_TX"), "account_locker_claim_fungibles_and_non_fungibles.dat" - )); + ))); let transaction_manifest = TransactionManifest::new( instructions_string, @@ -982,4 +969,13 @@ mod tests { ) ); } + + fn deserialize_receipt( + value: impl AsRef, + ) -> ScryptoSerializableToolkitTransactionReceipt { + serde_json::from_str::( + &value.as_ref(), + ) + .unwrap() + } } diff --git a/crates/sargon/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/transaction_manifest_uniffi_fn.rs b/crates/sargon/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/transaction_manifest_uniffi_fn.rs index 64de480c8..98a281f67 100644 --- a/crates/sargon/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/transaction_manifest_uniffi_fn.rs +++ b/crates/sargon/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/transaction_manifest_uniffi_fn.rs @@ -48,14 +48,6 @@ pub fn transaction_manifest_involved_pool_addresses( manifest.involved_pool_addresses() } -#[uniffi::export] -pub fn transaction_manifest_execution_summary( - manifest: &TransactionManifest, - engine_toolkit_receipt: String, -) -> Result { - manifest.execution_summary(engine_toolkit_receipt) -} - #[uniffi::export] pub fn transaction_manifest_network_id( manifest: &TransactionManifest, @@ -148,35 +140,6 @@ mod tests { ); } - #[test] - fn test_execution_summary() { - let receipt = include_str!(concat!( - env!("FIXTURES_TX"), - "unstake_partially_from_one_validator.dat" - )); - - let instructions_string = include_str!(concat!( - env!("FIXTURES_TX"), - "unstake_partially_from_one_validator.rtm" - )); - - let transaction_manifest = TransactionManifest::new( - instructions_string, - NetworkID::Stokenet, - Blobs::default(), - ) - .unwrap(); - - let sut = transaction_manifest_execution_summary( - &transaction_manifest, - receipt.to_owned(), - ) - .unwrap(); - - let acc_gk: AccountAddress = "account_tdx_2_129uv9r46an4hwng8wc97qwpraspvnrc7v2farne4lr6ff7yaevaz2a".into(); - assert_eq!(sut.addresses_of_accounts_requiring_auth, vec![acc_gk]) - } - #[test] fn test_involved_pool_addresses() { assert_eq!( diff --git a/crates/sargon/tests/integration/main.rs b/crates/sargon/tests/integration/main.rs index 82f9d2ea0..cf9d12b7f 100644 --- a/crates/sargon/tests/integration/main.rs +++ b/crates/sargon/tests/integration/main.rs @@ -109,7 +109,8 @@ mod integration_tests { ); // ACT - let engine_toolkit_receipt = timeout(MAX, sut).await.unwrap().unwrap(); + let engine_toolkit_receipt = + timeout(MAX, sut).await.unwrap().unwrap().unwrap(); let execution_summary = manifest.execution_summary(engine_toolkit_receipt).unwrap(); diff --git a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/TransactionManifest.kt b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/TransactionManifest.kt index bb82840ee..008fac1d8 100644 --- a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/TransactionManifest.kt +++ b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/TransactionManifest.kt @@ -3,7 +3,6 @@ package com.radixdlt.sargon.extensions import com.radixdlt.sargon.AccountAddress import com.radixdlt.sargon.AccountLockerClaimableResource import com.radixdlt.sargon.AddressOfAccountOrPersona -import com.radixdlt.sargon.BagOfBytes import com.radixdlt.sargon.Blobs import com.radixdlt.sargon.Decimal192 import com.radixdlt.sargon.LockerAddress @@ -36,7 +35,6 @@ import com.radixdlt.sargon.modifyManifestAddGuarantees import com.radixdlt.sargon.modifyManifestLockFee import com.radixdlt.sargon.newTransactionManifestFromInstructionsStringAndBlobs import com.radixdlt.sargon.transactionManifestBlobs -import com.radixdlt.sargon.transactionManifestExecutionSummary import com.radixdlt.sargon.transactionManifestInstructionsString import com.radixdlt.sargon.transactionManifestInvolvedPoolAddresses import com.radixdlt.sargon.transactionManifestInvolvedResourceAddresses @@ -189,17 +187,6 @@ val TransactionManifest.involvedResourceAddresses: List val TransactionManifest.summary: ManifestSummary get() = transactionManifestSummary(manifest = this) -/** - * Creates the `ExecutionSummary` based on the `engineToolkitReceipt` data. - * - * Such value should be obtained from the Gateway `/transaction/preview` endpoint, - * under the `radix_engine_toolkit_receipt` field. - * The content should be parsed as a String. - */ -@Throws(SargonException::class) -fun TransactionManifest.executionSummary(engineToolkitReceipt: String) = - transactionManifestExecutionSummary(manifest = this, engineToolkitReceipt = engineToolkitReceipt) - fun TransactionManifest.Companion.accountLockerClaim( lockerAddress: LockerAddress, claimant: AccountAddress, diff --git a/jvm/sargon-android/src/test/java/com/radixdlt/sargon/TransactionManifestTest.kt b/jvm/sargon-android/src/test/java/com/radixdlt/sargon/TransactionManifestTest.kt index 934764802..d1c48fb89 100644 --- a/jvm/sargon-android/src/test/java/com/radixdlt/sargon/TransactionManifestTest.kt +++ b/jvm/sargon-android/src/test/java/com/radixdlt/sargon/TransactionManifestTest.kt @@ -7,12 +7,10 @@ import com.radixdlt.sargon.extensions.createFungibleTokenWithMetadata import com.radixdlt.sargon.extensions.createMultipleFungibleTokens import com.radixdlt.sargon.extensions.createMultipleNonFungibleTokens import com.radixdlt.sargon.extensions.createNonFungibleToken -import com.radixdlt.sargon.extensions.executionSummary import com.radixdlt.sargon.extensions.faucet import com.radixdlt.sargon.extensions.hexToBagOfBytes import com.radixdlt.sargon.extensions.init import com.radixdlt.sargon.extensions.instructionsString -import com.radixdlt.sargon.extensions.intId import com.radixdlt.sargon.extensions.involvedPoolAddresses import com.radixdlt.sargon.extensions.involvedResourceAddresses import com.radixdlt.sargon.extensions.markingAccountAsDAppDefinitionType @@ -24,7 +22,6 @@ import com.radixdlt.sargon.extensions.perRecipientTransfers import com.radixdlt.sargon.extensions.setOwnerKeysHashes import com.radixdlt.sargon.extensions.stakesClaim import com.radixdlt.sargon.extensions.string -import com.radixdlt.sargon.extensions.stringId import com.radixdlt.sargon.extensions.summary import com.radixdlt.sargon.extensions.thirdPartyDepositUpdate import com.radixdlt.sargon.extensions.toDecimal192 @@ -33,12 +30,12 @@ import com.radixdlt.sargon.samples.Sample import com.radixdlt.sargon.samples.sample import com.radixdlt.sargon.samples.sampleMainnet import com.radixdlt.sargon.samples.sampleStokenet -import java.io.File -import java.util.regex.Pattern import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test +import java.io.File +import java.util.regex.Pattern class TransactionManifestTest : SampleTestable { @@ -582,23 +579,6 @@ class TransactionManifestTest : SampleTestable { ) } - @Test - fun test_execution_summary() { - val name = "third_party_deposits_update" - val receipt = engineToolkitReceipt(name) - val manifest = manifest(name) - - val summary = manifest.executionSummary(engineToolkitReceipt = receipt) - assertEquals( - listOf( - AccountAddress.init( - "account_tdx_2_129uv9r46an4hwng8wc97qwpraspvnrc7v2farne4lr6ff7yaevaz2a" - ) - ), - summary.addressesOfAccountsRequiringAuth - ) - } - @Test fun testAccountLockerClaim() { val expectedManifest = TransactionManifest.init(