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
19pub fn create_env() -> Env {
21 create_ownable_env(String::new(), None)
22}
23
24pub 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
39pub 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
56pub 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
73pub 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
89pub 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
112pub fn get_random_color(hash: String) -> String {
114 let (red, green, blue) = derive_rgb_values(hash);
115 rgb_hex(red, green, blue)
116}
117
118pub fn derive_rgb_values(hash: String) -> (u8, u8, u8) {
120 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
138pub fn rgb_hex(r: u8, g: u8, b: u8) -> String {
141 format!("#{:02X}{:02X}{:02X}", r, g, b)
142}
143
144pub struct IdbStorage {
146 pub storage: MemoryStorage,
147}
148
149impl IdbStorage {
150 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 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)]
169pub struct IdbStateDump {
171 #[serde_as(as = "Vec<(serde_with::Bytes, serde_with::Bytes)>")]
173 pub state_dump: HashMap<Vec<u8>, Vec<u8>>,
174}
175
176impl IdbStateDump {
177 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#[derive(Copy, Clone)]
190pub struct EmptyApi {
191 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 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 out.resize(self.canonical_length, 0x00);
223 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 let trimmed = tmp.into_iter().filter(|&x| x != 0x00).collect();
250 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#[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 #[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 #[test]
358 fn derive_rgb_values_reads_last_three_bytes_reversed() {
359 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 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 assert_eq!(
393 derive_rgb_values("aabbccdd11223344".to_string()),
394 (0x44, 0x33, 0x22)
395 );
396 }
397
398 #[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 #[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 #[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#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug, Default)]
478pub 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 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")]
493pub struct ExternalEventMsg {
495 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)]
503pub 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)]
511pub struct NFT {
513 pub network: String, pub id: Uint128,
515 pub address: String, pub lock_service: Option<String>,
517}
518
519#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
520pub struct InfoResponse {
522 pub owner: Addr,
523 pub issuer: Addr,
524 pub nft: Option<NFT>,
525 pub ownable_type: Option<String>,
526}