hmip721/
state.rs

1use std::any::type_name;
2
3use cosmwasm_std::{Api, BlockInfo, CanonicalAddr, ReadonlyStorage, StdError, StdResult, Storage};
4use cosmwasm_storage::{PrefixedStorage, ReadonlyPrefixedStorage};
5use hermit_toolkit_serialization::{Json, Bincode2, Serde};
6use hermit_toolkit_storage::{AppendStore, AppendStoreMut};
7use serde::{de::DeserializeOwned, Deserialize, Serialize};
8
9use crate::expiration::Expiration;
10use crate::msg::{Tx, TxAction};
11
12/// storage key for config
13pub const CONFIG_KEY: &[u8] = b"config";
14/// storage key for the BlockInfo when the last handle was executed
15pub const BLOCK_KEY: &[u8] = b"blockinfo";
16/// storage key for minters
17pub const MINTERS_KEY: &[u8] = b"minters";
18/// storage key for this contract's address
19pub const MY_ADDRESS_KEY: &[u8] = b"myaddr";
20/// storage key for prng seed
21pub const PRNG_SEED_KEY: &[u8] = b"prngseed";
22/// storage key for the contract instantiator
23pub const CREATOR_KEY: &[u8] = b"creator";
24/// storage key for the default RoyaltyInfo to use if none is supplied when minting
25pub const DEFAULT_ROYALTY_KEY: &[u8] = b"defaultroy";
26/// prefix for storage that maps ids to indices
27pub const PREFIX_MAP_TO_INDEX: &[u8] = b"map2idx";
28/// prefix for storage that maps indices to ids
29pub const PREFIX_MAP_TO_ID: &[u8] = b"idx2id";
30/// prefix for storage of token infos
31pub const PREFIX_INFOS: &[u8] = b"infos";
32/// prefix for the storage of public metadata
33pub const PREFIX_PUB_META: &[u8] = b"publicmeta";
34/// prefix for the storage of private metadata
35pub const PREFIX_PRIV_META: &[u8] = b"privatemeta";
36/// prefix for the storage of royalty information
37pub const PREFIX_ROYALTY_INFO: &[u8] = b"royalty";
38/// prefix for the storage of mint run information
39pub const PREFIX_MINT_RUN: &[u8] = b"mintrun";
40/// prefix for storage of txs
41pub const PREFIX_TXS: &[u8] = b"rawtxs";
42/// prefix for storage of tx ids
43pub const PREFIX_TX_IDS: &[u8] = b"txids";
44/// prefix for storage of owner's list of "all" permissions
45pub const PREFIX_ALL_PERMISSIONS: &[u8] = b"allpermissions";
46/// prefix for storage of owner's list of tokens permitted to addresses
47pub const PREFIX_AUTHLIST: &[u8] = b"authlist";
48/// prefix for storage of an address' ownership prvicacy
49pub const PREFIX_OWNER_PRIV: &[u8] = b"ownerpriv";
50/// prefix for storage of viewing keys
51pub const PREFIX_VIEW_KEY: &[u8] = b"viewkeys";
52/// prefix for the storage of the code hashes of contract's that have implemented ReceiveNft
53pub const PREFIX_RECEIVERS: &[u8] = b"receivers";
54/// prefix for the storage of mint run numbers
55pub const PREFIX_MINT_RUN_NUM: &[u8] = b"runnum";
56/// prefix for the storage of revoked permits
57pub const PREFIX_REVOKED_PERMITS: &str = "revoke";
58
59/// Token contract config
60#[derive(Serialize, Debug, Deserialize, Clone, PartialEq)]
61pub struct Config {
62    /// name of token contract
63    pub name: String,
64    /// token contract symbol
65    pub symbol: String,
66    /// admin address
67    pub admin: CanonicalAddr,
68    /// count of mint ops
69    pub mint_cnt: u32,
70    /// count of tx
71    pub tx_cnt: u64,
72    /// token count
73    pub token_cnt: u32,
74    /// contract status
75    pub status: u8,
76    /// are token IDs/count public
77    pub token_supply_is_public: bool,
78    /// is ownership public
79    pub owner_is_public: bool,
80    /// is sealed metadata enabled
81    pub sealed_metadata_is_enabled: bool,
82    /// should Reveal unwrap to private metadata
83    pub unwrap_to_private: bool,
84    /// is a minter permitted to update a token's metadata
85    pub minter_may_update_metadata: bool,
86    /// is the token's owner permitted to update the token's metadata
87    pub owner_may_update_metadata: bool,
88    /// is burn enabled
89    pub burn_is_enabled: bool,
90}
91
92/// tx type and specifics
93#[derive(Serialize, Deserialize, Clone, Debug)]
94#[serde(rename_all = "snake_case")]
95pub enum StoredTxAction {
96    /// transferred token ownership
97    Transfer {
98        /// previous owner
99        from: CanonicalAddr,
100        /// optional sender if not owner
101        sender: Option<CanonicalAddr>,
102        /// new owner
103        recipient: CanonicalAddr,
104    },
105    /// minted new token
106    Mint {
107        /// minter's address
108        minter: CanonicalAddr,
109        /// token's first owner
110        recipient: CanonicalAddr,
111    },
112    /// burned a token
113    Burn {
114        /// previous owner
115        owner: CanonicalAddr,
116        /// burner's address if not owner
117        burner: Option<CanonicalAddr>,
118    },
119}
120
121/// tx in storage
122#[derive(Serialize, Deserialize, Clone, Debug)]
123#[serde(rename_all = "snake_case")]
124pub struct StoredTx {
125    /// tx id
126    pub tx_id: u64,
127    /// the block containing this tx
128    pub block_height: u64,
129    /// the time (in seconds since 01/01/1970) of the block containing this tx
130    pub block_time: u64,
131    /// token id
132    pub token_id: String,
133    /// tx type and specifics
134    pub action: StoredTxAction,
135    /// optional memo
136    pub memo: Option<String>,
137}
138
139impl StoredTx {
140    /// Returns StdResult<Tx> from converting a stored tx to a displayable tx
141    ///
142    /// # Arguments
143    ///
144    /// * `api` - a reference to the Api used to convert human and canonical addresses
145    pub fn into_humanized<A: Api>(self, api: &A) -> StdResult<Tx> {
146        let action = match self.action {
147            StoredTxAction::Transfer {
148                from,
149                sender,
150                recipient,
151            } => {
152                let sndr = if let Some(s) = sender {
153                    Some(api.human_address(&s)?)
154                } else {
155                    None
156                };
157                TxAction::Transfer {
158                    from: api.human_address(&from)?,
159                    sender: sndr,
160                    recipient: api.human_address(&recipient)?,
161                }
162            }
163            StoredTxAction::Mint { minter, recipient } => TxAction::Mint {
164                minter: api.human_address(&minter)?,
165                recipient: api.human_address(&recipient)?,
166            },
167            StoredTxAction::Burn { owner, burner } => {
168                let bnr = if let Some(b) = burner {
169                    Some(api.human_address(&b)?)
170                } else {
171                    None
172                };
173                TxAction::Burn {
174                    owner: api.human_address(&owner)?,
175                    burner: bnr,
176                }
177            }
178        };
179        let tx = Tx {
180            tx_id: self.tx_id,
181            block_height: self.block_height,
182            block_time: self.block_time,
183            token_id: self.token_id,
184            action,
185            memo: self.memo,
186        };
187
188        Ok(tx)
189    }
190}
191
192/// Returns StdResult<()> after storing tx
193///
194/// # Arguments
195///
196/// * `storage` - a mutable reference to the storage this item should go to
197/// * `config` - a mutable reference to the contract Config
198/// * `block` - a reference to the current BlockInfo
199/// * `token_id` - token id being minted
200/// * `from` - the previouis owner's address
201/// * `sender` - optional address that sent the token
202/// * `recipient` - the recipient's address
203/// * `memo` - optional memo for the tx
204#[allow(clippy::too_many_arguments)]
205pub fn store_transfer<S: Storage>(
206    storage: &mut S,
207    config: &mut Config,
208    block: &BlockInfo,
209    token_id: String,
210    from: CanonicalAddr,
211    sender: Option<CanonicalAddr>,
212    recipient: CanonicalAddr,
213    memo: Option<String>,
214) -> StdResult<()> {
215    let action = StoredTxAction::Transfer {
216        from,
217        sender,
218        recipient,
219    };
220    let tx = StoredTx {
221        tx_id: config.tx_cnt,
222        block_height: block.height,
223        block_time: block.time,
224        token_id,
225        action,
226        memo,
227    };
228    let mut tx_store = PrefixedStorage::new(PREFIX_TXS, storage);
229    json_save(&mut tx_store, &config.tx_cnt.to_le_bytes(), &tx)?;
230    if let StoredTxAction::Transfer {
231        from,
232        sender,
233        recipient,
234    } = tx.action
235    {
236        append_tx_for_addr(storage, config.tx_cnt, &from)?;
237        append_tx_for_addr(storage, config.tx_cnt, &recipient)?;
238        if let Some(sndr) = sender.as_ref() {
239            if *sndr != recipient {
240                append_tx_for_addr(storage, config.tx_cnt, sndr)?;
241            }
242        }
243    }
244    config.tx_cnt += 1;
245    Ok(())
246}
247
248/// Returns StdResult<()> after storing tx
249///
250/// # Arguments
251///
252/// * `storage` - a mutable reference to the storage this item should go to
253/// * `config` - a mutable reference to the contract Config
254/// * `block` - a reference to the current BlockInfo
255/// * `token_id` - token id being minted
256/// * `minter` - the minter's address
257/// * `recipient` - the recipient's address
258/// * `memo` - optional memo for the tx
259pub fn store_mint<S: Storage>(
260    storage: &mut S,
261    config: &mut Config,
262    block: &BlockInfo,
263    token_id: String,
264    minter: CanonicalAddr,
265    recipient: CanonicalAddr,
266    memo: Option<String>,
267) -> StdResult<()> {
268    let action = StoredTxAction::Mint { minter, recipient };
269    let tx = StoredTx {
270        tx_id: config.tx_cnt,
271        block_height: block.height,
272        block_time: block.time,
273        token_id,
274        action,
275        memo,
276    };
277    let mut tx_store = PrefixedStorage::new(PREFIX_TXS, storage);
278    json_save(&mut tx_store, &config.tx_cnt.to_le_bytes(), &tx)?;
279    if let StoredTxAction::Mint { minter, recipient } = tx.action {
280        append_tx_for_addr(storage, config.tx_cnt, &recipient)?;
281        if recipient != minter {
282            append_tx_for_addr(storage, config.tx_cnt, &minter)?;
283        }
284    }
285    config.tx_cnt += 1;
286    Ok(())
287}
288
289/// Returns StdResult<()> after storing tx
290///
291/// # Arguments
292///
293/// * `storage` - a mutable reference to the storage this item should go to
294/// * `config` - a mutable reference to the contract Config
295/// * `block` - a reference to the current BlockInfo
296/// * `token_id` - token id being minted
297/// * `owner` - the previous owner's address
298/// * `burner` - optional address that burnt the token
299/// * `memo` - optional memo for the tx
300pub fn store_burn<S: Storage>(
301    storage: &mut S,
302    config: &mut Config,
303    block: &BlockInfo,
304    token_id: String,
305    owner: CanonicalAddr,
306    burner: Option<CanonicalAddr>,
307    memo: Option<String>,
308) -> StdResult<()> {
309    let action = StoredTxAction::Burn { owner, burner };
310    let tx = StoredTx {
311        tx_id: config.tx_cnt,
312        block_height: block.height,
313        block_time: block.time,
314        token_id,
315        action,
316        memo,
317    };
318    let mut tx_store = PrefixedStorage::new(PREFIX_TXS, storage);
319    json_save(&mut tx_store, &config.tx_cnt.to_le_bytes(), &tx)?;
320    if let StoredTxAction::Burn { owner, burner } = tx.action {
321        append_tx_for_addr(storage, config.tx_cnt, &owner)?;
322        if let Some(bnr) = burner.as_ref() {
323            append_tx_for_addr(storage, config.tx_cnt, bnr)?;
324        }
325    }
326    config.tx_cnt += 1;
327    Ok(())
328}
329
330/// Returns StdResult<()> after saving tx id
331///
332/// # Arguments
333///
334/// * `storage` - a mutable reference to the storage this item should go to
335/// * `tx_id` - the tx id to store
336/// * `address` - a reference to the address for which to store this tx id
337fn append_tx_for_addr<S: Storage>(
338    storage: &mut S,
339    tx_id: u64,
340    address: &CanonicalAddr,
341) -> StdResult<()> {
342    let mut store = PrefixedStorage::multilevel(&[PREFIX_TX_IDS, address.as_slice()], storage);
343    let mut store = AppendStoreMut::attach_or_create(&mut store)?;
344    store.push(&tx_id)
345}
346
347/// Returns StdResult<(Vec<Tx>, u64)> of the txs to display and the total count of txs
348///
349/// # Arguments
350///
351/// * `api` - a reference to the Api used to convert human and canonical addresses
352/// * `storage` - a reference to the contract's storage
353/// * `address` - a reference to the address whose txs to display
354/// * `page` - page to start displaying
355/// * `page_size` - number of txs per page
356pub fn get_txs<A: Api, S: ReadonlyStorage>(
357    api: &A,
358    storage: &S,
359    address: &CanonicalAddr,
360    page: u32,
361    page_size: u32,
362) -> StdResult<(Vec<Tx>, u64)> {
363    let id_store =
364        ReadonlyPrefixedStorage::multilevel(&[PREFIX_TX_IDS, address.as_slice()], storage);
365
366    // Try to access the storage of tx ids for the account.
367    // If it doesn't exist yet, return an empty list of txs.
368    let id_store = if let Some(result) = AppendStore::<u64, _>::attach(&id_store) {
369        result?
370    } else {
371        return Ok((vec![], 0));
372    };
373    let count = id_store.len() as u64;
374    // access tx storage
375    let tx_store = ReadonlyPrefixedStorage::new(PREFIX_TXS, storage);
376    // Take `page_size` txs starting from the latest tx, potentially skipping `page * page_size`
377    // txs from the start.
378    let txs: StdResult<Vec<Tx>> = id_store
379        .iter()
380        .rev()
381        .skip((page * page_size) as usize)
382        .take(page_size as usize)
383        .map(|id| {
384            id.map(|id| {
385                json_load(&tx_store, &id.to_le_bytes())
386                    .and_then(|tx: StoredTx| tx.into_humanized(api))
387            })
388            .and_then(|x| x)
389        })
390        .collect();
391
392    txs.map(|t| (t, count))
393}
394
395/// permission to view token info/transfer tokens
396#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
397pub struct Permission {
398    /// permitted address
399    pub address: CanonicalAddr,
400    /// list of permission expirations for this address
401    pub expirations: [Option<Expiration>; 3],
402}
403
404/// permission types
405#[derive(Serialize, Deserialize, Debug)]
406pub enum PermissionType {
407    ViewOwner,
408    ViewMetadata,
409    Transfer,
410}
411
412impl PermissionType {
413    /// Returns usize representation of the enum variant
414    pub fn to_usize(&self) -> usize {
415        match self {
416            PermissionType::ViewOwner => 0,
417            PermissionType::ViewMetadata => 1,
418            PermissionType::Transfer => 2,
419        }
420    }
421
422    /// returns the number of permission types
423    pub fn num_types(&self) -> usize {
424        3
425    }
426}
427
428/// list of one owner's tokens authorized to a single address
429#[derive(Serialize, Deserialize, Debug)]
430pub struct AuthList {
431    /// whitelisted address
432    pub address: CanonicalAddr,
433    /// lists of tokens address has access to
434    pub tokens: [Vec<u32>; 3],
435}
436
437/// a contract's code hash and whether they implement BatchReceiveNft
438#[derive(Serialize, Deserialize, Debug, Clone)]
439pub struct ReceiveRegistration {
440    /// code hash of the contract
441    pub code_hash: String,
442    /// true if the contract implements BatchReceiveNft
443    pub impl_batch: bool,
444}
445
446/// Returns StdResult<()> resulting from saving an item to storage
447///
448/// # Arguments
449///
450/// * `storage` - a mutable reference to the storage this item should go to
451/// * `key` - a byte slice representing the key to access the stored item
452/// * `value` - a reference to the item to store
453pub fn save<T: Serialize, S: Storage>(storage: &mut S, key: &[u8], value: &T) -> StdResult<()> {
454    storage.set(key, &Bincode2::serialize(value)?);
455    Ok(())
456}
457
458/// Removes an item from storage
459///
460/// # Arguments
461///
462/// * `storage` - a mutable reference to the storage this item is in
463/// * `key` - a byte slice representing the key that accesses the stored item
464pub fn remove<S: Storage>(storage: &mut S, key: &[u8]) {
465    storage.remove(key);
466}
467
468/// Returns StdResult<T> from retrieving the item with the specified key.  Returns a
469/// StdError::NotFound if there is no item with that key
470///
471/// # Arguments
472///
473/// * `storage` - a reference to the storage this item is in
474/// * `key` - a byte slice representing the key that accesses the stored item
475pub fn load<T: DeserializeOwned, S: ReadonlyStorage>(storage: &S, key: &[u8]) -> StdResult<T> {
476    Bincode2::deserialize(
477        &storage
478            .get(key)
479            .ok_or_else(|| StdError::not_found(type_name::<T>()))?,
480    )
481}
482
483/// Returns StdResult<Option<T>> from retrieving the item with the specified key.
484/// Returns Ok(None) if there is no item with that key
485///
486/// # Arguments
487///
488/// * `storage` - a reference to the storage this item is in
489/// * `key` - a byte slice representing the key that accesses the stored item
490pub fn may_load<T: DeserializeOwned, S: ReadonlyStorage>(
491    storage: &S,
492    key: &[u8],
493) -> StdResult<Option<T>> {
494    match storage.get(key) {
495        Some(value) => Bincode2::deserialize(&value).map(Some),
496        None => Ok(None),
497    }
498}
499
500/// Returns StdResult<()> resulting from saving an item to storage using Json (de)serialization
501/// because bincode2 annoyingly uses a float op when deserializing an enum
502///
503/// # Arguments
504///
505/// * `storage` - a mutable reference to the storage this item should go to
506/// * `key` - a byte slice representing the key to access the stored item
507/// * `value` - a reference to the item to store
508pub fn json_save<T: Serialize, S: Storage>(
509    storage: &mut S,
510    key: &[u8],
511    value: &T,
512) -> StdResult<()> {
513    storage.set(key, &Json::serialize(value)?);
514    Ok(())
515}
516
517/// Returns StdResult<T> from retrieving the item with the specified key using Json
518/// (de)serialization because bincode2 annoyingly uses a float op when deserializing an enum.  
519/// Returns a StdError::NotFound if there is no item with that key
520///
521/// # Arguments
522///
523/// * `storage` - a reference to the storage this item is in
524/// * `key` - a byte slice representing the key that accesses the stored item
525pub fn json_load<T: DeserializeOwned, S: ReadonlyStorage>(storage: &S, key: &[u8]) -> StdResult<T> {
526    Json::deserialize(
527        &storage
528            .get(key)
529            .ok_or_else(|| StdError::not_found(type_name::<T>()))?,
530    )
531}
532
533/// Returns StdResult<Option<T>> from retrieving the item with the specified key using Json
534/// (de)serialization because bincode2 annoyingly uses a float op when deserializing an enum.
535/// Returns Ok(None) if there is no item with that key
536///
537/// # Arguments
538///
539/// * `storage` - a reference to the storage this item is in
540/// * `key` - a byte slice representing the key that accesses the stored item
541pub fn json_may_load<T: DeserializeOwned, S: ReadonlyStorage>(
542    storage: &S,
543    key: &[u8],
544) -> StdResult<Option<T>> {
545    match storage.get(key) {
546        Some(value) => Json::deserialize(&value).map(Some),
547        None => Ok(None),
548    }
549}