diff --git a/Cargo.lock b/Cargo.lock index 9b32b541fd..61a2abe7ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -514,6 +514,7 @@ dependencies = [ "serde_json", "sha2", "thiserror", + "uuid", ] [[package]] @@ -2105,6 +2106,12 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + [[package]] name = "sha2" version = "0.10.8" @@ -2419,6 +2426,10 @@ name = "uuid" version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" +dependencies = [ + "serde", + "sha1_smol", +] [[package]] name = "version_check" diff --git a/packages/std/Cargo.toml b/packages/std/Cargo.toml index 32d3573214..455abe5e99 100644 --- a/packages/std/Cargo.toml +++ b/packages/std/Cargo.toml @@ -66,6 +66,7 @@ sha2 = "0.10.3" serde = { workspace = true, features = ["std"] } serde-json-wasm = { version = "1.0.1", default-features = false, features = ["std"] } thiserror = "1.0.26" +uuid = { version = "1.0.0", features = ["v5", "serde"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] bech32 = "0.11.0" diff --git a/packages/std/src/lib.rs b/packages/std/src/lib.rs index 6860b9bc09..3755adc74f 100644 --- a/packages/std/src/lib.rs +++ b/packages/std/src/lib.rs @@ -25,6 +25,7 @@ mod stdack; mod storage; mod traits; mod types; +mod uuid; /// This module is to simplify no_std imports pub(crate) mod prelude; @@ -79,6 +80,7 @@ pub use crate::stdack::StdAck; pub use crate::storage::MemoryStorage; pub use crate::traits::{Api, Querier, QuerierResult, QuerierWrapper, Storage}; pub use crate::types::{BlockInfo, ContractInfo, Env, MessageInfo, TransactionInfo}; +pub use crate::uuid::{new_uuid, Uuid}; // Exposed in wasm build only diff --git a/packages/std/src/uuid.rs b/packages/std/src/uuid.rs new file mode 100644 index 0000000000..5609b98c3f --- /dev/null +++ b/packages/std/src/uuid.rs @@ -0,0 +1,118 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::ops::Deref; +use std::str::FromStr; +use uuid as raw_uuid; + +use crate::{from_json, to_json_vec}; +use crate::{Api, Env, StdResult, Storage}; + +/// Uuid Provides a Uuid that can be used deterministically. +/// Use internally Uuidv5 and NAMESPACE_OID. +/// The name is combined with contract address, block height, and increased sequential. +#[derive( + Serialize, Deserialize, Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, JsonSchema, +)] +pub struct Uuid(#[schemars(with = "String")] raw_uuid::Uuid); +impl Uuid { + pub fn as_slice(&self) -> &[u8] { + &self.as_bytes()[0..16] + } + + // Port the new_v5 implementation of uuid to use deps.api + // https://github.com/uuid-rs/uuid/blob/2d6c147bdfca9612263dd7e82e26155f7ef8bf32/src/v5.rs#L33 + fn new_v5(api: &dyn Api, namespace: &Uuid, name: &[u8]) -> StdResult { + let message = [namespace.as_bytes(), name].concat(); + let buffer = api.sha1_calculate(&message)?; + + let mut bytes = raw_uuid::Bytes::default(); + bytes.copy_from_slice(&buffer[..16]); + let mut builder = raw_uuid::Builder::from_bytes(bytes); + builder + .set_variant(raw_uuid::Variant::RFC4122) + .set_version(raw_uuid::Version::Sha1); + + Ok(Uuid(builder.into_uuid())) + } +} + +const CONTRACT_UUID_SEQ_NUM_KEY: &[u8] = b"_contract_uuid_seq_num"; + +pub fn new_uuid(env: &Env, storage: &mut dyn Storage, api: &dyn Api) -> StdResult { + let raw_seq_num = storage.get(CONTRACT_UUID_SEQ_NUM_KEY); + let seq_num: u16 = match raw_seq_num { + Some(data) => from_json(data).unwrap(), + None => 0, + }; + let next_seq_num: u16 = seq_num.wrapping_add(1); + let uuid_name = &[ + env.contract.address.as_bytes(), + &env.block.height.to_be_bytes(), + &seq_num.to_be_bytes(), + ] + .concat(); + storage.set( + CONTRACT_UUID_SEQ_NUM_KEY, + &(to_json_vec(&next_seq_num).unwrap()), + ); + + Uuid::new_v5(api, &Uuid(raw_uuid::Uuid::NAMESPACE_OID), uuid_name) +} + +impl Deref for Uuid { + type Target = raw_uuid::Uuid; + fn deref(&self) -> &raw_uuid::Uuid { + &self.0 + } +} + +impl FromStr for Uuid { + type Err = uuid::Error; + fn from_str(s: &str) -> Result { + let parsed = raw_uuid::Uuid::parse_str(s); + match parsed { + Ok(data) => Ok(Uuid(data)), + Err(err) => Err(err), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::testing::{mock_env, MockApi, MockStorage}; + use uuid as raw_uuid; + + #[test] + fn generate_uuid_v5() { + let env = mock_env(); + let api = MockApi::default(); + let mut storage = MockStorage::new(); + + let uuid1 = new_uuid(&env, &mut storage, &api).unwrap(); + + assert_eq!(uuid1.get_variant(), uuid::Variant::RFC4122); + assert_eq!(uuid1.get_version(), Some(uuid::Version::Sha1)); + + let uuid2 = new_uuid(&env, &mut storage, &api).unwrap(); + assert_ne!(uuid1, uuid2); + } + + #[test] + fn same_output_as_raw_uuid() { + let env = mock_env(); + let api = MockApi::default(); + let mut storage = MockStorage::new(); + let our_uuid = new_uuid(&env, &mut storage, &api).unwrap(); + + let uuid_name = &[ + env.contract.address.as_bytes(), + &env.block.height.to_be_bytes(), + &0u16.to_be_bytes(), + ] + .concat(); + let raw = raw_uuid::Uuid::new_v5(&raw_uuid::Uuid::NAMESPACE_OID, uuid_name); + + assert_eq!(our_uuid.to_string(), raw.to_string()); + } +}