Skip to main content

ownable_std/
lib.rs

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