Skip to main content

ownable_std/
lib.rs

1use cosmwasm_std::{
2    Addr, Api, BlockInfo, CanonicalAddr, ContractInfo, Empty, Env, Order, OwnedDeps, Querier,
3    RecoverPubkeyError, StdError, StdResult, Storage, Timestamp, Uint128, VerificationError,
4};
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use serde_with::serde_as;
8use std::collections::HashMap;
9use std::marker::PhantomData;
10
11pub mod abi;
12mod memory_storage;
13pub use memory_storage::MemoryStorage;
14#[cfg(feature = "macros")]
15pub use ownable_std_macros::*;
16
17const CANONICAL_LENGTH: usize = 54;
18
19/// Creates a default [`Env`] for host-side execution.
20pub fn create_env() -> Env {
21    create_ownable_env(String::new(), None)
22}
23
24/// Creates an [`Env`] with a configurable chain id and optional timestamp.
25pub fn create_ownable_env(chain_id: impl Into<String>, time: Option<Timestamp>) -> Env {
26    Env {
27        block: BlockInfo {
28            height: 0,
29            time: time.unwrap_or_else(|| Timestamp::from_seconds(0)),
30            chain_id: chain_id.into(),
31        },
32        contract: ContractInfo {
33            address: Addr::unchecked(""),
34        },
35        transaction: None,
36    }
37}
38
39/// convert an ownable package name into a display title
40/// e.g. `ownable-my-first` -> `My First`
41pub fn package_title_from_name(name: &str) -> String {
42    name.trim_start_matches("ownable-")
43        .split(['-', '_'])
44        .filter(|part| !part.is_empty())
45        .map(|part| {
46            let mut chars = part.chars();
47            match chars.next() {
48                Some(first) => format!("{}{}", first.to_ascii_uppercase(), chars.as_str()),
49                None => String::new(),
50            }
51        })
52        .collect::<Vec<_>>()
53        .join(" ")
54}
55
56/// Builds in-memory dependencies for contract execution, optionally preloaded from IndexedDB dump data.
57pub fn load_owned_deps(
58    state_dump: Option<IdbStateDump>,
59) -> OwnedDeps<MemoryStorage, EmptyApi, EmptyQuerier, Empty> {
60    match state_dump {
61        None => OwnedDeps {
62            storage: MemoryStorage::default(),
63            api: EmptyApi::default(),
64            querier: EmptyQuerier::default(),
65            custom_query_type: PhantomData,
66        },
67        Some(dump) => {
68            let idb_storage = IdbStorage::load(dump);
69            OwnedDeps {
70                storage: idb_storage.storage,
71                api: EmptyApi::default(),
72                querier: EmptyQuerier::default(),
73                custom_query_type: PhantomData,
74            }
75        }
76    }
77}
78
79/// returns a hex color in string format from a hash
80pub fn get_random_color(hash: String) -> String {
81    let (red, green, blue) = derive_rgb_values(hash);
82    rgb_hex(red, green, blue)
83}
84
85/// takes a hex-encoded hash and derives a seemingly-random rgb tuple
86pub fn derive_rgb_values(hash: String) -> (u8, u8, u8) {
87    // allow optional 0x and odd length
88    let mut s = hash.trim().trim_start_matches("0x").to_string();
89    if s.len() % 2 == 1 {
90        s.insert(0, '0');
91    }
92
93    match hex::decode(&s) {
94        Ok(mut bytes) => {
95            bytes.reverse();
96            let r = *bytes.get(0).unwrap_or(&0);
97            let g = *bytes.get(1).unwrap_or(&0);
98            let b = *bytes.get(2).unwrap_or(&0);
99            (r, g, b)
100        }
101        Err(_) => (0, 0, 0),
102    }
103}
104
105/// takes three u8 values representing rgb values (0-255)f
106/// and returns a hex string
107pub fn rgb_hex(r: u8, g: u8, b: u8) -> String {
108    format!("#{:02X}{:02X}{:02X}", r, g, b)
109}
110
111/// Wrapper around [`MemoryStorage`] with helpers to load state from browser IndexedDB dumps.
112pub struct IdbStorage {
113    pub storage: MemoryStorage,
114}
115
116impl IdbStorage {
117    /// Creates a new [`IdbStorage`] and populates it from a serialized state dump.
118    pub fn load(idb: IdbStateDump) -> Self {
119        let mut store = IdbStorage {
120            storage: MemoryStorage::new(),
121        };
122        store.load_to_mem_storage(idb);
123        store
124    }
125
126    /// takes a IdbStateDump and loads the values into MemoryStorage
127    pub fn load_to_mem_storage(&mut self, idb_state: IdbStateDump) {
128        for (k, v) in idb_state.state_dump.into_iter() {
129            self.storage.set(&k, &v);
130        }
131    }
132}
133
134#[serde_as]
135#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
136/// Serialized contract storage dump used to move state between JS and Rust.
137pub struct IdbStateDump {
138    // map of the indexed db key value pairs of the state object store
139    #[serde_as(as = "Vec<(serde_with::Bytes, serde_with::Bytes)>")]
140    pub state_dump: HashMap<Vec<u8>, Vec<u8>>,
141}
142
143impl IdbStateDump {
144    /// generates a state dump from all key-value pairs in MemoryStorage
145    pub fn from(store: MemoryStorage) -> IdbStateDump {
146        let mut state: HashMap<Vec<u8>, Vec<u8>> = HashMap::new();
147
148        for (key, value) in store.range(None, None, Order::Ascending) {
149            state.insert(key, value);
150        }
151        IdbStateDump { state_dump: state }
152    }
153}
154
155// EmptyApi that is meant to conform the traits by the cosmwasm standard contract syntax. The functions of this implementation are not meant to be used or produce any sensible results.
156#[derive(Copy, Clone)]
157pub struct EmptyApi {
158    /// Length of canonical addresses created with this API. Contracts should not make any assumtions
159    /// what this value is.
160    canonical_length: usize,
161}
162
163impl Default for EmptyApi {
164    fn default() -> Self {
165        EmptyApi {
166            canonical_length: CANONICAL_LENGTH,
167        }
168    }
169}
170
171impl Api for EmptyApi {
172    fn addr_validate(&self, human: &str) -> StdResult<Addr> {
173        self.addr_canonicalize(human).map(|_canonical| ())?;
174        Ok(Addr::unchecked(human))
175    }
176
177    fn addr_canonicalize(&self, human: &str) -> StdResult<CanonicalAddr> {
178        // Dummy input validation. This is more sophisticated for formats like bech32, where format and checksum are validated.
179        if human.len() < 3 {
180            return Err(StdError::msg("Invalid input: human address too short"));
181        }
182        if human.len() > self.canonical_length {
183            return Err(StdError::msg("Invalid input: human address too long"));
184        }
185
186        let mut out = Vec::from(human);
187
188        // pad to canonical length with NULL bytes
189        out.resize(self.canonical_length, 0x00);
190        // // content-dependent rotate followed by shuffle to destroy
191        // // the most obvious structure (https://github.com/CosmWasm/cosmwasm/issues/552)
192        // let rotate_by = digit_sum(&out) % self.canonical_length;
193        // out.rotate_left(rotate_by);
194        // for _ in 0..SHUFFLES_ENCODE {
195        //     out = riffle_shuffle(&out);
196        // }
197        Ok(out.into())
198    }
199
200    fn addr_humanize(&self, canonical: &CanonicalAddr) -> StdResult<Addr> {
201        if canonical.len() != self.canonical_length {
202            return Err(StdError::msg(
203                "Invalid input: canonical address length not correct",
204            ));
205        }
206
207        let tmp: Vec<u8> = canonical.clone().into();
208        // // Shuffle two more times which restored the original value (24 elements are back to original after 20 rounds)
209        // for _ in 0..SHUFFLES_DECODE {
210        //     tmp = riffle_shuffle(&tmp);
211        // }
212        // // Rotate back
213        // let rotate_by = digit_sum(&tmp) % self.canonical_length;
214        // tmp.rotate_right(rotate_by);
215        // Remove NULL bytes (i.e. the padding)
216        let trimmed = tmp.into_iter().filter(|&x| x != 0x00).collect();
217        // decode UTF-8 bytes into string
218        let human = String::from_utf8(trimmed)?;
219        Ok(Addr::unchecked(human))
220    }
221
222    fn secp256k1_verify(
223        &self,
224        _message_hash: &[u8],
225        _signature: &[u8],
226        _public_key: &[u8],
227    ) -> Result<bool, VerificationError> {
228        Err(VerificationError::unknown_err(0))
229    }
230
231    fn secp256k1_recover_pubkey(
232        &self,
233        _message_hash: &[u8],
234        _signature: &[u8],
235        _recovery_param: u8,
236    ) -> Result<Vec<u8>, RecoverPubkeyError> {
237        Err(RecoverPubkeyError::unknown_err(0))
238    }
239
240    fn ed25519_verify(
241        &self,
242        _message: &[u8],
243        _signature: &[u8],
244        _public_key: &[u8],
245    ) -> Result<bool, VerificationError> {
246        Ok(true)
247    }
248
249    fn ed25519_batch_verify(
250        &self,
251        _messages: &[&[u8]],
252        _signatures: &[&[u8]],
253        _public_keys: &[&[u8]],
254    ) -> Result<bool, VerificationError> {
255        Ok(true)
256    }
257
258    fn debug(&self, message: &str) {
259        println!("{}", message);
260    }
261}
262
263/// Empty Querier that is meant to conform the traits expected by the cosmwasm standard contract syntax. It should not be used whatsoever
264#[derive(Default)]
265pub struct EmptyQuerier {}
266
267impl Querier for EmptyQuerier {
268    fn raw_query(&self, _bin_request: &[u8]) -> cosmwasm_std::QuerierResult {
269        todo!()
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    // rgb_hex
278
279    #[test]
280    fn rgb_hex_formats_correctly() {
281        assert_eq!(rgb_hex(0, 0, 0), "#000000");
282        assert_eq!(rgb_hex(255, 255, 255), "#FFFFFF");
283        assert_eq!(rgb_hex(255, 0, 0), "#FF0000");
284        assert_eq!(rgb_hex(0, 128, 255), "#0080FF");
285    }
286
287    // derive_rgb_values
288
289    #[test]
290    fn derive_rgb_values_reads_last_three_bytes_reversed() {
291        // bytes: [0x01, 0x02, 0x03] → reversed → [0x03, 0x02, 0x01] → r=3, g=2, b=1
292        assert_eq!(derive_rgb_values("010203".to_string()), (3, 2, 1));
293    }
294
295    #[test]
296    fn derive_rgb_values_strips_0x_prefix() {
297        assert_eq!(
298            derive_rgb_values("0x010203".to_string()),
299            derive_rgb_values("010203".to_string())
300        );
301    }
302
303    #[test]
304    fn derive_rgb_values_pads_odd_length_input() {
305        // "abc" → padded to "0abc" → bytes [0x0a, 0xbc] → reversed [0xbc, 0x0a]
306        assert_eq!(derive_rgb_values("abc".to_string()), (0xbc, 0x0a, 0));
307    }
308
309    #[test]
310    fn derive_rgb_values_returns_zeros_for_invalid_hex() {
311        assert_eq!(derive_rgb_values("xyz".to_string()), (0, 0, 0));
312    }
313
314    #[test]
315    fn derive_rgb_values_returns_zeros_for_empty_input() {
316        assert_eq!(derive_rgb_values("".to_string()), (0, 0, 0));
317    }
318
319    #[test]
320    fn derive_rgb_values_uses_last_three_bytes_of_long_input() {
321        // 8 bytes: [0xaa, 0xbb, 0xcc, 0xdd, 0x11, 0x22, 0x33, 0x44]
322        // reversed: [0x44, 0x33, 0x22, 0x11, 0xdd, 0xcc, 0xbb, 0xaa]
323        // r=0x44, g=0x33, b=0x22
324        assert_eq!(
325            derive_rgb_values("aabbccdd11223344".to_string()),
326            (0x44, 0x33, 0x22)
327        );
328    }
329
330    // get_random_color
331
332    #[test]
333    fn get_random_color_returns_hash_prefixed_hex() {
334        let color = get_random_color("010203".to_string());
335        assert!(color.starts_with('#'));
336        assert_eq!(color.len(), 7);
337    }
338
339    #[test]
340    fn get_random_color_is_deterministic() {
341        let hash = "deadbeef".to_string();
342        assert_eq!(get_random_color(hash.clone()), get_random_color(hash));
343    }
344
345    // IdbStateDump / IdbStorage round-trip
346
347    #[test]
348    fn idb_state_dump_round_trips_through_storage() {
349        let mut storage = MemoryStorage::new();
350        storage.set(b"key1", b"value1");
351        storage.set(b"key2", b"value2");
352
353        let dump = IdbStateDump::from(storage);
354        assert_eq!(
355            dump.state_dump.get(b"key1".as_ref()),
356            Some(&b"value1".to_vec())
357        );
358        assert_eq!(
359            dump.state_dump.get(b"key2".as_ref()),
360            Some(&b"value2".to_vec())
361        );
362    }
363
364    #[test]
365    fn idb_storage_load_restores_all_keys() {
366        let mut storage = MemoryStorage::new();
367        storage.set(b"foo", b"bar");
368        storage.set(b"baz", b"qux");
369
370        let dump = IdbStateDump::from(storage);
371        let loaded = IdbStorage::load(dump);
372
373        assert_eq!(loaded.storage.get(b"foo"), Some(b"bar".to_vec()));
374        assert_eq!(loaded.storage.get(b"baz"), Some(b"qux".to_vec()));
375    }
376
377    #[test]
378    fn idb_state_dump_empty_storage_produces_empty_map() {
379        let storage = MemoryStorage::new();
380        let dump = IdbStateDump::from(storage);
381        assert!(dump.state_dump.is_empty());
382    }
383
384    // create_ownable_env
385
386    #[test]
387    fn create_env_produces_default_env() {
388        let env = create_env();
389        assert_eq!(env.block.height, 0);
390        assert_eq!(env.block.chain_id, "");
391    }
392
393    #[test]
394    fn create_ownable_env_sets_chain_id() {
395        let env = create_ownable_env("my-chain", None);
396        assert_eq!(env.block.chain_id, "my-chain");
397    }
398
399    #[test]
400    fn create_ownable_env_sets_timestamp() {
401        use cosmwasm_std::Timestamp;
402        let ts = Timestamp::from_seconds(12345);
403        let env = create_ownable_env("", Some(ts));
404        assert_eq!(env.block.time, ts);
405    }
406}
407
408// from github.com/CosmWasm/cw-nfts/blob/main/contracts/cw721-metadata-onchain
409#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug, Default)]
410/// Standard NFT metadata object.
411pub struct Metadata {
412    pub image: Option<String>,
413    pub image_data: Option<String>,
414    pub external_url: Option<String>,
415    pub description: Option<String>,
416    pub name: Option<String>,
417    // pub attributes: Option<Vec<Trait>>,
418    pub background_color: Option<String>,
419    pub animation_url: Option<String>,
420    pub youtube_url: Option<String>,
421}
422
423#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
424#[serde(rename_all = "snake_case")]
425/// External event emitted by ownable contracts.
426pub struct ExternalEventMsg {
427    // CAIP-2 format: <namespace + ":" + reference>
428    // e.g. ethereum: eip155:1
429    pub network: Option<String>,
430    pub event_type: String,
431    pub attributes: HashMap<String, String>,
432}
433
434#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
435/// Core ownable ownership metadata.
436pub struct OwnableInfo {
437    pub owner: Addr,
438    pub issuer: Addr,
439    pub ownable_type: Option<String>,
440}
441
442#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
443/// NFT reference used by ownables.
444pub struct NFT {
445    pub network: String, // eip155:1
446    pub id: Uint128,
447    pub address: String, // 0x341...
448    pub lock_service: Option<String>,
449}
450
451#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
452/// Response payload for ownable info queries.
453pub struct InfoResponse {
454    pub owner: Addr,
455    pub issuer: Addr,
456    pub nft: Option<NFT>,
457    pub ownable_type: Option<String>,
458}