odra_modules/cep78/
token.rs

1#![allow(clippy::too_many_arguments)]
2use odra::named_keys::single_value_storage;
3
4use super::{
5    constants::{PREFIX_PAGE_DICTIONARY, TRANSFER_FILTER_CONTRACT},
6    data::CollectionData,
7    error::CEP78Error,
8    events::{
9        Approval, ApprovalForAll, ApprovalRevoked, Burn, MetadataUpdated, Mint, RevokedForAll,
10        Transfer, VariablesSet
11    },
12    metadata::Metadata,
13    modalities::{
14        BurnMode, EventsMode, MetadataMutability, MintingMode, NFTHolderMode, NFTIdentifierMode,
15        NFTKind, NFTMetadataKind, OwnerReverseLookupMode, OwnershipMode, TokenIdentifier,
16        TransferFilterContractResult, WhitelistMode
17    },
18    reverse_lookup::{ReverseLookup, PAGE_SIZE},
19    settings::Settings,
20    utils,
21    whitelist::ACLWhitelist
22};
23use odra::{
24    args::Maybe, casper_event_standard::EventInstance, casper_types::bytesrepr::ToBytes,
25    prelude::*, ContractRef
26};
27
28single_value_storage!(
29    Cep78TransferFilterContract,
30    Address,
31    TRANSFER_FILTER_CONTRACT
32);
33
34/// CEP-78 is a standard for non-fungible tokens (NFTs) on the Casper network.
35/// It defines a set of interfaces that allow for the creation, management, and
36/// transfer of NFTs. The standard is designed to be flexible and modular, allowing
37/// developers to customize the behavior of their NFTs to suit their specific needs.
38/// The CEP-78 standard is inspired by the ERC-721 standard for NFTs on the Ethereum network.
39/// The CEP-78 standard is designed to be simple and easy to use, while still providing
40/// powerful features for developers to build on.
41///
42/// A list of mandatory init arguments:
43/// - `collection_name`: The name of the NFT collection.
44/// - `collection_symbol`: The symbol of the NFT collection.
45/// - `total_token_supply`: The total number of tokens that can be minted in the collection.
46/// - `ownership_mode`: The ownership mode of the collection. See [OwnershipMode] for more details.
47/// - `nft_kind`: The kind of NFTs in the collection. See [NFTKind] for more details.
48/// - `nft_identifier_mode`: The identifier mode of the NFTs in the collection. See [NFTIdentifierMode] for more details.
49/// - `nft_metadata_kind`: The kind of metadata associated with the NFTs in the collection. See [NFTMetadataKind] for more details.
50/// - `metadata_mutability`: The mutability of the metadata associated with the NFTs in the collection. See [MetadataMutability] for more details.
51#[odra::module(
52    version = "1.5.1",
53    events = [Approval, ApprovalForAll, ApprovalRevoked, Burn, MetadataUpdated, Mint, RevokedForAll, Transfer, VariablesSet],
54    errors = CEP78Error
55)]
56pub struct Cep78 {
57    data: SubModule<CollectionData>,
58    metadata: SubModule<Metadata>,
59    settings: SubModule<Settings>,
60    whitelist: SubModule<ACLWhitelist>,
61    reverse_lookup: SubModule<ReverseLookup>,
62    transfer_filter_contract: SubModule<Cep78TransferFilterContract>
63}
64
65#[odra::module]
66impl Cep78 {
67    /// Initializes the module.
68    pub fn init(
69        &mut self,
70        collection_name: String,
71        collection_symbol: String,
72        total_token_supply: u64,
73        ownership_mode: OwnershipMode,
74        nft_kind: NFTKind,
75        identifier_mode: NFTIdentifierMode,
76        nft_metadata_kind: NFTMetadataKind,
77        metadata_mutability: MetadataMutability,
78        receipt_name: String,
79        allow_minting: Maybe<bool>,
80        minting_mode: Maybe<MintingMode>,
81        holder_mode: Maybe<NFTHolderMode>,
82        whitelist_mode: Maybe<WhitelistMode>,
83        acl_whitelist: Maybe<Vec<Address>>,
84        json_schema: Maybe<String>,
85        burn_mode: Maybe<BurnMode>,
86        operator_burn_mode: Maybe<bool>,
87        owner_reverse_lookup_mode: Maybe<OwnerReverseLookupMode>,
88        events_mode: Maybe<EventsMode>,
89        transfer_filter_contract_contract: Maybe<Address>,
90        additional_required_metadata: Maybe<Vec<NFTMetadataKind>>,
91        optional_metadata: Maybe<Vec<NFTMetadataKind>>
92    ) {
93        let installer = self.caller();
94        let minting_mode = minting_mode.unwrap_or_default();
95        let owner_reverse_lookup_mode = owner_reverse_lookup_mode.unwrap_or_default();
96        let acl_white_list = acl_whitelist.unwrap_or_default();
97        let whitelist_mode = whitelist_mode.unwrap_or_default();
98        let json_schema = json_schema.unwrap_or_default();
99        let is_whitelist_empty = acl_white_list.is_empty();
100
101        // Revert if minting mode is not ACL and acl list is not empty
102        if MintingMode::Acl != minting_mode && !is_whitelist_empty {
103            self.revert(CEP78Error::InvalidMintingMode)
104        }
105
106        // Revert if minting mode is ACL or holder_mode is contracts and acl list is locked and empty
107        if MintingMode::Acl == minting_mode
108            && is_whitelist_empty
109            && WhitelistMode::Locked == whitelist_mode
110        {
111            self.revert(CEP78Error::EmptyACLWhitelist)
112        }
113
114        // NOTE: It is commented out to allow having mutable metadata with hash identifier.
115        // NOTE: It's left for future reference.
116        // if identifier_mode == NFTIdentifierMode::Hash
117        //     && metadata_mutability == MetadataMutability::Mutable
118        // {
119        //     self.revert(CEP78Error::InvalidMetadataMutability)
120        // }
121
122        if ownership_mode == OwnershipMode::Minter
123            && minting_mode == MintingMode::Installer
124            && owner_reverse_lookup_mode == OwnerReverseLookupMode::Complete
125        {
126            self.revert(CEP78Error::InvalidReportingMode)
127        }
128
129        // Check if schema is missing before checking its validity
130        if nft_metadata_kind == NFTMetadataKind::CustomValidated && json_schema.is_empty() {
131            self.revert(CEP78Error::MissingJsonSchema)
132        }
133
134        // OwnerReverseLookup TransfersOnly mode should be Transferable
135        if OwnerReverseLookupMode::TransfersOnly == owner_reverse_lookup_mode
136            && OwnershipMode::Transferable != ownership_mode
137        {
138            self.revert(CEP78Error::OwnerReverseLookupModeNotTransferable)
139        }
140
141        if ownership_mode != OwnershipMode::Transferable
142            && transfer_filter_contract_contract.is_some()
143        {
144            self.revert(CEP78Error::TransferFilterContractNeedsTransferableMode)
145        }
146
147        self.data.init(
148            collection_name,
149            collection_symbol,
150            total_token_supply,
151            installer
152        );
153        self.settings.init(
154            allow_minting.unwrap_or(true),
155            minting_mode,
156            ownership_mode,
157            nft_kind,
158            holder_mode.unwrap_or_default(),
159            burn_mode.unwrap_or_default(),
160            events_mode.unwrap_or_default(),
161            operator_burn_mode.unwrap_or_default()
162        );
163
164        self.reverse_lookup
165            .init(owner_reverse_lookup_mode, receipt_name, total_token_supply);
166
167        self.whitelist.init(acl_white_list.clone(), whitelist_mode);
168
169        self.metadata.init(
170            nft_metadata_kind,
171            additional_required_metadata,
172            optional_metadata,
173            metadata_mutability,
174            identifier_mode,
175            json_schema
176        );
177
178        if let Maybe::Some(transfer_filter_contract_contract) = transfer_filter_contract_contract {
179            self.transfer_filter_contract
180                .set(transfer_filter_contract_contract);
181        }
182    }
183
184    /// Exposes all variables that can be changed by managing account post
185    /// installation. Meant to be called by the managing account (`Installer`)
186    /// if a variable needs to be changed.
187    /// By switching `allow_minting` to false minting is paused.
188    pub fn set_variables(
189        &mut self,
190        allow_minting: Maybe<bool>,
191        acl_whitelist: Maybe<Vec<Address>>,
192        operator_burn_mode: Maybe<bool>
193    ) {
194        let installer = self.data.installer();
195        self.ensure_caller(installer);
196
197        if let Maybe::Some(allow_minting) = allow_minting {
198            self.settings.set_allow_minting(allow_minting);
199        }
200
201        if let Maybe::Some(operator_burn_mode) = operator_burn_mode {
202            self.settings.set_operator_burn_mode(operator_burn_mode);
203        }
204
205        self.whitelist.update(acl_whitelist);
206        self.emit_ces_event(VariablesSet::new());
207    }
208
209    /// Mints a new token with provided metadata.
210    /// Reverts with [CEP78Error::MintingIsPaused] error if `allow_minting` is false.
211    /// When a token is minted, the calling account is listed as its owner and the token is
212    /// automatically assigned an `u64` ID equal to the current `number_of_minted_tokens`.
213    /// Before minting, the token checks if `number_of_minted_tokens`
214    /// exceeds the `total_token_supply`. If so, it reverts the minting with an error
215    /// [CEP78Error::TokenSupplyDepleted]. The `mint` function also checks whether the calling account
216    /// is the managing account (the installer) If not, and if `public_minting` is set to
217    /// false, it reverts with the error [CEP78Error::InvalidAccount].
218    /// After minting is successful the number_of_minted_tokens is incremented by one.
219    pub fn mint(
220        &mut self,
221        token_owner: Address,
222        token_meta_data: String,
223        token_hash: Maybe<String>
224    ) {
225        if !self.settings.allow_minting() {
226            self.revert(CEP78Error::MintingIsPaused);
227        }
228
229        let total_token_supply = self.data.total_token_supply();
230        let minted_tokens_count = self.data.number_of_minted_tokens();
231
232        if minted_tokens_count >= total_token_supply {
233            self.revert(CEP78Error::TokenSupplyDepleted);
234        }
235
236        let minting_mode = self.settings.minting_mode();
237        let caller = self.verified_caller();
238
239        if MintingMode::Installer == minting_mode {
240            match caller {
241                Address::Account(_) => {
242                    let installer_account = self.data.installer();
243                    if caller != installer_account {
244                        self.revert(CEP78Error::InvalidMinter)
245                    }
246                }
247                _ => self.revert(CEP78Error::InvalidKey)
248            }
249        }
250
251        if MintingMode::Acl == minting_mode && !self.whitelist.is_whitelisted(&caller) {
252            match caller {
253                Address::Contract(_) => self.revert(CEP78Error::UnlistedContractHash),
254                Address::Account(_) => self.revert(CEP78Error::InvalidMinter)
255            }
256        }
257
258        let identifier_mode = self.metadata.get_identifier_mode();
259        let optional_token_hash: String = token_hash.unwrap_or_default();
260        let token_identifier: TokenIdentifier = match identifier_mode {
261            NFTIdentifierMode::Ordinal => TokenIdentifier::Index(minted_tokens_count),
262            NFTIdentifierMode::Hash => TokenIdentifier::Hash(if optional_token_hash.is_empty() {
263                let hash = self.__env.hash(token_meta_data.clone());
264                base16::encode_lower(&hash)
265            } else {
266                optional_token_hash
267            })
268        };
269        let token_id = token_identifier.to_string();
270
271        // Check if token already exists.
272        if self.token_exists_by_hash(&token_id) {
273            self.revert(CEP78Error::DuplicateIdentifier)
274        }
275
276        self.metadata.update_or_revert(&token_meta_data, &token_id);
277
278        let token_owner = if self.is_transferable_or_assigned() {
279            token_owner
280        } else {
281            caller
282        };
283
284        self.data.set_owner(&token_id, token_owner);
285        self.data.set_issuer(&token_id, caller);
286        self.data.mark_not_burnt(&token_id);
287
288        self.data.increment_counter(&token_owner);
289        self.data.increment_number_of_minted_tokens();
290
291        self.emit_ces_event(Mint::new(token_owner, token_id.clone(), token_meta_data));
292
293        self.reverse_lookup.on_mint(&token_owner, &token_identifier)
294    }
295
296    /// Burns the token with provided `token_id` argument, after which it is no
297    /// longer possible to transfer it.
298    /// Looks up the owner of the supplied token_id arg. If caller is not owner we revert with
299    /// error [CEP78Error::InvalidTokenOwner]. If the token id is invalid (e.g. out of bounds) it reverts
300    /// with error [CEP78Error::InvalidTokenIdentifier]. If the token is listed as already burnt we revert with
301    /// error [CEP78Error::PreviouslyBurntToken]. If not the token is then registered as burnt.
302    pub fn burn(&mut self, token_id: Maybe<u64>, token_hash: Maybe<String>) {
303        let token_identifier = self.token_identifier(token_id, token_hash);
304        let token_id = token_identifier.to_string();
305
306        let token_owner = self.owner_of_by_id(&token_id);
307        let caller = self.__env.caller();
308
309        let is_owner = token_owner == caller;
310        let is_operator = if !is_owner {
311            self.data.operator(token_owner, caller)
312        } else {
313            false
314        };
315
316        if !is_owner && !is_operator {
317            self.revert(CEP78Error::InvalidTokenOwner)
318        };
319
320        // NOTE: Bellow code is almost the same as in `burn_token_unchecked`
321        // function, but it is copied here to avoid checking owner twice.
322        self.ensure_burnable();
323        self.ensure_not_burned(&token_id);
324        self.data.mark_burnt(&token_id);
325        self.data.decrement_counter(&token_owner);
326        self.data.decrement_number_of_minted_tokens();
327        self.emit_ces_event(Burn::new(token_owner, token_id, caller));
328
329        self.reverse_lookup.on_burn(&token_owner, &token_identifier);
330    }
331
332    /// Transfers ownership of the token from one account to another.
333    /// It looks up the owner of the supplied token_id arg. Reverts if the token is already burnt,
334    /// `token_id` is invalid, or if caller is not owner nor an approved account nor operator.
335    /// If token id is invalid it reverts with error [CEP78Error::InvalidTokenIdentifier].
336    pub fn transfer(
337        &mut self,
338        token_id: Maybe<u64>,
339        token_hash: Maybe<String>,
340        source_key: Address,
341        target_key: Address
342    ) {
343        self.ensure_not_minter_or_assigned();
344
345        let token_identifier = self.checked_token_identifier(token_id, token_hash);
346        let token_id = token_identifier.to_string();
347        self.ensure_not_burned(&token_id);
348        self.ensure_owner(&token_id, &source_key);
349
350        let caller = self.caller();
351        let owner = self.owner_of_by_id(&token_id);
352        let is_owner = owner == caller;
353
354        let is_approved = !is_owner
355            && match self.data.approved(&token_id) {
356                Some(maybe_approved) => caller == maybe_approved,
357                _ => false
358            };
359
360        let is_operator = if !is_owner && !is_approved {
361            self.data.operator(source_key, caller)
362        } else {
363            false
364        };
365
366        if let Some(filter_contract) = self.transfer_filter_contract.get() {
367            let result = TransferFilterContractContractRef::new(self.env(), filter_contract)
368                .can_transfer(source_key, target_key, token_identifier.clone());
369
370            if TransferFilterContractResult::DenyTransfer == result {
371                self.revert(CEP78Error::TransferFilterContractDenied);
372            }
373        }
374
375        if !is_owner && !is_approved && !is_operator {
376            self.revert(CEP78Error::InvalidTokenOwner);
377        }
378
379        match self.data.owner_of(&token_id) {
380            Some(token_actual_owner) => {
381                if token_actual_owner != source_key {
382                    self.revert(CEP78Error::InvalidTokenOwner)
383                }
384            }
385            None => self.revert(CEP78Error::MissingOwnerTokenIdentifierKey)
386        }
387
388        let spender = if caller == owner { None } else { Some(caller) };
389        self.transfer_unchecked(token_id, source_key, spender, target_key);
390
391        self.reverse_lookup
392            .on_transfer(&token_identifier, &source_key, &target_key)
393    }
394
395    /// Approves another token holder (an approved account) to transfer tokens. It
396    /// reverts if token_id is invalid, if caller is not the owner nor operator, if token has already
397    /// been burnt, or if caller tries to approve themselves as an approved account.
398    pub fn approve(&mut self, spender: Address, token_id: Maybe<u64>, token_hash: Maybe<String>) {
399        self.ensure_not_minter_or_assigned();
400
401        let caller = self.caller();
402        let token_identifier = self.checked_token_identifier(token_id, token_hash);
403        let token_id = token_identifier.to_string();
404
405        let owner = self.owner_of_by_id(&token_id);
406
407        let is_owner = caller == owner;
408        let is_operator = !is_owner && self.data.operator(owner, caller);
409
410        if !is_owner && !is_operator {
411            self.revert(CEP78Error::InvalidTokenOwner);
412        }
413
414        self.ensure_not_burned(&token_id);
415
416        self.ensure_not_caller(spender);
417        self.data.approve(&token_id, spender);
418        self.emit_ces_event(Approval::new(owner, spender, token_id));
419    }
420
421    /// Revokes an approved account to transfer tokens. It reverts
422    /// if token_id is invalid, if caller is not the owner, if token has already
423    /// been burnt, if caller tries to approve itself.
424    pub fn revoke(&mut self, token_id: Maybe<u64>, token_hash: Maybe<String>) {
425        self.ensure_not_minter_or_assigned();
426
427        let caller = self.caller();
428        let token_identifier = self.checked_token_identifier(token_id, token_hash);
429        let token_id = token_identifier.to_string();
430
431        let owner = self.owner_of_by_id(&token_id);
432        let is_owner = caller == owner;
433        let is_operator = !is_owner && self.data.operator(owner, caller);
434
435        if !is_owner && !is_operator {
436            self.revert(CEP78Error::InvalidTokenOwner);
437        }
438
439        self.ensure_not_burned(&token_id);
440        self.data.revoke(&token_id);
441
442        self.emit_ces_event(ApprovalRevoked::new(owner, token_id));
443    }
444
445    /// Approves all tokens owned by the caller and future to another token holder
446    /// (an operator) to transfer tokens. It reverts if token_id is invalid, if caller is not the
447    /// owner, if caller tries to approve itself as an operator.
448    pub fn set_approval_for_all(&mut self, approve_all: bool, operator: Address) {
449        self.ensure_not_minter_or_assigned();
450        self.ensure_not_caller(operator);
451
452        let caller = self.caller();
453        self.data.set_operator(caller, operator, approve_all);
454
455        if let EventsMode::CES = self.settings.events_mode() {
456            if approve_all {
457                self.__env.emit_event(ApprovalForAll::new(caller, operator));
458            } else {
459                self.__env.emit_event(RevokedForAll::new(caller, operator));
460            }
461        }
462    }
463
464    /// Returns if an account is operator for a token owner
465    pub fn is_approved_for_all(&mut self, token_owner: Address, operator: Address) -> bool {
466        self.data.operator(token_owner, operator)
467    }
468
469    /// Returns the token owner given a token_id. It reverts if token_id
470    /// is invalid. A burnt token still has an associated owner.
471    pub fn owner_of(&self, token_id: Maybe<u64>, token_hash: Maybe<String>) -> Address {
472        let token_identifier = self.checked_token_identifier(token_id, token_hash);
473        self.owner_of_by_id(&token_identifier.to_string())
474    }
475
476    /// Returns the approved account (if any) associated with the provided token_id
477    /// Reverts if token has been burnt.
478    pub fn get_approved(
479        &mut self,
480        token_id: Maybe<u64>,
481        token_hash: Maybe<String>
482    ) -> Option<Address> {
483        let token_identifier: TokenIdentifier = self.checked_token_identifier(token_id, token_hash);
484        let token_id = token_identifier.to_string();
485
486        self.ensure_not_burned(&token_id);
487        self.data.approved(&token_id)
488    }
489
490    /// Returns the metadata associated with the provided token_id
491    pub fn metadata(&self, token_id: Maybe<u64>, token_hash: Maybe<String>) -> String {
492        let token_identifier = self.checked_token_identifier(token_id, token_hash);
493        self.metadata.get_or_revert(&token_identifier)
494    }
495
496    /// Updates the metadata if valid.
497    pub fn set_token_metadata(
498        &mut self,
499        token_id: Maybe<u64>,
500        token_hash: Maybe<String>,
501        token_meta_data: String
502    ) {
503        let token_identifier = self.checked_token_identifier(token_id, token_hash);
504        let token_id = token_identifier.to_string();
505        self.ensure_caller_is_owner(&token_id);
506        self.set_token_metadata_unchecked(&token_id, token_meta_data);
507    }
508
509    /// Returns number of owned tokens associated with the provided token holder
510    pub fn balance_of(&self, token_owner: Address) -> u64 {
511        self.data.token_count(&token_owner)
512    }
513
514    /// This entrypoint allows users to register with a give CEP-78 instance,
515    /// allocating the necessary page table to enable the reverse lookup
516    /// functionality and allowing users to pay the upfront cost of allocation
517    /// resulting in more stable gas costs when minting and transferring
518    /// Note: This entrypoint MUST be invoked if the reverse lookup is enabled
519    /// in order to own NFTs.
520    pub fn register_owner(&mut self, token_owner: Maybe<Address>) -> String {
521        let ownership_mode = self.ownership_mode();
522        self.reverse_lookup
523            .register_owner(token_owner, ownership_mode);
524        // runtime::ret(CLValue::from_t((collection_name, package_uref)).unwrap_or_revert())
525        "".to_string()
526    }
527}
528
529impl Cep78 {
530    #[inline]
531    fn caller(&self) -> Address {
532        self.__env.caller()
533    }
534
535    #[inline]
536    fn revert<E: Into<OdraError>>(&self, e: E) -> ! {
537        self.__env.revert(e)
538    }
539
540    #[inline]
541    pub fn is_minter_or_assigned(&self) -> bool {
542        matches!(
543            self.ownership_mode(),
544            OwnershipMode::Minter | OwnershipMode::Assigned
545        )
546    }
547
548    #[inline]
549    pub fn is_transferable_or_assigned(&self) -> bool {
550        matches!(
551            self.ownership_mode(),
552            OwnershipMode::Transferable | OwnershipMode::Assigned
553        )
554    }
555
556    #[inline]
557    pub fn ensure_not_minter_or_assigned(&self) {
558        if self.is_minter_or_assigned() {
559            self.revert(CEP78Error::InvalidOwnershipMode)
560        }
561    }
562
563    #[inline]
564    pub fn token_identifier(
565        &self,
566        token_id: Maybe<u64>,
567        token_hash: Maybe<String>
568    ) -> TokenIdentifier {
569        let env = self.env();
570        let identifier_mode: NFTIdentifierMode = self.metadata.get_identifier_mode();
571        match identifier_mode {
572            NFTIdentifierMode::Ordinal => TokenIdentifier::Index(token_id.unwrap(&env)),
573            NFTIdentifierMode::Hash => TokenIdentifier::Hash(token_hash.unwrap(&env))
574        }
575    }
576
577    pub fn token_id(&self, token_id: Maybe<u64>, token_hash: Maybe<String>) -> String {
578        let token_identifier = self.token_identifier(token_id, token_hash);
579        token_identifier.to_string()
580    }
581
582    #[inline]
583    pub fn checked_token_identifier(
584        &self,
585        token_id: Maybe<u64>,
586        token_hash: Maybe<String>
587    ) -> TokenIdentifier {
588        let identifier_mode: NFTIdentifierMode = self.metadata.get_identifier_mode();
589        let token_identifier = match identifier_mode {
590            NFTIdentifierMode::Ordinal => TokenIdentifier::Index(token_id.unwrap(&self.__env)),
591            NFTIdentifierMode::Hash => TokenIdentifier::Hash(token_hash.unwrap(&self.__env))
592        };
593
594        let number_of_minted_tokens = self.data.number_of_minted_tokens();
595        if let NFTIdentifierMode::Ordinal = identifier_mode {
596            // Revert if token_id is out of bounds
597            if token_identifier.get_index().unwrap_or_revert(self) >= number_of_minted_tokens {
598                self.revert(CEP78Error::InvalidTokenIdentifier);
599            }
600        }
601        token_identifier
602    }
603
604    #[inline]
605    pub fn owner_of_by_id(&self, id: &str) -> Address {
606        match self.data.owner_of(id) {
607            Some(token_owner) => token_owner,
608            None => self
609                .env()
610                .revert(CEP78Error::MissingOwnerTokenIdentifierKey)
611        }
612    }
613
614    #[inline]
615    pub fn is_token_burned(&self, token_id: &str) -> bool {
616        self.data.is_burnt(token_id)
617    }
618
619    #[inline]
620    pub fn ensure_owner(&self, token_id: &str, address: &Address) {
621        let owner = self.owner_of_by_id(token_id);
622        if address != &owner {
623            self.revert(CEP78Error::InvalidAccount);
624        }
625    }
626
627    #[inline]
628    pub fn ensure_caller_is_owner(&self, token_id: &str) {
629        let owner = self.owner_of_by_id(token_id);
630        if self.caller() != owner {
631            self.revert(CEP78Error::InvalidTokenOwner);
632        }
633    }
634
635    #[inline]
636    pub fn ensure_not_burned(&self, token_id: &str) {
637        if self.is_token_burned(token_id) {
638            self.revert(CEP78Error::PreviouslyBurntToken);
639        }
640    }
641
642    #[inline]
643    pub fn ensure_not_caller(&self, address: Address) {
644        if self.caller() == address {
645            self.revert(CEP78Error::InvalidAccount);
646        }
647    }
648
649    #[inline]
650    pub fn ensure_caller(&self, address: Address) {
651        if self.caller() != address {
652            self.revert(CEP78Error::InvalidAccount);
653        }
654    }
655
656    #[inline]
657    pub fn emit_ces_event<T: ToBytes + EventInstance>(&self, event: T) {
658        let events_mode = self.settings.events_mode();
659        if let EventsMode::CES = events_mode {
660            self.env().emit_event(event);
661        }
662    }
663
664    #[inline]
665    pub fn ensure_burnable(&self) {
666        if let BurnMode::NonBurnable = self.settings.burn_mode() {
667            self.revert(CEP78Error::InvalidBurnMode)
668        }
669    }
670
671    #[inline]
672    pub fn ownership_mode(&self) -> OwnershipMode {
673        self.settings.ownership_mode()
674    }
675
676    #[inline]
677    pub fn verified_caller(&self) -> Address {
678        let holder_mode = self.settings.holder_mode();
679        let caller = self.caller();
680
681        match (caller, holder_mode) {
682            (Address::Account(_), NFTHolderMode::Contracts)
683            | (Address::Contract(_), NFTHolderMode::Accounts) => {
684                self.revert(CEP78Error::InvalidHolderMode);
685            }
686            _ => caller
687        }
688    }
689
690    // Check if token exists by hash.
691    pub fn token_exists_by_hash(&self, token_id: &str) -> bool {
692        self.data.owner_of(token_id).is_some() && !self.is_token_burned(token_id)
693    }
694
695    // Update metadata without ownership check.
696    pub fn set_token_metadata_unchecked(&mut self, token_id: &String, token_meta_data: String) {
697        self.metadata
698            .ensure_mutability(CEP78Error::ForbiddenMetadataUpdate);
699        self.metadata.update_or_revert(&token_meta_data, token_id);
700        self.emit_ces_event(MetadataUpdated::new(
701            String::from(token_id),
702            token_meta_data
703        ));
704    }
705
706    // Burn token without ownership check.
707    pub fn burn_token_unchecked(&mut self, token_id: String, burner: Address) {
708        self.ensure_burnable();
709        let token_owner = self.owner_of_by_id(&token_id);
710        self.ensure_not_burned(&token_id);
711        self.data.mark_burnt(&token_id);
712        self.data.decrement_counter(&token_owner);
713        self.data.decrement_number_of_minted_tokens();
714        self.emit_ces_event(Burn::new(token_owner, token_id, burner));
715    }
716
717    // Returns collection name.
718    pub fn get_collection_name(&self) -> String {
719        self.data.collection_name()
720    }
721
722    // Returns collection symbol.
723    pub fn get_collection_symbol(&self) -> String {
724        self.data.collection_symbol()
725    }
726
727    // Returns if address has admin rights.
728    pub fn is_whitelisted(&self, address: &Address) -> bool {
729        self.whitelist.is_whitelisted(address)
730    }
731
732    pub fn transfer_unchecked(
733        &mut self,
734        token_id: String,
735        owner: Address,
736        spender: Option<Address>,
737        reciepient: Address
738    ) {
739        self.data.set_owner(&token_id, reciepient);
740        self.data.decrement_counter(&owner);
741        self.data.increment_counter(&reciepient);
742        self.data.revoke(&token_id);
743
744        self.emit_ces_event(Transfer::new(owner, spender, reciepient, token_id));
745    }
746}
747
748#[odra::external_contract]
749pub trait TransferFilterContract {
750    fn can_transfer(
751        &self,
752        source_key: Address,
753        target_key: Address,
754        token_id: TokenIdentifier
755    ) -> TransferFilterContractResult;
756}
757
758#[odra::module]
759pub struct TestCep78 {
760    token: SubModule<Cep78>
761}
762
763#[odra::module]
764impl TestCep78 {
765    delegate! {
766        to self.token {
767            fn init(
768                &mut self,
769                collection_name: String,
770                collection_symbol: String,
771                total_token_supply: u64,
772                ownership_mode: OwnershipMode,
773                nft_kind: NFTKind,
774                identifier_mode: NFTIdentifierMode,
775                nft_metadata_kind: NFTMetadataKind,
776                metadata_mutability: MetadataMutability,
777                receipt_name: String,
778                allow_minting: Maybe<bool>,
779                minting_mode: Maybe<MintingMode>,
780                holder_mode: Maybe<NFTHolderMode>,
781                whitelist_mode: Maybe<WhitelistMode>,
782                acl_whitelist: Maybe<Vec<Address>>,
783                json_schema: Maybe<String>,
784                burn_mode: Maybe<BurnMode>,
785                operator_burn_mode: Maybe<bool>,
786                owner_reverse_lookup_mode: Maybe<OwnerReverseLookupMode>,
787                events_mode: Maybe<EventsMode>,
788                transfer_filter_contract_contract: Maybe<Address>,
789                additional_required_metadata: Maybe<Vec<NFTMetadataKind>>,
790                optional_metadata: Maybe<Vec<NFTMetadataKind>>
791            );
792            fn set_variables(
793                &mut self,
794                allow_minting: Maybe<bool>,
795                acl_whitelist: Maybe<Vec<Address>>,
796                operator_burn_mode: Maybe<bool>
797            );
798            fn mint(
799                &mut self,
800                token_owner: Address,
801                token_meta_data: String,
802                token_hash: Maybe<String>
803            );
804            fn burn(&mut self, token_id: Maybe<u64>, token_hash: Maybe<String>);
805            fn transfer(
806                &mut self,
807                token_id: Maybe<u64>,
808                token_hash: Maybe<String>,
809                source_key: Address,
810                target_key: Address
811            );
812            fn approve(&mut self, spender: Address, token_id: Maybe<u64>, token_hash: Maybe<String>);
813            fn revoke(&mut self, token_id: Maybe<u64>, token_hash: Maybe<String>);
814            fn set_approval_for_all(&mut self, approve_all: bool, operator: Address);
815            fn is_approved_for_all(&mut self, token_owner: Address, operator: Address) -> bool;
816            fn owner_of(&self, token_id: Maybe<u64>, token_hash: Maybe<String>) -> Address;
817            fn get_approved(
818                &mut self,
819                token_id: Maybe<u64>,
820                token_hash: Maybe<String>
821            ) -> Option<Address>;
822            fn metadata(&self, token_id: Maybe<u64>, token_hash: Maybe<String>) -> String;
823            fn set_token_metadata(
824                &mut self,
825                token_id: Maybe<u64>,
826                token_hash: Maybe<String>,
827                token_meta_data: String
828            );
829            fn balance_of(&self, token_owner: Address) -> u64;
830            fn register_owner(&mut self, token_owner: Maybe<Address>) -> String;
831            fn is_whitelisted(&self, address: &Address) -> bool;
832        }
833    }
834
835    pub fn get_whitelist_mode(&self) -> WhitelistMode {
836        self.token.whitelist.get_mode()
837    }
838
839    pub fn get_collection_name(&self) -> String {
840        self.token.data.collection_name()
841    }
842
843    pub fn get_collection_symbol(&self) -> String {
844        self.token.data.collection_symbol()
845    }
846
847    pub fn is_minting_allowed(&self) -> bool {
848        self.token.settings.allow_minting()
849    }
850
851    pub fn is_operator_burn_mode(&self) -> bool {
852        self.token.settings.operator_burn_mode()
853    }
854
855    pub fn get_total_supply(&self) -> u64 {
856        self.token.data.total_token_supply()
857    }
858
859    pub fn get_minting_mode(&self) -> MintingMode {
860        self.token.settings.minting_mode()
861    }
862
863    pub fn get_holder_mode(&self) -> NFTHolderMode {
864        self.token.settings.holder_mode()
865    }
866
867    pub fn get_number_of_minted_tokens(&self) -> u64 {
868        self.token.data.number_of_minted_tokens()
869    }
870
871    pub fn get_page(&self, page_number: u64) -> Vec<bool> {
872        let env = self.env();
873        let owner = env.caller();
874
875        let owner_key = utils::address_to_key(&owner);
876        let page_dict = format!("{PREFIX_PAGE_DICTIONARY}_{}", page_number);
877        env.get_dictionary_value(page_dict, owner_key.as_bytes())
878            .unwrap_or_revert_with(&self.env(), CEP78Error::InvalidPageNumber)
879    }
880
881    pub fn get_page_by_token_id(&self, token_id: u64) -> Vec<bool> {
882        let env = self.env();
883        let owner = env.caller();
884        let page_table_entry = token_id / PAGE_SIZE;
885
886        let page_dict = format!("{PREFIX_PAGE_DICTIONARY}_{}", page_table_entry);
887        let owner_key = utils::address_to_key(&owner);
888
889        env.get_dictionary_value(page_dict, owner_key.as_bytes())
890            .unwrap_or_revert_with(&env, CEP78Error::MissingPage)
891    }
892
893    pub fn get_page_by_token_hash(&self, token_hash: String) -> Vec<bool> {
894        let identifier = TokenIdentifier::Hash(token_hash);
895        let token_id = self
896            .token
897            .reverse_lookup
898            .get_token_index_checked(&identifier);
899        self.get_page_by_token_id(token_id)
900    }
901
902    pub fn get_page_table(&self) -> Vec<bool> {
903        self.token
904            .reverse_lookup
905            .get_page_table(&self.__env.caller(), CEP78Error::MissingPage)
906    }
907
908    pub fn get_metadata_by_kind(
909        &self,
910        kind: NFTMetadataKind,
911        token_id: Maybe<u64>,
912        token_hash: Maybe<String>
913    ) -> String {
914        let token_identifier = self.token.checked_token_identifier(token_id, token_hash);
915        self.token
916            .metadata
917            .get_metadata_by_kind(token_identifier.to_string(), &kind)
918    }
919
920    pub fn get_token_issuer(&self, token_id: Maybe<u64>, token_hash: Maybe<String>) -> Address {
921        let token_identifier = self.token.checked_token_identifier(token_id, token_hash);
922        self.token.data.issuer(&token_identifier.to_string())
923    }
924
925    pub fn token_burned(&self, token_id: Maybe<u64>, token_hash: Maybe<String>) -> bool {
926        let token_identifier = self.token.token_identifier(token_id, token_hash);
927        let token_id = token_identifier.to_string();
928        self.token.is_token_burned(&token_id)
929    }
930}