From e357f56fdb709dcb7187652f482e0e09894b6320 Mon Sep 17 00:00:00 2001 From: Alexander Cyon <116169792+CyonAlexRDX@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:27:53 +0100 Subject: [PATCH] Wrap RET fourth batch (#15) --- src/core/error/common_error.rs | 22 ++ src/core/hash.rs | 196 ++++++++++++++- src/core/types/bag_of_bytes.rs | 5 +- src/core/types/epoch.rs | 24 ++ src/core/types/keys/ed25519/private_key.rs | 8 +- src/core/types/keys/ed25519/public_key.rs | 4 +- src/core/types/keys/is_private_key.rs | 23 +- src/core/types/keys/public_key.rs | 143 ++++++++++- src/core/types/keys/secp256k1/private_key.rs | 78 +++--- src/core/types/keys/secp256k1/public_key.rs | 96 ++++--- src/core/types/signatures/signature.rs | 67 +++++ .../signatures/signature_with_public_key.rs | 238 ++++++++++++++---- src/profile/v100/address/account_address.rs | 11 +- .../v100/address/non_fungible_local_id.rs | 21 +- .../app_preferences/p2p_links/p2p_link.rs | 2 - .../p2p_links/radix_connect_password.rs | 1 - .../v100/networks/network/network_id.rs | 13 +- src/sargon.udl | 3 + .../high_level/assert_manifest.rs | 4 +- .../high_level/manifests_crate_tokens.rs | 6 +- .../high_level/ret_api.rs | 7 +- .../high_level/token_definition_metadata.rs | 2 +- .../low_level/dummy_types.rs | 10 +- .../low_level/intent_signature.rs | 94 +++++++ .../low_level/intent_signatures.rs | 88 +++++++ .../low_level/message.rs | 53 ---- .../low_level/message/message.rs | 111 ++++++++ .../low_level/message/mod.rs | 5 + .../message/plaintext_message/mod.rs | 5 + .../plaintext_message/plaintext_message.rs | 81 ++++++ .../plaintext_message_contents.rs | 84 +++++++ .../low_level/mod.rs | 10 +- .../low_level/notarized_transaction.rs | 127 +++++++++- .../low_level/notary_signature.rs | 131 ++++++++++ .../low_level/signed_intent.rs | 222 +++++++++++++--- .../low_level/transaction_hash.rs | 7 - .../transaction_hashes/intent_hash.rs | 62 +++++ .../low_level/transaction_hashes/mod.rs | 8 + .../transaction_hashes/signed_intent_hash.rs | 51 ++++ .../transaction_hashes/transaction_hashes.rs | 104 ++++++++ .../validate_and_decode_hash.rs | 57 +++++ .../low_level/transaction_header.rs | 65 ++++- .../low_level/transaction_intent.rs | 94 ++++++- .../transaction_manifest/blobs/blob.rs | 147 +++++++++++ .../transaction_manifest/blobs/blobs.rs | 200 +++++++++++++++ .../blobs/blobs_secret_magic.rs | 126 ++++++++++ .../transaction_manifest/blobs/mod.rs | 7 + .../instructions/instructions.rs | 47 ++++ .../instructions/instructions_secret_magic.rs | 27 +- .../low_level/transaction_manifest/mod.rs | 2 + .../transaction_manifest.rs | 70 ++++-- .../transaction_manifest_secret_magic.rs | 10 +- tests/uniffi/bindings/test_tx_manifest.swift | 15 +- 53 files changed, 2770 insertions(+), 324 deletions(-) create mode 100644 src/wrapped_radix_engine_toolkit/low_level/intent_signature.rs create mode 100644 src/wrapped_radix_engine_toolkit/low_level/intent_signatures.rs delete mode 100644 src/wrapped_radix_engine_toolkit/low_level/message.rs create mode 100644 src/wrapped_radix_engine_toolkit/low_level/message/message.rs create mode 100644 src/wrapped_radix_engine_toolkit/low_level/message/mod.rs create mode 100644 src/wrapped_radix_engine_toolkit/low_level/message/plaintext_message/mod.rs create mode 100644 src/wrapped_radix_engine_toolkit/low_level/message/plaintext_message/plaintext_message.rs create mode 100644 src/wrapped_radix_engine_toolkit/low_level/message/plaintext_message/plaintext_message_contents.rs create mode 100644 src/wrapped_radix_engine_toolkit/low_level/notary_signature.rs delete mode 100644 src/wrapped_radix_engine_toolkit/low_level/transaction_hash.rs create mode 100644 src/wrapped_radix_engine_toolkit/low_level/transaction_hashes/intent_hash.rs create mode 100644 src/wrapped_radix_engine_toolkit/low_level/transaction_hashes/mod.rs create mode 100644 src/wrapped_radix_engine_toolkit/low_level/transaction_hashes/signed_intent_hash.rs create mode 100644 src/wrapped_radix_engine_toolkit/low_level/transaction_hashes/transaction_hashes.rs create mode 100644 src/wrapped_radix_engine_toolkit/low_level/transaction_hashes/validate_and_decode_hash.rs create mode 100644 src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/blobs/blob.rs create mode 100644 src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/blobs/blobs.rs create mode 100644 src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/blobs/blobs_secret_magic.rs create mode 100644 src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/blobs/mod.rs diff --git a/src/core/error/common_error.rs b/src/core/error/common_error.rs index 78ed6bb62..f826e2a3a 100644 --- a/src/core/error/common_error.rs +++ b/src/core/error/common_error.rs @@ -355,6 +355,28 @@ pub enum CommonError { "Failed to UniFFI decode bytes into Transaction Manifest Instructions" )] FailedToUniFFIDecodeBytesToManifestInstructions = 10096, + + #[error("Failed to decode Transaction Hash, value: {bad_value}")] + FailedToDecodeTransactionHash { bad_value: String } = 10097, + + #[error("Failed to hash transaction intent")] + FailedToHashIntent = 10098, + + #[error("Encrypted Messages are not yet supported")] + EncryptedMessagesAreNotYetSupported = 10099, + + #[error("Failed to Bech32 decode transaction Hash after having tested all Network IDs, from: {bad_value}")] + FailedToBech32DecodeTransactionHashAfterHavingTestedAllNetworkID { + bad_value: String, + } = 10100, + + #[error("Failed to parse Signature from {bad_value}")] + FailedToParseSignatureFromString { bad_value: String } = 10101, + + #[error( + "Invalid IntentSignatures for Intent some didn't validate IntentHash" + )] + InvalidSignaturesForIntentSomeDidNotValidateIntentHash = 10102, } /* diff --git a/src/core/hash.rs b/src/core/hash.rs index 5b0f161bc..dd66613d1 100644 --- a/src/core/hash.rs +++ b/src/core/hash.rs @@ -1,6 +1,198 @@ -use radix_engine_common::crypto::{blake2b_256_hash, Hash}; +use crate::prelude::*; +use radix_engine::types::IsHash; +use radix_engine_common::crypto::{ + blake2b_256_hash, Hash as ScryptoHash, IsHash as ScryptoIsHash, +}; + +/// Represents a 32-byte hash digest. +/// +/// Made UniFFI convertible via bytes (BagOfBytes). +#[derive( + Clone, + Debug, + PartialEq, + Eq, + std::hash::Hash, + derive_more::Display, + derive_more::FromStr, +)] +pub struct HashSecretMagic(ScryptoHash); + +impl From for Exactly32Bytes { + fn from(value: HashSecretMagic) -> Self { + Exactly32Bytes::from_bytes(value.0.as_bytes()) + } +} + +impl crate::UniffiCustomTypeConverter for HashSecretMagic { + type Builtin = BagOfBytes; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Exactly32Bytes::try_from(val.bytes) + .map(|e| e.bytes()) + .map(|b: [u8; 32]| HashSecretMagic(ScryptoHash::from_bytes(b))) + .map_err(|e| e.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + BagOfBytes::from(obj.0.into_bytes().as_slice()) + } +} + +/// Represents a 32-byte hash digest. +/// +/// Made UniFFI convertible via HashSecretMagic, +/// exposed in Swift/Kotlin as its own struct/data class, with +/// hidden secret magic. +#[derive( + Clone, + Debug, + PartialEq, + Eq, + std::hash::Hash, + derive_more::Display, + derive_more::FromStr, + uniffi::Record, +)] +pub struct Hash { + pub(crate) secret_magic: HashSecretMagic, +} + +impl From for Exactly32Bytes { + fn from(value: Hash) -> Self { + value.secret_magic.into() + } +} + +impl AsRef for Hash { + fn as_ref(&self) -> &ScryptoHash { + &self.secret_magic.0 + } +} + +impl AsRef<[u8]> for Hash { + fn as_ref(&self) -> &[u8] { + self.secret_magic.0.as_ref() + } +} + +impl ScryptoIsHash for Hash {} + +impl Hash { + pub fn bytes(&self) -> Vec { + self.secret_magic.0.clone().to_vec() + } +} + +impl From for Hash { + fn from(value: ScryptoHash) -> Self { + Self { + secret_magic: HashSecretMagic(value), + } + } +} +impl From for ScryptoHash { + fn from(value: Hash) -> Self { + value.secret_magic.0 + } +} /// Computes the hash digest of a message. pub fn hash_of>(data: T) -> Hash { - blake2b_256_hash(data) + blake2b_256_hash(data).into() +} + +impl HasSampleValues for Hash { + fn sample() -> Self { + // "Hello Radix".as_bytes() + "48f1bd08444b5e713db9e14caac2faae71836786ac94d645b00679728202a935" + .parse::() + .unwrap() + } + + fn sample_other() -> Self { + // "Radix... just imagine".as_bytes() + "196ca16b4a35c1c3df18e20cb199c99d129cec6ea62eb03a29fde16db897f4b1" + .parse::() + .unwrap() + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = Hash; + + #[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()); + } + + #[test] + fn test_hash() { + assert_eq!( + hash_of("Hello Radix".as_bytes()).to_string(), + "48f1bd08444b5e713db9e14caac2faae71836786ac94d645b00679728202a935" + ); + } + + #[test] + fn to_string() { + assert_eq!( + SUT::sample_other().to_string(), + "196ca16b4a35c1c3df18e20cb199c99d129cec6ea62eb03a29fde16db897f4b1" + ); + } + + #[test] + fn from_str() { + assert_eq!( + "48f1bd08444b5e713db9e14caac2faae71836786ac94d645b00679728202a935" + .parse::() + .unwrap(), + hash_of("Hello Radix".as_bytes()) + ); + } + + #[test] + fn manual_perform_uniffi_conversion_successful() { + let sut = SUT::sample().secret_magic; + let builtin = BagOfBytes::from_hex( + "48f1bd08444b5e713db9e14caac2faae71836786ac94d645b00679728202a935", + ) + .unwrap(); + + let ffi_side = + ::from_custom( + sut.clone(), + ); + + assert_eq!(ffi_side.to_hex(), builtin.to_hex()); + + let from_ffi_side = + ::into_custom( + ffi_side, + ) + .unwrap(); + + assert_eq!(sut, from_ffi_side); + } + + #[test] + fn manual_perform_uniffi_conversion_fail() { + assert!( + ::into_custom( + BagOfBytes::from_hex("deadbeef").unwrap(), + ) + .is_err() + ); + } } diff --git a/src/core/types/bag_of_bytes.rs b/src/core/types/bag_of_bytes.rs index 969fb4e75..8e8763ec0 100644 --- a/src/core/types/bag_of_bytes.rs +++ b/src/core/types/bag_of_bytes.rs @@ -1,7 +1,6 @@ use std::ops::{Deref, Neg}; use crate::prelude::*; -use radix_engine_common::crypto::{Hash, IsHash}; /// This is a TEMPORARY workaround until Kotlin => ByteArray equatable issue for /// Records has been solved, see: https://github.com/mozilla/uniffi-rs/issues/1985 @@ -138,7 +137,7 @@ impl BagOfBytes { impl From for BagOfBytes { /// Instantiates a new `BagOfBytes` from the `Hash` (32 bytes). fn from(value: Hash) -> Self { - value.into_bytes().as_slice().into() + value.bytes().as_slice().into() } } @@ -382,7 +381,7 @@ mod tests { #[test] fn from_hash() { let digest = hash_of(vec![0xde, 0xad]); - assert_eq!(BagOfBytes::from(digest).to_vec(), digest.to_vec()); + assert_eq!(BagOfBytes::from(digest.clone()).to_vec(), digest.bytes()); } #[test] diff --git a/src/core/types/epoch.rs b/src/core/types/epoch.rs index 5fea3e208..97f81a2d6 100644 --- a/src/core/types/epoch.rs +++ b/src/core/types/epoch.rs @@ -1,4 +1,5 @@ pub use crate::prelude::*; +use radix_engine::types::Epoch as ScryptoEpoch; // use radix_engine_common::types::Epoch as ScryptoEpoch; @@ -31,10 +32,33 @@ impl From for u64 { } } +impl From for ScryptoEpoch { + fn from(value: Epoch) -> Self { + Self::of(value.0) + } +} + +impl From for Epoch { + fn from(value: ScryptoEpoch) -> Self { + Self(value.number()) + } +} + #[cfg(test)] mod tests { + use super::*; use crate::prelude::*; + #[test] + fn into_from_scrypto() { + let test = + |u: u64| assert_eq!(Into::::into(ScryptoEpoch::of(u)).0, u); + test(0); + test(1); + test(2); + test(1337); + } + #[test] fn from_u64() { let test = diff --git a/src/core/types/keys/ed25519/private_key.rs b/src/core/types/keys/ed25519/private_key.rs index 96a171b97..bb16b0140 100644 --- a/src/core/types/keys/ed25519/private_key.rs +++ b/src/core/types/keys/ed25519/private_key.rs @@ -42,14 +42,14 @@ impl IsPrivateKey for Ed25519PrivateKey { ) } - fn sign(&self, msg_hash: &impl IsHash) -> Self::Signature { + fn sign(&self, msg_hash: &Hash) -> Self::Signature { self.0.sign(msg_hash).into() } } impl Ed25519PrivateKey { - pub fn from_engine(engine: ScryptoEd25519PrivateKey) -> Self { - Self(engine) + pub fn from_scrypto(scrypto: ScryptoEd25519PrivateKey) -> Self { + Self(scrypto) } pub fn to_bytes(&self) -> Vec { @@ -65,7 +65,7 @@ impl Ed25519PrivateKey { .map_err(|_| CommonError::InvalidEd25519PrivateKeyFromBytes { bad_value: slice.into(), }) - .map(Self::from_engine) + .map(Self::from_scrypto) } pub fn from_vec(bytes: Vec) -> Result { diff --git a/src/core/types/keys/ed25519/public_key.rs b/src/core/types/keys/ed25519/public_key.rs index caadd6ec8..8edc677ee 100644 --- a/src/core/types/keys/ed25519/public_key.rs +++ b/src/core/types/keys/ed25519/public_key.rs @@ -3,7 +3,7 @@ use crate::{prelude::*, UniffiCustomTypeConverter}; use radix_engine_common::crypto::{ verify_ed25519 as scrypto_verify_ed25519, Ed25519PublicKey as ScryptoEd25519PublicKey, - Ed25519Signature as ScryptoEd25519Signature, IsHash, + Ed25519Signature as ScryptoEd25519Signature, IsHash as ScryptoIsHash, }; /// An Ed25519 public key used to verify cryptographic signatures (EdDSA signatures). @@ -89,7 +89,7 @@ impl IsPublicKey for Ed25519PublicKey { fn is_valid( &self, signature: &Ed25519Signature, - for_hash: &impl IsHash, + for_hash: &impl ScryptoIsHash, ) -> bool { scrypto_verify_ed25519( for_hash.as_hash(), diff --git a/src/core/types/keys/is_private_key.rs b/src/core/types/keys/is_private_key.rs index 9cd24378f..781bbfc15 100644 --- a/src/core/types/keys/is_private_key.rs +++ b/src/core/types/keys/is_private_key.rs @@ -7,8 +7,25 @@ pub trait IsPrivateKey>: Sized { fn public_key(&self) -> P; - fn sign( + fn sign(&self, msg_hash: &Hash) -> Self::Signature; + + fn sign_intent_hash(&self, intent_hash: &IntentHash) -> IntentSignature + where + (P, Self::Signature): Into, + { + let public_key: P = self.public_key(); + let signature = self.sign(&intent_hash.hash); + let tuple: SignatureWithPublicKey = (public_key, signature).into(); + tuple.into() + } + + fn notarize_hash( &self, - msg_hash: &impl radix_engine_common::crypto::IsHash, - ) -> Self::Signature; + signed_intent_hash: &SignedIntentHash, + ) -> NotarySignature + where + Self::Signature: Into, + { + self.sign(&signed_intent_hash.hash).into() + } } diff --git a/src/core/types/keys/public_key.rs b/src/core/types/keys/public_key.rs index cdfc6ff1f..03a850e17 100644 --- a/src/core/types/keys/public_key.rs +++ b/src/core/types/keys/public_key.rs @@ -1,6 +1,8 @@ use crate::prelude::*; -use radix_engine_common::crypto::PublicKey as ScryptoPublicKey; +use radix_engine_common::crypto::{ + IsHash as ScryptoIsHash, PublicKey as ScryptoPublicKey, +}; /// A tagged union of supported public keys on different curves, supported /// curves are `secp256k1` and `Curve25519` @@ -23,6 +25,37 @@ pub enum PublicKey { Secp256k1 { value: Secp256k1PublicKey }, } +impl PublicKey { + /// Verifies an EdDSA signature over Curve25519. + pub fn is_valid( + &self, + signature: impl Into, + for_hash: &impl ScryptoIsHash, + ) -> bool { + let signature = signature.into(); + match self { + PublicKey::Ed25519 { value: key } => match &signature { + Signature::Secp256k1 { value: _ } => { + error!("Trying to validate Secp256k1 signature with Ed25519 PublicKey, that does not work."); + false + } + Signature::Ed25519 { value: signature } => { + key.is_valid(signature, for_hash) + } + }, + PublicKey::Secp256k1 { value: key } => match &signature { + Signature::Secp256k1 { value: signature } => { + key.is_valid(signature, for_hash) + } + Signature::Ed25519 { value: _ } => { + error!("Trying to validate Ed25519 signature with Secp256k1 PublicKey, that does not work."); + false + } + }, + } + } +} + impl From for PublicKey { /// Enables: /// @@ -196,6 +229,21 @@ impl From for ScryptoPublicKey { } } +impl TryFrom for PublicKey { + type Error = crate::CommonError; + + fn try_from(value: ScryptoPublicKey) -> Result { + match value { + ScryptoPublicKey::Secp256k1(key) => { + Secp256k1PublicKey::try_from(key).map(|k| k.into()) + } + ScryptoPublicKey::Ed25519(key) => { + Ed25519PublicKey::try_from(key).map(|k| k.into()) + } + } + } +} + #[cfg(test)] mod tests { @@ -417,4 +465,97 @@ mod tests { let key: PublicKey = secp256k1.clone().into(); assert_eq!(key.as_secp256k1().unwrap(), &secp256k1); } + + #[test] + fn is_valid_with_secp256k1() { + let secp256k1_public_key: Secp256k1PublicKey = + "02f0d85a3b9082683f689e6115f37e1e24b7448fff14b14877e3a4e750e86fba8b" + .parse() + .unwrap(); + + let message = "All those moments will be lost in time, like tears in rain. Time to die..."; + + let hash = hash_of(message.as_bytes()); + let secp256k1_signature: Secp256k1Signature = "01aa1c4f46f8437b7f8ec9008ae10e6f33bb8be3e81e35c63f3498070dfbd6a20b2daee6073ead3c9e72d8909bc32a02e46cede3885cf8568d4c380ac97aa7fbcd".parse().unwrap(); + + assert!(PublicKey::Secp256k1 { + value: secp256k1_public_key + } + .is_valid( + Signature::Secp256k1 { + value: secp256k1_signature + }, + &hash + )); + } + + #[test] + fn is_valid_with_ed25519() { + let message = "All those moments will be lost in time, like tears in rain. Time to die..."; + let hash = hash_of(message.as_bytes()); + + let ed25519_public_key: Ed25519PublicKey = + "08740a2fd178c40ce71966a6537f780978f7f00548cfb59196344b5d7d67e9cf" + .parse() + .unwrap(); + + let ed25519_signature: Ed25519Signature = "06cd3772c5c70d44819db80192a5b2521525e2529f770bff970ec4edc7c1bd76e41fcfa8e59ff93b1675c48f4af3b1697765286d999ee8b5bb8257691e3b7b09".parse().unwrap(); + + assert!(PublicKey::Ed25519 { + value: ed25519_public_key + } + .is_valid( + Signature::Ed25519 { + value: ed25519_signature + }, + &hash + )); + } + + #[test] + fn is_valid_with_ed25519_public_key_for_secp256k1_signature_fails() { + let message = "All those moments will be lost in time, like tears in rain. Time to die..."; + + let hash = hash_of(message.as_bytes()); + let secp256k1_signature: Secp256k1Signature = "01aa1c4f46f8437b7f8ec9008ae10e6f33bb8be3e81e35c63f3498070dfbd6a20b2daee6073ead3c9e72d8909bc32a02e46cede3885cf8568d4c380ac97aa7fbcd".parse().unwrap(); + + let ed25519_public_key: Ed25519PublicKey = + "08740a2fd178c40ce71966a6537f780978f7f00548cfb59196344b5d7d67e9cf" + .parse() + .unwrap(); + + assert!(!PublicKey::Ed25519 { + value: ed25519_public_key + } + .is_valid( + Signature::Secp256k1 { + value: secp256k1_signature + }, + &hash + )); + } + + #[test] + fn is_valid_with_secp256k1_public_key_for_ed25519_signature_fails() { + let secp256k1_public_key: Secp256k1PublicKey = + "02f0d85a3b9082683f689e6115f37e1e24b7448fff14b14877e3a4e750e86fba8b" + .parse() + .unwrap(); + + let message = "All those moments will be lost in time, like tears in rain. Time to die..."; + + let hash = hash_of(message.as_bytes()); + + let ed25519_signature: Ed25519Signature = "06cd3772c5c70d44819db80192a5b2521525e2529f770bff970ec4edc7c1bd76e41fcfa8e59ff93b1675c48f4af3b1697765286d999ee8b5bb8257691e3b7b09".parse().unwrap(); + + assert!(!PublicKey::Secp256k1 { + value: secp256k1_public_key + } + .is_valid( + Signature::Ed25519 { + value: ed25519_signature + }, + &hash + )); + } } diff --git a/src/core/types/keys/secp256k1/private_key.rs b/src/core/types/keys/secp256k1/private_key.rs index 3a9195c53..25c6b9737 100644 --- a/src/core/types/keys/secp256k1/private_key.rs +++ b/src/core/types/keys/secp256k1/private_key.rs @@ -10,6 +10,12 @@ use radix_engine_common::crypto::{ #[debug("{}", self.to_hex())] pub struct Secp256k1PrivateKey(ScryptoSecp256k1PrivateKey); +impl From for Secp256k1PrivateKey { + fn from(value: ScryptoSecp256k1PrivateKey) -> Self { + Self(value) + } +} + impl Secp256k1PrivateKey { /// Generates a new `Secp256k1PrivateKey` from random bytes /// generated by a CSRNG, note that this is typically never @@ -30,8 +36,8 @@ impl PartialEq for Secp256k1PrivateKey { impl Eq for Secp256k1PrivateKey {} impl Secp256k1PrivateKey { - pub fn from_engine(engine: ScryptoSecp256k1PrivateKey) -> Self { - Self(engine) + pub fn from_scrypto(scrypto: ScryptoSecp256k1PrivateKey) -> Self { + scrypto.into() } pub fn to_bytes(&self) -> Vec { @@ -47,7 +53,7 @@ impl Secp256k1PrivateKey { .map_err(|_| CommonError::InvalidSecp256k1PrivateKeyFromBytes { bad_value: slice.to_owned().into(), }) - .map(Self::from_engine) + .map(Self::from_scrypto) } pub fn from_vec(bytes: Vec) -> Result { @@ -92,11 +98,20 @@ impl IsPrivateKey for Secp256k1PrivateKey { ) } - fn sign(&self, msg_hash: &impl IsHash) -> Self::Signature { + fn sign(&self, msg_hash: &Hash) -> Self::Signature { self.0.sign(msg_hash).into() } } +#[cfg(test)] +impl Secp256k1PrivateKey { + /// ONLY Use this in a test or when creating sample (preview) values. + pub unsafe fn from_u64(n: u64) -> Self { + assert!(n > 0); + Self::from_scrypto(ScryptoSecp256k1PrivateKey::from_u64(n).unwrap()) + } +} + impl HasSampleValues for Secp256k1PrivateKey { /// A sample used to facilitate unit tests. fn sample() -> Self { @@ -144,29 +159,31 @@ mod tests { use super::*; use crate::prelude::*; + #[allow(clippy::upper_case_acronyms)] + type SUT = Secp256k1PrivateKey; + #[test] fn equality() { - assert_eq!( - Secp256k1PrivateKey::sample(), - Secp256k1PrivateKey::sample() - ); - assert_eq!( - Secp256k1PrivateKey::sample_other(), - Secp256k1PrivateKey::sample_other() - ); + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); } #[test] fn inequality() { - assert_ne!( - Secp256k1PrivateKey::sample(), - Secp256k1PrivateKey::sample_other() + assert_ne!(SUT::sample(), SUT::sample_other()); + } + + #[test] + fn unsafe_from_u64() { + assert_eq!( + unsafe { SUT::from_u64(1) }.to_hex(), + "0000000000000000000000000000000000000000000000000000000000000001" ); } #[test] fn curve() { - assert_eq!(Secp256k1PrivateKey::curve(), SLIP10Curve::Secp256k1); + assert_eq!(SUT::curve(), SLIP10Curve::Secp256k1); } #[test] @@ -196,9 +213,7 @@ mod tests { ) .unwrap(); assert_eq!( - Secp256k1PrivateKey::from_bytes(bytes.as_slice()) - .unwrap() - .to_bytes(), + SUT::from_bytes(bytes.as_slice()).unwrap().to_bytes(), bytes.as_slice() ); } @@ -207,13 +222,13 @@ mod tests { fn hex_roundtrip() { let hex = "0000000000000000000000000000000000000000000000000000000000000001"; - assert_eq!(Secp256k1PrivateKey::from_str(hex).unwrap().to_hex(), hex); + assert_eq!(SUT::from_str(hex).unwrap().to_hex(), hex); } #[test] fn invalid_hex() { assert_eq!( - Secp256k1PrivateKey::from_str("not hex"), + SUT::from_str("not hex"), Err(CommonError::InvalidSecp256k1PrivateKeyFromString { bad_value: "not hex".to_owned() }) @@ -223,7 +238,7 @@ mod tests { #[test] fn invalid_hex_too_short() { assert_eq!( - Secp256k1PrivateKey::from_str("dead"), + SUT::from_str("dead"), Err(CommonError::InvalidSecp256k1PrivateKeyFromString { bad_value: "dead".to_owned() }) @@ -233,7 +248,7 @@ mod tests { #[test] fn invalid_bytes() { assert_eq!( - Secp256k1PrivateKey::from_bytes(&[0u8] as &[u8]), + SUT::from_bytes(&[0u8] as &[u8]), Err(CommonError::InvalidSecp256k1PrivateKeyFromBytes { bad_value: vec![0].into() }) @@ -244,7 +259,7 @@ mod tests { fn invalid_too_large() { let bytes = [0xFFu8; 32]; assert_eq!( - Secp256k1PrivateKey::from_bytes(&bytes), + SUT::from_bytes(&bytes), Err(CommonError::InvalidSecp256k1PrivateKeyFromBytes { bad_value: bytes.to_vec().into() }) @@ -255,7 +270,7 @@ mod tests { fn invalid_zero() { let bytes = [0u8; 32]; assert_eq!( - Secp256k1PrivateKey::from_bytes(&bytes), + SUT::from_bytes(&bytes), Err(CommonError::InvalidSecp256k1PrivateKeyFromBytes { bad_value: bytes.to_vec().into() }) @@ -266,10 +281,7 @@ mod tests { fn debug() { let hex = "0000000000000000000000000000000000000000000000000000000000000001"; - assert_eq!( - format!("{:?}", Secp256k1PrivateKey::from_str(hex).unwrap()), - hex - ); + assert_eq!(format!("{:?}", SUT::from_str(hex).unwrap()), hex); } #[test] @@ -277,7 +289,7 @@ mod tests { let str = "0000000000000000000000000000000000000000000000000000000000000001"; let hex32 = Exactly32Bytes::from_hex(str).unwrap(); - let key = Secp256k1PrivateKey::from_exactly32_bytes(hex32).unwrap(); + let key = SUT::from_exactly32_bytes(hex32).unwrap(); assert_eq!(key.to_hex(), str); } @@ -286,7 +298,7 @@ mod tests { let str = "0000000000000000000000000000000000000000000000000000000000000001"; let vec = hex_decode(str).unwrap(); - let key = Secp256k1PrivateKey::try_from(vec.as_slice()).unwrap(); + let key = SUT::try_from(vec.as_slice()).unwrap(); assert_eq!(key.to_hex(), str); } @@ -295,7 +307,7 @@ mod tests { let mut set: HashSet> = HashSet::new(); let n = 100; for _ in 0..n { - let key = Secp256k1PrivateKey::generate(); + let key = SUT::generate(); let bytes = key.to_bytes(); assert_eq!(bytes.len(), 32); set.insert(bytes); @@ -306,7 +318,7 @@ mod tests { #[test] fn sample() { assert_eq!( - Secp256k1PrivateKey::sample().public_key().to_hex(), + SUT::sample().public_key().to_hex(), "02517b88916e7f315bb682f9926b14bc67a0e4246f8a419b986269e1a7e61fffa7" ); } diff --git a/src/core/types/keys/secp256k1/public_key.rs b/src/core/types/keys/secp256k1/public_key.rs index 2feb7ff6f..aabc55506 100644 --- a/src/core/types/keys/secp256k1/public_key.rs +++ b/src/core/types/keys/secp256k1/public_key.rs @@ -3,7 +3,7 @@ use crate::{prelude::*, UniffiCustomTypeConverter}; use bip32::secp256k1::PublicKey as BIP32Secp256k1PublicKey; // the bip32 crate actually does validation of the PublicKey whereas `radix_engine_common` does not. use radix_engine_common::crypto::{ - verify_secp256k1, Hash, IsHash, + verify_secp256k1 as scrypto_verify_secp256k1, IsHash as ScryptoIsHash, Secp256k1PublicKey as ScryptoSecp256k1PublicKey, Secp256k1Signature as ScryptoSecp256k1Signature, }; @@ -94,9 +94,9 @@ impl IsPublicKey for Secp256k1PublicKey { fn is_valid( &self, signature: &Secp256k1Signature, - for_hash: &impl IsHash, + for_hash: &impl ScryptoIsHash, ) -> bool { - verify_secp256k1( + scrypto_verify_secp256k1( for_hash.as_hash(), &self.scrypto(), &signature.clone().into(), @@ -191,30 +191,25 @@ impl FromStr for Secp256k1PublicKey { #[cfg(test)] mod tests { - use crate::prelude::*; + use super::*; - use radix_engine_common::crypto::Secp256k1PublicKey as ScryptoSecp256k1PublicKey; + #[allow(clippy::upper_case_acronyms)] + type SUT = Secp256k1PublicKey; #[test] fn equality() { - assert_eq!(Secp256k1PublicKey::sample(), Secp256k1PublicKey::sample()); - assert_eq!( - Secp256k1PublicKey::sample_other(), - Secp256k1PublicKey::sample_other() - ); + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); } #[test] fn inequality() { - assert_ne!( - Secp256k1PublicKey::sample(), - Secp256k1PublicKey::sample_other() - ); + assert_ne!(SUT::sample(), SUT::sample_other()); } #[test] fn from_str() { - assert!(Secp256k1PublicKey::from_str( + assert!(SUT::from_str( "02517b88916e7f315bb682f9926b14bc67a0e4246f8a419b986269e1a7e61fffa7" ) .is_ok()); @@ -227,7 +222,7 @@ mod tests { 0xf9, 0x92, 0x6b, 0x14, 0xbc, 0x67, 0xa0, 0xe4, 0x24, 0x6f, 0x8a, 0x41, 0x9b, 0x98, 0x62, 0x69, 0xe1, 0xa7, 0xe6, 0x1f, 0xff, 0xa7, ]; - let key = Secp256k1PublicKey::try_from(bytes).unwrap(); + let key = SUT::try_from(bytes).unwrap(); assert_eq!( key.to_hex(), "02517b88916e7f315bb682f9926b14bc67a0e4246f8a419b986269e1a7e61fffa7" @@ -238,14 +233,14 @@ mod tests { #[test] fn sample_alice() { assert_eq!( - Secp256k1PublicKey::sample_alice().to_hex(), + SUT::sample_alice().to_hex(), "02517b88916e7f315bb682f9926b14bc67a0e4246f8a419b986269e1a7e61fffa7" ); } #[test] fn from_scrypto() { - let from_scrypto: Secp256k1PublicKey = ScryptoSecp256k1PublicKey::from_str( + let from_scrypto: SUT = ScryptoSecp256k1PublicKey::from_str( "033083620d1596d3f8988ff3270e42970dd2a031e2b9b6488052a4170ff999f3e8", ) .unwrap() @@ -254,7 +249,7 @@ mod tests { assert_eq!( from_scrypto.clone(), - Secp256k1PublicKey::from_str( + SUT::from_str( "033083620d1596d3f8988ff3270e42970dd2a031e2b9b6488052a4170ff999f3e8" ) .unwrap() @@ -262,9 +257,7 @@ mod tests { // and back assert_eq!( - TryInto::::try_into(Into::< - ScryptoSecp256k1PublicKey, - >::into( + TryInto::::try_into(Into::::into( from_scrypto.clone() )) .unwrap(), @@ -275,7 +268,7 @@ mod tests { #[test] fn sample_bob() { assert_eq!( - Secp256k1PublicKey::sample_bob().to_hex(), + SUT::sample_bob().to_hex(), "033083620d1596d3f8988ff3270e42970dd2a031e2b9b6488052a4170ff999f3e8" ); } @@ -283,7 +276,7 @@ mod tests { #[test] fn invalid_hex_str() { assert_eq!( - Secp256k1PublicKey::from_str("hi"), + SUT::from_str("hi"), Err(CommonError::InvalidSecp256k1PublicKeyFromString { bad_value: "hi".to_owned() }) @@ -293,7 +286,7 @@ mod tests { #[test] fn invalid_str_too_short() { assert_eq!( - Secp256k1PublicKey::from_str("dead"), + SUT::from_str("dead"), Err(CommonError::InvalidSecp256k1PublicKeyFromBytes { bad_value: vec![0xde, 0xad].into() }) @@ -304,7 +297,7 @@ mod tests { fn invalid_bytes() { let bytes: &[u8] = &[0u8]; assert_eq!( - Secp256k1PublicKey::try_from(bytes), + SUT::try_from(bytes), Err(CommonError::InvalidSecp256k1PublicKeyFromBytes { bad_value: bytes.to_vec().into() }) @@ -314,7 +307,7 @@ mod tests { #[test] fn invalid_key_not_on_curve() { assert_eq!( - Secp256k1PublicKey::from_str( + SUT::from_str( "99deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" ), Err(CommonError::InvalidSecp256k1PublicKeyPointNotOnCurve) @@ -324,14 +317,14 @@ mod tests { #[test] fn debug() { assert_eq!( - format!("{:?}", Secp256k1PublicKey::sample_alice()), + format!("{:?}", SUT::sample_alice()), "02517b88916e7f315bb682f9926b14bc67a0e4246f8a419b986269e1a7e61fffa7" ); } #[test] fn json() { - let model = Secp256k1PublicKey::sample(); + let model = SUT::sample(); assert_json_value_eq_after_roundtrip( &model, json!("02517b88916e7f315bb682f9926b14bc67a0e4246f8a419b986269e1a7e61fffa7"), @@ -341,21 +334,54 @@ mod tests { #[test] fn try_into_from_str() { let str = "02517b88916e7f315bb682f9926b14bc67a0e4246f8a419b986269e1a7e61fffa7"; - let key: Secp256k1PublicKey = str.parse().unwrap(); + let key: SUT = str.parse().unwrap(); assert_eq!(key.to_hex(), str); } #[test] fn hash() { assert_eq!( - BTreeSet::from_iter([ - Secp256k1PublicKey::sample_alice(), - Secp256k1PublicKey::sample_alice() - ]) - .len(), + BTreeSet::from_iter([SUT::sample_alice(), SUT::sample_alice()]) + .len(), 1 ); } + + #[test] + fn is_valid_is_false_for_mismatch() { + assert!(!SUT::sample() + .is_valid(&Secp256k1Signature::sample(), &Hash::sample())); + assert!(!SUT::sample() + .is_valid(&Secp256k1Signature::sample(), &Hash::sample_other())); + + assert!(!SUT::sample() + .is_valid(&Secp256k1Signature::sample_other(), &Hash::sample())); + assert!(!SUT::sample().is_valid( + &Secp256k1Signature::sample_other(), + &Hash::sample_other() + )); + + assert!(!SUT::sample_other() + .is_valid(&Secp256k1Signature::sample(), &Hash::sample())); + assert!(!SUT::sample_other() + .is_valid(&Secp256k1Signature::sample(), &Hash::sample_other())); + + assert!(!SUT::sample_other() + .is_valid(&Secp256k1Signature::sample_other(), &Hash::sample())); + assert!(!SUT::sample_other().is_valid( + &Secp256k1Signature::sample_other(), + &Hash::sample_other() + )); + } + + #[test] + fn is_valid_is_true_for_valid() { + let sut: SUT = "02f0d85a3b9082683f689e6115f37e1e24b7448fff14b14877e3a4e750e86fba8b".parse().unwrap(); + let message = "All those moments will be lost in time, like tears in rain. Time to die..."; + let hash = hash_of(message.as_bytes()); + let signature: Secp256k1Signature = "01aa1c4f46f8437b7f8ec9008ae10e6f33bb8be3e81e35c63f3498070dfbd6a20b2daee6073ead3c9e72d8909bc32a02e46cede3885cf8568d4c380ac97aa7fbcd".parse().unwrap(); + assert!(sut.is_valid(&signature, &hash)); + } } #[cfg(test)] diff --git a/src/core/types/signatures/signature.rs b/src/core/types/signatures/signature.rs index e8027e1a3..d78978d07 100644 --- a/src/core/types/signatures/signature.rs +++ b/src/core/types/signatures/signature.rs @@ -1,3 +1,5 @@ +use transaction::model::SignatureV1 as ScryptoSignature; + use crate::prelude::*; /// Either a Signature on `Curve25519` or `Secp256k1` @@ -16,6 +18,25 @@ pub enum Signature { Ed25519 { value: Ed25519Signature }, } +impl From for Signature { + fn from(value: ScryptoSignature) -> Self { + match value { + ScryptoSignature::Secp256k1(s) => { + Self::Secp256k1 { value: s.into() } + } + ScryptoSignature::Ed25519(s) => Self::Ed25519 { value: s.into() }, + } + } +} +impl From for ScryptoSignature { + fn from(value: Signature) -> Self { + match value { + Signature::Secp256k1 { value } => Self::Secp256k1(value.into()), + Signature::Ed25519 { value } => Self::Ed25519(value.into()), + } + } +} + impl From for Signature { fn from(signature: Secp256k1Signature) -> Self { Self::Secp256k1 { value: signature } @@ -38,6 +59,22 @@ impl HasSampleValues for Signature { } } +impl FromStr for Signature { + type Err = crate::CommonError; + + fn from_str(s: &str) -> Result { + if let Ok(sig) = Ed25519Signature::from_str(s) { + Ok(Self::Ed25519 { value: sig }) + } else if let Ok(sig) = Secp256k1Signature::from_str(s) { + Ok(Self::Secp256k1 { value: sig }) + } else { + Err(CommonError::FailedToParseSignatureFromString { + bad_value: s.to_owned(), + }) + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -67,4 +104,34 @@ mod tests { &Secp256k1Signature::sample() ); } + + #[test] + fn parse_bad_str() { + assert_eq!( + "foobar".parse::(), + Err(CommonError::FailedToParseSignatureFromString { + bad_value: "foobar".to_owned() + }) + ); + } + + #[test] + fn parse_ed25519() { + assert_eq!( + "2150c2f6b6c496d197ae03afb23f6adf23b275c675394f23786250abd006d5a2c7543566403cb414f70d0e229b0a9b55b4c74f42fc38cdf1aba2307f97686f0b".parse::().unwrap(), SUT::sample()); + } + + #[test] + fn parse_secp256k1() { + assert_eq!( + "018ad795353658a0cd1b513c4414cbafd0f990d329522977f8885a27876976a7d41ed8a81c1ac34551819627689cf940c4e27cacab217f00a0a899123c021ff6ef".parse::().unwrap(), SUT::sample_other()); + } + + #[test] + fn to_from_scrypto() { + let roundtrip = + |s: SUT| Into::::into(Into::::into(s)); + roundtrip(SUT::sample()); + roundtrip(SUT::sample_other()); + } } diff --git a/src/core/types/signatures/signature_with_public_key.rs b/src/core/types/signatures/signature_with_public_key.rs index 24f088283..325288225 100644 --- a/src/core/types/signatures/signature_with_public_key.rs +++ b/src/core/types/signatures/signature_with_public_key.rs @@ -1,42 +1,196 @@ -// use crate::prelude::*; - -// use radix_engine_common::crypto::Ed25519Signature as ScryptoEd25519Signature; -// use radix_engine_common::crypto::Secp256k1Signature as ScryptoSecp256k1Signature; - -// /// Represents any natively supported signature, including public key. -// #[derive(Debug, Clone, PartialEq, Eq, Hash, uniffi::Enum, EnumAsInner)] -// pub enum SignatureWithPublicKeyV1 { -// Secp256k1 { -// signature: ScryptoSecp256k1Signature, -// }, -// Ed25519 { -// public_key: Ed25519PublicKey, -// signature: ScryptoEd25519Signature, -// }, -// } - -// impl SignatureWithPublicKeyV1 { -// pub fn signature(&self) -> SignatureV1 { -// match &self { -// Self::Secp256k1 { signature } => signature.clone().into(), -// Self::Ed25519 { signature, .. } => signature.clone().into(), -// } -// } -// } - -// impl From for SignatureWithPublicKeyV1 { -// fn from(signature: Secp256k1Signature) -> Self { -// Self::Secp256k1 { signature } -// } -// } - -// impl From<(Ed25519PublicKey, Ed25519Signature)> for SignatureWithPublicKeyV1 { -// fn from( -// (public_key, signature): (Ed25519PublicKey, Ed25519Signature), -// ) -> Self { -// Self::Ed25519 { -// public_key, -// signature, -// } -// } -// } +use crate::prelude::*; + +use radix_engine_common::crypto::{ + Ed25519Signature as ScryptoEd25519Signature, + Secp256k1PublicKey as ScryptoSecp256k1PublicKey, + Secp256k1Signature as ScryptoSecp256k1Signature, +}; +use transaction::model::SignatureWithPublicKeyV1 as ScryptoSignatureWithPublicKey; + +/// Represents any natively supported signature, including public key. +#[derive(Debug, Clone, PartialEq, Eq, Hash, uniffi::Enum, EnumAsInner)] +pub enum SignatureWithPublicKey { + // N.B. `transaction::model::SignatureWithPublicKeyV1::Secp256k1` does + // NOT include the public key, it relies on ECDSA Signature supporting + // recovery, but it is not reliable since passing the wrong hash to + // a signature will return the WRONG public key. In other words one might + // naively believe that recovery should fail for the wrong hash passed in, + // but instead the wrong public key is returned. In the context of Scrypto + // or Node, they might have a mechanism by which they can validate the + // public key against some address or sub-state, but we play it safe, the + // cost of having the public key around in the ephemeral operations working + // with `SignatureWithPublicKey` is near-zero, so we have it explicit in state. + Secp256k1 { + public_key: Secp256k1PublicKey, + signature: Secp256k1Signature, + }, + Ed25519 { + public_key: Ed25519PublicKey, + signature: Ed25519Signature, + }, +} + +impl From for ScryptoSignatureWithPublicKey { + fn from(value: SignatureWithPublicKey) -> Self { + match value { + SignatureWithPublicKey::Secp256k1 { + public_key: _, + signature, + } => Self::Secp256k1 { + signature: signature.into(), + }, + SignatureWithPublicKey::Ed25519 { + public_key, + signature, + } => Self::Ed25519 { + public_key: public_key.into(), + signature: signature.into(), + }, + } + } +} + +impl SignatureWithPublicKey { + pub fn signature(&self) -> Signature { + match &self { + Self::Secp256k1 { signature, .. } => signature.clone().into(), + Self::Ed25519 { signature, .. } => signature.clone().into(), + } + } + + pub fn public_key(&self) -> PublicKey { + match &self { + Self::Secp256k1 { public_key, .. } => public_key.clone().into(), + Self::Ed25519 { public_key, .. } => public_key.clone().into(), + } + } +} + +impl From<(Secp256k1PublicKey, Secp256k1Signature)> for SignatureWithPublicKey { + fn from( + (public_key, signature): (Secp256k1PublicKey, Secp256k1Signature), + ) -> Self { + Self::Secp256k1 { + public_key, + signature, + } + } +} + +impl From<(Ed25519PublicKey, Ed25519Signature)> for SignatureWithPublicKey { + fn from( + (public_key, signature): (Ed25519PublicKey, Ed25519Signature), + ) -> Self { + Self::Ed25519 { + public_key, + signature, + } + } +} + +impl SignatureWithPublicKey { + fn sample_message() -> String { + "There is a computer disease that anybody who works with computers knows about. It's a very serious disease and it interferes completely with the work. The trouble with computers is that you 'play' with them!".to_owned() + } + fn sample_hash() -> Hash { + hash_of(Self::sample_message().as_bytes()) + } +} + +impl HasSampleValues for SignatureWithPublicKey { + fn sample() -> Self { + let private_key: Ed25519PrivateKey = + "cf52dbc7bb2663223e99fb31799281b813b939440a372d0aa92eb5f5b8516003" + .parse() + .unwrap(); + let public_key = private_key.public_key(); + assert_eq!( + &public_key.to_hex(), + "d24cc6af91c3f103d7f46e5691ce2af9fea7d90cfb89a89d5bba4b513b34be3b" + ); + let signature: Ed25519Signature = "2150c2f6b6c496d197ae03afb23f6adf23b275c675394f23786250abd006d5a2c7543566403cb414f70d0e229b0a9b55b4c74f42fc38cdf1aba2307f97686f0b".parse().unwrap(); + public_key.is_valid(&signature, &Self::sample_hash()); + + (public_key, signature).into() + } + + fn sample_other() -> Self { + let private_key: Secp256k1PrivateKey = + "111323d507d9d690836798e3ef2e5292cfd31092b75b9b59fa584ff593a3d7e4" + .parse() + .unwrap(); + let public_key = private_key.public_key(); + assert_eq!(&public_key.to_hex(), "03e78cdb2e0b7ea6e55e121a58560ccf841a913d3a4a9b8349e0ef00c2102f48d8"); + + let signature: Secp256k1Signature = "018ad795353658a0cd1b513c4414cbafd0f990d329522977f8885a27876976a7d41ed8a81c1ac34551819627689cf940c4e27cacab217f00a0a899123c021ff6ef".parse().unwrap(); + public_key.is_valid(&signature, &Self::sample_hash()); + + (public_key, signature).into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::prelude::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = SignatureWithPublicKey; + + #[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()); + } + + #[test] + fn signature() { + let pubkey: Ed25519PublicKey = + "d24cc6af91c3f103d7f46e5691ce2af9fea7d90cfb89a89d5bba4b513b34be3b" + .parse() + .unwrap(); + assert!(pubkey.is_valid( + &SUT::sample().signature().into_ed25519().unwrap(), + &SUT::sample_hash(), + )); + } + + #[test] + fn to_scrypto() { + match Into::::into(SUT::sample()) { + ScryptoSignatureWithPublicKey::Ed25519 { + signature, + public_key: _, + } => { + assert_eq!( + Into::::into(signature), + Ed25519Signature::sample() + ); + } + ScryptoSignatureWithPublicKey::Secp256k1 { signature: _ } => { + panic!("wrong curve") + } + } + } + + #[test] + fn to_scrypto_other() { + match Into::::into(SUT::sample_other()) { + ScryptoSignatureWithPublicKey::Secp256k1 { signature } => { + assert_eq!( + Into::::into(signature), + Secp256k1Signature::sample() + ) + } + ScryptoSignatureWithPublicKey::Ed25519 { + public_key: _, + signature: _, + } => panic!("wrong curve"), + } + } +} diff --git a/src/profile/v100/address/account_address.rs b/src/profile/v100/address/account_address.rs index 363f75d82..cabc78e15 100644 --- a/src/profile/v100/address/account_address.rs +++ b/src/profile/v100/address/account_address.rs @@ -347,7 +347,7 @@ mod tests { } #[test] - fn manual_perform_uniffi_conversion() { + fn manual_perform_uniffi_conversion_successful() { type RetAddr = ::RetAddress; let sut = SUT::sample(); let bech32 = sut.to_string(); @@ -364,6 +364,15 @@ mod tests { assert_eq!(ret, from_ffi_side); } + #[test] + fn manual_perform_uniffi_conversion_fail() { + type RetAddr = ::RetAddress; + assert!(::into_custom( + "invalid".to_string() + ) + .is_err()); + } + #[test] fn json_roundtrip() { let a: SUT = diff --git a/src/profile/v100/address/non_fungible_local_id.rs b/src/profile/v100/address/non_fungible_local_id.rs index d106be0be..2263a56b5 100644 --- a/src/profile/v100/address/non_fungible_local_id.rs +++ b/src/profile/v100/address/non_fungible_local_id.rs @@ -210,7 +210,10 @@ mod tests { ScryptoRUIDNonFungibleLocalId::new(bytes.clone().bytes()), ); assert_eq!(value.clone(), scrypto.clone().into()); - assert_eq!(value.clone().try_into(), Ok(scrypto.clone())); + assert_eq!( + Into::::into(value.clone()), + scrypto.clone() + ); assert_eq!( SUT::from_str(value.clone().to_string().as_str()), Ok(value) @@ -219,16 +222,19 @@ mod tests { fn test_from_bytes() { let bytes = [0xab; N]; - let non_native = SUT::bytes(bytes).unwrap(); - let native = ScryptoNonFungibleLocalId::Bytes( + let value = SUT::bytes(bytes).unwrap(); + let scrypto = ScryptoNonFungibleLocalId::Bytes( ScryptoBytesNonFungibleLocalId::new(bytes.clone().to_vec()) .unwrap(), ); - assert_eq!(non_native.clone(), native.clone().into()); - assert_eq!(non_native.clone().try_into(), Ok(native.clone())); + assert_eq!( - SUT::from_str(non_native.clone().to_string().as_str()), - Ok(non_native) + Into::::into(value.clone()), + scrypto.clone() + ); + assert_eq!( + SUT::from_str(value.clone().to_string().as_str()), + Ok(value) ); } @@ -248,7 +254,6 @@ mod tests { ScryptoStringNonFungibleLocalId::new("test").unwrap(), ); assert_eq!(value.clone(), scrypto.clone().into()); - assert_eq!(value.clone().try_into(), Ok(scrypto.clone())); assert_eq!( SUT::from_str(value.clone().to_string().as_str()), Ok(value) diff --git a/src/profile/v100/app_preferences/p2p_links/p2p_link.rs b/src/profile/v100/app_preferences/p2p_links/p2p_link.rs index 34edf471a..6aa6f978a 100644 --- a/src/profile/v100/app_preferences/p2p_links/p2p_link.rs +++ b/src/profile/v100/app_preferences/p2p_links/p2p_link.rs @@ -1,7 +1,5 @@ use crate::prelude::*; -use radix_engine_common::crypto::Hash; - /// A client the user have connected P2P with, typically a /// WebRTC connections with a DApp, but might be Android or iPhone /// client as well. diff --git a/src/profile/v100/app_preferences/p2p_links/radix_connect_password.rs b/src/profile/v100/app_preferences/p2p_links/radix_connect_password.rs index dccd62143..101c17860 100644 --- a/src/profile/v100/app_preferences/p2p_links/radix_connect_password.rs +++ b/src/profile/v100/app_preferences/p2p_links/radix_connect_password.rs @@ -1,5 +1,4 @@ use crate::prelude::*; -use radix_engine_common::crypto::Hash; /// The hash of the connection password is used to connect to the Radix Connect Signaling Server, /// over web sockets. The actual `ConnectionPassword` is used to encrypt all messages sent via diff --git a/src/profile/v100/networks/network/network_id.rs b/src/profile/v100/networks/network/network_id.rs index 98b0cbfb8..46e599a5b 100644 --- a/src/profile/v100/networks/network/network_id.rs +++ b/src/profile/v100/networks/network/network_id.rs @@ -85,6 +85,12 @@ impl Default for NetworkID { } } +impl From for u8 { + fn from(value: NetworkID) -> Self { + value.discriminant() + } +} + impl NetworkID { /// The raw representation of this network id, an `u8`. pub fn discriminant(&self) -> u8 { @@ -179,6 +185,11 @@ mod tests { assert_json_value_fails::(json!("1")); } + #[test] + fn into_u8() { + assert_eq!(Into::::into(NetworkID::Mainnet), 1); + } + #[test] fn from_repr() { assert_eq!(NetworkID::Mainnet, NetworkID::from_repr(0x01).unwrap()); @@ -243,7 +254,6 @@ mod tests { ids.len() ); } - /* #[test] fn lookup_network_definition() { assert_eq!( @@ -259,7 +269,6 @@ mod tests { NetworkID::Enkinet.discriminant() ) } - */ #[test] fn logical_name() { diff --git a/src/sargon.udl b/src/sargon.udl index eb4929501..ad58ae258 100644 --- a/src/sargon.udl +++ b/src/sargon.udl @@ -23,6 +23,9 @@ typedef sequence BagOfBytes; [Custom] typedef BagOfBytes InstructionsSecretMagic; +[Custom] +typedef BagOfBytes HashSecretMagic; + [Custom] typedef string ScryptoDecimal192; diff --git a/src/wrapped_radix_engine_toolkit/high_level/assert_manifest.rs b/src/wrapped_radix_engine_toolkit/high_level/assert_manifest.rs index 4d07ad295..e98093bc3 100644 --- a/src/wrapped_radix_engine_toolkit/high_level/assert_manifest.rs +++ b/src/wrapped_radix_engine_toolkit/high_level/assert_manifest.rs @@ -20,7 +20,7 @@ use crate::prelude::*; #[cfg(test)] pub fn manifest_eq(manifest: TransactionManifest, expected: impl AsRef) { let trim = - |s: &str| s.replace(" ", "").replace("\t", " ").trim().to_owned(); + |s: &str| s.replace(' ', "").replace('\t', " ").trim().to_owned(); pretty_assertions::assert_eq!( trim(&manifest.to_string()), trim(expected.as_ref()) @@ -47,7 +47,7 @@ pub fn manifest_eq(manifest: TransactionManifest, expected: impl AsRef) { #[cfg(test)] pub fn instructions_eq(instructions: Instructions, expected: impl AsRef) { let trim = - |s: &str| s.replace(" ", "").replace("\t", " ").trim().to_owned(); + |s: &str| s.replace(' ', "").replace('\t', " ").trim().to_owned(); pretty_assertions::assert_eq!( trim(&instructions.to_string()), trim(expected.as_ref()) diff --git a/src/wrapped_radix_engine_toolkit/high_level/manifests_crate_tokens.rs b/src/wrapped_radix_engine_toolkit/high_level/manifests_crate_tokens.rs index 32e92a931..e45ce1939 100644 --- a/src/wrapped_radix_engine_toolkit/high_level/manifests_crate_tokens.rs +++ b/src/wrapped_radix_engine_toolkit/high_level/manifests_crate_tokens.rs @@ -205,10 +205,8 @@ mod tests { #[test] fn manifest_create_fungible_token_stella() { assert_eq!( - SUT::create_fungible_token( - &AccountAddress::sample_mainnet().into(), - ) - .to_string(), + SUT::create_fungible_token(&AccountAddress::sample_mainnet(),) + .to_string(), r#"CREATE_FUNGIBLE_RESOURCE_WITH_INITIAL_SUPPLY Enum<0u8>() true diff --git a/src/wrapped_radix_engine_toolkit/high_level/ret_api.rs b/src/wrapped_radix_engine_toolkit/high_level/ret_api.rs index a9caf35ba..2ebcae89b 100644 --- a/src/wrapped_radix_engine_toolkit/high_level/ret_api.rs +++ b/src/wrapped_radix_engine_toolkit/high_level/ret_api.rs @@ -93,7 +93,7 @@ pub fn manifest_third_party_deposit_update( // #[uniffi::export] // pub async fn manifest_assets_transfers( // _transfers: AssetsTransfersTransactionPrototype, -// _message: Option, +// _message: Message, // ) -> Result { // todo!() // } @@ -132,8 +132,7 @@ pub fn build_information() -> SargonBuildInformation { #[uniffi::export] pub fn hash(data: BagOfBytes) -> Exactly32Bytes { - let h: radix_engine_common::crypto::Hash = - hash_of::>(data.to_vec()); + let h = hash_of::>(data.to_vec()); h.into() } @@ -383,7 +382,7 @@ mod tests { let info = build_information(); std::env::remove_var(RADIX_ENGINE_TOOLKIT_DEPENDENCY); std::env::remove_var(RADIX_ENGINE_DEPENDENCY); - assert_eq!(info.sargon_version.matches(".").count(), 2); + assert_eq!(info.sargon_version.matches('.').count(), 2); assert_eq!( info.dependencies .radix_engine_toolkit diff --git a/src/wrapped_radix_engine_toolkit/high_level/token_definition_metadata.rs b/src/wrapped_radix_engine_toolkit/high_level/token_definition_metadata.rs index c202e5bad..7e0e508ab 100644 --- a/src/wrapped_radix_engine_toolkit/high_level/token_definition_metadata.rs +++ b/src/wrapped_radix_engine_toolkit/high_level/token_definition_metadata.rs @@ -94,7 +94,7 @@ mod tests { use super::*; - #[allow(clippy::uppercase_acronyms)] + #[allow(clippy::upper_case_acronyms)] type SUT = TokenDefinitionMetadata; #[test] diff --git a/src/wrapped_radix_engine_toolkit/low_level/dummy_types.rs b/src/wrapped_radix_engine_toolkit/low_level/dummy_types.rs index 379807d20..ca083d978 100644 --- a/src/wrapped_radix_engine_toolkit/low_level/dummy_types.rs +++ b/src/wrapped_radix_engine_toolkit/low_level/dummy_types.rs @@ -1,7 +1,15 @@ use crate::prelude::*; macro_rules! dummy_sargon { - ($struct_name:ident) => { + ( + $( + #[doc = $expr: expr] + )* + $struct_name:ident + ) => { + $( + #[doc = $expr] + )* #[derive( Serialize, Deserialize, diff --git a/src/wrapped_radix_engine_toolkit/low_level/intent_signature.rs b/src/wrapped_radix_engine_toolkit/low_level/intent_signature.rs new file mode 100644 index 000000000..99a38e28a --- /dev/null +++ b/src/wrapped_radix_engine_toolkit/low_level/intent_signature.rs @@ -0,0 +1,94 @@ +use crate::prelude::*; + +use transaction::model::IntentSignatureV1 as ScryptoIntentSignature; + +#[derive(Clone, Debug, PartialEq, Eq, Hash, uniffi::Record)] +pub struct IntentSignature { + pub(crate) secret_magic: SignatureWithPublicKey, +} + +impl IntentSignature { + pub fn signature(&self) -> Signature { + self.secret_magic.clone().signature() + } + + pub fn public_key(&self) -> PublicKey { + self.secret_magic.clone().public_key() + } + + pub fn validate(&self, hash: impl Into) -> bool { + let hash = hash.into(); + self.public_key().is_valid(self.signature(), &hash) + } +} + +impl From for Signature { + fn from(value: IntentSignature) -> Self { + value.signature() + } +} + +impl From for IntentSignature { + fn from(value: SignatureWithPublicKey) -> Self { + Self { + secret_magic: value, + } + } +} + +impl From for ScryptoIntentSignature { + fn from(value: IntentSignature) -> Self { + ScryptoIntentSignature(value.secret_magic.into()) + } +} + +impl HasSampleValues for IntentSignature { + fn sample() -> Self { + SignatureWithPublicKey::sample().into() + } + + fn sample_other() -> Self { + SignatureWithPublicKey::sample_other().into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = IntentSignature; + + #[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()); + } + + #[test] + fn into_signature_for_ed25519() { + assert_eq!(Into::::into(SUT::sample()), Signature::sample()); + } + + #[test] + fn into_signature_for_secp256k1() { + assert_eq!( + Into::::into(SUT::sample_other()), + Signature::sample_other() + ); + } + + #[test] + fn into_scrypto() { + let scrypto: ScryptoIntentSignature = SUT::sample_other().into(); + assert_eq!( + scrypto.0.signature(), + SUT::sample_other().signature().into() + ) + } +} diff --git a/src/wrapped_radix_engine_toolkit/low_level/intent_signatures.rs b/src/wrapped_radix_engine_toolkit/low_level/intent_signatures.rs new file mode 100644 index 000000000..22427f3f6 --- /dev/null +++ b/src/wrapped_radix_engine_toolkit/low_level/intent_signatures.rs @@ -0,0 +1,88 @@ +use crate::prelude::*; + +use transaction::model::IntentSignaturesV1 as ScryptoIntentSignatures; + +#[derive(Clone, Debug, PartialEq, Eq, Default, Hash, uniffi::Record)] +pub struct IntentSignatures { + pub signatures: Vec, +} + +impl IntentSignatures { + pub fn new(signatures: I) -> Self + where + I: IntoIterator, + { + Self { + signatures: signatures.into_iter().collect_vec(), + } + } + + pub fn validate(&self, hash: impl Into) -> bool { + let hash = hash.into(); + + self.signatures.iter().all(|s| s.validate(hash.clone())) + } +} + +impl From for ScryptoIntentSignatures { + fn from(value: IntentSignatures) -> Self { + Self { + signatures: value + .signatures + .into_iter() + .map(|s| s.into()) + .collect_vec(), + } + } +} + +impl HasSampleValues for IntentSignatures { + fn sample() -> Self { + let intent = TransactionIntent::sample(); + let mut signatures = Vec::::new(); + for n in 1..4 { + let private_key: Secp256k1PrivateKey = + radix_engine::types::Secp256k1PrivateKey::from_u64(n) + .unwrap() + .into(); + + signatures.push(private_key.sign_intent_hash(&intent.intent_hash())) + } + + IntentSignatures::new(signatures) + } + + fn sample_other() -> Self { + let intent = TransactionIntent::sample_other(); + let mut signatures = Vec::::new(); + for n in 1..4 { + let private_key: Secp256k1PrivateKey = + radix_engine::types::Secp256k1PrivateKey::from_u64(n) + .unwrap() + .into(); + + signatures.push(private_key.sign_intent_hash(&intent.intent_hash())) + } + + IntentSignatures::new(signatures) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = IntentSignatures; + + #[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/src/wrapped_radix_engine_toolkit/low_level/message.rs b/src/wrapped_radix_engine_toolkit/low_level/message.rs deleted file mode 100644 index d3c5601ee..000000000 --- a/src/wrapped_radix_engine_toolkit/low_level/message.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::prelude::*; - -#[derive( - Clone, - Debug, - PartialEq, - EnumAsInner, - Eq, - Hash, - PartialOrd, - Ord, - uniffi::Enum, -)] -pub enum Message { - PlainText { string: String }, -} - -impl Message { - pub fn plain_text(message: impl AsRef) -> Self { - Self::PlainText { - string: message.as_ref().to_owned(), - } - } -} - -impl HasSampleValues for Message { - fn sample() -> Self { - Self::plain_text("Hello Radix!") - } - - fn sample_other() -> Self { - Self::plain_text("Lorem ipsum!!") - } -} - -#[cfg(test)] -mod tests { - use crate::prelude::*; - - #[allow(clippy::upper_case_acronyms)] - type SUT = Message; - - #[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/src/wrapped_radix_engine_toolkit/low_level/message/message.rs b/src/wrapped_radix_engine_toolkit/low_level/message/message.rs new file mode 100644 index 000000000..2b2b4f85e --- /dev/null +++ b/src/wrapped_radix_engine_toolkit/low_level/message/message.rs @@ -0,0 +1,111 @@ +use crate::prelude::*; + +use transaction::model::{ + EncryptedMessageV1, MessageContentsV1 as ScryptoMessageContents, + MessageV1 as ScryptoMessage, PlaintextMessageV1 as ScryptoPlaintextMessage, +}; + +#[derive(Clone, Debug, PartialEq, EnumAsInner, Eq, Hash, uniffi::Enum)] +pub enum Message { + PlainText { plaintext: PlaintextMessage }, + None, +} + +impl Message { + pub fn plain_text(message: impl AsRef) -> Self { + Self::PlainText { + plaintext: PlaintextMessage::new(message), + } + } +} + +impl From for ScryptoMessage { + fn from(value: Message) -> Self { + match value { + Message::PlainText { plaintext } => { + ScryptoMessage::Plaintext(plaintext.into()) + } + Message::None => ScryptoMessage::None, + } + } +} + +impl TryFrom for Message { + type Error = crate::CommonError; + + fn try_from(value: ScryptoMessage) -> Result { + match value { + ScryptoMessage::None => Ok(Self::None), + ScryptoMessage::Plaintext(p) => Ok(Self::PlainText { + plaintext: p.into(), + }), + ScryptoMessage::Encrypted(_) => { + Err(CommonError::EncryptedMessagesAreNotYetSupported) + } + } + } +} + +impl HasSampleValues for Message { + fn sample() -> Self { + Self::plain_text("Hello Radix!") + } + + fn sample_other() -> Self { + Self::plain_text("Lorem ipsum!!") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::prelude::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = Message; + + #[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()); + } + + #[test] + fn to_from_scrypto() { + let roundtrip = |s: SUT| { + TryInto::::try_into(Into::::into(s)).unwrap() + }; + roundtrip(SUT::sample()); + roundtrip(SUT::sample_other()); + } + + #[test] + fn into_scrypto() { + assert_eq!( + Into::::into(SUT::sample()), + ScryptoMessage::Plaintext(ScryptoPlaintextMessage { + message: ScryptoMessageContents::String( + "Hello Radix!".to_owned() + ), + mime_type: "text/plain".to_owned(), + }) + ); + } + + #[test] + fn encrypted_msg_are_not_yet_supported() { + let dummy = EncryptedMessageV1 { + encrypted: transaction::prelude::AesGcmPayload(vec![]), + decryptors_by_curve: [].into(), + }; + assert_eq!( + TryInto::::try_into(ScryptoMessage::Encrypted(dummy)), + Err(CommonError::EncryptedMessagesAreNotYetSupported) + ); + } +} diff --git a/src/wrapped_radix_engine_toolkit/low_level/message/mod.rs b/src/wrapped_radix_engine_toolkit/low_level/message/mod.rs new file mode 100644 index 000000000..57649aa4b --- /dev/null +++ b/src/wrapped_radix_engine_toolkit/low_level/message/mod.rs @@ -0,0 +1,5 @@ +mod message; +mod plaintext_message; + +pub use message::*; +pub use plaintext_message::*; diff --git a/src/wrapped_radix_engine_toolkit/low_level/message/plaintext_message/mod.rs b/src/wrapped_radix_engine_toolkit/low_level/message/plaintext_message/mod.rs new file mode 100644 index 000000000..c638ff882 --- /dev/null +++ b/src/wrapped_radix_engine_toolkit/low_level/message/plaintext_message/mod.rs @@ -0,0 +1,5 @@ +mod plaintext_message; +mod plaintext_message_contents; + +pub use plaintext_message::*; +pub use plaintext_message_contents::*; diff --git a/src/wrapped_radix_engine_toolkit/low_level/message/plaintext_message/plaintext_message.rs b/src/wrapped_radix_engine_toolkit/low_level/message/plaintext_message/plaintext_message.rs new file mode 100644 index 000000000..ed5432294 --- /dev/null +++ b/src/wrapped_radix_engine_toolkit/low_level/message/plaintext_message/plaintext_message.rs @@ -0,0 +1,81 @@ +use crate::prelude::*; + +use transaction::model::{ + MessageContentsV1 as ScryptoMessageContents, MessageV1 as ScryptoMessage, + PlaintextMessageV1 as ScryptoPlaintextMessage, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, uniffi::Record)] +pub struct PlaintextMessage { + pub mime_type: String, + pub message: MessageContents, +} + +impl PlaintextMessage { + pub fn new(message: impl AsRef) -> Self { + Self { + mime_type: "text/plain".to_owned(), + message: MessageContents::StringMessage { + string: message.as_ref().to_owned(), + }, + } + } +} + +impl From for ScryptoPlaintextMessage { + fn from(value: PlaintextMessage) -> Self { + Self { + mime_type: value.mime_type, + message: value.message.into(), + } + } +} + +impl From for PlaintextMessage { + fn from(value: ScryptoPlaintextMessage) -> Self { + Self { + mime_type: value.mime_type, + message: value.message.into(), + } + } +} + +impl HasSampleValues for PlaintextMessage { + fn sample() -> Self { + Self::new("Hello Radix!") + } + + fn sample_other() -> Self { + Self::new("Lorem ipsum!!") + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::prelude::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = PlaintextMessage; + + #[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()); + } + + #[test] + fn to_from_scrypto() { + let roundtrip = |s: SUT| { + Into::::into(Into::::into(s)) + }; + roundtrip(SUT::sample()); + roundtrip(SUT::sample_other()); + } +} diff --git a/src/wrapped_radix_engine_toolkit/low_level/message/plaintext_message/plaintext_message_contents.rs b/src/wrapped_radix_engine_toolkit/low_level/message/plaintext_message/plaintext_message_contents.rs new file mode 100644 index 000000000..40652cbfe --- /dev/null +++ b/src/wrapped_radix_engine_toolkit/low_level/message/plaintext_message/plaintext_message_contents.rs @@ -0,0 +1,84 @@ +use crate::prelude::*; + +use transaction::model::{ + MessageContentsV1 as ScryptoMessageContents, MessageV1 as ScryptoMessage, + PlaintextMessageV1 as ScryptoPlaintextMessage, +}; + +/// We explicitly mark content as either String or Bytes - this distinguishes (along with the mime type) +/// whether the message is intended to be displayable as text, or not. +/// +/// This data model ensures that messages intended to be displayable as text are valid unicode strings. +#[derive(Debug, Clone, PartialEq, Eq, Hash, EnumAsInner, uniffi::Enum)] +pub enum MessageContents { + StringMessage { string: String }, + BinaryMessage { bag_of_bytes: BagOfBytes }, +} + +impl From for MessageContents { + fn from(value: ScryptoMessageContents) -> Self { + match value { + ScryptoMessageContents::String(string) => { + Self::StringMessage { string } + } + ScryptoMessageContents::Bytes(vec) => Self::BinaryMessage { + bag_of_bytes: vec.into(), + }, + } + } +} +impl From for ScryptoMessageContents { + fn from(value: MessageContents) -> Self { + match value { + MessageContents::StringMessage { string } => { + ScryptoMessageContents::String(string) + } + MessageContents::BinaryMessage { bag_of_bytes } => { + ScryptoMessageContents::Bytes(bag_of_bytes.to_vec()) + } + } + } +} + +impl HasSampleValues for MessageContents { + fn sample() -> Self { + Self::StringMessage { + string: "Hello Radix!".to_owned(), + } + } + + fn sample_other() -> Self { + Self::BinaryMessage { + bag_of_bytes: BagOfBytes::from_hex("deadbeef").unwrap(), + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::prelude::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = MessageContents; + + #[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()); + } + + #[test] + fn to_from_scrypto() { + let roundtrip = + |s: SUT| Into::::into(Into::::into(s)); + roundtrip(SUT::sample()); + roundtrip(SUT::sample_other()); + } +} diff --git a/src/wrapped_radix_engine_toolkit/low_level/mod.rs b/src/wrapped_radix_engine_toolkit/low_level/mod.rs index 48f43825c..b8c8d153b 100644 --- a/src/wrapped_radix_engine_toolkit/low_level/mod.rs +++ b/src/wrapped_radix_engine_toolkit/low_level/mod.rs @@ -3,13 +3,16 @@ mod dummy_types; mod compiled_notarized_intent; mod execution_summary; +mod intent_signature; +mod intent_signatures; mod manifest_summary; mod message; mod notarized_transaction; +mod notary_signature; mod public_key_hash; mod signed_intent; mod transaction_classes; -mod transaction_hash; +mod transaction_hashes; mod transaction_header; mod transaction_intent; mod transaction_manifest; @@ -18,13 +21,16 @@ mod transaction_receipt; pub use compiled_notarized_intent::*; pub use dummy_types::*; pub use execution_summary::*; +pub use intent_signature::*; +pub use intent_signatures::*; pub use manifest_summary::*; pub use message::*; pub use notarized_transaction::*; +pub use notary_signature::*; pub use public_key_hash::*; pub use signed_intent::*; pub use transaction_classes::*; -pub use transaction_hash::*; +pub use transaction_hashes::*; pub use transaction_header::*; pub use transaction_intent::*; pub use transaction_manifest::*; diff --git a/src/wrapped_radix_engine_toolkit/low_level/notarized_transaction.rs b/src/wrapped_radix_engine_toolkit/low_level/notarized_transaction.rs index 0c61ba1ef..d12e0a022 100644 --- a/src/wrapped_radix_engine_toolkit/low_level/notarized_transaction.rs +++ b/src/wrapped_radix_engine_toolkit/low_level/notarized_transaction.rs @@ -1,19 +1,120 @@ use crate::prelude::*; +use transaction::model::{ + NotarizedTransactionV1 as ScryptoNotarizedTransaction, + SignedIntentV1 as ScryptoSignedIntent, +}; -dummy_sargon!(NotarizedTransaction); +use radix_engine_toolkit::functions::notarized_transaction::compile as RET_compile_notarized_tx; -// impl NotarizedTransaction { -// pub fn new -/* -public init( - signedIntent: SignedIntent, - notarySignature: SLIP10.Signature - ) throws { - sargon() +#[derive(Debug, Clone, Eq, PartialEq, uniffi::Record)] +pub struct NotarizedTransaction { + pub signed_intent: SignedIntent, + pub notary_signature: NotarySignature, +} + +impl NotarizedTransaction { + pub fn new( + signed_intent: SignedIntent, + notary_signature: NotarySignature, + ) -> Self { + Self { + signed_intent, + notary_signature, + } + } + + pub fn compile(&self) -> BagOfBytes { + let scrypto: ScryptoNotarizedTransaction = self.clone().into(); + RET_compile_notarized_tx(&scrypto) + .expect("Should always be able to compile a notarized tx") + .into() + } +} + +impl From for ScryptoNotarizedTransaction { + fn from(value: NotarizedTransaction) -> Self { + ScryptoNotarizedTransaction { + signed_intent: value.signed_intent.into(), + notary_signature: value.notary_signature.into(), + } + } +} + +impl HasSampleValues for NotarizedTransaction { + fn sample() -> Self { + let private_key = Ed25519PrivateKey::sample_alice(); + let intent = TransactionIntent::sample(); + + let signed_intent = SignedIntent::new_validating_signatures( + intent, + IntentSignatures::default(), + ) + .unwrap(); + + let signed_intent_hash = signed_intent.hash(); + + Self::new( + signed_intent, + private_key.notarize_hash(&signed_intent_hash), + ) + } + + // Identical to: https://github.com/radixdlt/radixdlt-scrypto/blob/ff21f24952318387803ae720105eec079afe33f3/transaction/src/model/hash/encoder.rs#L115 + // intent hash: `"60e5617d670e6c8a42ba5f3749f4ff1079f66221f282554ecdda9ad385ecb195"` + // bech32 encoded (mainnet): `"txid_rdx1vrjkzlt8pekg5s46tum5na8lzpulvc3p72p92nkdm2dd8p0vkx2syss63y"` + // bech32 encoded (simulator): `"txid_sim1vrjkzlt8pekg5s46tum5na8lzpulvc3p72p92nkdm2dd8p0vkx2svr7ejr"` + fn sample_other() -> Self { + let private_key: Secp256k1PrivateKey = + radix_engine::types::Secp256k1PrivateKey::from_u64(1) + .unwrap() + .into(); + + let intent = TransactionIntent::sample_other(); + assert_eq!(intent.intent_hash().to_string(), "txid_sim1vrjkzlt8pekg5s46tum5na8lzpulvc3p72p92nkdm2dd8p0vkx2svr7ejr"); + let signed_intent = SignedIntent::new_validating_signatures( + intent, + IntentSignatures::new(Vec::new()), + ) + .unwrap(); + + let signed_intent_hash = signed_intent.hash(); + + Self::new( + signed_intent, + private_key.notarize_hash(&signed_intent_hash), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = NotarizedTransaction; + + #[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()); + } + + #[test] + fn test_compile() { + // FIXME: replace with cross checked values, these values have just + // been recorded here to catch if the value ever changes + assert_eq!(SUT::sample().compile().to_hex(), "4d22030221022104210707010a872c0100000000000a912c01000000000009092f2400220101200720ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf010108000020220441038000d1be9c042f627d98a01383987916d43cf439631ca1d8c8076d6754ab263d0c086c6f636b5f6665652101850000fda0c42777080000000000000000000000000000000041038000d1be9c042f627d98a01383987916d43cf439631ca1d8c8076d6754ab263d0c087769746864726177210280005da66318c6318c61f5a61b4c6318c6318cf794aa8d295f14e6318c6318c6850000443945309a7a48000000000000000000000000000000000280005da66318c6318c61f5a61b4c6318c6318cf794aa8d295f14e6318c6318c6850000443945309a7a4800000000000000000000000000000041038000d1127918c16af09af521951adcf3a20ab2cc87c0e72e85814764853ce5e70c147472795f6465706f7369745f6f725f61626f72742102810000000022000020200022010121020c0a746578742f706c61696e2200010c0c48656c6c6f205261646978212022002201012101200740839ac9c47db45950fc0cd453c5ebbbfa7ae5f7c20753abe2370b5b40fdee89e522c4d810d060e0c56211d036043fd32b9908e97bf114c1835ca02d74018fdd09") } - public func compile() throws -> Data { - sargon() + #[test] + fn test_compile_other() { + // FIXME: replace with cross checked values, these values have just + // been recorded here to catch if the value ever changes + assert_eq!(SUT::sample_other().compile().to_hex(), "4d22030221022104210707f20a00000000000000000a0a00000000000000090a0000002200012007210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980101080000202200202000220000202200220001210120074101ebfc1f10a3b6ed83531f16249477ab86b77ce85980ef330abafbbd758caa98c665f68b8536112b6d1519feddeea01fd8429124dd75121d4bd88c14a27b68a123") } -*/ -// } +} diff --git a/src/wrapped_radix_engine_toolkit/low_level/notary_signature.rs b/src/wrapped_radix_engine_toolkit/low_level/notary_signature.rs new file mode 100644 index 000000000..f7d94d256 --- /dev/null +++ b/src/wrapped_radix_engine_toolkit/low_level/notary_signature.rs @@ -0,0 +1,131 @@ +use crate::prelude::*; + +use transaction::model::NotarySignatureV1 as ScryptoNotarySignature; + +#[derive( + Debug, + Clone, + Eq, + PartialEq, + derive_more::Display, + derive_more::FromStr, + uniffi::Record, +)] +pub struct NotarySignature { + pub(crate) secret_magic: Signature, +} + +impl From for NotarySignature { + fn from(value: ScryptoNotarySignature) -> Self { + Self { + secret_magic: value.0.into(), + } + } +} + +impl From for ScryptoNotarySignature { + fn from(value: NotarySignature) -> Self { + ScryptoNotarySignature(value.secret_magic.into()) + } +} + +impl From for NotarySignature { + fn from(value: Signature) -> Self { + Self { + secret_magic: value, + } + } +} + +impl From for NotarySignature { + fn from(value: Secp256k1Signature) -> Self { + Signature::from(value).into() + } +} + +impl From for NotarySignature { + fn from(value: Ed25519Signature) -> Self { + Signature::from(value).into() + } +} + +impl HasSampleValues for NotarySignature { + fn sample() -> Self { + Signature::sample().into() + } + + fn sample_other() -> Self { + Signature::sample_other().into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = NotarySignature; + + #[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()); + } + + #[test] + fn to_from_scrypto() { + let roundtrip = + |s: SUT| Into::::into(Into::::into(s)); + roundtrip(SUT::sample()); + roundtrip(SUT::sample_other()); + } + + #[test] + fn parse_bad_str() { + assert_eq!( + "foobar".parse::(), + Err(CommonError::FailedToParseSignatureFromString { + bad_value: "foobar".to_owned() + }) + ); + } + + #[test] + fn parse_ed25519() { + assert_eq!( + "2150c2f6b6c496d197ae03afb23f6adf23b275c675394f23786250abd006d5a2c7543566403cb414f70d0e229b0a9b55b4c74f42fc38cdf1aba2307f97686f0b".parse::().unwrap(), SUT::sample()); + } + + #[test] + fn parse_secp256k1() { + assert_eq!( + "018ad795353658a0cd1b513c4414cbafd0f990d329522977f8885a27876976a7d41ed8a81c1ac34551819627689cf940c4e27cacab217f00a0a899123c021ff6ef".parse::().unwrap(), SUT::sample_other()); + } + + #[test] + fn from_scrypto_notary() { + let sig: radix_engine_common::crypto::Ed25519Signature = "2150c2f6b6c496d197ae03afb23f6adf23b275c675394f23786250abd006d5a2c7543566403cb414f70d0e229b0a9b55b4c74f42fc38cdf1aba2307f97686f0b".parse().unwrap(); + let scrypto_notary = ScryptoNotarySignature( + transaction::model::SignatureV1::Ed25519(sig), + ); + assert_eq!(SUT::from(scrypto_notary), SUT::sample()); + } + + #[test] + fn from_ed25519() { + assert_eq!(SUT::from(Ed25519Signature::sample()), SUT::sample()); + } + + #[test] + fn from_secp256k1() { + assert_eq!( + SUT::from(Secp256k1Signature::sample()), + SUT::sample_other() + ); + } +} diff --git a/src/wrapped_radix_engine_toolkit/low_level/signed_intent.rs b/src/wrapped_radix_engine_toolkit/low_level/signed_intent.rs index e0c7e6c0f..7853a3fce 100644 --- a/src/wrapped_radix_engine_toolkit/low_level/signed_intent.rs +++ b/src/wrapped_radix_engine_toolkit/low_level/signed_intent.rs @@ -1,39 +1,183 @@ -// use crate::prelude::*; - -// #[derive( -// Clone, -// Debug, -// Default, -// PartialEq, -// Eq, -// Hash, -// uniffi::Record, -// )] -// pub struct IntentSignature(pub SignatureWithPublicKeyV1); - -// #[derive( -// Clone, -// Debug, -// Default, -// PartialEq, -// Eq, -// Hash, -// uniffi::Record, -// )] -// pub struct IntentSignatures { -// pub signatures: Vec, -// } - -// #[derive( -// Clone, -// Debug, -// Default, -// PartialEq, -// Eq, -// Hash, -// uniffi::Record, -// )] -// pub struct SignedIntent { -// pub intent: TransactionIntent, -// pub intent_signatures: IntentSignatures, -// } +use crate::prelude::*; + +use radix_engine_toolkit::functions::signed_intent::hash as RET_signed_intent_hash; +use transaction::model::{ + IntentSignatureV1 as ScryptoIntentSignature, + IntentSignaturesV1 as ScryptoIntentSignatures, + SignatureWithPublicKeyV1 as ScryptoSignatureWithPublicKey, + SignedIntentHash as ScryptoSignedIntentHash, + SignedIntentV1 as ScryptoSignedIntent, +}; + +#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)] +pub struct SignedIntent { + pub intent: TransactionIntent, + pub intent_signatures: IntentSignatures, +} + +impl SignedIntent { + pub fn new_validating_signatures( + intent: TransactionIntent, + intent_signatures: IntentSignatures, + ) -> Result { + if !intent_signatures.validate(intent.intent_hash()) { + return Err(CommonError::InvalidSignaturesForIntentSomeDidNotValidateIntentHash); + } + + Ok(Self { + intent, + intent_signatures, + }) + } +} + +impl SignedIntent { + pub fn network_id(&self) -> NetworkID { + self.intent.network_id() + } + + pub fn hash(&self) -> SignedIntentHash { + let scrypto_signed_intent: ScryptoSignedIntent = self.clone().into(); + let hash = RET_signed_intent_hash(&scrypto_signed_intent).expect("Sargon should only produce valid SignedIntent, should never fail to produce signed intent hash using RET."); + let scrypto_signed_intent_hash = ScryptoSignedIntentHash(hash.hash); + SignedIntentHash::from_scrypto( + scrypto_signed_intent_hash, + self.network_id(), + ) + } +} + +impl From for ScryptoSignedIntent { + fn from(value: SignedIntent) -> Self { + Self { + intent: value.intent.into(), + intent_signatures: value.intent_signatures.into(), + } + } +} + +impl HasSampleValues for SignedIntent { + fn sample() -> Self { + Self::new_validating_signatures( + TransactionIntent::sample(), + IntentSignatures::default(), + ) + .unwrap() + } + + fn sample_other() -> Self { + let intent = TransactionIntent::sample_other(); + let private_key: Secp256k1PrivateKey = + radix_engine::types::Secp256k1PrivateKey::from_u64(1) + .unwrap() + .into(); + let intent_signature = + private_key.sign_intent_hash(&intent.intent_hash()); + + Self::new_validating_signatures( + intent, + IntentSignatures::new([intent_signature]), + ) + .unwrap() + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::prelude::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = SignedIntent; + + #[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()); + } + + #[test] + fn many_intent_signatures_all_valid() { + let intent = TransactionIntent::sample_other(); + + let mut signatures = Vec::::new(); + for n in 1..4 { + let private_key: Secp256k1PrivateKey = + radix_engine::types::Secp256k1PrivateKey::from_u64(n) + .unwrap() + .into(); + + let intent_signature = + private_key.sign_intent_hash(&intent.intent_hash()); + signatures.push(intent_signature) + } + + let intent_signatures = IntentSignatures::new(signatures); + assert_eq!(intent_signatures.clone().signatures.into_iter().map(|s| s.signature().to_string()).collect_vec(), ["01da59c65684d07f1997bf9615c1e9330a54d8f3b13d8caaef1a8b32f64259be05544dc9290b64294a174c2857dd1043b3a5c0ca50bfc4ff35a95dd4338edee80b", "01427f6b48420da77ecb31c62b693d1970fb6cd3bcf68ea4ae21ae6c4e4521eff80100fed2410cba034a46cc5c546c9470cce1b44ff1c2d2e31c7ded420aa84024", "00570e538b1f84b323ea21b87930debed81d46a1a1abec5007c72106c4985ab515501af9a4ebbbfb75570416e0cc52dc93e064478c254fafb5065159e40b606612"]); + + assert_eq!( + SUT::new_validating_signatures(intent, intent_signatures.clone()) + .unwrap() + .intent_signatures, + intent_signatures + ); + } + + #[test] + fn many_intent_signatures_one_invalid() { + let intent = TransactionIntent::sample_other(); + + let mut signatures = Vec::::new(); + for n in 1..4 { + let private_key: Secp256k1PrivateKey = + radix_engine::types::Secp256k1PrivateKey::from_u64(n) + .unwrap() + .into(); + + let intent_signature = + private_key.sign_intent_hash(&intent.intent_hash()); + signatures.push(intent_signature) + } + + signatures.push(IntentSignature::sample()); + + let intent_signatures = IntentSignatures::new(signatures); + + assert_eq!( + SUT::new_validating_signatures(intent, intent_signatures.clone()), + Err(CommonError::InvalidSignaturesForIntentSomeDidNotValidateIntentHash) + ); + } + + #[test] + fn many_intent_signatures_invalid_because_mismatching_intent() { + let intent = TransactionIntent::sample_other(); + + let mut signatures = Vec::::new(); + for n in 1..4 { + let private_key: Secp256k1PrivateKey = + radix_engine::types::Secp256k1PrivateKey::from_u64(n) + .unwrap() + .into(); + let hash = intent.intent_hash(); + let intent_signature = private_key.sign_intent_hash(&hash); + signatures.push(intent_signature) + } + + let intent_signatures = IntentSignatures::new(signatures); + + assert_eq!( + SUT::new_validating_signatures( + TransactionIntent::sample(), // <-- WRONG Intent, not was signed. + intent_signatures.clone() + ), + Err(CommonError::InvalidSignaturesForIntentSomeDidNotValidateIntentHash) + ); + } +} diff --git a/src/wrapped_radix_engine_toolkit/low_level/transaction_hash.rs b/src/wrapped_radix_engine_toolkit/low_level/transaction_hash.rs deleted file mode 100644 index 82e4c638c..000000000 --- a/src/wrapped_radix_engine_toolkit/low_level/transaction_hash.rs +++ /dev/null @@ -1,7 +0,0 @@ -use radix_engine_toolkit::models::transaction_hash::TransactionHash as RETTransactionHash; - -use crate::prelude::*; - -dummy_sargon!(TransactionHash); - -impl TransactionIntent {} diff --git a/src/wrapped_radix_engine_toolkit/low_level/transaction_hashes/intent_hash.rs b/src/wrapped_radix_engine_toolkit/low_level/transaction_hashes/intent_hash.rs new file mode 100644 index 000000000..1175fa7c6 --- /dev/null +++ b/src/wrapped_radix_engine_toolkit/low_level/transaction_hashes/intent_hash.rs @@ -0,0 +1,62 @@ +use crate::prelude::*; + +impl HasSampleValues for IntentHash { + fn sample() -> Self { + IntentHash::new(Hash::sample(), NetworkID::Mainnet) + } + + fn sample_other() -> Self { + let intent = TransactionIntent::sample_other(); + intent.intent_hash() + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::prelude::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = IntentHash; + + #[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()); + } + + #[test] + fn to_string() { + assert_eq!(SUT::sample_other().to_string(), "txid_sim1vrjkzlt8pekg5s46tum5na8lzpulvc3p72p92nkdm2dd8p0vkx2svr7ejr"); + } + + #[test] + fn parse() { + assert_eq!("txid_sim1vrjkzlt8pekg5s46tum5na8lzpulvc3p72p92nkdm2dd8p0vkx2svr7ejr".parse::().unwrap(), SUT::sample_other()); + } + + #[test] + fn from_hash() { + let hash: Hash = + "60e5617d670e6c8a42ba5f3749f4ff1079f66221f282554ecdda9ad385ecb195" + .parse() + .unwrap(); + assert_eq!(SUT::new(hash, NetworkID::Simulator), SUT::sample_other()) + } + + #[test] + fn into_hash() { + assert_eq!( + Into::::into(SUT::sample_other()), + "60e5617d670e6c8a42ba5f3749f4ff1079f66221f282554ecdda9ad385ecb195" + .parse::() + .unwrap() + ); + } +} diff --git a/src/wrapped_radix_engine_toolkit/low_level/transaction_hashes/mod.rs b/src/wrapped_radix_engine_toolkit/low_level/transaction_hashes/mod.rs new file mode 100644 index 000000000..f86dafcf9 --- /dev/null +++ b/src/wrapped_radix_engine_toolkit/low_level/transaction_hashes/mod.rs @@ -0,0 +1,8 @@ +mod intent_hash; +mod signed_intent_hash; +mod transaction_hashes; +mod validate_and_decode_hash; + +pub use intent_hash::*; +pub use signed_intent_hash::*; +pub use transaction_hashes::*; diff --git a/src/wrapped_radix_engine_toolkit/low_level/transaction_hashes/signed_intent_hash.rs b/src/wrapped_radix_engine_toolkit/low_level/transaction_hashes/signed_intent_hash.rs new file mode 100644 index 000000000..080c35aac --- /dev/null +++ b/src/wrapped_radix_engine_toolkit/low_level/transaction_hashes/signed_intent_hash.rs @@ -0,0 +1,51 @@ +use crate::prelude::*; + +impl HasSampleValues for SignedIntentHash { + fn sample() -> Self { + Self::new(Hash::sample(), NetworkID::Mainnet) + } + + fn sample_other() -> Self { + Self::new(Hash::sample_other(), NetworkID::Simulator) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::prelude::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = SignedIntentHash; + + #[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()); + } + + #[test] + fn to_string() { + assert_eq!(SUT::sample().to_string(), "signedintent_rdx1frcm6zzyfd08z0deu9x24sh64eccxeux4j2dv3dsqeuh9qsz4y6sxsk6nl"); + } + + #[test] + fn to_string_other() { + assert_eq!(SUT::sample_other().to_string(), "signedintent_sim1r9k2z662xhqu8hccugxtrxwfn5ffemrw5chtqw3flhskmwyh7jcslkptg7"); + } + + #[test] + fn from_str() { + assert_eq!(SUT::sample(), "signedintent_rdx1frcm6zzyfd08z0deu9x24sh64eccxeux4j2dv3dsqeuh9qsz4y6sxsk6nl".parse::().unwrap()); + } + + #[test] + fn from_str_other() { + assert_eq!(SUT::sample_other(), "signedintent_sim1r9k2z662xhqu8hccugxtrxwfn5ffemrw5chtqw3flhskmwyh7jcslkptg7".parse::().unwrap()); + } +} diff --git a/src/wrapped_radix_engine_toolkit/low_level/transaction_hashes/transaction_hashes.rs b/src/wrapped_radix_engine_toolkit/low_level/transaction_hashes/transaction_hashes.rs new file mode 100644 index 000000000..044b01bba --- /dev/null +++ b/src/wrapped_radix_engine_toolkit/low_level/transaction_hashes/transaction_hashes.rs @@ -0,0 +1,104 @@ +use crate::prelude::*; +use paste::*; +use transaction::model::{ + HashHasHrp as ScryptoHashHasHrp, IntentHash as ScryptoIntentHash, + NotarizedTransactionV1 as ScryptoNotarizedTransaction, + SignedIntentHash as ScryptoSignedIntentHash, + TransactionHashBech32Decoder as ScryptoTransactionHashBech32Decoder, + TransactionHashBech32Encoder as ScryptoTransactionHashBech32Encoder, +}; + +use crate::wrapped_radix_engine_toolkit::low_level::transaction_hashes::validate_and_decode_hash::validate_and_decode_hash; + +use radix_engine_common::crypto::{ + Hash as ScryptoHash, IsHash as ScryptoIsHash, +}; + +macro_rules! decl_tx_hash { + ( + $( + #[doc = $expr: expr] + )* + $hash_type:ident + ) => { + paste! { + $( + #[doc = $expr] + )* + + #[derive( + Debug, Clone, PartialEq, Eq, Hash, derive_more::Display, uniffi::Record, + )] + #[display("{}", self.bech32_encoded_tx_id)] + pub struct [< $hash_type:camel Hash >] { + /// Which network this transaction hash is used on + pub network_id: NetworkID, + /// the hash of the intent + pub hash: Hash, + /// Bech32 encoded TX id + pub bech32_encoded_tx_id: String, + } + + impl [< $hash_type:camel Hash >] { + pub(crate) fn from_scrypto( + [< $hash_type:snake _hash >]: [], + network_id: NetworkID, + ) -> Self { + let scrypto = [< $hash_type:snake _hash >]; + let bech32_encoder = ScryptoTransactionHashBech32Encoder::new( + &network_id.network_definition(), + ); + let bech32_encoded_tx_id = bech32_encoder + .encode(&scrypto) + .expect("should never fail"); + let scrypto_hash: ScryptoHash = *scrypto.as_hash(); + + Self { + network_id, + hash: scrypto_hash.into(), + bech32_encoded_tx_id, + } + } + + pub fn new(hash: Hash, network_id: NetworkID) -> Self { + let scrypto_hash: ScryptoHash = hash.clone().into_hash(); + Self::from_scrypto( + []::from_hash(scrypto_hash), + network_id, + ) + } + + pub fn from_bech32(s: &str) -> Result { + validate_and_decode_hash::<[]>(s) + .map(|t| Self::from_scrypto(t.0, t.1)) + } + } + + impl FromStr for [< $hash_type:camel Hash >] { + type Err = crate::CommonError; + + fn from_str(s: &str) -> Result { + Self::from_bech32(s) + } + } + + impl From<[< $hash_type:camel Hash >]> for Hash { + fn from(value: [< $hash_type:camel Hash >]) -> Hash { + value.hash.clone() + } + } + } + }; +} + +decl_tx_hash!( + /// `IntentHash` used to identify transactions. + /// Representation is bech32 encoded string starting with `txid_` e.g.: + /// `"txid_rdx19rpveua6xuhvz0axu0mwpqk8fywr83atv8mkrugchvw6uuslgppqh9cnj4"` + Intent +); + +decl_tx_hash!( + /// A Signed Intent Hash is a bech32 encoded string starting with `"signedintent_" + SignedIntent +); diff --git a/src/wrapped_radix_engine_toolkit/low_level/transaction_hashes/validate_and_decode_hash.rs b/src/wrapped_radix_engine_toolkit/low_level/transaction_hashes/validate_and_decode_hash.rs new file mode 100644 index 000000000..b568be6d7 --- /dev/null +++ b/src/wrapped_radix_engine_toolkit/low_level/transaction_hashes/validate_and_decode_hash.rs @@ -0,0 +1,57 @@ +use crate::prelude::*; + +use transaction::model::{ + HashHasHrp as ScryptoHashHasHrp, IntentHash as ScryptoIntentHash, + TransactionHashBech32Decoder as ScryptoTransactionHashBech32Decoder, +}; + +fn validate_and_decode_hash_try_network( + bech32_encoded_hash: &str, + network_id: NetworkID, +) -> Result { + ScryptoTransactionHashBech32Decoder::new(&network_id.network_definition()) + .validate_and_decode::(bech32_encoded_hash) + .map_err(|_| ()) +} + +pub(crate) fn validate_and_decode_hash( + bech32_encoded_hash: &str, +) -> Result<(T, NetworkID)> { + if let Some(t) = enum_iterator::all::() + .map(|n| { + validate_and_decode_hash_try_network(bech32_encoded_hash, n) + .map(|v| (v, n)) + }) + .find_map(Result::ok) + { + Ok(t) + } else { + Err(CommonError::FailedToBech32DecodeTransactionHashAfterHavingTestedAllNetworkID { bad_value: bech32_encoded_hash.to_owned() }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unknown_network() { + // valid bech32 encoded string, unknown network (id: 0xfa, hrp: "fake") + let s = "txid_fake_1frcm6zzyfd08z0deu9x24sh64eccxeux4j2dv3dsqeuh9qsz4y6sfken4s"; + assert_eq!( + validate_and_decode_hash::(s), + Err(CommonError::FailedToBech32DecodeTransactionHashAfterHavingTestedAllNetworkID { bad_value: s.to_owned() }) + ); + } + + #[test] + fn decode_sim_success() { + let s = "txid_sim1vrjkzlt8pekg5s46tum5na8lzpulvc3p72p92nkdm2dd8p0vkx2svr7ejr"; + assert_eq!( + validate_and_decode_hash::(s) + .unwrap() + .1, + NetworkID::Simulator.into() + ); + } +} diff --git a/src/wrapped_radix_engine_toolkit/low_level/transaction_header.rs b/src/wrapped_radix_engine_toolkit/low_level/transaction_header.rs index b0e1ab119..8b057a325 100644 --- a/src/wrapped_radix_engine_toolkit/low_level/transaction_header.rs +++ b/src/wrapped_radix_engine_toolkit/low_level/transaction_header.rs @@ -1,3 +1,5 @@ +use transaction::model::TransactionHeaderV1 as ScryptoTransactionHeader; + use crate::prelude::*; #[derive( @@ -46,6 +48,39 @@ impl TransactionHeader { } } +impl From for ScryptoTransactionHeader { + fn from(value: TransactionHeader) -> Self { + Self { + network_id: value.network_id.into(), + start_epoch_inclusive: value.start_epoch_inclusive.into(), + end_epoch_exclusive: value.end_epoch_exclusive.into(), + nonce: value.nonce.into(), + notary_public_key: value.notary_public_key.into(), + notary_is_signatory: value.notary_is_signatory, + tip_percentage: value.tip_percentage, + } + } +} + +impl TryFrom for TransactionHeader { + type Error = crate::CommonError; + + fn try_from(value: ScryptoTransactionHeader) -> Result { + let network_id: NetworkID = value.network_id.try_into()?; + let notary_public_key: PublicKey = + value.notary_public_key.try_into()?; + Ok(Self { + network_id, + start_epoch_inclusive: value.start_epoch_inclusive.into(), + end_epoch_exclusive: value.end_epoch_exclusive.into(), + nonce: value.nonce.into(), + notary_public_key, + notary_is_signatory: value.notary_is_signatory, + tip_percentage: value.tip_percentage, + }) + } +} + impl HasSampleValues for TransactionHeader { fn sample() -> Self { Self::new( @@ -59,22 +94,22 @@ impl HasSampleValues for TransactionHeader { ) } + // The Header of: + // https://github.com/radixdlt/radixdlt-scrypto/blob/ff21f24952318387803ae720105eec079afe33f3/transaction/src/model/hash/encoder.rs#L115 fn sample_other() -> Self { - Self::new( - NetworkID::Mainnet, - 237, - 237, - 421337237, - Ed25519PublicKey::sample_other(), - false, - 10, - ) + let private_key: Secp256k1PrivateKey = + radix_engine::types::Secp256k1PrivateKey::from_u64(1) + .unwrap() + .into(); + let public_key: Secp256k1PublicKey = private_key.public_key(); + let network_id = NetworkID::Simulator; + Self::new(network_id, 0, 10, 10, public_key, true, 0) } } #[cfg(test)] mod tests { - use crate::prelude::*; + use super::*; #[allow(clippy::upper_case_acronyms)] type SUT = TransactionHeader; @@ -90,6 +125,16 @@ mod tests { assert_ne!(SUT::sample(), SUT::sample_other()); } + #[test] + fn to_from_scrypto() { + let roundtrip = |s: SUT| { + TryInto::::try_into(Into::::into(s)) + .unwrap() + }; + roundtrip(SUT::sample()); + roundtrip(SUT::sample_other()); + } + #[test] #[should_panic( expected = "End epoch MUST be greater than or equal start epoch." diff --git a/src/wrapped_radix_engine_toolkit/low_level/transaction_intent.rs b/src/wrapped_radix_engine_toolkit/low_level/transaction_intent.rs index c3b3619a5..44deec912 100644 --- a/src/wrapped_radix_engine_toolkit/low_level/transaction_intent.rs +++ b/src/wrapped_radix_engine_toolkit/low_level/transaction_intent.rs @@ -1,4 +1,11 @@ use crate::prelude::*; +use radix_engine_toolkit::functions::intent::hash as ret_hash_intent; +use transaction::model::{ + InstructionsV1 as ScryptoInstructions, IntentHash as ScryptoIntentHash, + IntentV1 as ScryptoIntent, MessageV1 as ScryptoMessage, +}; + +use radix_engine_toolkit::functions::intent::compile as RET_intent_compile; #[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)] pub struct TransactionIntent { @@ -8,6 +15,10 @@ pub struct TransactionIntent { } impl TransactionIntent { + pub fn network_id(&self) -> NetworkID { + self.header.network_id + } + pub fn new( header: TransactionHeader, manifest: TransactionManifest, @@ -20,12 +31,57 @@ impl TransactionIntent { } } - pub fn intent_hash(&self) -> TransactionHash { - todo!() + pub fn intent_hash(&self) -> IntentHash { + let hash = ret_hash_intent(&self.clone().into()) + .expect("Should never fail to hash an intent. Sargon should only produce valid Intents"); + + IntentHash::from_scrypto( + ScryptoIntentHash(hash.hash), + self.header.network_id, + ) + } + + pub fn compile(&self) -> BagOfBytes { + let scrypto_intent: ScryptoIntent = self.clone().into(); + let compiled = RET_intent_compile(&scrypto_intent) + .expect("Should always be able to compile an Intent"); + + compiled.into() + } +} + +impl From for ScryptoIntent { + fn from(value: TransactionIntent) -> Self { + Self { + header: value.header.into(), + instructions: ScryptoInstructions( + value.manifest.instructions().clone(), + ), + blobs: value.manifest.blobs().clone().into(), + message: value.message.into(), + } } +} +impl TryFrom for TransactionIntent { + type Error = crate::CommonError; + + fn try_from(value: ScryptoIntent) -> Result { + let message: Message = value.message.try_into()?; + let header: TransactionHeader = value.header.try_into()?; + let network_id = header.network_id; + let instructions = + Instructions::from_scrypto(value.instructions, network_id); + let blobs: Blobs = value.blobs.into(); + let manifest = TransactionManifest::with_instructions_and_blobs( + instructions, + blobs, + ); - pub fn compile(&self) -> Result { - todo!() + Ok(Self { + header, + manifest, + message, + }) } } @@ -38,18 +94,20 @@ impl HasSampleValues for TransactionIntent { ) } + // The Intent of: + // https://github.com/radixdlt/radixdlt-scrypto/blob/ff21f24952318387803ae720105eec079afe33f3/transaction/src/model/hash/encoder.rs#L115 fn sample_other() -> Self { Self::new( TransactionHeader::sample_other(), - TransactionManifest::sample_other(), - Message::sample_other(), + TransactionManifest::empty(NetworkID::Simulator), + Message::None, ) } } #[cfg(test)] mod tests { - use crate::prelude::*; + use super::*; #[allow(clippy::upper_case_acronyms)] type SUT = TransactionIntent; @@ -66,14 +124,28 @@ mod tests { } #[test] - #[should_panic(expected = "not yet implemented")] fn intent_hash() { - _ = SUT::sample().intent_hash() + let hash = SUT::sample().intent_hash(); + assert_eq!(hash.to_string(), "txid_rdx12nnrygyt3p5v5pft5e3vu93v38qp5k7fh9v59kd6vtu8506880nq5vsxx6") + } + + #[test] + fn network_id() { + assert_eq!(SUT::sample().network_id(), NetworkID::Mainnet); + assert_eq!(SUT::sample_other().network_id(), NetworkID::Simulator); + } + + #[test] + fn to_from_scrypto() { + let roundtrip = |s: SUT| { + TryInto::::try_into(Into::::into(s)).unwrap() + }; + roundtrip(SUT::sample()); + roundtrip(SUT::sample_other()); } #[test] - #[should_panic(expected = "not yet implemented")] fn compile() { - _ = SUT::sample().compile() + assert_eq!(SUT::sample().compile().to_string(), "4d220104210707010a872c0100000000000a912c01000000000009092f2400220101200720ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf010108000020220441038000d1be9c042f627d98a01383987916d43cf439631ca1d8c8076d6754ab263d0c086c6f636b5f6665652101850000fda0c42777080000000000000000000000000000000041038000d1be9c042f627d98a01383987916d43cf439631ca1d8c8076d6754ab263d0c087769746864726177210280005da66318c6318c61f5a61b4c6318c6318cf794aa8d295f14e6318c6318c6850000443945309a7a48000000000000000000000000000000000280005da66318c6318c61f5a61b4c6318c6318cf794aa8d295f14e6318c6318c6850000443945309a7a4800000000000000000000000000000041038000d1127918c16af09af521951adcf3a20ab2cc87c0e72e85814764853ce5e70c147472795f6465706f7369745f6f725f61626f72742102810000000022000020200022010121020c0a746578742f706c61696e2200010c0c48656c6c6f20526164697821"); } } diff --git a/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/blobs/blob.rs b/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/blobs/blob.rs new file mode 100644 index 000000000..9ab441bbb --- /dev/null +++ b/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/blobs/blob.rs @@ -0,0 +1,147 @@ +use crate::prelude::*; + +use transaction::model::BlobV1 as ScryptoBlob; + +/// Blob is a wrapper a bag of bytes +#[derive(Clone, PartialEq, Eq, Debug, derive_more::Display, uniffi::Record)] +pub struct Blob { + pub(crate) secret_magic: BagOfBytes, +} + +impl From for Blob { + fn from(value: BagOfBytes) -> Self { + Self { + secret_magic: value, + } + } +} + +impl From for BagOfBytes { + fn from(value: Blob) -> BagOfBytes { + value.secret_magic + } +} + +impl From for Blob { + fn from(value: ScryptoBlob) -> Self { + Self { + secret_magic: value.0.into(), + } + } +} + +impl From for ScryptoBlob { + fn from(value: Blob) -> Self { + ScryptoBlob(value.secret_magic.to_vec()) + } +} + +impl From<&Vec> for Blob { + fn from(value: &Vec) -> Self { + Self { + secret_magic: value.clone().into(), + } + } +} + +impl HasSampleValues for Blob { + fn sample() -> Self { + BagOfBytes::sample_aced().into() + } + + fn sample_other() -> Self { + BagOfBytes::from_hex(&"deadbeefabbafadecafe".repeat(100)) + .unwrap() + .into() + } +} + +#[uniffi::export] +pub fn new_blob_from_bytes(bytes: BagOfBytes) -> Blob { + bytes.into() +} + +#[uniffi::export] +pub fn blob_to_bytes(blob: &Blob) -> BagOfBytes { + blob.secret_magic.clone() +} + +#[uniffi::export] +pub fn blob_to_string(blob: &Blob) -> String { + blob.to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = Blob; + + #[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()); + } + + #[test] + fn display() { + assert_eq!( + SUT::sample().to_string(), + "acedacedacedacedacedacedacedacedacedacedacedacedacedacedacedaced" + ); + } + + #[test] + fn to_from_scrypto() { + let roundtrip = + |s: SUT| Into::::into(Into::::into(s)); + roundtrip(SUT::sample()); + roundtrip(SUT::sample_other()); + } + + #[test] + fn to_from_bag_of_bytes() { + let roundtrip = |s: SUT| Into::::into(Into::::into(s)); + roundtrip(SUT::sample()); + roundtrip(SUT::sample_other()); + } + + #[test] + fn from_vec() { + let vec = vec![0xde, 0xad]; + assert_eq!(SUT::from(&vec).to_string(), "dead"); + } +} + +#[cfg(test)] +mod uniffi_tests { + use crate::prelude::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = Blob; + + #[test] + fn test_blob_to_string() { + assert_eq!( + blob_to_string(&SUT::sample()), + "acedacedacedacedacedacedacedacedacedacedacedacedacedacedacedaced" + ); + } + + #[test] + fn test_new_blob_from_bytes() { + let bytes = BagOfBytes::from_hex("dead").unwrap(); + assert_eq!(new_blob_from_bytes(bytes.clone()).secret_magic, bytes); + } + + #[test] + fn test_blob_to_bytes() { + assert_eq!(blob_to_bytes(&SUT::sample()), BagOfBytes::sample_aced()); + } +} diff --git a/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/blobs/blobs.rs b/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/blobs/blobs.rs new file mode 100644 index 000000000..adf8966a4 --- /dev/null +++ b/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/blobs/blobs.rs @@ -0,0 +1,200 @@ +use crate::prelude::*; + +use std::collections::BTreeMap; +use transaction::model::{BlobV1 as ScryptoBlob, BlobsV1 as ScryptoBlobs}; + +/// Vec of Blobs +#[derive(Clone, PartialEq, Eq, Debug, uniffi::Record)] +pub struct Blobs { + pub(crate) secret_magic: BlobsSecretMagic, +} + +impl From for Blobs { + fn from(value: BlobsSecretMagic) -> Self { + Self { + secret_magic: value, + } + } +} + +#[uniffi::export] +pub fn blobs_list_of_blobs(blobs: &Blobs) -> Vec { + blobs.blobs() +} + +impl Blobs { + pub fn blobs(&self) -> Vec { + self.secret_magic.blobs() + } + + pub fn new(blobs: I) -> Self + where + I: IntoIterator, + { + BlobsSecretMagic::new(blobs).into() + } +} + +impl Default for Blobs { + /// Empty blobs + fn default() -> Self { + Self { + secret_magic: BlobsSecretMagic { + secret_magic: Vec::new(), + }, + } + } +} + +impl From for Blobs { + fn from(value: ScryptoBlobsMap) -> Self { + Blobs { + secret_magic: value.into(), + } + } +} + +impl From for ScryptoBlobsMap { + fn from(value: Blobs) -> Self { + value + .secret_magic + .clone() + .secret_magic + .into_iter() + .map(|b| { + let bytes = b.secret_magic.to_vec(); + ( + radix_engine::types::Hash::from(hash_of(bytes.clone())), + bytes, + ) + }) + .collect() + } +} + +// To From `ScryptoBlobs` (via `BlobsSecretMagic`) +impl From for ScryptoBlobs { + fn from(value: Blobs) -> Self { + value.secret_magic.into() + } +} + +impl From for Blobs { + fn from(value: ScryptoBlobs) -> Self { + Self { + secret_magic: value.into(), + } + } +} + +impl HasSampleValues for Blobs { + fn sample() -> Self { + BlobsSecretMagic::sample().into() + } + + fn sample_other() -> Self { + BlobsSecretMagic::sample_other().into() + } +} + +#[uniffi::export] +pub fn new_blobs_from_blob_list(blobs: Vec) -> Blobs { + Blobs::new(blobs) +} + +#[uniffi::export] +pub fn new_blobs_sample() -> Blobs { + Blobs::sample() +} + +#[uniffi::export] +pub fn new_blobs_sample_other() -> Blobs { + Blobs::sample_other() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = Blobs; + + #[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()); + } + + #[test] + fn blobs() { + assert_eq!( + SUT::sample() + .blobs() + .into_iter() + .map(|b| b.secret_magic) + .collect_vec(), + [ + BagOfBytes::sample_aced(), + BagOfBytes::sample_babe(), + BagOfBytes::sample_cafe(), + BagOfBytes::sample_dead(), + ] + ); + } + + #[test] + fn to_from_scrypto() { + let roundtrip = + |s: SUT| Into::::into(Into::::into(s)); + roundtrip(SUT::sample()); + roundtrip(SUT::sample_other()); + } + + #[test] + fn to_from_scrypto_blobs_map() { + let roundtrip = + |s: SUT| Into::::into(Into::::into(s)); + roundtrip(SUT::sample()); + roundtrip(SUT::sample_other()); + } +} + +#[cfg(test)] +mod uniffi_tests { + use crate::prelude::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = Blobs; + + #[test] + fn sample() { + assert_eq!(new_blobs_sample(), SUT::sample()); + } + + #[test] + fn sample_other() { + assert_eq!(new_blobs_sample_other(), SUT::sample_other()); + } + + #[test] + fn test_blobs_list_of_blobs() { + assert_eq!(blobs_list_of_blobs(&new_blobs_sample()).len(), 4); + } + + #[test] + fn test_new_blobs_from_blob_list() { + assert_eq!( + new_blobs_from_blob_list(vec![ + Blob::sample(), + Blob::sample_other(), + ]) + .blobs(), + [Blob::sample(), Blob::sample_other(),] + ); + } +} diff --git a/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/blobs/blobs_secret_magic.rs b/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/blobs/blobs_secret_magic.rs new file mode 100644 index 000000000..5ebe1c29e --- /dev/null +++ b/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/blobs/blobs_secret_magic.rs @@ -0,0 +1,126 @@ +use crate::prelude::*; + +use std::collections::BTreeMap; +use transaction::model::BlobsV1 as ScryptoBlobs; + +/// Vec of Blobs +#[derive(Clone, PartialEq, Eq, Debug, uniffi::Record)] +pub struct BlobsSecretMagic { + pub(crate) secret_magic: Vec, +} + +impl BlobsSecretMagic { + pub fn blobs(&self) -> Vec { + self.secret_magic.clone() + } + + pub fn new(blobs: I) -> Self + where + I: IntoIterator, + { + Self { + secret_magic: blobs.into_iter().collect_vec(), + } + } + + pub(crate) fn from_bags(bags: I) -> Self + where + I: IntoIterator, + { + Self::new(bags.into_iter().map(Blob::from)) + } +} + +impl From for BlobsSecretMagic { + fn from(value: ScryptoBlobs) -> Self { + Self { + secret_magic: value + .blobs + .into_iter() + .map(|b| b.into()) + .collect_vec(), + } + } +} + +pub(crate) type ScryptoBlobsMap = BTreeMap>; + +impl From for BlobsSecretMagic { + fn from(value: ScryptoBlobsMap) -> Self { + BlobsSecretMagic { + secret_magic: value.values().map(Into::::into).collect_vec(), + } + } +} + +impl From for ScryptoBlobs { + fn from(value: BlobsSecretMagic) -> Self { + ScryptoBlobs { + blobs: value + .secret_magic + .clone() + .into_iter() + .map(|b| b.into()) + .collect_vec(), + } + } +} + +impl HasSampleValues for BlobsSecretMagic { + fn sample() -> Self { + Self::from_bags([ + BagOfBytes::sample_aced(), + BagOfBytes::sample_babe(), + BagOfBytes::sample_cafe(), + BagOfBytes::sample_dead(), + ]) + } + + fn sample_other() -> Self { + Self::new([Blob::sample_other()]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = BlobsSecretMagic; + + #[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()); + } + + #[test] + fn blobs() { + assert_eq!( + SUT::sample() + .blobs() + .into_iter() + .map(|b| b.secret_magic) + .collect_vec(), + [ + BagOfBytes::sample_aced(), + BagOfBytes::sample_babe(), + BagOfBytes::sample_cafe(), + BagOfBytes::sample_dead(), + ] + ); + } + + #[test] + fn to_from_scrypto() { + let roundtrip = + |s: SUT| Into::::into(Into::::into(s)); + roundtrip(SUT::sample()); + roundtrip(SUT::sample_other()); + } +} diff --git a/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/blobs/mod.rs b/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/blobs/mod.rs new file mode 100644 index 000000000..e12751dae --- /dev/null +++ b/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/blobs/mod.rs @@ -0,0 +1,7 @@ +mod blob; +mod blobs; +mod blobs_secret_magic; + +pub use blob::*; +pub use blobs::*; +pub use blobs_secret_magic::*; diff --git a/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/instructions/instructions.rs b/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/instructions/instructions.rs index 1190bdbc6..c14ed652b 100644 --- a/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/instructions/instructions.rs +++ b/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/instructions/instructions.rs @@ -9,6 +9,7 @@ use transaction::{ CompileError as ScryptoCompileError, MockBlobProvider as ScryptoMockBlobProvider, }, + model::InstructionsV1 as ScryptoInstructions, prelude::InstructionV1 as ScryptoInstruction, }; @@ -27,6 +28,18 @@ impl Deref for Instructions { } } +impl Instructions { + pub(crate) fn from_scrypto( + instructions: ScryptoInstructions, + network_id: NetworkID, + ) -> Self { + Self { + secret_magic: instructions.into(), + network_id, + } + } +} + impl Instructions { pub fn instructions_string(&self) -> String { let network_definition = self.network_id.network_definition(); @@ -102,6 +115,15 @@ impl HasSampleValues for Instructions { } } +impl Instructions { + pub(crate) fn empty(network_id: NetworkID) -> Self { + Self { + secret_magic: InstructionsSecretMagic(Vec::new()), + network_id, + } + } +} + impl Instructions { pub(crate) fn sample_mainnet_instructions_string() -> String { include_str!("resource_transfer.rtm").to_owned() @@ -158,6 +180,13 @@ mod tests { ); } + #[test] + fn empty() { + let sut = SUT::empty(NetworkID::Simulator); + assert_eq!(sut.network_id, NetworkID::Simulator); + assert_eq!(sut.instructions_string(), ""); + } + #[test] fn new_from_instructions_string_wrong_network_id() { assert_eq!( @@ -201,6 +230,24 @@ mod tests { ); } + #[test] + fn from_scrypto() { + let network_id = NetworkID::Mainnet; + assert_eq!( + SUT { + secret_magic: InstructionsSecretMagic::sample(), + network_id + }, + SUT::from_scrypto( + ScryptoInstructions(vec![ + ScryptoInstruction::DropAuthZoneProofs, + ScryptoInstruction::DropAuthZoneRegularProofs, + ]), + network_id + ) + ); + } + #[test] fn extract_error_from_error_gen_non_addr_err() { assert_eq!( diff --git a/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/instructions/instructions_secret_magic.rs b/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/instructions/instructions_secret_magic.rs index 48c7053a6..6fafba6aa 100644 --- a/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/instructions/instructions_secret_magic.rs +++ b/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/instructions/instructions_secret_magic.rs @@ -6,6 +6,8 @@ use radix_engine_toolkit::functions::instructions::{ }; use transaction::prelude::InstructionV1 as ScryptoInstruction; +use transaction::model::InstructionsV1 as ScryptoInstructions; + /// An internal representation of a collection of Instructions, /// which intentions is to allow the `struct Instructions` /// to have no public initializers in Swift/Kotlin land, since it @@ -37,21 +39,28 @@ impl crate::UniffiCustomTypeConverter for InstructionsSecretMagic { } } +impl From for InstructionsSecretMagic { + fn from(value: ScryptoInstructions) -> Self { + Self(value.0) + } +} + impl HasSampleValues for InstructionsSecretMagic { fn sample() -> Self { Self(vec![ - ScryptoInstruction::DropAuthZoneProofs, // 0x12 - ScryptoInstruction::DropAuthZoneRegularProofs, // 0x13 + ScryptoInstruction::DropAuthZoneProofs, // sbor: 0x12 + ScryptoInstruction::DropAuthZoneRegularProofs, // sbor: 0x13 ]) } fn sample_other() -> Self { - Self(vec![ScryptoInstruction::DropAuthZoneSignatureProofs]) // 0x17 + Self(vec![ScryptoInstruction::DropAuthZoneSignatureProofs]) // sbor: 0x17 } } #[cfg(test)] mod tests { + use super::*; use crate::prelude::*; #[allow(clippy::upper_case_acronyms)] @@ -63,6 +72,18 @@ mod tests { assert_eq!(SUT::sample_other(), SUT::sample_other()); } + #[test] + fn from_scrypto() { + assert_eq!( + SUT::sample(), + ScryptoInstructions(vec![ + ScryptoInstruction::DropAuthZoneProofs, + ScryptoInstruction::DropAuthZoneRegularProofs, + ]) + .into() + ); + } + #[test] fn inequality() { assert_ne!(SUT::sample(), SUT::sample_other()); diff --git a/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/mod.rs b/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/mod.rs index f1f09cab3..a7f003208 100644 --- a/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/mod.rs +++ b/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/mod.rs @@ -1,7 +1,9 @@ +mod blobs; mod instructions; mod transaction_manifest; mod transaction_manifest_secret_magic; +pub use blobs::*; pub use instructions::*; pub use transaction_manifest::*; pub use transaction_manifest_secret_magic::*; diff --git a/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/transaction_manifest.rs b/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/transaction_manifest.rs index b7b40aa3d..70185df64 100644 --- a/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/transaction_manifest.rs +++ b/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/transaction_manifest.rs @@ -9,6 +9,8 @@ use radix_engine_toolkit::functions::instructions::extract_addresses as RET_ins_ use radix_engine_toolkit::functions::manifest::{ execution_summary as RET_execution_summary, summary as RET_summary, }; +use transaction::model::{BlobV1 as ScryptoBlob, BlobsV1 as ScryptoBlobs}; + use transaction::{ manifest::compile as scrypto_compile, manifest::decompile as scrypto_decompile, @@ -23,7 +25,32 @@ use transaction::{ #[derive(Clone, Debug, PartialEq, Eq, uniffi::Record, derive_more::Display)] #[display("{}", self.instructions_string())] // TODO add blobs pub struct TransactionManifest { - secret_magic: TransactionManifestSecretMagic, + pub(crate) secret_magic: TransactionManifestSecretMagic, +} + +impl TransactionManifest { + pub fn with_instructions_and_blobs( + instructions: Instructions, + blobs: Blobs, + ) -> Self { + Self { + secret_magic: TransactionManifestSecretMagic::new( + instructions, + blobs, + ), + } + } +} + +impl TransactionManifest { + pub(crate) fn empty(network_id: NetworkID) -> Self { + Self { + secret_magic: TransactionManifestSecretMagic { + instructions: Instructions::empty(network_id), + blobs: Blobs::default(), + }, + } + } } impl From for TransactionManifest { @@ -38,14 +65,7 @@ impl TransactionManifest { fn scrypto_manifest(&self) -> ScryptoTransactionManifest { ScryptoTransactionManifest { instructions: self.instructions().clone(), - blobs: self - .secret_magic - .blobs - .clone() - .into_iter() - .map(|b| b.to_vec()) - .map(|blob| (hash_of(blob.clone()), blob)) - .collect(), + blobs: self.secret_magic.blobs.clone().into(), } } } @@ -61,6 +81,10 @@ impl TransactionManifest { &self.secret_magic.instructions.secret_magic.0 } + pub(crate) fn blobs(&self) -> &Blobs { + &self.secret_magic.blobs + } + pub(crate) fn from_scrypto( scrypto_manifest: ScryptoTransactionManifest, network_id: NetworkID, @@ -73,12 +97,7 @@ impl TransactionManifest { ), network_id, }, - blobs: scrypto_manifest - .blobs - .clone() - .values() - .map(|b| b.to_owned().into()) - .collect_vec(), + blobs: scrypto_manifest.blobs.clone().into(), }, }; assert_eq!(value.scrypto_manifest(), scrypto_manifest); @@ -188,7 +207,7 @@ mod tests { type Err = crate::CommonError; fn from_str(s: &str) -> Result { - Self::new(s, NetworkID::Simulator, Vec::new()) + Self::new(s, NetworkID::Simulator, Blobs::default()) } } @@ -244,7 +263,7 @@ mod tests { secret_magic: InstructionsSecretMagic(ins), network_id: NetworkID::Mainnet, }, - Vec::new(), + Blobs::default(), ), }; assert_eq!(scrypto.clone(), sut.clone().into()); @@ -260,7 +279,7 @@ mod tests { "#; assert_eq!( - SUT::new(instructions_str, NetworkID::Simulator, Blobs::new()) + SUT::new(instructions_str, NetworkID::Simulator, Blobs::default()) .unwrap() .instructions() .len(), @@ -277,7 +296,7 @@ mod tests { "#; assert_eq!( - SUT::new(instructions_str, NetworkID::Mainnet, Blobs::new()), + SUT::new(instructions_str, NetworkID::Mainnet, Blobs::default()), Err(CommonError::InvalidInstructionsWrongNetwork { found_in_instructions: NetworkID::Simulator, specified_to_instructions_ctor: NetworkID::Mainnet @@ -294,7 +313,7 @@ mod tests { "#; assert_eq!( - SUT::new(instructions_str, NetworkID::Stokenet, Blobs::new()), + SUT::new(instructions_str, NetworkID::Stokenet, Blobs::default()), Err(CommonError::InvalidInstructionsWrongNetwork { found_in_instructions: NetworkID::Mainnet, specified_to_instructions_ctor: NetworkID::Stokenet @@ -306,9 +325,12 @@ mod tests { #[should_panic(expected = "not yet implemented")] fn execution_summary() { let instructions_string = "CALL_METHOD Address(\"account_tdx_2_128h2zv5m4mnprnfjxn4nf96pglgx064mut8np26hp7w9mm064es2dn\") \"withdraw\" Address(\"resource_tdx_2_1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxtfd2jc\") Decimal(\"123\"); TAKE_FROM_WORKTOP Address(\"resource_tdx_2_1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxtfd2jc\") Decimal(\"123\") Bucket(\"bucket1\"); CALL_METHOD Address(\"account_tdx_2_128x8q5es2dstqtcc8wqm843xdtfs0lgetfcdn62a54wxspj6yhpxkf\") \"try_deposit_or_abort\" Bucket(\"bucket1\") Enum<0u8>();"; - let manifest = - SUT::new(instructions_string, NetworkID::Stokenet, Blobs::new()) - .unwrap(); + let manifest = SUT::new( + instructions_string, + NetworkID::Stokenet, + Blobs::default(), + ) + .unwrap(); let summary = manifest .execution_summary(TransactionReceipt::sample().encoded) @@ -386,7 +408,7 @@ mod uniffi_tests { new_transaction_manifest_from_instructions_string_and_blobs( s.clone(), NetworkID::Mainnet, - Blobs::new() + Blobs::default() ) .unwrap() .instructions_string(), diff --git a/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/transaction_manifest_secret_magic.rs b/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/transaction_manifest_secret_magic.rs index 6543066ea..b55dbf71e 100644 --- a/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/transaction_manifest_secret_magic.rs +++ b/src/wrapped_radix_engine_toolkit/low_level/transaction_manifest/transaction_manifest_secret_magic.rs @@ -1,11 +1,5 @@ use crate::prelude::*; -/// Blob is just a bag of bytes (must be, for Kotlin compat) -pub type Blob = BagOfBytes; - -/// Vec of Blobs -pub type Blobs = Vec; - /// An internal representation of a TransactionManifest, /// which intentions is to allow the `struct TransactionManifest` /// to have no public initializers in Swift/Kotlin land, since it @@ -28,11 +22,11 @@ impl TransactionManifestSecretMagic { impl HasSampleValues for TransactionManifestSecretMagic { fn sample() -> Self { - Self::new(Instructions::sample_mainnet(), Vec::new()) + Self::new(Instructions::sample_mainnet(), Blobs::default()) } fn sample_other() -> Self { - Self::new(Instructions::sample_simulator_other(), Vec::new()) + Self::new(Instructions::sample_simulator_other(), Blobs::default()) } } diff --git a/tests/uniffi/bindings/test_tx_manifest.swift b/tests/uniffi/bindings/test_tx_manifest.swift index 9c7f020bd..c8a8cfc40 100644 --- a/tests/uniffi/bindings/test_tx_manifest.swift +++ b/tests/uniffi/bindings/test_tx_manifest.swift @@ -1,3 +1,4 @@ +import Foundation import Sargon extension TransactionManifest { @@ -9,8 +10,16 @@ extension TransactionManifest: CustomStringConvertible { transactionManifestToString(manifest: self) } } -public typealias Blob = BagOfBytes -public typealias Blobs = [Blob] +extension Blob { + public init(data: Data) { + self = newBlobFromBytes(bytes: data) + } +} +extension Blobs: ExpressibleByArrayLiteral { + public init(arrayLiteral blobs: Blob...) { + self = newBlobsFromBlobList(blobs: blobs) + } +} extension TransactionManifest { public init(instructionsString: String, networkID: NetworkId, blobs: Blobs = []) throws { self = try newTransactionManifestFromInstructionsStringAndBlobs( @@ -55,7 +64,7 @@ func test() throws { print("✨ ✨ ✨ ✨ ✨") assert(TransactionManifest.sample.description == instructionsString) let sut = try TransactionManifest( - instructionsString: instructionsString, + instructionsString: instructionsString, networkID: .mainnet ) assert(sut == TransactionManifest.sample)