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