From 31aef8b6b00a1915ab59da5d147186788e38acdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Fri, 12 Apr 2024 10:04:13 +0200 Subject: [PATCH 01/38] cep-78 basics --- modules/src/cep78/constants.rs | 124 ++++++++++++ modules/src/cep78/error.rs | 174 ++++++++++++++++ modules/src/cep78/events.rs | 149 ++++++++++++++ modules/src/cep78/mod.rs | 7 + modules/src/cep78/modalities.rs | 345 ++++++++++++++++++++++++++++++++ modules/src/cep78/token.rs | 148 ++++++++++++++ modules/src/lib.rs | 1 + 7 files changed, 948 insertions(+) create mode 100644 modules/src/cep78/constants.rs create mode 100644 modules/src/cep78/error.rs create mode 100644 modules/src/cep78/events.rs create mode 100644 modules/src/cep78/mod.rs create mode 100644 modules/src/cep78/modalities.rs create mode 100644 modules/src/cep78/token.rs diff --git a/modules/src/cep78/constants.rs b/modules/src/cep78/constants.rs new file mode 100644 index 00000000..293ce412 --- /dev/null +++ b/modules/src/cep78/constants.rs @@ -0,0 +1,124 @@ +pub const PREFIX_ACCESS_KEY_NAME: &str = "cep78_contract_package_access"; +pub const PREFIX_CEP78: &str = "cep78"; +pub const PREFIX_CONTRACT_NAME: &str = "cep78_contract_hash"; +pub const PREFIX_CONTRACT_VERSION: &str = "cep78_contract_version"; +pub const PREFIX_HASH_KEY_NAME: &str = "cep78_contract_package"; +pub const PREFIX_PAGE_DICTIONARY: &str = "page"; + +pub const ARG_ACCESS_KEY_NAME_1_0_0: &str = "access_key_name"; +pub const ARG_ACL_PACKAGE_MODE: &str = "acl_package_mode"; +pub const ARG_ACL_WHITELIST: &str = "acl_whitelist"; +pub const ARG_ADDITIONAL_REQUIRED_METADATA: &str = "additional_required_metadata"; +pub const ARG_ALLOW_MINTING: &str = "allow_minting"; +pub const ARG_APPROVE_ALL: &str = "approve_all"; +pub const ARG_BURN_MODE: &str = "burn_mode"; +pub const ARG_COLLECTION_NAME: &str = "collection_name"; +pub const ARG_COLLECTION_SYMBOL: &str = "collection_symbol"; +pub const ARG_CONTRACT_WHITELIST: &str = "contract_whitelist"; +pub const ARG_EVENTS_MODE: &str = "events_mode"; +pub const ARG_HASH_KEY_NAME_1_0_0: &str = "hash_key_name"; +pub const ARG_HOLDER_MODE: &str = "holder_mode"; +pub const ARG_IDENTIFIER_MODE: &str = "identifier_mode"; +pub const ARG_JSON_SCHEMA: &str = "json_schema"; +pub const ARG_METADATA_MUTABILITY: &str = "metadata_mutability"; +pub const ARG_MINTING_MODE: &str = "minting_mode"; +pub const ARG_NAMED_KEY_CONVENTION: &str = "named_key_convention"; +pub const ARG_NFT_KIND: &str = "nft_kind"; +pub const ARG_NFT_METADATA_KIND: &str = "nft_metadata_kind"; +pub const ARG_NFT_PACKAGE_KEY: &str = "cep78_package_key"; +pub const ARG_OPTIONAL_METADATA: &str = "optional_metadata"; +pub const ARG_OPERATOR: &str = "operator"; +pub const ARG_OPERATOR_BURN_MODE: &str = "operator_burn_mode"; +pub const ARG_OWNERSHIP_MODE: &str = "ownership_mode"; +pub const ARG_OWNER_LOOKUP_MODE: &str = "owner_reverse_lookup_mode"; +pub const ARG_PACKAGE_OPERATOR_MODE: &str = "package_operator_mode"; +pub const ARG_RECEIPT_NAME: &str = "receipt_name"; +pub const ARG_SOURCE_KEY: &str = "source_key"; +pub const ARG_SPENDER: &str = "spender"; +pub const ARG_TARGET_KEY: &str = "target_key"; +pub const ARG_TOKEN_HASH: &str = "token_hash"; +pub const ARG_TOKEN_ID: &str = "token_id"; +pub const ARG_TOKEN_META_DATA: &str = "token_meta_data"; +pub const ARG_TOKEN_OWNER: &str = "token_owner"; +pub const ARG_TOTAL_TOKEN_SUPPLY: &str = "total_token_supply"; +pub const ARG_TRANSFER_FILTER_CONTRACT: &str = "transfer_filter_contract"; +pub const ARG_WHITELIST_MODE: &str = "whitelist_mode"; + +pub const ENTRY_POINT_APPROVE: &str = "approve"; +pub const ENTRY_POINT_BALANCE_OF: &str = "balance_of"; +pub const ENTRY_POINT_BURN: &str = "burn"; +pub const ENTRY_POINT_GET_APPROVED: &str = "get_approved"; +pub const ENTRY_POINT_INIT: &str = "init"; +pub const ENTRY_POINT_IS_APPROVED_FOR_ALL: &str = "is_approved_for_all"; +pub const ENTRY_POINT_METADATA: &str = "metadata"; +pub const ENTRY_POINT_MIGRATE: &str = "migrate"; +pub const ENTRY_POINT_MINT: &str = "mint"; +pub const ENTRY_POINT_OWNER_OF: &str = "owner_of"; +pub const ENTRY_POINT_REVOKE: &str = "revoke"; +pub const ENTRY_POINT_REGISTER_OWNER: &str = "register_owner"; +pub const ENTRY_POINT_SET_APPROVALL_FOR_ALL: &str = "set_approval_for_all"; +pub const ENTRY_POINT_SET_TOKEN_METADATA: &str = "set_token_metadata"; +pub const ENTRY_POINT_SET_VARIABLES: &str = "set_variables"; +pub const ENTRY_POINT_TRANSFER: &str = "transfer"; +pub const ENTRY_POINT_UPDATED_RECEIPTS: &str = "updated_receipts"; + +pub const ACL_PACKAGE_MODE: &str = "acl_package_mode"; +pub const ACL_WHITELIST: &str = "acl_whitelist"; +pub const ALLOW_MINTING: &str = "allow_minting"; +pub const APPROVED: &str = "approved"; +pub const BURN_MODE: &str = "burn_mode"; +pub const BURNT_TOKENS: &str = "burnt_tokens"; +pub const COLLECTION_NAME: &str = "collection_name"; +pub const COLLECTION_SYMBOL: &str = "collection_symbol"; +pub const CONTRACT_WHITELIST: &str = "contract_whitelist"; +pub const EVENT_TYPE: &str = "event_type"; +pub const EVENTS: &str = "events"; +pub const EVENTS_MODE: &str = "events_mode"; +pub const HASH_BY_INDEX: &str = "hash_by_index"; +pub const HOLDER_MODE: &str = "holder_mode"; +pub const IDENTIFIER_MODE: &str = "identifier_mode"; +pub const INDEX_BY_HASH: &str = "index_by_hash"; +pub const INSTALLER: &str = "installer"; +pub const JSON_SCHEMA: &str = "json_schema"; +pub const METADATA_CEP78: &str = "metadata_cep78"; +pub const METADATA_CUSTOM_VALIDATED: &str = "metadata_custom_validated"; +pub const METADATA_MUTABILITY: &str = "metadata_mutability"; +pub const METADATA_NFT721: &str = "metadata_nft721"; +pub const METADATA_RAW: &str = "metadata_raw"; +pub const MIGRATION_FLAG: &str = "migration_flag"; +pub const MINTING_MODE: &str = "minting_mode"; +pub const NFT_KIND: &str = "nft_kind"; +pub const NFT_METADATA_KIND: &str = "nft_metadata_kind"; +pub const NFT_METADATA_KINDS: &str = "nft_metadata_kinds"; +pub const NUMBER_OF_MINTED_TOKENS: &str = "number_of_minted_tokens"; +pub const OPERATOR: &str = "operator"; +pub const OPERATORS: &str = "operators"; +pub const OPERATOR_BURN_MODE: &str = "operator_burn_mode"; +pub const OWNED_TOKENS: &str = "owned_tokens"; +pub const OWNER: &str = "owner"; +pub const BURNER: &str = "burner"; +pub const OWNERSHIP_MODE: &str = "ownership_mode"; +pub const PACKAGE_OPERATOR_MODE: &str = "package_operator_mode"; +pub const PAGE_LIMIT: &str = "page_limit"; +pub const PAGE_TABLE: &str = "page_table"; +pub const RECEIPT_NAME: &str = "receipt_name"; +pub const RECIPIENT: &str = "recipient"; +pub const REPORTING_MODE: &str = "reporting_mode"; +pub const RLO_MFLAG: &str = "rlo_mflag"; +pub const SENDER: &str = "sender"; +pub const SPENDER: &str = "spender"; +pub const TOKEN_COUNT: &str = "balances"; +pub const TOKEN_ID: &str = "token_id"; +pub const TOKEN_ISSUERS: &str = "token_issuers"; +pub const TOKEN_OWNERS: &str = "token_owners"; +pub const TOTAL_TOKEN_SUPPLY: &str = "total_token_supply"; +pub const TRANSFER_FILTER_CONTRACT: &str = "transfer_filter_contract"; +pub const TRANSFER_FILTER_CONTRACT_METHOD: &str = "can_transfer"; +pub const UNMATCHED_HASH_COUNT: &str = "unmatched_hash_count"; +pub const WHITELIST_MODE: &str = "whitelist_mode"; + +// The cap on the amount of tokens within a given CEP-78 collection. +pub const MAX_TOTAL_TOKEN_SUPPLY: u64 = 1_000_000u64; + +pub const ACCESS_KEY_NAME_1_0_0: &str = "nft_contract_package_access"; +pub const HASH_KEY_NAME_1_0_0: &str = "nft_contract_package"; diff --git a/modules/src/cep78/error.rs b/modules/src/cep78/error.rs new file mode 100644 index 00000000..a10f45d8 --- /dev/null +++ b/modules/src/cep78/error.rs @@ -0,0 +1,174 @@ +#[odra::odra_error] +pub enum CEP78Error { + InvalidAccount = 1, + MissingInstaller = 2, + InvalidInstaller = 3, + UnexpectedKeyVariant = 4, + MissingTokenOwner = 5, + InvalidTokenOwner = 6, + FailedToGetArgBytes = 7, + FailedToCreateDictionary = 8, + MissingStorageUref = 9, + InvalidStorageUref = 10, + MissingOwnersUref = 11, + InvalidOwnersUref = 12, + FailedToAccessStorageDictionary = 13, + FailedToAccessOwnershipDictionary = 14, + DuplicateMinted = 15, + FailedToConvertToCLValue = 16, + MissingCollectionName = 17, + InvalidCollectionName = 18, + FailedToSerializeMetaData = 19, + MissingAccount = 20, + MissingMintingStatus = 21, + InvalidMintingStatus = 22, + MissingCollectionSymbol = 23, + InvalidCollectionSymbol = 24, + MissingTotalTokenSupply = 25, + InvalidTotalTokenSupply = 26, + MissingTokenID = 27, + InvalidTokenIdentifier = 28, + MissingTokenOwners = 29, + MissingAccountHash = 30, + InvalidAccountHash = 31, + TokenSupplyDepleted = 32, + MissingOwnedTokensDictionary = 33, + TokenAlreadyBelongsToMinterFatal = 34, + FatalTokenIdDuplication = 35, + InvalidMinter = 36, + MissingMintingMode = 37, + InvalidMintingMode = 38, + MissingInstallerKey = 39, + FailedToConvertToAccountHash = 40, + InvalidBurner = 41, + PreviouslyBurntToken = 42, + MissingAllowMinting = 43, + InvalidAllowMinting = 44, + MissingNumberOfMintedTokens = 45, + InvalidNumberOfMintedTokens = 46, + MissingTokenMetaData = 47, + InvalidTokenMetaData = 48, + MissingApprovedAccountHash = 49, + InvalidApprovedAccountHash = 50, + MissingApprovedTokensDictionary = 51, + TokenAlreadyApproved = 52, + MissingApproveAll = 53, + InvalidApproveAll = 54, + MissingOperator = 55, + InvalidOperator = 56, + Phantom = 57, + ContractAlreadyInitialized = 58, + MintingIsPaused = 59, + FailureToParseAccountHash = 60, + VacantValueInDictionary = 61, + MissingOwnershipMode = 62, + InvalidOwnershipMode = 63, + InvalidTokenMinter = 64, + MissingOwnedTokens = 65, + InvalidAccountKeyInDictionary = 66, + MissingJsonSchema = 67, + InvalidJsonSchema = 68, + InvalidKey = 69, + InvalidOwnedTokens = 70, + MissingTokenURI = 71, + InvalidTokenURI = 72, + MissingNftKind = 73, + InvalidNftKind = 74, + MissingHolderMode = 75, + InvalidHolderMode = 76, + MissingWhitelistMode = 77, + InvalidWhitelistMode = 78, + MissingContractWhiteList = 79, + InvalidContractWhitelist = 80, + UnlistedContractHash = 81, + InvalidContract = 82, + EmptyContractWhitelist = 83, + MissingReceiptName = 84, + InvalidReceiptName = 85, + InvalidJsonMetadata = 86, + InvalidJsonFormat = 87, + FailedToParseCep99Metadata = 88, + FailedToParse721Metadata = 89, + FailedToParseCustomMetadata = 90, + InvalidCEP99Metadata = 91, + FailedToJsonifyCEP99Metadata = 92, + InvalidNFT721Metadata = 93, + FailedToJsonifyNFT721Metadata = 94, + InvalidCustomMetadata = 95, + MissingNFTMetadataKind = 96, + InvalidNFTMetadataKind = 97, + MissingIdentifierMode = 98, + InvalidIdentifierMode = 99, + FailedToParseTokenId = 100, + MissingMetadataMutability = 101, + InvalidMetadataMutability = 102, + FailedToJsonifyCustomMetadata = 103, + ForbiddenMetadataUpdate = 104, + MissingBurnMode = 105, + InvalidBurnMode = 106, + MissingHashByIndex = 107, + InvalidHashByIndex = 108, + MissingIndexByHash = 109, + InvalidIndexByHash = 110, + MissingPageTableURef = 111, + InvalidPageTableURef = 112, + MissingPageLimit = 113, + InvalidPageLimit = 114, + InvalidPageNumber = 115, + InvalidPageIndex = 116, + MissingUnmatchedHashCount = 117, + InvalidUnmatchedHashCount = 118, + MissingPackageHashForUpgrade = 119, + MissingPageUref = 120, + InvalidPageUref = 121, + CannotUpgradeWithZeroSupply = 122, + CannotInstallWithZeroSupply = 123, + MissingMigrationFlag = 124, + InvalidMigrationFlag = 125, + ContractAlreadyMigrated = 126, + UnregisteredOwnerInMint = 127, + UnregisteredOwnerInTransfer = 128, + MissingReportingMode = 129, + InvalidReportingMode = 130, + MissingPage = 131, + UnregisteredOwnerFromMigration = 132, + ExceededMaxTotalSupply = 133, + MissingCep78PackageHash = 134, + InvalidCep78InvalidHash = 135, + InvalidPackageHashName = 136, + InvalidAccessKeyName = 137, + InvalidCheckForUpgrade = 138, + InvalidNamedKeyConvention = 139, + OwnerReverseLookupModeNotTransferable = 140, + InvalidAdditionalRequiredMetadata = 141, + InvalidOptionalMetadata = 142, + MissingOptionalNFTMetadataKind = 143, + InvalidOptionalNFTMetadataKind = 144, + MissingAdditionalNFTMetadataKind = 145, + InvalidAdditionalNFTMetadataKind = 146, + InvalidRequirement = 147, + MissingEventsMode = 148, + InvalidEventsMode = 149, + CannotUpgradeToMoreSupply = 150, + MissingOperatorDict = 151, + MissingApprovedDict = 152, + MissingSpenderAccountHash = 153, + InvalidSpenderAccountHash = 154, + MissingOwnerTokenIdentifierKey = 155, + InvalidTransferFilterContract = 156, + MissingTransferFilterContract = 157, + TransferFilterContractNeedsTransferableMode = 158, + TransferFilterContractDenied = 159, + MissingACLWhiteList = 160, + InvalidACLWhitelist = 161, + EmptyACLWhitelist = 162, + InvalidACLPackageMode = 163, + MissingACLPackageMode = 164, + InvalidPackageOperatorMode = 165, + MissingPackageOperatorMode = 166, + InvalidOperatorBurnMode = 167, + MissingOperatorBurnMode = 168, + InvalidIdentifier = 169, + DuplicateIdentifier = 170, +} + diff --git a/modules/src/cep78/events.rs b/modules/src/cep78/events.rs new file mode 100644 index 00000000..81ffc25f --- /dev/null +++ b/modules/src/cep78/events.rs @@ -0,0 +1,149 @@ +use odra::{prelude::*, Address}; +use super::modalities::TokenIdentifier; + +#[odra::event] +pub struct Mint { + recipient: Address, + token_id: String, + data: String, +} + +impl Mint { + pub fn new(recipient: Address, token_id: TokenIdentifier, data: String) -> Self { + Self { + recipient, + token_id: token_id.to_string(), + data, + } + } +} + +#[odra::event] +pub struct Burn { + owner: Address, + token_id: String, + burner: Address, +} + +impl Burn { + pub fn new(owner: Address, token_id: TokenIdentifier, burner: Address) -> Self { + Self { + owner, + token_id: token_id.to_string(), + burner, + } + } +} + +#[odra::event] +pub struct Approval { + owner: Address, + spender: Address, + token_id: String, +} + +impl Approval { + pub fn new(owner: Address, spender: Address, token_id: TokenIdentifier) -> Self { + Self { + owner, + spender, + token_id: token_id.to_string(), + } + } +} + +#[odra::event] +pub struct ApprovalRevoked { + owner: Address, + token_id: String, +} + +impl ApprovalRevoked { + pub fn new(owner: Address, token_id: TokenIdentifier) -> Self { + Self { + owner, + token_id: token_id.to_string(), + } + } +} + +#[odra::event] +pub struct ApprovalForAll { + owner: Address, + operator: Address, +} + +impl ApprovalForAll { + pub fn new(owner: Address, operator: Address) -> Self { + Self { owner, operator } + } +} + +#[odra::event] +pub struct RevokedForAll { + owner: Address, + operator: Address, +} + +impl RevokedForAll { + pub fn new(owner: Address, operator: Address) -> Self { + Self { owner, operator } + } +} + +#[odra::event] +pub struct Transfer { + owner: Address, + spender: Option
, + recipient: Address, + token_id: String, +} + +impl Transfer { + pub fn new( + owner: Address, + spender: Option
, + recipient: Address, + token_id: TokenIdentifier, + ) -> Self { + Self { + owner, + spender, + recipient, + token_id: token_id.to_string(), + } + } +} + +#[odra::event] +pub struct MetadataUpdated { + token_id: String, + data: String, +} + +impl MetadataUpdated { + pub fn new(token_id: TokenIdentifier, data: String) -> Self { + Self { + token_id: token_id.to_string(), + data, + } + } +} + +#[odra::event] +pub struct VariablesSet {} + +impl VariablesSet { + pub fn new() -> Self { + Self {} + } +} + +#[odra::event] +pub struct Migration {} + +impl Migration { + pub fn new() -> Self { + Self {} + } +} diff --git a/modules/src/cep78/mod.rs b/modules/src/cep78/mod.rs new file mode 100644 index 00000000..941146b4 --- /dev/null +++ b/modules/src/cep78/mod.rs @@ -0,0 +1,7 @@ +#![allow(missing_docs)] + +pub mod events; +pub mod modalities; +pub mod error; +pub mod constants; +pub mod token; \ No newline at end of file diff --git a/modules/src/cep78/modalities.rs b/modules/src/cep78/modalities.rs new file mode 100644 index 00000000..746d08b3 --- /dev/null +++ b/modules/src/cep78/modalities.rs @@ -0,0 +1,345 @@ +use odra::prelude::*; +use super::error::CEP78Error; + +#[repr(u8)] +#[derive(PartialEq, Eq)] +pub enum WhitelistMode { + Unlocked = 0, + Locked = 1, +} + +impl TryFrom for WhitelistMode { + type Error = CEP78Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(WhitelistMode::Unlocked), + 1 => Ok(WhitelistMode::Locked), + _ => Err(CEP78Error::InvalidWhitelistMode), + } + } +} + +#[repr(u8)] +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum NFTHolderMode { + Accounts = 0, + Contracts = 1, + Mixed = 2, +} + +impl TryFrom for NFTHolderMode { + type Error = CEP78Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(NFTHolderMode::Accounts), + 1 => Ok(NFTHolderMode::Contracts), + 2 => Ok(NFTHolderMode::Mixed), + _ => Err(CEP78Error::InvalidHolderMode), + } + } +} + +#[derive(PartialEq, Eq)] +#[repr(u8)] +pub enum MintingMode { + /// The ability to mint NFTs is restricted to the installing account only. + Installer = 0, + /// The ability to mint NFTs is not restricted. + Public = 1, + /// The ability to mint NFTs is restricted by an ACL. + Acl = 2, +} + +impl TryFrom for MintingMode { + type Error = CEP78Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(MintingMode::Installer), + 1 => Ok(MintingMode::Public), + 2 => Ok(MintingMode::Acl), + _ => Err(CEP78Error::InvalidMintingMode), + } + } +} + +#[repr(u8)] +pub enum NFTKind { + /// The NFT represents a real-world physical + /// like a house. + Physical = 0, + /// The NFT represents a digital asset like a unique + /// JPEG or digital art. + Digital = 1, + /// The NFT is the virtual representation + /// of a physical notion, e.g a patent + /// or copyright. + Virtual = 2, +} + +impl TryFrom for NFTKind { + type Error = CEP78Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(NFTKind::Physical), + 1 => Ok(NFTKind::Digital), + 2 => Ok(NFTKind::Virtual), + _ => Err(CEP78Error::InvalidNftKind), + } + } +} + +pub type MetadataRequirement = BTreeMap; + +#[odra::odra_type] +#[repr(u8)] +pub enum Requirement { + Required = 0, + Optional = 1, + Unneeded = 2, +} + +impl TryFrom for Requirement { + type Error = CEP78Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Requirement::Required), + 1 => Ok(Requirement::Optional), + 2 => Ok(Requirement::Unneeded), + _ => Err(CEP78Error::InvalidRequirement), + } + } +} + +#[repr(u8)] +#[odra::odra_type] +pub enum NFTMetadataKind { + CEP78 = 0, + NFT721 = 1, + Raw = 2, + CustomValidated = 3, +} + +impl TryFrom for NFTMetadataKind { + type Error = CEP78Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(NFTMetadataKind::CEP78), + 1 => Ok(NFTMetadataKind::NFT721), + 2 => Ok(NFTMetadataKind::Raw), + 3 => Ok(NFTMetadataKind::CustomValidated), + _ => Err(CEP78Error::InvalidNFTMetadataKind), + } + } +} + +#[repr(u8)] +#[derive(PartialEq, Eq)] +pub enum OwnershipMode { + /// The minter owns it and can never transfer it. + Minter = 0, + /// The minter assigns it to an address and can never be transferred. + Assigned = 1, + /// The NFT can be transferred even to an recipient that does not exist. + Transferable = 2, +} + +impl TryFrom for OwnershipMode { + type Error = CEP78Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(OwnershipMode::Minter), + 1 => Ok(OwnershipMode::Assigned), + 2 => Ok(OwnershipMode::Transferable), + _ => Err(CEP78Error::InvalidOwnershipMode), + } + } +} + +#[repr(u8)] +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum NFTIdentifierMode { + Ordinal = 0, + Hash = 1, +} + +impl TryFrom for NFTIdentifierMode { + type Error = CEP78Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(NFTIdentifierMode::Ordinal), + 1 => Ok(NFTIdentifierMode::Hash), + _ => Err(CEP78Error::InvalidIdentifierMode), + } + } +} + +#[repr(u8)] +pub enum MetadataMutability { + Immutable = 0, + Mutable = 1, +} + +impl TryFrom for MetadataMutability { + type Error = CEP78Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(MetadataMutability::Immutable), + 1 => Ok(MetadataMutability::Mutable), + _ => Err(CEP78Error::InvalidMetadataMutability), + } + } +} + +#[odra::odra_type] +pub enum TokenIdentifier { + Index(u64), + Hash(String), +} + +impl TokenIdentifier { + pub fn new_index(index: u64) -> Self { + TokenIdentifier::Index(index) + } + + pub fn new_hash(hash: String) -> Self { + TokenIdentifier::Hash(hash) + } + + pub fn get_index(&self) -> Option { + if let Self::Index(index) = self { + return Some(*index); + } + None + } + + pub fn get_hash(self) -> Option { + if let Self::Hash(hash) = self { + return Some(hash); + } + None + } + + pub fn get_dictionary_item_key(&self) -> String { + match self { + TokenIdentifier::Index(token_index) => token_index.to_string(), + TokenIdentifier::Hash(hash) => hash.clone(), + } + } +} + +impl ToString for TokenIdentifier { + fn to_string(&self) -> String { + match self { + TokenIdentifier::Index(index) => index.to_string(), + TokenIdentifier::Hash(hash) => hash.to_string(), + } + } +} + + +#[repr(u8)] +pub enum BurnMode { + Burnable = 0, + NonBurnable = 1, +} + +impl TryFrom for BurnMode { + type Error = CEP78Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(BurnMode::Burnable), + 1 => Ok(BurnMode::NonBurnable), + _ => Err(CEP78Error::InvalidBurnMode), + } + } +} + +#[repr(u8)] +#[derive(Clone, PartialEq, Eq)] +pub enum OwnerReverseLookupMode { + NoLookUp = 0, + Complete = 1, + TransfersOnly = 2, +} + +impl TryFrom for OwnerReverseLookupMode { + type Error = CEP78Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(OwnerReverseLookupMode::NoLookUp), + 1 => Ok(OwnerReverseLookupMode::Complete), + 2 => Ok(OwnerReverseLookupMode::TransfersOnly), + _ => Err(CEP78Error::InvalidReportingMode), + } + } +} + +#[repr(u8)] +pub enum NamedKeyConventionMode { + DerivedFromCollectionName = 0, + V1_0Standard = 1, + V1_0Custom = 2, +} + +impl TryFrom for NamedKeyConventionMode { + type Error = CEP78Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(NamedKeyConventionMode::DerivedFromCollectionName), + 1 => Ok(NamedKeyConventionMode::V1_0Standard), + 2 => Ok(NamedKeyConventionMode::V1_0Custom), + _ => Err(CEP78Error::InvalidNamedKeyConvention), + } + } +} + +#[repr(u8)] +#[derive(PartialEq, Eq, Clone, Copy)] +#[allow(clippy::upper_case_acronyms)] +pub enum EventsMode { + NoEvents = 0, + CEP47 = 1, + CES = 2, +} + +impl TryFrom for EventsMode { + type Error = CEP78Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(EventsMode::NoEvents), + 1 => Ok(EventsMode::CEP47), + 2 => Ok(EventsMode::CES), + _ => Err(CEP78Error::InvalidEventsMode), + } + } +} + +#[repr(u8)] +#[non_exhaustive] +#[derive(PartialEq, Eq)] +pub enum TransferFilterContractResult { + DenyTransfer = 0, + ProceedTransfer, +} + +impl From for TransferFilterContractResult { + fn from(value: u8) -> Self { + match value { + 0 => TransferFilterContractResult::DenyTransfer, + _ => TransferFilterContractResult::ProceedTransfer, + } + } +} diff --git a/modules/src/cep78/token.rs b/modules/src/cep78/token.rs new file mode 100644 index 00000000..a197c89b --- /dev/null +++ b/modules/src/cep78/token.rs @@ -0,0 +1,148 @@ +use odra::{args::Maybe, casper_types::URef, prelude::*, Address}; +use super::error::CEP78Error; + +#[odra::module] +pub struct CEP78 { + +} + +// #[odra::module] +impl CEP78 { + /// Initializes the module. + pub fn init(&mut self) { + } + + /// Exposes all variables that can be changed by managing account post + /// installation. Meant to be called by the managing account (INSTALLER) post + /// installation if a variable needs to be changed. + /// By switching allow_minting to false we pause minting. + pub fn set_variables( + &mut self, + allow_minting: Maybe, + contract_whitelist: Maybe>, + acl_whitelist: Maybe>, + acl_package_mode: Maybe, + package_operator_mode: Maybe, + operator_burn_mode: Maybe, + ) { + + } + + + /// Mints a new token with provided metadata. + /// Reverts with [CEP78Error::MintingIsPaused] error if `allow_minting` is false. + /// When a token is minted the calling account is listed as its owner and the token is + /// automatically assigned an `u64` ID equal to the current `number_of_minted_tokens`. + /// Before minting the token, checks if `number_of_minted_tokens` + /// exceeds the `total_token_supply`. If so, it reverts the minting with an error + /// [CEP78Error::TokenSupplyDepleted]. The `mint` function also checks whether the calling account + /// is the managing account (the installer) If not, and if `public_minting` is set to + /// false, it reverts with the error [CEP78Error::InvalidAccount]. + /// After minting is successful the number_of_minted_tokens is incremented by one. + pub fn mint( + &mut self, + token_owner: Address, + token_metadata: String, + ) -> (String, Address, String) { + todo!() + } + + + /// Burns the token with provided `token_id` argument, after which it is no + /// longer possible to transfer it. + /// Looks up the owner of the supplied token_id arg. If caller is not owner we revert with + /// error [CEP78Error::InvalidTokenOwner]. If the token id is invalid (e.g. out of bounds) it reverts + /// with error [CEP78Error::InvalidTokenIdentifier]. If the token is listed as already burnt we revert with + /// error [CEP78Error::PreviouslyBurntToken]. If not the token is then registered as burnt. + pub fn burn(&mut self, token_id: Maybe, token_hash: Maybe) { + todo!() + } + + /// Transfers ownership of the token from one account to another. + /// It looks up the owner of the supplied token_id arg. Reverts if the token is already burnt, + /// `token_id` is invalid, or if caller is not owner nor an approved account nor operator. + /// If token id is invalid it reverts with error [CEP78Error::InvalidTokenIdentifier]. + pub fn transfer(&mut self, token_id: Maybe, token_hash: Maybe, source_key: Address, target_key: Address) -> (String, Address) { + todo!() + } + + /// Approves another token holder (an approved account) to transfer tokens. It + /// reverts if token_id is invalid, if caller is not the owner nor operator, if token has already + /// been burnt, or if caller tries to approve themselves as an approved account. + pub fn approve(&mut self, spender: Address, token_id: Maybe, token_hash: Maybe) { + todo!() + } + + /// Revokes an approved account to transfer tokens. It reverts + /// if token_id is invalid, if caller is not the owner, if token has already + /// been burnt, if caller tries to approve itself. + pub fn revoke(&mut self, token_id: Maybe, token_hash: Maybe) { + todo!() + } + + /// Approves all tokens owned by the caller and future to another token holder + /// (an operator) to transfer tokens. It reverts if token_id is invalid, if caller is not the + /// owner, if caller tries to approve itself as an operator. + pub fn set_approval_for_all(&mut self, approve_all: bool, operator: Address) { + todo!() + } + + /// Returns if an account is operator for a token owner + pub fn is_approved_for_all(&mut self, token_owner: Address, operator: Address) -> bool { + todo!() + } + + /// Returns the token owner given a token_id. It reverts if token_id + /// is invalid. A burnt token still has an associated owner. + pub fn owner_of(&mut self, token_id: Maybe, token_hash: Maybe) -> Address { + todo!() + } + + /// Returns the approved account (if any) associated with the provided token_id + /// Reverts if token has been burnt. + pub fn get_approved(&mut self, token_id: Maybe, token_hash: Maybe) -> Option
{ + todo!() + } + + /// Returns number of owned tokens associated with the provided token holder + pub fn balance_of(&mut self, token_owner: Address) -> u64 { + todo!() + } + + /// Returns the metadata associated with the provided token_id + pub fn get_token_metadata(&mut self, token_id: Maybe, token_hash: Maybe) -> String { + todo!() + } + + /// Updates the metadata if valid. + pub fn set_token_metadata(&mut self, token_metadata: String) { + todo!() + } + + /// This entrypoint will upgrade the contract from the 1_0 version to the + /// 1_1 version. The contract will insert any addition dictionaries and + /// sentinel values that were absent in the previous version of the contract. + /// It will also perform the necessary data transformations of historical + /// data if needed + pub fn migrate(&mut self, nft_package_key: String) { + todo!() + } + + /// This entrypoint will allow NFT owners to update their receipts from + /// the previous owned_tokens list model to the current pagination model + /// scheme. Calling the entrypoint will return a list of receipt names + /// alongside the dictionary addressed to the relevant pages. + pub fn updated_receipts(&mut self) -> Vec<(String, Address)> { + todo!() + } + + /// This entrypoint allows users to register with a give CEP-78 instance, + /// allocating the necessary page table to enable the reverse lookup + /// functionality and allowing users to pay the upfront cost of allocation + /// resulting in more stable gas costs when minting and transferring + /// Note: This entrypoint MUST be invoked if the reverse lookup is enabled + /// in order to own NFTs. + pub fn register_owner(&mut self) -> (String, URef) { + todo!() + } +} diff --git a/modules/src/lib.rs b/modules/src/lib.rs index afd71575..2abeace4 100644 --- a/modules/src/lib.rs +++ b/modules/src/lib.rs @@ -14,3 +14,4 @@ pub mod erc721_receiver; pub mod erc721_token; pub mod security; pub mod wrapped_native; +pub mod cep78; From c00f9411b3c8c38da48f96c9f431c7d055e7e2bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Fri, 12 Apr 2024 10:06:57 +0200 Subject: [PATCH 02/38] cep-78 entrypoints implementation --- modules/Cargo.toml | 4 + modules/src/cep78/constants.rs | 39 -- modules/src/cep78/metadata.rs | 278 +++++++++++ modules/src/cep78/mod.rs | 5 +- modules/src/cep78/modalities.rs | 24 +- modules/src/cep78/token.rs | 793 ++++++++++++++++++++++++++++++-- modules/src/cep78/utils.rs | 35 ++ modules/src/cep78/whitelist.rs | 60 +++ odra-schema/src/custom_type.rs | 4 +- odra-schema/src/ty.rs | 8 +- 10 files changed, 1175 insertions(+), 75 deletions(-) create mode 100644 modules/src/cep78/metadata.rs create mode 100644 modules/src/cep78/utils.rs create mode 100644 modules/src/cep78/whitelist.rs diff --git a/modules/Cargo.toml b/modules/Cargo.toml index 90b3817a..89de542c 100644 --- a/modules/Cargo.toml +++ b/modules/Cargo.toml @@ -12,6 +12,10 @@ categories = ["wasm"] [dependencies] odra = { path = "../odra", version = "0.9.1", default-features = false } hex = { version = "0.4.3", default-features = false } +serde = { version = "1.0.80", default-features = false } +serde_json = { version = "1.0.59", default-features = false } +serde-json-wasm = { version = "1.0.1", default-features = false } +base16 = { version = "0.2.1", default-features = false } [dev-dependencies] odra-test = { path = "../odra-test", version = "0.9.1" } diff --git a/modules/src/cep78/constants.rs b/modules/src/cep78/constants.rs index 293ce412..94bc2b92 100644 --- a/modules/src/cep78/constants.rs +++ b/modules/src/cep78/constants.rs @@ -5,45 +5,6 @@ pub const PREFIX_CONTRACT_VERSION: &str = "cep78_contract_version"; pub const PREFIX_HASH_KEY_NAME: &str = "cep78_contract_package"; pub const PREFIX_PAGE_DICTIONARY: &str = "page"; -pub const ARG_ACCESS_KEY_NAME_1_0_0: &str = "access_key_name"; -pub const ARG_ACL_PACKAGE_MODE: &str = "acl_package_mode"; -pub const ARG_ACL_WHITELIST: &str = "acl_whitelist"; -pub const ARG_ADDITIONAL_REQUIRED_METADATA: &str = "additional_required_metadata"; -pub const ARG_ALLOW_MINTING: &str = "allow_minting"; -pub const ARG_APPROVE_ALL: &str = "approve_all"; -pub const ARG_BURN_MODE: &str = "burn_mode"; -pub const ARG_COLLECTION_NAME: &str = "collection_name"; -pub const ARG_COLLECTION_SYMBOL: &str = "collection_symbol"; -pub const ARG_CONTRACT_WHITELIST: &str = "contract_whitelist"; -pub const ARG_EVENTS_MODE: &str = "events_mode"; -pub const ARG_HASH_KEY_NAME_1_0_0: &str = "hash_key_name"; -pub const ARG_HOLDER_MODE: &str = "holder_mode"; -pub const ARG_IDENTIFIER_MODE: &str = "identifier_mode"; -pub const ARG_JSON_SCHEMA: &str = "json_schema"; -pub const ARG_METADATA_MUTABILITY: &str = "metadata_mutability"; -pub const ARG_MINTING_MODE: &str = "minting_mode"; -pub const ARG_NAMED_KEY_CONVENTION: &str = "named_key_convention"; -pub const ARG_NFT_KIND: &str = "nft_kind"; -pub const ARG_NFT_METADATA_KIND: &str = "nft_metadata_kind"; -pub const ARG_NFT_PACKAGE_KEY: &str = "cep78_package_key"; -pub const ARG_OPTIONAL_METADATA: &str = "optional_metadata"; -pub const ARG_OPERATOR: &str = "operator"; -pub const ARG_OPERATOR_BURN_MODE: &str = "operator_burn_mode"; -pub const ARG_OWNERSHIP_MODE: &str = "ownership_mode"; -pub const ARG_OWNER_LOOKUP_MODE: &str = "owner_reverse_lookup_mode"; -pub const ARG_PACKAGE_OPERATOR_MODE: &str = "package_operator_mode"; -pub const ARG_RECEIPT_NAME: &str = "receipt_name"; -pub const ARG_SOURCE_KEY: &str = "source_key"; -pub const ARG_SPENDER: &str = "spender"; -pub const ARG_TARGET_KEY: &str = "target_key"; -pub const ARG_TOKEN_HASH: &str = "token_hash"; -pub const ARG_TOKEN_ID: &str = "token_id"; -pub const ARG_TOKEN_META_DATA: &str = "token_meta_data"; -pub const ARG_TOKEN_OWNER: &str = "token_owner"; -pub const ARG_TOTAL_TOKEN_SUPPLY: &str = "total_token_supply"; -pub const ARG_TRANSFER_FILTER_CONTRACT: &str = "transfer_filter_contract"; -pub const ARG_WHITELIST_MODE: &str = "whitelist_mode"; - pub const ENTRY_POINT_APPROVE: &str = "approve"; pub const ENTRY_POINT_BALANCE_OF: &str = "balance_of"; pub const ENTRY_POINT_BURN: &str = "burn"; diff --git a/modules/src/cep78/metadata.rs b/modules/src/cep78/metadata.rs new file mode 100644 index 00000000..2281eb96 --- /dev/null +++ b/modules/src/cep78/metadata.rs @@ -0,0 +1,278 @@ +use odra::{args::Maybe, prelude::*, Mapping, UnwrapOrRevert, Var}; +use serde::{Deserialize, Serialize}; + +use super::{constants::{METADATA_CEP78, METADATA_CUSTOM_VALIDATED, METADATA_NFT721, METADATA_RAW}, error::CEP78Error, modalities::{MetadataMutability, MetadataRequirement, NFTIdentifierMode, NFTMetadataKind, Requirement, TokenIdentifier}, utils::IntoOrRevert}; + +#[odra::module] +pub struct Metadata { + requirements: Var, + identifier_mode: Var, + mutability: Var, + json_schema: Var, + validated_metadata: Mapping, +} + +impl Metadata { + pub fn init( + &mut self, + base_metadata_kind: u8, + additional_required_metadata: Maybe>, + optional_metadata: Maybe>, + metadata_mutability: u8, + identifier_mode: u8, + json_schema: Maybe, + ) { + let env = self.env(); + + let mut requirements = MetadataRequirement::new(); + for optional in optional_metadata.unwrap_or_default() { + requirements.insert(optional.into_or_revert(&env),Requirement::Optional); + } + for required in additional_required_metadata.unwrap_or_default() { + requirements.insert(required.into_or_revert(&env),Requirement::Required); + } + let base: NFTMetadataKind = base_metadata_kind.into_or_revert(&env); + requirements.insert(base, Requirement::Required); + + self.requirements.set(requirements); + self.identifier_mode.set(identifier_mode.into_or_revert(&env)); + self.mutability.set(metadata_mutability.into_or_revert(&env)); + self.json_schema.set(json_schema.unwrap_or_default()); + } + + pub fn get_requirements(&self) -> MetadataRequirement { + self.requirements.get_or_default() + } + + pub fn get_identifier_mode(&self) -> NFTIdentifierMode { + self.identifier_mode.get_or_revert_with(CEP78Error::InvalidIdentifierMode) + } + + pub fn get_or_revert(&self, token_identifier: &TokenIdentifier) -> String { + let env = self.env(); + let metadata_kind_list = self.get_requirements(); + + for (metadata_kind, required) in metadata_kind_list { + match required { + Requirement::Required => { + let id = token_identifier.to_string(); + let kind = get_metadata_key(&metadata_kind); + let key = format!("{}{}", kind, id); + let metadata = self.validated_metadata + .get(&key) + .unwrap_or_revert_with(&env, CEP78Error::InvalidTokenIdentifier); + return metadata; + } + _ => continue, + } + } + env.revert(CEP78Error::MissingTokenMetaData) + } + + pub fn ensure_mutability(&self, error: CEP78Error) { + let current_mutability = self.mutability.get_or_revert_with(CEP78Error::InvalidMetadataMutability); + if current_mutability != MetadataMutability::Mutable { + self.env().revert(error) + } + } + + pub fn update_or_revert(&mut self, token_metadata: &str, token_identifier: &TokenIdentifier) { + let requirements = self.get_requirements(); + for (metadata_kind, required) in requirements { + if required == Requirement::Unneeded { + continue; + } + let token_metadata_validation = self.validate(&metadata_kind, token_metadata); + match token_metadata_validation { + Ok(validated_token_metadata) => { + let id = token_identifier.to_string(); + let kind = get_metadata_key(&metadata_kind); + let key = format!("{}{}", kind, id); + self.validated_metadata.set(&key, validated_token_metadata); + } + Err(err) => { + self.env().revert(err); + } + } + } + } + + fn validate(&self, kind: &NFTMetadataKind, metadata: &str) -> Result { + let token_schema = self.get_metadata_schema(&kind); + match kind { + NFTMetadataKind::CEP78 => { + let metadata = serde_json_wasm::from_str::(&metadata) + .map_err(|_| CEP78Error::FailedToParseCep99Metadata)?; + + if let Some(name_property) = token_schema.properties.get("name") { + if name_property.required && metadata.name.is_empty() { + self.env().revert(CEP78Error::InvalidCEP99Metadata) + } + } + if let Some(token_uri_property) = token_schema.properties.get("token_uri") { + if token_uri_property.required && metadata.token_uri.is_empty() { + self.env().revert(CEP78Error::InvalidCEP99Metadata) + } + } + if let Some(checksum_property) = token_schema.properties.get("checksum") { + if checksum_property.required && metadata.checksum.is_empty() { + self.env().revert(CEP78Error::InvalidCEP99Metadata) + } + } + serde_json::to_string_pretty(&metadata) + .map_err(|_| CEP78Error::FailedToJsonifyCEP99Metadata) + } + NFTMetadataKind::NFT721 => { + let metadata = serde_json_wasm::from_str::(&metadata) + .map_err(|_| CEP78Error::FailedToParse721Metadata)?; + + if let Some(name_property) = token_schema.properties.get("name") { + if name_property.required && metadata.name.is_empty() { + self.env().revert(CEP78Error::InvalidNFT721Metadata) + } + } + if let Some(token_uri_property) = token_schema.properties.get("token_uri") { + if token_uri_property.required && metadata.token_uri.is_empty() { + self.env().revert(CEP78Error::InvalidNFT721Metadata) + } + } + if let Some(symbol_property) = token_schema.properties.get("symbol") { + if symbol_property.required && metadata.symbol.is_empty() { + self.env().revert(CEP78Error::InvalidNFT721Metadata) + } + } + serde_json::to_string_pretty(&metadata) + .map_err(|_| CEP78Error::FailedToJsonifyNFT721Metadata) + } + NFTMetadataKind::Raw => Ok(metadata.to_owned()), + NFTMetadataKind::CustomValidated => { + let custom_metadata = + serde_json_wasm::from_str::>(&metadata) + .map(|attributes| CustomMetadata { attributes }) + .map_err(|_| CEP78Error::FailedToParseCustomMetadata)?; + + for (property_name, property_type) in token_schema.properties.iter() { + if property_type.required && custom_metadata.attributes.get(property_name).is_none() + { + self.env().revert(CEP78Error::InvalidCustomMetadata) + } + } + serde_json::to_string_pretty(&custom_metadata.attributes) + .map_err(|_| CEP78Error::FailedToJsonifyCustomMetadata) + } + } + } + + fn get_metadata_schema(&self, kind: &NFTMetadataKind) -> CustomMetadataSchema { + match kind { + NFTMetadataKind::Raw => CustomMetadataSchema { + properties: BTreeMap::new(), + }, + NFTMetadataKind::NFT721 => { + let mut properties = BTreeMap::new(); + properties.insert( + "name".to_string(), + MetadataSchemaProperty { + name: "name".to_string(), + description: "The name of the NFT".to_string(), + required: true, + }, + ); + properties.insert( + "symbol".to_string(), + MetadataSchemaProperty { + name: "symbol".to_string(), + description: "The symbol of the NFT collection".to_string(), + required: true, + }, + ); + properties.insert( + "token_uri".to_string(), + MetadataSchemaProperty { + name: "token_uri".to_string(), + description: "The URI pointing to an off chain resource".to_string(), + required: true, + }, + ); + CustomMetadataSchema { properties } + } + NFTMetadataKind::CEP78 => { + let mut properties = BTreeMap::new(); + properties.insert( + "name".to_string(), + MetadataSchemaProperty { + name: "name".to_string(), + description: "The name of the NFT".to_string(), + required: true, + }, + ); + properties.insert( + "token_uri".to_string(), + MetadataSchemaProperty { + name: "token_uri".to_string(), + description: "The URI pointing to an off chain resource".to_string(), + required: true, + }, + ); + properties.insert( + "checksum".to_string(), + MetadataSchemaProperty { + name: "checksum".to_string(), + description: "A SHA256 hash of the content at the token_uri".to_string(), + required: true, + }, + ); + CustomMetadataSchema { properties } + } + NFTMetadataKind::CustomValidated => { + serde_json_wasm::from_str::(&self.json_schema.get_or_default()) + .map_err(|_| CEP78Error::InvalidJsonSchema) + .unwrap_or_revert(&self.env()) + } + } + } +} + +#[derive(Serialize, Deserialize)] +#[odra::odra_type] +pub(crate) struct MetadataSchemaProperty { + name: String, + description: String, + required: bool, +} + +#[derive(Serialize, Deserialize, Clone)] +pub(crate) struct CustomMetadataSchema { + properties: BTreeMap, +} + + +// Using a structure for the purposes of serialization formatting. +#[derive(Serialize, Deserialize)] +pub(crate) struct MetadataNFT721 { + name: String, + symbol: String, + token_uri: String, +} + +#[derive(Serialize, Deserialize)] +pub(crate) struct MetadataCEP78 { + name: String, + token_uri: String, + checksum: String, +} + +// Using a structure for the purposes of serialization formatting. +#[derive(Serialize, Deserialize)] +pub(crate) struct CustomMetadata { + attributes: BTreeMap, +} + +pub(crate) fn get_metadata_key(metadata_kind: &NFTMetadataKind) -> String { + match metadata_kind { + NFTMetadataKind::CEP78 => METADATA_CEP78, + NFTMetadataKind::NFT721 => METADATA_NFT721, + NFTMetadataKind::Raw => METADATA_RAW, + NFTMetadataKind::CustomValidated => METADATA_CUSTOM_VALIDATED, + }.to_string() +} \ No newline at end of file diff --git a/modules/src/cep78/mod.rs b/modules/src/cep78/mod.rs index 941146b4..2e7c1bae 100644 --- a/modules/src/cep78/mod.rs +++ b/modules/src/cep78/mod.rs @@ -4,4 +4,7 @@ pub mod events; pub mod modalities; pub mod error; pub mod constants; -pub mod token; \ No newline at end of file +pub mod token; +mod whitelist; +mod metadata; +mod utils; diff --git a/modules/src/cep78/modalities.rs b/modules/src/cep78/modalities.rs index 746d08b3..40f0ec58 100644 --- a/modules/src/cep78/modalities.rs +++ b/modules/src/cep78/modalities.rs @@ -1,8 +1,10 @@ +use core::default; + use odra::prelude::*; use super::error::CEP78Error; #[repr(u8)] -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Clone)] pub enum WhitelistMode { Unlocked = 0, Locked = 1, @@ -41,7 +43,7 @@ impl TryFrom for NFTHolderMode { } } -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Clone)] #[repr(u8)] pub enum MintingMode { /// The ability to mint NFTs is restricted to the installing account only. @@ -66,9 +68,11 @@ impl TryFrom for MintingMode { } #[repr(u8)] +#[derive(Default, Clone)] pub enum NFTKind { /// The NFT represents a real-world physical /// like a house. + #[default] Physical = 0, /// The NFT represents a digital asset like a unique /// JPEG or digital art. @@ -116,8 +120,10 @@ impl TryFrom for Requirement { } #[repr(u8)] +#[derive(Default, PartialOrd, Ord)] #[odra::odra_type] pub enum NFTMetadataKind { + #[default] CEP78 = 0, NFT721 = 1, Raw = 2, @@ -139,9 +145,10 @@ impl TryFrom for NFTMetadataKind { } #[repr(u8)] -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Clone, Default)] pub enum OwnershipMode { /// The minter owns it and can never transfer it. + #[default] Minter = 0, /// The minter assigns it to an address and can never be transferred. Assigned = 1, @@ -163,7 +170,7 @@ impl TryFrom for OwnershipMode { } #[repr(u8)] -#[derive(PartialEq, Eq, Clone, Copy)] +#[odra::odra_type] pub enum NFTIdentifierMode { Ordinal = 0, Hash = 1, @@ -182,7 +189,10 @@ impl TryFrom for NFTIdentifierMode { } #[repr(u8)] +#[derive(Default)] +#[odra::odra_type] pub enum MetadataMutability { + #[default] Immutable = 0, Mutable = 1, } @@ -247,6 +257,7 @@ impl ToString for TokenIdentifier { #[repr(u8)] +#[odra::odra_type] pub enum BurnMode { Burnable = 0, NonBurnable = 1, @@ -306,11 +317,12 @@ impl TryFrom for NamedKeyConventionMode { } #[repr(u8)] -#[derive(PartialEq, Eq, Clone, Copy)] +#[derive(PartialEq, Eq, Clone, Copy, Default)] #[allow(clippy::upper_case_acronyms)] pub enum EventsMode { NoEvents = 0, CEP47 = 1, + #[default] CES = 2, } @@ -329,7 +341,7 @@ impl TryFrom for EventsMode { #[repr(u8)] #[non_exhaustive] -#[derive(PartialEq, Eq)] +#[odra::odra_type] pub enum TransferFilterContractResult { DenyTransfer = 0, ProceedTransfer, diff --git a/modules/src/cep78/token.rs b/modules/src/cep78/token.rs index a197c89b..c17824f1 100644 --- a/modules/src/cep78/token.rs +++ b/modules/src/cep78/token.rs @@ -1,15 +1,124 @@ -use odra::{args::Maybe, casper_types::URef, prelude::*, Address}; -use super::error::CEP78Error; +use odra::{args::Maybe, casper_types::{bytesrepr::ToBytes, ContractHash, ContractPackage, Key, URef}, prelude::*, Address, Mapping, Sequence, SubModule, UnwrapOrRevert, Var}; +use super::{constants, error::CEP78Error, events::{Approval, ApprovalForAll, ApprovalRevoked, Burn, MetadataUpdated, Mint, RevokedForAll, VariablesSet}, metadata::Metadata, modalities::{BurnMode, EventsMode, MintingMode, NFTIdentifierMode, OwnershipMode, TokenIdentifier, TransferFilterContractResult}, utils::{self, GetAs}, whitelist::ACLWhitelist}; + -#[odra::module] -pub struct CEP78 { +#[odra::module( + //package_hash_key = "cep78", + //access_key = "cep78", +)] +pub struct CEP78 { + installer: Var
, + collection_name: Var, + collection_symbol: Var, + cap: Var, + allow_minting: Var, + minting_mode: Var, + ownership_mode: Var, + nft_kind: Var, + holder_mode: Var, + package_operator_mode: Var, + operator_burn_mode: Var, + burn_mode: Var, + events_mode: Var, + whitelist_mode: Var, + counter: Sequence, + whitelist: SubModule, + metadata: SubModule, + owners: Mapping, + issuers: Mapping, + approved: Mapping>, + token_count: Mapping, + burnt_tokens: Mapping, + operators: Mapping<(Address, Address), bool> } -// #[odra::module] +#[odra::module] impl CEP78 { /// Initializes the module. - pub fn init(&mut self) { + pub fn init( + &mut self, + collection_name: String, + collection_symbol: String, + total_token_supply: u64, + allow_minting: Maybe, + minting_mode: Maybe, + ownership_mode: u8, + nft_kind: u8, + holder_mode: Maybe, + whitelist_mode: Maybe, + acl_white_list: Maybe>, + acl_package_mode: Maybe, + package_operator_mode: Maybe, + json_schema: Maybe, + receipt_name: Maybe, + identifier_mode: u8, + burn_mode: Maybe, + operator_burn_mode: Maybe, + nft_metadata_kind: u8, + metadata_mutability: u8, + owner_reverse_lookup_mode: Maybe, + events_mode: u8, + transfer_filter_contract_contract_key: Maybe
, + + additional_required_metadata: Maybe>, + optional_metadata: Maybe>, + ) { + let installer = self.env().caller(); + self.installer.set(installer); + self.collection_name.set(collection_name); + self.collection_symbol.set(collection_symbol); + + if total_token_supply == 0 { + self.env().revert(CEP78Error::CannotInstallWithZeroSupply) + + } + + if total_token_supply > constants::MAX_TOTAL_TOKEN_SUPPLY { + self.env().revert(CEP78Error::ExceededMaxTotalSupply) + } + + self.cap.set(total_token_supply); + self.allow_minting.set(allow_minting.unwrap_or(true)); + self.minting_mode.set(minting_mode.clone().unwrap_or(0)); + self.ownership_mode.set(ownership_mode); + self.nft_kind.set(nft_kind); + self.holder_mode.set(holder_mode.unwrap_or(2u8)); + self.burn_mode.set(burn_mode.unwrap_or(0)); + self.operator_burn_mode.set(operator_burn_mode.unwrap_or_default()); + + self.whitelist.init( + acl_white_list.unwrap_or_default(), + whitelist_mode.unwrap_or_default(), + acl_package_mode.unwrap_or_default() + ); + + + // Deprecated in 1.4 in favor of following acl whitelist + // A whitelist of keys specifying which entity can mint + // NFTs in the contract holder mode with restricted minting. + // This value can only be modified if the whitelist lock is + // set to be unlocked. + // let contract_white_list: Vec = utils::get_optional_named_arg_with_user_errors( + // ARG_CONTRACT_WHITELIST, + // NFTCoreError::InvalidContractWhitelist, + // ) + // .unwrap_or_default(); + + self.package_operator_mode.set(package_operator_mode.unwrap_or_default()); + self.metadata.init(nft_metadata_kind, additional_required_metadata, optional_metadata, metadata_mutability, identifier_mode, json_schema); + + + if identifier_mode == 1 && metadata_mutability == 1 { + self.env().revert(CEP78Error::InvalidMetadataMutability) + } + + if ownership_mode == 0 && minting_mode.unwrap_or_default() == 0 && owner_reverse_lookup_mode.unwrap_or_default() == 1 { + self.env().revert(CEP78Error::InvalidReportingMode) + } + + self.counter.next_value(); + } /// Exposes all variables that can be changed by managing account post @@ -19,13 +128,33 @@ impl CEP78 { pub fn set_variables( &mut self, allow_minting: Maybe, - contract_whitelist: Maybe>, + contract_whitelist: Maybe>, acl_whitelist: Maybe>, acl_package_mode: Maybe, package_operator_mode: Maybe, operator_burn_mode: Maybe, ) { + let installer = self.installer.get_or_revert_with(CEP78Error::MissingInstaller); + + // Only the installing account can change the mutable variables. + self.ensure_not_caller(installer); + + if let Maybe::Some(allow_minting) = allow_minting { + self.allow_minting.set(allow_minting); + } + + self.whitelist.update_package_mode(acl_package_mode); + self.whitelist.update_addresses(acl_whitelist, contract_whitelist); + if let Maybe::Some(package_operator_mode) = package_operator_mode { + self.package_operator_mode.set(package_operator_mode); + } + + if let Maybe::Some(operator_burn_mode) = operator_burn_mode { + self.operator_burn_mode.set(operator_burn_mode); + } + + self.emit_ces_event(VariablesSet::new()); } @@ -43,8 +172,157 @@ impl CEP78 { &mut self, token_owner: Address, token_metadata: String, + token_hash: Maybe, ) -> (String, Address, String) { - todo!() + // The contract owner can toggle the minting behavior on and off over time. + // The contract is toggled on by default. + let allow_minting = self.allow_minting.get_or_default(); + + // If contract minting behavior is currently toggled off we revert. + if !allow_minting { + self.env().revert(CEP78Error::MintingIsPaused); + } + + let total_token_supply = self.cap.get_or_revert_with(CEP78Error::MissingTotalTokenSupply); + + // The minted_tokens_count is the number of minted tokens so far. + let minted_tokens_count = self.counter.get_current_value(); + + // Revert if the token supply has been exhausted. + if minted_tokens_count >= total_token_supply { + self.env().revert(CEP78Error::TokenSupplyDepleted); + } + + let minting_mode: MintingMode = self.minting_mode.get_as(&self.env()); + + // let (caller, contract_package): (Key, Option) = + // match self.env().caller() { + // Caller::Session(account_hash) => (account_hash.into(), None), + // Caller::StoredCaller(contract_hash, contract_package_hash) => { + // (contract_hash.into(), Some(contract_package_hash.into())) + // } + // }; + + let (caller, contract_package) = (self.env().caller(), None::); + + // Revert if minting is private and caller is not installer. + if MintingMode::Installer == minting_mode { + match caller { + Address::Account(_) => { + let installer_account = self.installer.get_or_revert_with(CEP78Error::MissingInstaller); + // Revert if private minting is required and caller is not installer. + if caller != installer_account { + self.env().revert(CEP78Error::InvalidMinter) + } + } + _ => self.env().revert(CEP78Error::InvalidKey), + } + } + + // Revert if minting is acl and caller is not whitelisted. + if MintingMode::Acl == minting_mode { + // TODO: Implement the following + // let acl_package_mode: bool = self.whitelist.is_package_mode(); + // let is_whitelisted = match (acl_package_mode, contract_package) { + // (true, Some(contract_package)) => utils::get_dictionary_value_from_key::( + // ACL_WHITELIST, + // &utils::encode_dictionary_item_key(contract_package), + // ) + // .unwrap_or_default(), + // _ => utils::get_dictionary_value_from_key::( + // ACL_WHITELIST, + // &utils::encode_dictionary_item_key(caller), + // ) + // .unwrap_or_default(), + // }; + let is_whitelisted = false; + + match caller { + Address::Contract(_) => { + if !is_whitelisted { + self.env().revert(CEP78Error::UnlistedContractHash); + } + } + Address::Account(_) => { + if !is_whitelisted { + self.env().revert(CEP78Error::InvalidMinter); + } + } + } + } + + let identifier_mode = self.metadata.get_identifier_mode(); + + let optional_token_hash: String = token_hash.unwrap_or_default(); + let token_identifier: TokenIdentifier = match identifier_mode { + NFTIdentifierMode::Ordinal => TokenIdentifier::Index(minted_tokens_count), + NFTIdentifierMode::Hash => TokenIdentifier::Hash(if optional_token_hash.is_empty() { + // TODO: Implement the following + // base16::encode_lower(&runtime::blake2b(token_metadata.clone())) + "".to_string() + } else { + optional_token_hash + }), + }; + + self.metadata.update_or_revert(&token_metadata, &token_identifier); + + + // The contract's ownership behavior (determined at installation) determines, + // who owns the NFT we are about to mint.() + let ownership_mode: OwnershipMode = self.ownership_mode.get_as(&self.env()); + let token_owner_key = if let OwnershipMode::Assigned | OwnershipMode::Transferable = ownership_mode { + token_owner + } else { + caller + }; + + let id = token_identifier.to_string(); + self.owners.set(&id, token_owner_key); + self.issuers.set(&id, caller); + + // TODO: Implement the following + // if let NFTIdentifierMode::Hash = identifier_mode { + // // Update the forward and reverse trackers + // utils::insert_hash_id_lookups(minted_tokens_count, token_identifier.clone()); + // } + + //Increment the count of owned tokens. + self.token_count.add(&token_owner_key, 1); + + // Increment number_of_minted_tokens by one + self.counter.next_value(); + + + // Emit Mint event. + self.emit_ces_event(Mint::new( + token_owner_key, + token_identifier.clone(), + token_metadata.clone(), + )); + + // TODO: Implement the following + // if let OwnerReverseLookupMode::Complete = utils::get_reporting_mode() { + // if (NFTIdentifierMode::Hash == identifier_mode) + // && runtime::get_key(OWNED_TOKENS).is_some() + // && utils::should_migrate_token_hashes(token_owner_key) + // { + // utils::migrate_token_hashes(token_owner_key) + // } + + // let (page_table_entry, page_uref) = utils::add_page_entry_and_page_record( + // minted_tokens_count, + // &owned_tokens_item_key, + // true, + // ); + + // let receipt_string = utils::get_receipt_name(page_table_entry); + // let receipt_address = Key::dictionary(page_uref, owned_tokens_item_key.as_bytes()); + // let token_identifier_string = token_identifier.get_dictionary_item_key(); + + // (receipt_string, receipt_address, token_identifier_string) + // } + (id, token_owner_key, token_metadata) } @@ -55,7 +333,61 @@ impl CEP78 { /// with error [CEP78Error::InvalidTokenIdentifier]. If the token is listed as already burnt we revert with /// error [CEP78Error::PreviouslyBurntToken]. If not the token is then registered as burnt. pub fn burn(&mut self, token_id: Maybe, token_hash: Maybe) { - todo!() + self.ensure_burnable(); + + let token_identifier = self.token_identifier(token_id, token_hash); + + let (caller, contract_package): (Address, Option
) = (self.env().caller(), None); + // match utils::get_verified_caller().unwrap_or_revert() { + // Caller::Session(account_hash) => (account_hash.into(), None), + // Caller::StoredCaller(contract_hash, contract_package_hash) => { + // (contract_hash.into(), Some(contract_package_hash.into())) + // } + // }; + + let token_owner = self.owner_of_by_id(&token_identifier); + + // Check if caller is owner + let is_owner = token_owner == caller; + + // Check if caller is operator to execute burn + let is_operator = if !is_owner { + self.operators.get_or_default(&(token_owner, caller)) + } else { + false + }; + + // With operator package mode check if caller's package is operator to let contract execute burn + let is_package_operator = if !is_owner && !is_operator { + match (self.package_operator_mode.get_or_default(), contract_package) { + (true, Some(contract_package)) => { + // TODO: Implement the following + // self.operators.get_or_default(&(token_owner, contract_package)); + true + } + _ => false, + } + } else { + false + }; + + // Revert if caller is not token_owner nor operator for the owner + if !is_owner && !is_operator && !is_package_operator { + self.env().revert(CEP78Error::InvalidTokenOwner) + }; + + // It makes sense to keep this token as owned by the caller. It just happens that the caller + // owns a burnt token. That's all. Similarly, we should probably also not change the + // owned_tokens dictionary. + self.ensure_not_burned(&token_identifier); + + // Mark the token as burnt by adding the token_id to the burnt tokens dictionary. + self.burnt_tokens.set(&token_identifier.to_string(), ()); + self.token_count.subtract(&token_owner, 1); + + + // Emit Burn event. + self.emit_ces_event(Burn::new(token_owner, token_identifier, caller)); } /// Transfers ownership of the token from one account to another. @@ -63,62 +395,359 @@ impl CEP78 { /// `token_id` is invalid, or if caller is not owner nor an approved account nor operator. /// If token id is invalid it reverts with error [CEP78Error::InvalidTokenIdentifier]. pub fn transfer(&mut self, token_id: Maybe, token_hash: Maybe, source_key: Address, target_key: Address) -> (String, Address) { + // If we are in minter or assigned mode we are not allowed to transfer ownership of token, hence + // we revert. + self.ensure_minter_or_assigned(); + + + let token_identifier = self.checked_token_identifier(token_id, token_hash); + + // We assume we cannot transfer burnt tokens + self.ensure_not_burned(&token_identifier); + + + self.ensure_not_owner(&token_identifier, &source_key); + + let (caller, contract_package): (Address, Option) = (self.env().caller(), None); + + let owner = self.owner_of_by_id(&token_identifier); + // Check if caller is owner + let is_owner = owner == caller; + + // Check if caller is approved to execute transfer + let is_approved = !is_owner + && match self.approved.get(&token_identifier.to_string()) { + Some(Some(maybe_approved)) => caller == maybe_approved, + Some(None) | None => false, + }; + + // Check if caller is operator to execute transfer + let is_operator = if !is_owner && !is_approved { + self.operators.get_or_default(&(source_key, caller)) + } else { + false + }; + + // With operator package mode check if caller's package is operator to let contract execute + // transfer + let is_package_operator = if !is_owner && !is_approved && !is_operator { + match (self.package_operator_mode.get_or_default(), contract_package) { + (true, Some(contract_package)) => { + // TODO: Implement the following + // self.operators.get_or_default(&(source_key, contract_package)) + self.operators.get_or_default(&(source_key, caller)) + } + _ => false, + } + } else { + false + }; + + if let Some(filter_contract) = utils::get_transfer_filter_contract() { + let result = TransferFilterContractContractRef::new(self.env(), filter_contract) + .can_transfer(source_key, target_key, token_identifier); + + if TransferFilterContractResult::DenyTransfer == result { + self.env().revert(CEP78Error::TransferFilterContractDenied); + } + } + + // Revert if caller is not owner nor approved nor an operator. + if !is_owner && !is_approved && !is_operator && !is_package_operator { + self.env().revert(CEP78Error::InvalidTokenOwner); + } + + + // if NFTIdentifierMode::Hash == identifier_mode && runtime::get_key(OWNED_TOKENS).is_some() { + // if utils::should_migrate_token_hashes(source_key) { + // utils::migrate_token_hashes(source_key) + // } + + // if utils::should_migrate_token_hashes(target_key) { + // utils::migrate_token_hashes(target_key) + // } + // } + + // let target_owner_item_key = utils::encode_dictionary_item_key(target_owner_key); + + // // Updated token_owners dictionary. Revert if token_owner not found. + // match utils::get_dictionary_value_from_key::( + // TOKEN_OWNERS, + // &token_identifier.get_dictionary_item_key(), + // ) { + // Some(token_actual_owner) => { + // if token_actual_owner != source_owner_key { + // runtime::revert(NFTCoreError::InvalidTokenOwner) + // } + // utils::upsert_dictionary_value_from_key( + // TOKEN_OWNERS, + // &token_identifier.get_dictionary_item_key(), + // target_owner_key, + // ); + // } + // None => runtime::revert(NFTCoreError::MissingOwnerTokenIdentifierKey), + // } + + // let source_owner_item_key = utils::encode_dictionary_item_key(source_owner_key); + + // // Update the from_account balance + // let updated_from_account_balance = + // match utils::get_dictionary_value_from_key::(TOKEN_COUNT, &source_owner_item_key) { + // Some(balance) => { + // if balance > 0u64 { + // balance - 1u64 + // } else { + // // This should never happen... + // runtime::revert(NFTCoreError::FatalTokenIdDuplication); + // } + // } + // None => { + // // This should never happen... + // runtime::revert(NFTCoreError::FatalTokenIdDuplication); + // } + // }; + // utils::upsert_dictionary_value_from_key( + // TOKEN_COUNT, + // &source_owner_item_key, + // updated_from_account_balance, + // ); + + // // Update the to_account balance + // let updated_to_account_balance = + // match utils::get_dictionary_value_from_key::(TOKEN_COUNT, &target_owner_item_key) { + // Some(balance) => balance + 1u64, + // None => 1u64, + // }; + + // utils::upsert_dictionary_value_from_key( + // TOKEN_COUNT, + // &target_owner_item_key, + // updated_to_account_balance, + // ); + + // utils::upsert_dictionary_value_from_key( + // APPROVED, + // &token_identifier.get_dictionary_item_key(), + // Option::::None, + // ); + + // let events_mode = EventsMode::try_from(utils::get_stored_value_with_user_errors::( + // EVENTS_MODE, + // NFTCoreError::MissingEventsMode, + // NFTCoreError::InvalidEventsMode, + // )) + // .unwrap_or_revert(); + + // match events_mode { + // EventsMode::NoEvents => {} + // EventsMode::CEP47 => record_cep47_event_dictionary(CEP47Event::Transfer { + // sender: caller, + // recipient: target_owner_key, + // token_id: token_identifier.clone(), + // }), + // EventsMode::CES => { + // // Emit Transfer event. + // let spender = if caller == owner { None } else { Some(caller) }; + // casper_event_standard::emit(Transfer::new( + // owner, + // spender, + // target_owner_key, + // token_identifier.clone(), + // )); + // } + // } + + // let reporting_mode = utils::get_reporting_mode(); + + // if let OwnerReverseLookupMode::Complete | OwnerReverseLookupMode::TransfersOnly = reporting_mode + // { + // // Update to_account owned_tokens. Revert if owned_tokens list is not found + // let tokens_count = utils::get_token_index(&token_identifier); + // if OwnerReverseLookupMode::TransfersOnly == reporting_mode { + // utils::add_page_entry_and_page_record(tokens_count, &source_owner_item_key, false); + // } + + // let (page_table_entry, page_uref) = utils::update_page_entry_and_page_record( + // tokens_count, + // &source_owner_item_key, + // &target_owner_item_key, + // ); + + // let owned_tokens_actual_key = Key::dictionary(page_uref, source_owner_item_key.as_bytes()); + + // let receipt_string = utils::get_receipt_name(page_table_entry); + + // let receipt = CLValue::from_t((receipt_string, owned_tokens_actual_key)) + // .unwrap_or_revert_with(NFTCoreError::FailedToConvertToCLValue); + // runtime::ret(receipt) + // } todo!() } /// Approves another token holder (an approved account) to transfer tokens. It /// reverts if token_id is invalid, if caller is not the owner nor operator, if token has already /// been burnt, or if caller tries to approve themselves as an approved account. - pub fn approve(&mut self, spender: Address, token_id: Maybe, token_hash: Maybe) { - todo!() + pub fn approve(&mut self, spender: Address, token_id: Maybe, token_hash: Maybe, operator: Maybe
) { + // If we are in minter or assigned mode it makes no sense to approve an account. Hence we + // revert. + self.ensure_minter_or_assigned(); + + let (caller, contract_package): (Address, Option) = + (self.env().caller(), None); + + let token_identifier = self.checked_token_identifier(token_id, token_hash); + + let owner = self.owner_of_by_id(&token_identifier); + + // Revert if caller is not token owner nor operator. + // Only the token owner or an operator can approve an account + let is_owner = caller == owner; + let is_operator = !is_owner && self.operators.get_or_default(&(owner, caller)); + + let is_package_operator = if !is_owner && !is_operator { + match (self.package_operator_mode.get_or_default(), contract_package) { + (true, Some(contract_package)) => { + // TODO: Implement the following + // self.operators.get_or_default(&(owner, contract_package)) + true + } + _ => false, + } + } else { + false + }; + + if !is_owner && !is_operator && !is_package_operator { + self.env().revert(CEP78Error::InvalidTokenOwner); + } + + // We assume a burnt token cannot be approved + self.ensure_not_burned(&token_identifier); + + let spender = match operator { + Maybe::Some(deprecated_operator) => deprecated_operator, + Maybe::None => spender + }; + + // If token owner or operator tries to approve itself that's probably a mistake and we revert. + self.ensure_not_caller(spender); + self.approved.set(&token_identifier.to_string(), Some(spender)); + self.emit_ces_event(Approval::new(owner, spender, token_identifier)); } /// Revokes an approved account to transfer tokens. It reverts /// if token_id is invalid, if caller is not the owner, if token has already /// been burnt, if caller tries to approve itself. pub fn revoke(&mut self, token_id: Maybe, token_hash: Maybe) { - todo!() + let env = self.env(); + // If we are in minter or assigned mode it makes no sense to approve an account. Hence we + // revert. + self.ensure_minter_or_assigned(); + + let (caller, contract_package): (Address, Option) = (env.caller(), None); + let token_identifier = self.checked_token_identifier(token_id, token_hash); + + // Revert if caller is not the token owner or an operator. Only the token owner / operators can + // revoke an approved account + let owner = self.owner_of_by_id(&token_identifier); + let is_owner = caller == owner; + let is_operator = !is_owner && self.operators.get_or_default(&(owner, caller)); + + let is_package_operator = if !is_owner && !is_operator { + match ( + self.package_operator_mode.get_or_default(), + contract_package, + ) { + (true, Some(contract_package)) => { + // TODO: Implement the following + // self.operators.get_or_default(&(owner, contract_package)) + true + } + _ => false, + } + } else { + false + }; + + if !is_owner && !is_operator && !is_package_operator { + env.revert(CEP78Error::InvalidTokenOwner); + } + + // We assume a burnt token cannot be revoked + self.ensure_not_burned(&token_identifier); + self.approved.set(&token_identifier.to_string(), Option::
::None); + // Emit ApprovalRevoked event. + self.emit_ces_event(ApprovalRevoked::new(owner, token_identifier)); } /// Approves all tokens owned by the caller and future to another token holder /// (an operator) to transfer tokens. It reverts if token_id is invalid, if caller is not the /// owner, if caller tries to approve itself as an operator. pub fn set_approval_for_all(&mut self, approve_all: bool, operator: Address) { - todo!() + let env = self.env(); + // If we are in minter or assigned mode it makes no sense to approve an operator. Hence we + // revert. + self.ensure_minter_or_assigned(); + // If caller tries to approve itself as operator that's probably a mistake and we revert. + self.ensure_not_caller(operator); + + let caller = env.caller(); + // Depending on approve_all we either approve all or disapprove all. + self.operators.set(&(caller, operator), approve_all); + + let events_mode: EventsMode = self.events_mode.get_as(&env); + if let EventsMode::CES = events_mode { + if approve_all { + env.emit_event(ApprovalForAll::new(caller, operator)); + } else { + env.emit_event(RevokedForAll::new(caller, operator)); + } + } } /// Returns if an account is operator for a token owner - pub fn is_approved_for_all(&mut self, token_owner: Address, operator: Address) -> bool { - todo!() + pub fn is_approved_for_all(&mut self, token_owner: Address, operator: Address) -> bool { + self.operators.get_or_default(&(token_owner, operator)) } /// Returns the token owner given a token_id. It reverts if token_id /// is invalid. A burnt token still has an associated owner. pub fn owner_of(&mut self, token_id: Maybe, token_hash: Maybe) -> Address { - todo!() + let token_identifier = self.checked_token_identifier(token_id, token_hash); + self.owner_of_by_id(&token_identifier) } /// Returns the approved account (if any) associated with the provided token_id /// Reverts if token has been burnt. pub fn get_approved(&mut self, token_id: Maybe, token_hash: Maybe) -> Option
{ - todo!() - } - - /// Returns number of owned tokens associated with the provided token holder - pub fn balance_of(&mut self, token_owner: Address) -> u64 { - todo!() + let token_identifier: TokenIdentifier = self.checked_token_identifier(token_id, token_hash); + + self.ensure_not_burned(&token_identifier); + self.approved.get(&token_identifier.to_string()).flatten() } - + /// Returns the metadata associated with the provided token_id - pub fn get_token_metadata(&mut self, token_id: Maybe, token_hash: Maybe) -> String { - todo!() + pub fn metadata(&mut self, token_id: Maybe, token_hash: Maybe) -> String { + let token_identifier = self.checked_token_identifier(token_id, token_hash); + self.metadata.get_or_revert(&token_identifier) } /// Updates the metadata if valid. - pub fn set_token_metadata(&mut self, token_metadata: String) { - todo!() + pub fn set_token_metadata(&mut self, token_id: Maybe, token_hash: Maybe, updated_token_metadata: String) { + self.metadata.ensure_mutability(CEP78Error::ForbiddenMetadataUpdate); + + let token_identifier = self.checked_token_identifier(token_id, token_hash); + self.ensure_owner_not_caller(&token_identifier); + self.metadata.update_or_revert(&updated_token_metadata, &token_identifier); + + self.emit_ces_event(MetadataUpdated::new(token_identifier, updated_token_metadata)); } + /// Returns number of owned tokens associated with the provided token holder + pub fn balance_of(&mut self, token_owner: Address) -> u64 { + self.token_count.get(&token_owner).unwrap_or_default() + } + /// This entrypoint will upgrade the contract from the 1_0 version to the /// 1_1 version. The contract will insert any addition dictionaries and /// sentinel values that were absent in the previous version of the contract. @@ -146,3 +775,113 @@ impl CEP78 { todo!() } } + + +impl CEP78 { + #[inline] + fn is_minter_or_assigned(&self) -> bool { + let ownership_mode: OwnershipMode = self.ownership_mode.get_as(&self.env()); + matches!(ownership_mode, OwnershipMode::Minter | OwnershipMode::Assigned) + } + + #[inline] + fn ensure_minter_or_assigned(&self) { + if self.is_minter_or_assigned() { + self.env().revert(CEP78Error::InvalidOwnershipMode) + } + } + + #[inline] + fn token_identifier(&self, token_id: Maybe, token_hash: Maybe) -> TokenIdentifier { + let env = self.env(); + let identifier_mode: NFTIdentifierMode = self.metadata.get_identifier_mode(); + match identifier_mode { + NFTIdentifierMode::Ordinal => TokenIdentifier::Index(token_id.unwrap(&env)), + NFTIdentifierMode::Hash => TokenIdentifier::Hash(token_hash.unwrap(&env)), + } + } + + #[inline] + fn checked_token_identifier(&self, token_id: Maybe, token_hash: Maybe) -> TokenIdentifier { + let env = self.env(); + let identifier_mode: NFTIdentifierMode = self.metadata.get_identifier_mode(); + let token_identifier = match identifier_mode { + NFTIdentifierMode::Ordinal => TokenIdentifier::Index(token_id.unwrap(&env)), + NFTIdentifierMode::Hash => TokenIdentifier::Hash(token_hash.unwrap(&env)), + }; + + let number_of_minted_tokens = self.counter.get_current_value(); + if let NFTIdentifierMode::Ordinal = identifier_mode { + // Revert if token_id is out of bounds + if token_identifier.get_index().unwrap_or_revert(&env) >= number_of_minted_tokens { + env.revert(CEP78Error::InvalidTokenIdentifier); + } + } + token_identifier + } + + #[inline] + fn owner_of_by_id(&self, id: &TokenIdentifier) -> Address { + match self.owners.get(&id.to_string()) { + Some(token_owner) => token_owner, + None => self.env().revert(CEP78Error::MissingOwnerTokenIdentifierKey), + } + } + + #[inline] + fn is_token_burned(&self, token_identifier: &TokenIdentifier) -> bool { + self.burnt_tokens.get(&token_identifier.to_string()).is_some() + } + + + #[inline] + fn ensure_not_owner(&self, token_identifier: &TokenIdentifier, address: &Address) { + let owner = self.owner_of_by_id(token_identifier); + if address == &owner { + self.env().revert(CEP78Error::InvalidAccount); + } + } + + #[inline] + fn ensure_owner_not_caller(&self, token_identifier: &TokenIdentifier) { + let owner = self.owner_of_by_id(token_identifier); + if self.env().caller() == owner { + self.env().revert(CEP78Error::InvalidTokenOwner); + } + } + + #[inline] + fn ensure_not_burned(&self, token_identifier: &TokenIdentifier) { + if self.is_token_burned(token_identifier) { + self.env().revert(CEP78Error::PreviouslyBurntToken); + } + } + + #[inline] + fn ensure_not_caller(&self, address: Address) { + if self.env().caller() == address { + self.env().revert(CEP78Error::InvalidAccount); + } + } + + #[inline] + fn emit_ces_event(&self, event: T) { + let events_mode: EventsMode = self.events_mode.get_as(&self.env()); + if let EventsMode::CES = events_mode { + self.env().emit_event(event); + } + } + + #[inline] + fn ensure_burnable(&self) { + if let BurnMode::NonBurnable = self.burn_mode.get_as(&self.env()) { + self.env().revert(CEP78Error::InvalidBurnMode) + } + } +} + + +#[odra::external_contract] +pub trait TransferFilterContract { + fn can_transfer(&self, source_key: Address, target_key: Address, token_id: TokenIdentifier) -> TransferFilterContractResult; +} \ No newline at end of file diff --git a/modules/src/cep78/utils.rs b/modules/src/cep78/utils.rs new file mode 100644 index 00000000..93f55673 --- /dev/null +++ b/modules/src/cep78/utils.rs @@ -0,0 +1,35 @@ +use odra::{casper_types::{bytesrepr::FromBytes, ContractHash}, Address, ContractEnv, OdraError, UnwrapOrRevert, Var}; + +pub trait GetAs { + fn get_as(&self, env: &ContractEnv) -> T; +} + +impl GetAs for Var +where + R: TryInto + Default + FromBytes, + R::Error: Into, +{ + fn get_as(&self, env: &ContractEnv) -> T { + self.get_or_default().try_into().unwrap_or_revert(env) + } +} + +pub trait IntoOrRevert { + type Error; + fn into_or_revert(self, env: &ContractEnv) -> T; +} + +impl IntoOrRevert for R +where + R: TryInto, + R::Error: Into, +{ + type Error = R::Error; + fn into_or_revert(self, env: &ContractEnv) -> T { + self.try_into().unwrap_or_revert(env) + } +} + +pub fn get_transfer_filter_contract() -> Option
{ + None +} \ No newline at end of file diff --git a/modules/src/cep78/whitelist.rs b/modules/src/cep78/whitelist.rs new file mode 100644 index 00000000..1d620e78 --- /dev/null +++ b/modules/src/cep78/whitelist.rs @@ -0,0 +1,60 @@ +use odra::{args::Maybe, casper_types::{ContractHash, Key}, prelude::*, Address, List, UnwrapOrRevert, Var}; + +use super::{error::CEP78Error, modalities::WhitelistMode, utils::GetAs}; + +#[odra::module] +pub struct ACLWhitelist { + addresses: List
, + mode: Var, + package_mode: Var, +} + +impl ACLWhitelist { + pub fn init(&mut self, addresses: Vec
, mode: u8, package_mode: bool) { + for address in addresses { + self.addresses.push(address); + } + self.mode.set(mode); + self.package_mode.set(package_mode); + } + + pub fn get_mode(&self) -> WhitelistMode { + self.mode.get_as(&self.env()) + } + + pub fn is_package_mode(&self) -> bool { + self.package_mode.get_or_default() + } + + pub fn update_package_mode(&mut self, package_mode: Maybe) { + if let Maybe::Some(package_mode) = package_mode { + self.package_mode.set(package_mode); + } + } + + pub fn update_addresses(&mut self, addresses: Maybe>, contract_whitelist: Maybe>) { + let mut new_addresses = addresses.unwrap_or_default(); + + // Deprecated in 1.4 in favor of above ARG_ACL_WHITELIST + let new_contract_whitelist = contract_whitelist.unwrap_or_default(); + + for contract_hash in new_contract_whitelist.iter() { + let address = Address::try_from(Key::from(*contract_hash)).unwrap_or_revert(&self.env()); + new_addresses.push(address); + } + + if !new_addresses.is_empty() { + match self.get_mode() { + WhitelistMode::Unlocked => { + while let Some(_) = self.addresses.pop() { + + } + for address in new_addresses { + self.addresses.push(address); + } + } + WhitelistMode::Locked => self.env().revert(CEP78Error::InvalidWhitelistMode), + } + } + } +} \ No newline at end of file diff --git a/odra-schema/src/custom_type.rs b/odra-schema/src/custom_type.rs index 1b460fce..4784f261 100644 --- a/odra-schema/src/custom_type.rs +++ b/odra-schema/src/custom_type.rs @@ -2,6 +2,7 @@ use std::collections::BTreeMap; use casper_contract_schema::CustomType; use casper_types::bytesrepr::{FromBytes, ToBytes}; +use casper_types::URef; use casper_types::{bytesrepr::Bytes, CLTyped, PublicKey, U128, U256, U512}; use num_traits::{Num, One, Zero}; use odra_core::{args::Maybe, Address}; @@ -50,7 +51,8 @@ impl_schema_custom_types!( String, (), PublicKey, - Bytes + Bytes, + URef ); impl SchemaCustomTypes for Option {} diff --git a/odra-schema/src/ty.rs b/odra-schema/src/ty.rs index a243b499..a24f9540 100644 --- a/odra-schema/src/ty.rs +++ b/odra-schema/src/ty.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; pub use casper_contract_schema; use casper_contract_schema::NamedCLType; -use casper_types::{bytesrepr::Bytes, Key, PublicKey, URef, U128, U256, U512}; +use casper_types::{bytesrepr::Bytes, ContractHash, Key, PublicKey, URef, U128, U256, U512}; use odra_core::{args::Maybe, Address}; /// Trait for types that can be represented as a NamedCLType. @@ -159,6 +159,12 @@ impl NamedCLTyped for PublicKey { } } +impl NamedCLTyped for ContractHash { + fn ty() -> NamedCLType { + NamedCLType::ByteArray(32) + } +} + impl NamedCLTyped for Address { fn ty() -> NamedCLType { NamedCLType::Key From 17bd2e88af407ad54fa35e4bac7cfce96623c8f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Mon, 15 Apr 2024 11:46:28 +0200 Subject: [PATCH 03/38] reverse lookup wip --- core/src/args.rs | 10 +- modules/Cargo.toml | 1 + modules/src/cep78/error.rs | 3 +- modules/src/cep78/events.rs | 32 +- modules/src/cep78/metadata.rs | 107 +++-- modules/src/cep78/mod.rs | 12 +- modules/src/cep78/modalities.rs | 103 ++-- modules/src/cep78/pagination.rs | 163 +++++++ modules/src/cep78/reverse_lookup.rs | 80 ++++ modules/src/cep78/tests/utils.rs | 79 +++ modules/src/cep78/token.rs | 715 +++++++++++----------------- modules/src/cep78/utils.rs | 176 ++++++- modules/src/cep78/whitelist.rs | 48 +- modules/src/lib.rs | 2 +- 14 files changed, 950 insertions(+), 581 deletions(-) create mode 100644 modules/src/cep78/pagination.rs create mode 100644 modules/src/cep78/reverse_lookup.rs create mode 100644 modules/src/cep78/tests/utils.rs diff --git a/core/src/args.rs b/core/src/args.rs index a62d2222..2d64cb01 100644 --- a/core/src/args.rs +++ b/core/src/args.rs @@ -1,6 +1,6 @@ //! This module provides types and traits for working with entrypoint arguments. -use crate::{contract_def::Argument, prelude::*, ContractEnv, ExecutionError}; +use crate::{contract_def::Argument, prelude::*, ContractEnv, ExecutionEnv, ExecutionError, OdraError}; use casper_types::{ bytesrepr::{FromBytes, ToBytes}, CLType, CLTyped, Parameter, RuntimeArgs @@ -34,6 +34,14 @@ impl Maybe { Maybe::None => env.revert(ExecutionError::UnwrapError) } } + + /// Unwraps the value or returns the default value. + pub fn unwrap_or(self, default: T) -> T { + match self { + Maybe::Some(value) => value, + Maybe::None => default + } + } } impl Maybe { diff --git a/modules/Cargo.toml b/modules/Cargo.toml index 89de542c..c4b4452b 100644 --- a/modules/Cargo.toml +++ b/modules/Cargo.toml @@ -19,6 +19,7 @@ base16 = { version = "0.2.1", default-features = false } [dev-dependencies] odra-test = { path = "../odra-test", version = "0.9.1" } +derive_builder = "0.20.0" [build-dependencies] odra-build = { path = "../odra-build", version = "0.9.1" } diff --git a/modules/src/cep78/error.rs b/modules/src/cep78/error.rs index a10f45d8..61bd1875 100644 --- a/modules/src/cep78/error.rs +++ b/modules/src/cep78/error.rs @@ -169,6 +169,5 @@ pub enum CEP78Error { InvalidOperatorBurnMode = 167, MissingOperatorBurnMode = 168, InvalidIdentifier = 169, - DuplicateIdentifier = 170, + DuplicateIdentifier = 170 } - diff --git a/modules/src/cep78/events.rs b/modules/src/cep78/events.rs index 81ffc25f..3486bc41 100644 --- a/modules/src/cep78/events.rs +++ b/modules/src/cep78/events.rs @@ -1,11 +1,11 @@ -use odra::{prelude::*, Address}; use super::modalities::TokenIdentifier; +use odra::{prelude::*, Address}; #[odra::event] pub struct Mint { recipient: Address, token_id: String, - data: String, + data: String } impl Mint { @@ -13,7 +13,7 @@ impl Mint { Self { recipient, token_id: token_id.to_string(), - data, + data } } } @@ -22,7 +22,7 @@ impl Mint { pub struct Burn { owner: Address, token_id: String, - burner: Address, + burner: Address } impl Burn { @@ -30,7 +30,7 @@ impl Burn { Self { owner, token_id: token_id.to_string(), - burner, + burner } } } @@ -39,7 +39,7 @@ impl Burn { pub struct Approval { owner: Address, spender: Address, - token_id: String, + token_id: String } impl Approval { @@ -47,7 +47,7 @@ impl Approval { Self { owner, spender, - token_id: token_id.to_string(), + token_id: token_id.to_string() } } } @@ -55,14 +55,14 @@ impl Approval { #[odra::event] pub struct ApprovalRevoked { owner: Address, - token_id: String, + token_id: String } impl ApprovalRevoked { pub fn new(owner: Address, token_id: TokenIdentifier) -> Self { Self { owner, - token_id: token_id.to_string(), + token_id: token_id.to_string() } } } @@ -70,7 +70,7 @@ impl ApprovalRevoked { #[odra::event] pub struct ApprovalForAll { owner: Address, - operator: Address, + operator: Address } impl ApprovalForAll { @@ -82,7 +82,7 @@ impl ApprovalForAll { #[odra::event] pub struct RevokedForAll { owner: Address, - operator: Address, + operator: Address } impl RevokedForAll { @@ -96,7 +96,7 @@ pub struct Transfer { owner: Address, spender: Option
, recipient: Address, - token_id: String, + token_id: String } impl Transfer { @@ -104,13 +104,13 @@ impl Transfer { owner: Address, spender: Option
, recipient: Address, - token_id: TokenIdentifier, + token_id: TokenIdentifier ) -> Self { Self { owner, spender, recipient, - token_id: token_id.to_string(), + token_id: token_id.to_string() } } } @@ -118,14 +118,14 @@ impl Transfer { #[odra::event] pub struct MetadataUpdated { token_id: String, - data: String, + data: String } impl MetadataUpdated { pub fn new(token_id: TokenIdentifier, data: String) -> Self { Self { token_id: token_id.to_string(), - data, + data } } } diff --git a/modules/src/cep78/metadata.rs b/modules/src/cep78/metadata.rs index 2281eb96..7bb03902 100644 --- a/modules/src/cep78/metadata.rs +++ b/modules/src/cep78/metadata.rs @@ -1,7 +1,14 @@ use odra::{args::Maybe, prelude::*, Mapping, UnwrapOrRevert, Var}; use serde::{Deserialize, Serialize}; -use super::{constants::{METADATA_CEP78, METADATA_CUSTOM_VALIDATED, METADATA_NFT721, METADATA_RAW}, error::CEP78Error, modalities::{MetadataMutability, MetadataRequirement, NFTIdentifierMode, NFTMetadataKind, Requirement, TokenIdentifier}, utils::IntoOrRevert}; +use super::{ + constants::{METADATA_CEP78, METADATA_CUSTOM_VALIDATED, METADATA_NFT721, METADATA_RAW}, + error::CEP78Error, + modalities::{ + MetadataMutability, MetadataRequirement, NFTIdentifierMode, NFTMetadataKind, Requirement, + TokenIdentifier + }, +}; #[odra::module] pub struct Metadata { @@ -9,34 +16,31 @@ pub struct Metadata { identifier_mode: Var, mutability: Var, json_schema: Var, - validated_metadata: Mapping, + validated_metadata: Mapping } impl Metadata { pub fn init( &mut self, - base_metadata_kind: u8, - additional_required_metadata: Maybe>, - optional_metadata: Maybe>, - metadata_mutability: u8, - identifier_mode: u8, - json_schema: Maybe, + base_metadata_kind: NFTMetadataKind, + additional_required_metadata: Maybe>, + optional_metadata: Maybe>, + metadata_mutability: MetadataMutability, + identifier_mode: NFTIdentifierMode, + json_schema: Maybe ) { - let env = self.env(); - let mut requirements = MetadataRequirement::new(); for optional in optional_metadata.unwrap_or_default() { - requirements.insert(optional.into_or_revert(&env),Requirement::Optional); + requirements.insert(optional, Requirement::Optional); } for required in additional_required_metadata.unwrap_or_default() { - requirements.insert(required.into_or_revert(&env),Requirement::Required); + requirements.insert(required, Requirement::Required); } - let base: NFTMetadataKind = base_metadata_kind.into_or_revert(&env); - requirements.insert(base, Requirement::Required); + requirements.insert(base_metadata_kind, Requirement::Required); self.requirements.set(requirements); - self.identifier_mode.set(identifier_mode.into_or_revert(&env)); - self.mutability.set(metadata_mutability.into_or_revert(&env)); + self.identifier_mode.set(identifier_mode); + self.mutability.set(metadata_mutability); self.json_schema.set(json_schema.unwrap_or_default()); } @@ -45,32 +49,36 @@ impl Metadata { } pub fn get_identifier_mode(&self) -> NFTIdentifierMode { - self.identifier_mode.get_or_revert_with(CEP78Error::InvalidIdentifierMode) + self.identifier_mode + .get_or_revert_with(CEP78Error::InvalidIdentifierMode) } pub fn get_or_revert(&self, token_identifier: &TokenIdentifier) -> String { let env = self.env(); let metadata_kind_list = self.get_requirements(); - + for (metadata_kind, required) in metadata_kind_list { match required { Requirement::Required => { let id = token_identifier.to_string(); let kind = get_metadata_key(&metadata_kind); let key = format!("{}{}", kind, id); - let metadata = self.validated_metadata + let metadata = self + .validated_metadata .get(&key) .unwrap_or_revert_with(&env, CEP78Error::InvalidTokenIdentifier); return metadata; } - _ => continue, + _ => continue } } env.revert(CEP78Error::MissingTokenMetaData) } pub fn ensure_mutability(&self, error: CEP78Error) { - let current_mutability = self.mutability.get_or_revert_with(CEP78Error::InvalidMetadataMutability); + let current_mutability = self + .mutability + .get_or_revert_with(CEP78Error::InvalidMetadataMutability); if current_mutability != MetadataMutability::Mutable { self.env().revert(error) } @@ -152,7 +160,8 @@ impl Metadata { .map_err(|_| CEP78Error::FailedToParseCustomMetadata)?; for (property_name, property_type) in token_schema.properties.iter() { - if property_type.required && custom_metadata.attributes.get(property_name).is_none() + if property_type.required + && custom_metadata.attributes.get(property_name).is_none() { self.env().revert(CEP78Error::InvalidCustomMetadata) } @@ -166,7 +175,7 @@ impl Metadata { fn get_metadata_schema(&self, kind: &NFTMetadataKind) -> CustomMetadataSchema { match kind { NFTMetadataKind::Raw => CustomMetadataSchema { - properties: BTreeMap::new(), + properties: BTreeMap::new() }, NFTMetadataKind::NFT721 => { let mut properties = BTreeMap::new(); @@ -175,24 +184,24 @@ impl Metadata { MetadataSchemaProperty { name: "name".to_string(), description: "The name of the NFT".to_string(), - required: true, - }, + required: true + } ); properties.insert( "symbol".to_string(), MetadataSchemaProperty { name: "symbol".to_string(), description: "The symbol of the NFT collection".to_string(), - required: true, - }, + required: true + } ); properties.insert( "token_uri".to_string(), MetadataSchemaProperty { name: "token_uri".to_string(), description: "The URI pointing to an off chain resource".to_string(), - required: true, - }, + required: true + } ); CustomMetadataSchema { properties } } @@ -203,32 +212,32 @@ impl Metadata { MetadataSchemaProperty { name: "name".to_string(), description: "The name of the NFT".to_string(), - required: true, - }, + required: true + } ); properties.insert( "token_uri".to_string(), MetadataSchemaProperty { name: "token_uri".to_string(), description: "The URI pointing to an off chain resource".to_string(), - required: true, - }, + required: true + } ); properties.insert( "checksum".to_string(), MetadataSchemaProperty { name: "checksum".to_string(), description: "A SHA256 hash of the content at the token_uri".to_string(), - required: true, - }, + required: true + } ); CustomMetadataSchema { properties } } - NFTMetadataKind::CustomValidated => { - serde_json_wasm::from_str::(&self.json_schema.get_or_default()) - .map_err(|_| CEP78Error::InvalidJsonSchema) - .unwrap_or_revert(&self.env()) - } + NFTMetadataKind::CustomValidated => serde_json_wasm::from_str::( + &self.json_schema.get_or_default() + ) + .map_err(|_| CEP78Error::InvalidJsonSchema) + .unwrap_or_revert(&self.env()) } } } @@ -238,34 +247,33 @@ impl Metadata { pub(crate) struct MetadataSchemaProperty { name: String, description: String, - required: bool, + required: bool } #[derive(Serialize, Deserialize, Clone)] pub(crate) struct CustomMetadataSchema { - properties: BTreeMap, + properties: BTreeMap } - // Using a structure for the purposes of serialization formatting. #[derive(Serialize, Deserialize)] pub(crate) struct MetadataNFT721 { name: String, symbol: String, - token_uri: String, + token_uri: String } #[derive(Serialize, Deserialize)] pub(crate) struct MetadataCEP78 { name: String, token_uri: String, - checksum: String, + checksum: String } // Using a structure for the purposes of serialization formatting. #[derive(Serialize, Deserialize)] pub(crate) struct CustomMetadata { - attributes: BTreeMap, + attributes: BTreeMap } pub(crate) fn get_metadata_key(metadata_kind: &NFTMetadataKind) -> String { @@ -273,6 +281,7 @@ pub(crate) fn get_metadata_key(metadata_kind: &NFTMetadataKind) -> String { NFTMetadataKind::CEP78 => METADATA_CEP78, NFTMetadataKind::NFT721 => METADATA_NFT721, NFTMetadataKind::Raw => METADATA_RAW, - NFTMetadataKind::CustomValidated => METADATA_CUSTOM_VALIDATED, - }.to_string() -} \ No newline at end of file + NFTMetadataKind::CustomValidated => METADATA_CUSTOM_VALIDATED + } + .to_string() +} diff --git a/modules/src/cep78/mod.rs b/modules/src/cep78/mod.rs index 2e7c1bae..4692e7be 100644 --- a/modules/src/cep78/mod.rs +++ b/modules/src/cep78/mod.rs @@ -1,10 +1,14 @@ #![allow(missing_docs)] +pub mod constants; +pub mod error; pub mod events; +mod metadata; pub mod modalities; -pub mod error; -pub mod constants; +mod pagination; +mod reverse_lookup; +#[cfg(test)] +mod tests; pub mod token; -mod whitelist; -mod metadata; mod utils; +mod whitelist; diff --git a/modules/src/cep78/modalities.rs b/modules/src/cep78/modalities.rs index 40f0ec58..ec1f0504 100644 --- a/modules/src/cep78/modalities.rs +++ b/modules/src/cep78/modalities.rs @@ -1,13 +1,13 @@ -use core::default; - -use odra::prelude::*; use super::error::CEP78Error; +use odra::prelude::*; #[repr(u8)] -#[derive(PartialEq, Eq, Clone)] +#[odra::odra_type] +#[derive(Default)] pub enum WhitelistMode { + #[default] Unlocked = 0, - Locked = 1, + Locked = 1 } impl TryFrom for WhitelistMode { @@ -17,17 +17,19 @@ impl TryFrom for WhitelistMode { match value { 0 => Ok(WhitelistMode::Unlocked), 1 => Ok(WhitelistMode::Locked), - _ => Err(CEP78Error::InvalidWhitelistMode), + _ => Err(CEP78Error::InvalidWhitelistMode) } } } #[repr(u8)] -#[derive(PartialEq, Eq, Clone, Copy)] +#[odra::odra_type] +#[derive(Copy, Default)] pub enum NFTHolderMode { Accounts = 0, Contracts = 1, - Mixed = 2, + #[default] + Mixed = 2 } impl TryFrom for NFTHolderMode { @@ -38,20 +40,22 @@ impl TryFrom for NFTHolderMode { 0 => Ok(NFTHolderMode::Accounts), 1 => Ok(NFTHolderMode::Contracts), 2 => Ok(NFTHolderMode::Mixed), - _ => Err(CEP78Error::InvalidHolderMode), + _ => Err(CEP78Error::InvalidHolderMode) } } } -#[derive(PartialEq, Eq, Clone)] +#[odra::odra_type] #[repr(u8)] +#[derive(Default)] pub enum MintingMode { /// The ability to mint NFTs is restricted to the installing account only. + #[default] Installer = 0, /// The ability to mint NFTs is not restricted. Public = 1, /// The ability to mint NFTs is restricted by an ACL. - Acl = 2, + Acl = 2 } impl TryFrom for MintingMode { @@ -62,13 +66,14 @@ impl TryFrom for MintingMode { 0 => Ok(MintingMode::Installer), 1 => Ok(MintingMode::Public), 2 => Ok(MintingMode::Acl), - _ => Err(CEP78Error::InvalidMintingMode), + _ => Err(CEP78Error::InvalidMintingMode) } } } #[repr(u8)] -#[derive(Default, Clone)] +#[odra::odra_type] +#[derive(Default)] pub enum NFTKind { /// The NFT represents a real-world physical /// like a house. @@ -80,7 +85,7 @@ pub enum NFTKind { /// The NFT is the virtual representation /// of a physical notion, e.g a patent /// or copyright. - Virtual = 2, + Virtual = 2 } impl TryFrom for NFTKind { @@ -91,7 +96,7 @@ impl TryFrom for NFTKind { 0 => Ok(NFTKind::Physical), 1 => Ok(NFTKind::Digital), 2 => Ok(NFTKind::Virtual), - _ => Err(CEP78Error::InvalidNftKind), + _ => Err(CEP78Error::InvalidNftKind) } } } @@ -103,7 +108,7 @@ pub type MetadataRequirement = BTreeMap; pub enum Requirement { Required = 0, Optional = 1, - Unneeded = 2, + Unneeded = 2 } impl TryFrom for Requirement { @@ -114,7 +119,7 @@ impl TryFrom for Requirement { 0 => Ok(Requirement::Required), 1 => Ok(Requirement::Optional), 2 => Ok(Requirement::Unneeded), - _ => Err(CEP78Error::InvalidRequirement), + _ => Err(CEP78Error::InvalidRequirement) } } } @@ -127,7 +132,7 @@ pub enum NFTMetadataKind { CEP78 = 0, NFT721 = 1, Raw = 2, - CustomValidated = 3, + CustomValidated = 3 } impl TryFrom for NFTMetadataKind { @@ -139,13 +144,14 @@ impl TryFrom for NFTMetadataKind { 1 => Ok(NFTMetadataKind::NFT721), 2 => Ok(NFTMetadataKind::Raw), 3 => Ok(NFTMetadataKind::CustomValidated), - _ => Err(CEP78Error::InvalidNFTMetadataKind), + _ => Err(CEP78Error::InvalidNFTMetadataKind) } } } #[repr(u8)] -#[derive(PartialEq, Eq, Clone, Default)] +#[odra::odra_type] +#[derive(Default, PartialOrd, Ord, Copy)] pub enum OwnershipMode { /// The minter owns it and can never transfer it. #[default] @@ -153,7 +159,7 @@ pub enum OwnershipMode { /// The minter assigns it to an address and can never be transferred. Assigned = 1, /// The NFT can be transferred even to an recipient that does not exist. - Transferable = 2, + Transferable = 2 } impl TryFrom for OwnershipMode { @@ -164,16 +170,17 @@ impl TryFrom for OwnershipMode { 0 => Ok(OwnershipMode::Minter), 1 => Ok(OwnershipMode::Assigned), 2 => Ok(OwnershipMode::Transferable), - _ => Err(CEP78Error::InvalidOwnershipMode), + _ => Err(CEP78Error::InvalidOwnershipMode) } } } #[repr(u8)] #[odra::odra_type] +#[derive(PartialOrd, Ord, Copy)] pub enum NFTIdentifierMode { Ordinal = 0, - Hash = 1, + Hash = 1 } impl TryFrom for NFTIdentifierMode { @@ -183,18 +190,18 @@ impl TryFrom for NFTIdentifierMode { match value { 0 => Ok(NFTIdentifierMode::Ordinal), 1 => Ok(NFTIdentifierMode::Hash), - _ => Err(CEP78Error::InvalidIdentifierMode), + _ => Err(CEP78Error::InvalidIdentifierMode) } } } #[repr(u8)] -#[derive(Default)] +#[derive(Default, PartialOrd, Ord, Copy)] #[odra::odra_type] pub enum MetadataMutability { #[default] Immutable = 0, - Mutable = 1, + Mutable = 1 } impl TryFrom for MetadataMutability { @@ -204,7 +211,7 @@ impl TryFrom for MetadataMutability { match value { 0 => Ok(MetadataMutability::Immutable), 1 => Ok(MetadataMutability::Mutable), - _ => Err(CEP78Error::InvalidMetadataMutability), + _ => Err(CEP78Error::InvalidMetadataMutability) } } } @@ -212,7 +219,7 @@ impl TryFrom for MetadataMutability { #[odra::odra_type] pub enum TokenIdentifier { Index(u64), - Hash(String), + Hash(String) } impl TokenIdentifier { @@ -231,9 +238,9 @@ impl TokenIdentifier { None } - pub fn get_hash(self) -> Option { + pub fn get_hash(&self) -> Option { if let Self::Hash(hash) = self { - return Some(hash); + return Some(hash.to_owned()); } None } @@ -241,7 +248,7 @@ impl TokenIdentifier { pub fn get_dictionary_item_key(&self) -> String { match self { TokenIdentifier::Index(token_index) => token_index.to_string(), - TokenIdentifier::Hash(hash) => hash.clone(), + TokenIdentifier::Hash(hash) => hash.clone() } } } @@ -250,17 +257,18 @@ impl ToString for TokenIdentifier { fn to_string(&self) -> String { match self { TokenIdentifier::Index(index) => index.to_string(), - TokenIdentifier::Hash(hash) => hash.to_string(), + TokenIdentifier::Hash(hash) => hash.to_string() } } } - #[repr(u8)] #[odra::odra_type] +#[derive(Default)] pub enum BurnMode { + #[default] Burnable = 0, - NonBurnable = 1, + NonBurnable = 1 } impl TryFrom for BurnMode { @@ -270,17 +278,19 @@ impl TryFrom for BurnMode { match value { 0 => Ok(BurnMode::Burnable), 1 => Ok(BurnMode::NonBurnable), - _ => Err(CEP78Error::InvalidBurnMode), + _ => Err(CEP78Error::InvalidBurnMode) } } } #[repr(u8)] -#[derive(Clone, PartialEq, Eq)] +#[derive(Default, PartialOrd, Ord, Copy)] +#[odra::odra_type] pub enum OwnerReverseLookupMode { + #[default] NoLookUp = 0, Complete = 1, - TransfersOnly = 2, + TransfersOnly = 2 } impl TryFrom for OwnerReverseLookupMode { @@ -291,7 +301,7 @@ impl TryFrom for OwnerReverseLookupMode { 0 => Ok(OwnerReverseLookupMode::NoLookUp), 1 => Ok(OwnerReverseLookupMode::Complete), 2 => Ok(OwnerReverseLookupMode::TransfersOnly), - _ => Err(CEP78Error::InvalidReportingMode), + _ => Err(CEP78Error::InvalidReportingMode) } } } @@ -300,7 +310,7 @@ impl TryFrom for OwnerReverseLookupMode { pub enum NamedKeyConventionMode { DerivedFromCollectionName = 0, V1_0Standard = 1, - V1_0Custom = 2, + V1_0Custom = 2 } impl TryFrom for NamedKeyConventionMode { @@ -311,19 +321,20 @@ impl TryFrom for NamedKeyConventionMode { 0 => Ok(NamedKeyConventionMode::DerivedFromCollectionName), 1 => Ok(NamedKeyConventionMode::V1_0Standard), 2 => Ok(NamedKeyConventionMode::V1_0Custom), - _ => Err(CEP78Error::InvalidNamedKeyConvention), + _ => Err(CEP78Error::InvalidNamedKeyConvention) } } } #[repr(u8)] -#[derive(PartialEq, Eq, Clone, Copy, Default)] +#[odra::odra_type] +#[derive(Copy, Default)] #[allow(clippy::upper_case_acronyms)] pub enum EventsMode { + #[default] NoEvents = 0, CEP47 = 1, - #[default] - CES = 2, + CES = 2 } impl TryFrom for EventsMode { @@ -334,7 +345,7 @@ impl TryFrom for EventsMode { 0 => Ok(EventsMode::NoEvents), 1 => Ok(EventsMode::CEP47), 2 => Ok(EventsMode::CES), - _ => Err(CEP78Error::InvalidEventsMode), + _ => Err(CEP78Error::InvalidEventsMode) } } } @@ -344,14 +355,14 @@ impl TryFrom for EventsMode { #[odra::odra_type] pub enum TransferFilterContractResult { DenyTransfer = 0, - ProceedTransfer, + ProceedTransfer } impl From for TransferFilterContractResult { fn from(value: u8) -> Self { match value { 0 => TransferFilterContractResult::DenyTransfer, - _ => TransferFilterContractResult::ProceedTransfer, + _ => TransferFilterContractResult::ProceedTransfer } } } diff --git a/modules/src/cep78/pagination.rs b/modules/src/cep78/pagination.rs new file mode 100644 index 00000000..f19cabbd --- /dev/null +++ b/modules/src/cep78/pagination.rs @@ -0,0 +1,163 @@ +use odra::{casper_types::{AccessRights, URef}, prelude::*, Address, Mapping, UnwrapOrRevert}; + +use crate::cep78::constants::PREFIX_PAGE_DICTIONARY; + +use super::error::CEP78Error; + +// The size of a given page, it is currently set to 1000 +// to ease the math around addressing newly minted tokens. +pub const PAGE_SIZE: u64 = 1000; + +#[odra::module] +pub struct Pagination { + page_tables: Mapping>, + pages: Mapping<(String, u64, Address), Vec> +} + +impl Pagination { + pub fn add_page_entry_and_page_record( + &mut self, + tokens_count: u64, + item_key: &Address, + on_mint: bool + ) -> (u64, URef) { + // there is an explicit page_table; + // this is the entry in that overall page table which maps to the underlying page + // upon which this mint's address will exist + let page_table_entry = tokens_count / PAGE_SIZE; + let page_address = tokens_count % PAGE_SIZE; + + let mut page_table = match self.page_tables.get(item_key) { + Some(page_table) => page_table, + None => self.env().revert(if on_mint { + CEP78Error::UnregisteredOwnerInMint + } else { + CEP78Error::UnregisteredOwnerInTransfer + }) + }; + + let page_key = ( + PREFIX_PAGE_DICTIONARY.to_string(), + page_table_entry, + *item_key + ); + let mut page = if !page_table[page_table_entry as usize] { + // We mark the page table entry to true to signal the allocation of a page. + let _ = core::mem::replace(&mut page_table[page_table_entry as usize], true); + self.pages.set(&page_key, page_table); + vec![false; PAGE_SIZE as usize] + } else { + self.pages + .get(&page_key) + .unwrap_or_revert_with(&self.env(), CEP78Error::MissingPage) + }; + + let _ = core::mem::replace(&mut page[page_address as usize], true); + + self.pages.set(&page_key, page); + // storage::dictionary_put(page_uref, item_key, page); + let addr_array = [0u8; 32]; + let uref_a = URef::new(addr_array, AccessRights::READ); + // (page_table_entry, page_uref) + (page_table_entry, uref_a) + } + + pub fn update_page_entry_and_page_record( + &mut self, + tokens_count: u64, + old_item_key: &Address, + new_item_key: &Address + ) -> (u64, URef) { + let page_table_entry = tokens_count / PAGE_SIZE; + let page_address = tokens_count % PAGE_SIZE; + + let old_page_key = ( + PREFIX_PAGE_DICTIONARY.to_string(), + page_table_entry, + *old_item_key + ); + let new_page_key = ( + PREFIX_PAGE_DICTIONARY.to_string(), + page_table_entry, + *new_item_key + ); + + let mut source_page = self + .pages + .get(&old_page_key) + .unwrap_or_revert_with(&self.env(), CEP78Error::InvalidPageNumber); + + if !source_page[page_address as usize] { + self.env().revert(CEP78Error::InvalidTokenIdentifier) + } + + let _ = core::mem::replace(&mut source_page[page_address as usize], false); + + self.pages.set(&old_page_key, source_page); + + let mut target_page_table = self + .page_tables + .get(&new_item_key) + .unwrap_or_revert_with(&self.env(), CEP78Error::UnregisteredOwnerInTransfer); + + let mut target_page = if !target_page_table[page_table_entry as usize] { + // Create a new page here + let _ = core::mem::replace(&mut target_page_table[page_table_entry as usize], true); + self.page_tables.set(new_item_key, target_page_table); + vec![false; PAGE_SIZE as usize] + } else { + self.pages.get(&new_page_key).unwrap_or_revert(&self.env()) + }; + + let _ = core::mem::replace(&mut target_page[page_address as usize], true); + + self.pages + .set(&new_page_key, target_page); + // (page_table_entry, page_uref) + let addr_array = [0u8; 32]; + let uref_a = URef::new(addr_array, AccessRights::READ); + // (page_table_entry, page_uref) + (page_table_entry, uref_a) + } + + pub fn register_owner(&self, owner: &Address) { + let page = self.page_tables.get_or_default(&owner); + + + + // let page_table_uref = utils::get_uref( + // PAGE_TABLE, + // NFTCoreError::MissingPageTableURef, + // NFTCoreError::InvalidPageTableURef, + // ); + + // let owner_item_key = utils::encode_dictionary_item_key(owner_key); + + // if storage::dictionary_get::>(page_table_uref, &owner_item_key) + // .unwrap_or_revert() + // .is_none() + // { + // let page_table_width = utils::get_stored_value_with_user_errors::( + // PAGE_LIMIT, + // NFTCoreError::MissingPageLimit, + // NFTCoreError::InvalidPageLimit, + // ); + // storage::dictionary_put( + // page_table_uref, + // &owner_item_key, + // vec![false; page_table_width as usize], + // ); + // } + // let collection_name = utils::get_stored_value_with_user_errors::( + // COLLECTION_NAME, + // NFTCoreError::MissingCollectionName, + // NFTCoreError::InvalidCollectionName, + // ); + // let package_uref = storage::new_uref(utils::get_stored_value_with_user_errors::( + // &format!("{PREFIX_CEP78}_{collection_name}"), + // NFTCoreError::MissingCep78PackageHash, + // NFTCoreError::InvalidCep78InvalidHash, + // )); + // runtime::ret(CLValue::from_t((collection_name, package_uref)).unwrap_or_revert()) + } +} diff --git a/modules/src/cep78/reverse_lookup.rs b/modules/src/cep78/reverse_lookup.rs new file mode 100644 index 00000000..04ebbe2d --- /dev/null +++ b/modules/src/cep78/reverse_lookup.rs @@ -0,0 +1,80 @@ +use odra::{prelude::*, Mapping, UnwrapOrRevert, Var}; + +use super::{ + error::CEP78Error, + modalities::{OwnerReverseLookupMode, TokenIdentifier}, +}; + +#[odra::module] +pub struct ReverseLookup { + hash_by_index: Mapping, + index_by_hash: Mapping, + mode: Var +} + +impl ReverseLookup { + pub fn init(&mut self, mode: OwnerReverseLookupMode) { + self.mode.set(mode); + } + + #[inline] + pub fn get_mode(&self) -> OwnerReverseLookupMode { + self.mode.get_or_default() + } + + pub fn insert_hash( + &mut self, + current_number_of_minted_tokens: u64, + token_identifier: &TokenIdentifier + ) { + if token_identifier.get_index().is_some() { + return; + } + if self + .index_by_hash + .get(&token_identifier.to_string()) + .is_some() + { + self.env().revert(CEP78Error::DuplicateIdentifier) + } + if self + .hash_by_index + .get(¤t_number_of_minted_tokens) + .is_some() + { + self.env().revert(CEP78Error::DuplicateIdentifier) + } + + self.hash_by_index.set( + ¤t_number_of_minted_tokens, + token_identifier.get_hash().unwrap_or_revert(&self.env()) + ); + self.index_by_hash.set( + &token_identifier.to_string(), + current_number_of_minted_tokens + ); + } + + pub fn get_token_index(&self, token_identifier: &TokenIdentifier) -> u64 { + match token_identifier { + TokenIdentifier::Index(token_index) => *token_index, + TokenIdentifier::Hash(_) => self + .index_by_hash + .get(&token_identifier.to_string()) + .unwrap_or_revert_with(&self.env(), CEP78Error::InvalidTokenIdentifier) + } + } + + // pub fn remove(&mut self, index: u64, hash: String) { + // self.hash_by_index.remove(&index.to_string()); + // self.index_by_hash.remove(&hash); + // } + + // pub fn get_by_index(&self, index: u64) -> Option
{ + // self.hash_by_index.get(&index.to_string()).copied() + // } + + // pub fn get_by_hash(&self, hash: &str) -> Option
{ + // self.index_by_hash.get(hash).copied() + // } +} diff --git a/modules/src/cep78/tests/utils.rs b/modules/src/cep78/tests/utils.rs new file mode 100644 index 00000000..f723fe5b --- /dev/null +++ b/modules/src/cep78/tests/utils.rs @@ -0,0 +1,79 @@ +use crate::cep78::{ + modalities::{ + BurnMode, EventsMode, MetadataMutability, MintingMode, NFTHolderMode, NFTIdentifierMode, + NFTKind, NFTMetadataKind, OwnerReverseLookupMode, OwnershipMode, WhitelistMode + }, + token::{CEP78HostRef, CEP78InitArgs} +}; +use derive_builder::Builder; +use odra::{ + args::Maybe, + casper_types::{runtime_args, RuntimeArgs}, + Address +}; + +#[derive(Default, Builder)] +#[builder(setter(into))] +pub struct InitArgs { + collection_name: String, + collection_symbol: String, + total_token_supply: u64, + allow_minting: Maybe, + minting_mode: Maybe, + ownership_mode: OwnershipMode, + nft_kind: NFTKind, + holder_mode: Maybe, + whitelist_mode: Maybe, + acl_white_list: Maybe>, + acl_package_mode: Maybe>, + package_operator_mode: Maybe>, + json_schema: Maybe, + receipt_name: Maybe, + identifier_mode: u8, + burn_mode: Maybe, + operator_burn_mode: Maybe, + nft_metadata_kind: NFTMetadataKind, + metadata_mutability: MetadataMutability, + owner_reverse_lookup_mode: Maybe, + events_mode: EventsMode, + transfer_filter_contract_contract_key: Maybe
, + additional_required_metadata: Maybe>, + optional_metadata: Maybe> +} + +impl Into for InitArgs { + fn into(self) -> RuntimeArgs { + runtime_args! { + "collection_name" => self.collection_name, + "collection_symbol" => self.collection_symbol, + "total_token_supply" => self.total_token_supply, + "allow_minting" => self.allow_minting.unwrap_or_default(), + "minting_mode" => self.minting_mode.unwrap_or(MintingMode::Installer) as u8, + "ownership_mode" => self.ownership_mode as u8, + "nft_kind" => self.nft_kind as u8, + "holder_mode" => self.holder_mode.unwrap_or(NFTHolderMode::Accounts) as u8, + "whitelist_mode" => self.whitelist_mode.unwrap_or(WhitelistMode::Unlocked) as u8, + "acl_white_list" => self.acl_white_list.unwrap_or_default(), + "acl_package_mode" => self.acl_package_mode.unwrap_or_default(), + "package_operator_mode" => self.package_operator_mode.unwrap_or_default(), + "json_schema" => self.json_schema.unwrap_or_default(), + "receipt_name" => self.receipt_name.unwrap_or_default(), + "identifier_mode" => self.identifier_mode, + "burn_mode" => self.burn_mode.unwrap_or_default(), + "operator_burn_mode" => self.operator_burn_mode.unwrap_or_default(), + "nft_metadata_kind" => self.nft_metadata_kind as u8, + "metadata_mutability" => self.metadata_mutability as u8, + "owner_reverse_lookup_mode" => self.owner_reverse_lookup_mode.unwrap_or(OwnerReverseLookupMode::NoLookUp) as u8, + "events_mode" => self.events_mode as u8, + // "transfer_filter_contract_contract_key" => self.transfer_filter_contract_contract_key, + "additional_required_metadata" => self.additional_required_metadata.unwrap_or_default(), + "optional_metadata" => self.optional_metadata.unwrap_or_default(), + } + } +} + +impl odra::host::InitArgs for InitArgs { + fn validate(_expected_ident: &str) -> bool { + true + } +} diff --git a/modules/src/cep78/token.rs b/modules/src/cep78/token.rs index c17824f1..bbe44433 100644 --- a/modules/src/cep78/token.rs +++ b/modules/src/cep78/token.rs @@ -1,7 +1,24 @@ -use odra::{args::Maybe, casper_types::{bytesrepr::ToBytes, ContractHash, ContractPackage, Key, URef}, prelude::*, Address, Mapping, Sequence, SubModule, UnwrapOrRevert, Var}; -use super::{constants, error::CEP78Error, events::{Approval, ApprovalForAll, ApprovalRevoked, Burn, MetadataUpdated, Mint, RevokedForAll, VariablesSet}, metadata::Metadata, modalities::{BurnMode, EventsMode, MintingMode, NFTIdentifierMode, OwnershipMode, TokenIdentifier, TransferFilterContractResult}, utils::{self, GetAs}, whitelist::ACLWhitelist}; - - +use super::{ + constants, + error::CEP78Error, + events::{ + Approval, ApprovalForAll, ApprovalRevoked, Burn, MetadataUpdated, Mint, RevokedForAll, Transfer, VariablesSet + }, + metadata::Metadata, + modalities::{ + BurnMode, EventsMode, MetadataMutability, MintingMode, NFTHolderMode, NFTIdentifierMode, NFTKind, NFTMetadataKind, OwnerReverseLookupMode, OwnershipMode, TokenIdentifier, TransferFilterContractResult, WhitelistMode + }, + pagination::{Pagination, PAGE_SIZE}, + reverse_lookup::ReverseLookup, + utils::{self, GetAs, IntoOrRevert}, + whitelist::ACLWhitelist +}; +use odra::{ + args::Maybe, + casper_types::{bytesrepr::ToBytes, Key, URef}, + prelude::*, + Address, Mapping, Sequence, SubModule, UnwrapOrRevert, Var +}; #[odra::module( //package_hash_key = "cep78", @@ -13,56 +30,52 @@ pub struct CEP78 { collection_symbol: Var, cap: Var, allow_minting: Var, - minting_mode: Var, - ownership_mode: Var, - nft_kind: Var, - holder_mode: Var, - package_operator_mode: Var, - operator_burn_mode: Var, - burn_mode: Var, - events_mode: Var, - whitelist_mode: Var, + minting_mode: Var, + ownership_mode: Var, + nft_kind: Var, + holder_mode: Var, + burn_mode: Var, + events_mode: Var, counter: Sequence, - whitelist: SubModule, - metadata: SubModule, owners: Mapping, issuers: Mapping, approved: Mapping>, token_count: Mapping, burnt_tokens: Mapping, - operators: Mapping<(Address, Address), bool> + operators: Mapping<(Address, Address), bool>, + receipt_name: Var, + whitelist: SubModule, + metadata: SubModule, + reverse_lookup: SubModule, + pagination: SubModule, } #[odra::module] impl CEP78 { /// Initializes the module. pub fn init( - &mut self, - collection_name: String, - collection_symbol: String, - total_token_supply: u64, + &mut self, + collection_name: String, + collection_symbol: String, + total_token_supply: u64, + ownership_mode: OwnershipMode, + nft_kind: NFTKind, + nft_identifier_mode: NFTIdentifierMode, + nft_metadata_kind: NFTMetadataKind, + metadata_mutability: MetadataMutability, allow_minting: Maybe, - minting_mode: Maybe, - ownership_mode: u8, - nft_kind: u8, - holder_mode: Maybe, - whitelist_mode: Maybe, + minting_mode: Maybe, + holder_mode: Maybe, + whitelist_mode: Maybe, acl_white_list: Maybe>, - acl_package_mode: Maybe, - package_operator_mode: Maybe, json_schema: Maybe, receipt_name: Maybe, - identifier_mode: u8, - burn_mode: Maybe, - operator_burn_mode: Maybe, - nft_metadata_kind: u8, - metadata_mutability: u8, - owner_reverse_lookup_mode: Maybe, - events_mode: u8, + burn_mode: Maybe, + owner_reverse_lookup_mode: Maybe, + events_mode: Maybe, transfer_filter_contract_contract_key: Maybe
, - - additional_required_metadata: Maybe>, - optional_metadata: Maybe>, + additional_required_metadata: Maybe>, + optional_metadata: Maybe> ) { let installer = self.env().caller(); self.installer.set(installer); @@ -71,7 +84,6 @@ impl CEP78 { if total_token_supply == 0 { self.env().revert(CEP78Error::CannotInstallWithZeroSupply) - } if total_token_supply > constants::MAX_TOTAL_TOKEN_SUPPLY { @@ -80,45 +92,41 @@ impl CEP78 { self.cap.set(total_token_supply); self.allow_minting.set(allow_minting.unwrap_or(true)); - self.minting_mode.set(minting_mode.clone().unwrap_or(0)); + self.minting_mode.set(minting_mode.clone().unwrap_or_default()); self.ownership_mode.set(ownership_mode); self.nft_kind.set(nft_kind); - self.holder_mode.set(holder_mode.unwrap_or(2u8)); - self.burn_mode.set(burn_mode.unwrap_or(0)); - self.operator_burn_mode.set(operator_burn_mode.unwrap_or_default()); + self.holder_mode.set(holder_mode.unwrap_or_default()); + self.burn_mode.set(burn_mode.unwrap_or_default()); + self.events_mode.set(events_mode.unwrap_or_default()); + self.reverse_lookup.init(owner_reverse_lookup_mode.clone().unwrap_or_default()); + self.receipt_name.set(receipt_name.unwrap_or_default()); self.whitelist.init( acl_white_list.unwrap_or_default(), - whitelist_mode.unwrap_or_default(), - acl_package_mode.unwrap_or_default() + whitelist_mode.unwrap_or_default(), + ); + + self.metadata.init( + nft_metadata_kind, + additional_required_metadata, + optional_metadata, + metadata_mutability, + nft_identifier_mode, + json_schema ); - - // Deprecated in 1.4 in favor of following acl whitelist - // A whitelist of keys specifying which entity can mint - // NFTs in the contract holder mode with restricted minting. - // This value can only be modified if the whitelist lock is - // set to be unlocked. - // let contract_white_list: Vec = utils::get_optional_named_arg_with_user_errors( - // ARG_CONTRACT_WHITELIST, - // NFTCoreError::InvalidContractWhitelist, - // ) - // .unwrap_or_default(); - - self.package_operator_mode.set(package_operator_mode.unwrap_or_default()); - self.metadata.init(nft_metadata_kind, additional_required_metadata, optional_metadata, metadata_mutability, identifier_mode, json_schema); - - - if identifier_mode == 1 && metadata_mutability == 1 { + if nft_identifier_mode == NFTIdentifierMode::Hash && metadata_mutability == MetadataMutability::Mutable { self.env().revert(CEP78Error::InvalidMetadataMutability) } - if ownership_mode == 0 && minting_mode.unwrap_or_default() == 0 && owner_reverse_lookup_mode.unwrap_or_default() == 1 { + if ownership_mode == OwnershipMode::Minter + && minting_mode.unwrap_or_default() == MintingMode::Installer + && owner_reverse_lookup_mode.unwrap_or_default() == OwnerReverseLookupMode::NoLookUp + { self.env().revert(CEP78Error::InvalidReportingMode) } - + self.counter.next_value(); - } /// Exposes all variables that can be changed by managing account post @@ -126,38 +134,25 @@ impl CEP78 { /// installation if a variable needs to be changed. /// By switching allow_minting to false we pause minting. pub fn set_variables( - &mut self, + &mut self, allow_minting: Maybe, - contract_whitelist: Maybe>, acl_whitelist: Maybe>, - acl_package_mode: Maybe, - package_operator_mode: Maybe, - operator_burn_mode: Maybe, ) { - let installer = self.installer.get_or_revert_with(CEP78Error::MissingInstaller); - + let installer = self + .installer + .get_or_revert_with(CEP78Error::MissingInstaller); + // Only the installing account can change the mutable variables. self.ensure_not_caller(installer); - + if let Maybe::Some(allow_minting) = allow_minting { self.allow_minting.set(allow_minting); } - - self.whitelist.update_package_mode(acl_package_mode); - self.whitelist.update_addresses(acl_whitelist, contract_whitelist); - - if let Maybe::Some(package_operator_mode) = package_operator_mode { - self.package_operator_mode.set(package_operator_mode); - } - - if let Maybe::Some(operator_burn_mode) = operator_burn_mode { - self.operator_burn_mode.set(operator_burn_mode); - } + self.whitelist.update_addresses(acl_whitelist); self.emit_ces_event(VariablesSet::new()); } - /// Mints a new token with provided metadata. /// Reverts with [CEP78Error::MintingIsPaused] error if `allow_minting` is false. /// When a token is minted the calling account is listed as its owner and the token is @@ -166,13 +161,13 @@ impl CEP78 { /// exceeds the `total_token_supply`. If so, it reverts the minting with an error /// [CEP78Error::TokenSupplyDepleted]. The `mint` function also checks whether the calling account /// is the managing account (the installer) If not, and if `public_minting` is set to - /// false, it reverts with the error [CEP78Error::InvalidAccount]. + /// false, it reverts with the error [CEP78Error::InvalidAccount]. /// After minting is successful the number_of_minted_tokens is incremented by one. pub fn mint( &mut self, token_owner: Address, token_metadata: String, - token_hash: Maybe, + token_hash: Maybe ) -> (String, Address, String) { // The contract owner can toggle the minting behavior on and off over time. // The contract is toggled on by default. @@ -183,7 +178,9 @@ impl CEP78 { self.env().revert(CEP78Error::MintingIsPaused); } - let total_token_supply = self.cap.get_or_revert_with(CEP78Error::MissingTotalTokenSupply); + let total_token_supply = self + .cap + .get_or_revert_with(CEP78Error::MissingTotalTokenSupply); // The minted_tokens_count is the number of minted tokens so far. let minted_tokens_count = self.counter.get_current_value(); @@ -193,60 +190,33 @@ impl CEP78 { self.env().revert(CEP78Error::TokenSupplyDepleted); } - let minting_mode: MintingMode = self.minting_mode.get_as(&self.env()); + let minting_mode: MintingMode = self.minting_mode.get_or_default(); - // let (caller, contract_package): (Key, Option) = - // match self.env().caller() { - // Caller::Session(account_hash) => (account_hash.into(), None), - // Caller::StoredCaller(contract_hash, contract_package_hash) => { - // (contract_hash.into(), Some(contract_package_hash.into())) - // } - // }; - - let (caller, contract_package) = (self.env().caller(), None::); + let caller = self.env().caller(); // Revert if minting is private and caller is not installer. if MintingMode::Installer == minting_mode { match caller { Address::Account(_) => { - let installer_account = self.installer.get_or_revert_with(CEP78Error::MissingInstaller); + let installer_account = self + .installer + .get_or_revert_with(CEP78Error::MissingInstaller); // Revert if private minting is required and caller is not installer. if caller != installer_account { self.env().revert(CEP78Error::InvalidMinter) } } - _ => self.env().revert(CEP78Error::InvalidKey), + _ => self.env().revert(CEP78Error::InvalidKey) } } // Revert if minting is acl and caller is not whitelisted. if MintingMode::Acl == minting_mode { - // TODO: Implement the following - // let acl_package_mode: bool = self.whitelist.is_package_mode(); - // let is_whitelisted = match (acl_package_mode, contract_package) { - // (true, Some(contract_package)) => utils::get_dictionary_value_from_key::( - // ACL_WHITELIST, - // &utils::encode_dictionary_item_key(contract_package), - // ) - // .unwrap_or_default(), - // _ => utils::get_dictionary_value_from_key::( - // ACL_WHITELIST, - // &utils::encode_dictionary_item_key(caller), - // ) - // .unwrap_or_default(), - // }; - let is_whitelisted = false; - - match caller { - Address::Contract(_) => { - if !is_whitelisted { - self.env().revert(CEP78Error::UnlistedContractHash); - } - } - Address::Account(_) => { - if !is_whitelisted { - self.env().revert(CEP78Error::InvalidMinter); - } + let is_whitelisted = self.whitelist.is_whitelisted(&caller); + if !is_whitelisted { + match caller { + Address::Contract(_) => self.env().revert(CEP78Error::UnlistedContractHash), + Address::Account(_) => self.env().revert(CEP78Error::InvalidMinter) } } } @@ -257,75 +227,64 @@ impl CEP78 { let token_identifier: TokenIdentifier = match identifier_mode { NFTIdentifierMode::Ordinal => TokenIdentifier::Index(minted_tokens_count), NFTIdentifierMode::Hash => TokenIdentifier::Hash(if optional_token_hash.is_empty() { - // TODO: Implement the following - // base16::encode_lower(&runtime::blake2b(token_metadata.clone())) - "".to_string() + base16::encode_lower(&self.env().hash(token_metadata.clone())) } else { optional_token_hash - }), + }) }; - self.metadata.update_or_revert(&token_metadata, &token_identifier); - + self.metadata + .update_or_revert(&token_metadata, &token_identifier); // The contract's ownership behavior (determined at installation) determines, // who owns the NFT we are about to mint.() - let ownership_mode: OwnershipMode = self.ownership_mode.get_as(&self.env()); - let token_owner_key = if let OwnershipMode::Assigned | OwnershipMode::Transferable = ownership_mode { - token_owner - } else { - caller - }; + let token_owner_key = + if let OwnershipMode::Assigned | OwnershipMode::Transferable = self.ownership_mode() { + token_owner + } else { + caller + }; let id = token_identifier.to_string(); self.owners.set(&id, token_owner_key); self.issuers.set(&id, caller); - // TODO: Implement the following - // if let NFTIdentifierMode::Hash = identifier_mode { - // // Update the forward and reverse trackers - // utils::insert_hash_id_lookups(minted_tokens_count, token_identifier.clone()); - // } + if let NFTIdentifierMode::Hash = identifier_mode { + // Update the forward and reverse trackers + self.reverse_lookup + .insert_hash(minted_tokens_count, &token_identifier); + } //Increment the count of owned tokens. self.token_count.add(&token_owner_key, 1); - + // Increment number_of_minted_tokens by one self.counter.next_value(); - // Emit Mint event. self.emit_ces_event(Mint::new( token_owner_key, token_identifier.clone(), - token_metadata.clone(), + token_metadata.clone() )); - // TODO: Implement the following - // if let OwnerReverseLookupMode::Complete = utils::get_reporting_mode() { - // if (NFTIdentifierMode::Hash == identifier_mode) - // && runtime::get_key(OWNED_TOKENS).is_some() - // && utils::should_migrate_token_hashes(token_owner_key) - // { - // utils::migrate_token_hashes(token_owner_key) - // } - - // let (page_table_entry, page_uref) = utils::add_page_entry_and_page_record( - // minted_tokens_count, - // &owned_tokens_item_key, - // true, - // ); - - // let receipt_string = utils::get_receipt_name(page_table_entry); - // let receipt_address = Key::dictionary(page_uref, owned_tokens_item_key.as_bytes()); - // let token_identifier_string = token_identifier.get_dictionary_item_key(); - - // (receipt_string, receipt_address, token_identifier_string) - // } + if let OwnerReverseLookupMode::Complete = self.reverse_lookup.get_mode() { + let (page_table_entry, page_uref) = self.pagination.add_page_entry_and_page_record( + minted_tokens_count, + &token_owner_key, + true + ); + let receipt_name = self.receipt_name.get_or_default(); + let receipt_string = format!("{receipt_name}_m_{PAGE_SIZE}_p_{page_table_entry}"); + // TODO: Implement the following + // let receipt_address = Key::dictionary(page_uref, owned_tokens_item_key.as_bytes()); + let token_identifier_string = token_identifier.to_string(); + // should not return `token_owner` + return (receipt_string, token_owner, token_identifier_string); + } (id, token_owner_key, token_metadata) } - /// Burns the token with provided `token_id` argument, after which it is no /// longer possible to transfer it. /// Looks up the owner of the supplied token_id arg. If caller is not owner we revert with @@ -334,58 +293,36 @@ impl CEP78 { /// error [CEP78Error::PreviouslyBurntToken]. If not the token is then registered as burnt. pub fn burn(&mut self, token_id: Maybe, token_hash: Maybe) { self.ensure_burnable(); - + let token_identifier = self.token_identifier(token_id, token_hash); - - let (caller, contract_package): (Address, Option
) = (self.env().caller(), None); - // match utils::get_verified_caller().unwrap_or_revert() { - // Caller::Session(account_hash) => (account_hash.into(), None), - // Caller::StoredCaller(contract_hash, contract_package_hash) => { - // (contract_hash.into(), Some(contract_package_hash.into())) - // } - // }; - + let token_owner = self.owner_of_by_id(&token_identifier); - + let caller = self.env().caller(); + // Check if caller is owner let is_owner = token_owner == caller; - + // Check if caller is operator to execute burn let is_operator = if !is_owner { self.operators.get_or_default(&(token_owner, caller)) } else { false }; - - // With operator package mode check if caller's package is operator to let contract execute burn - let is_package_operator = if !is_owner && !is_operator { - match (self.package_operator_mode.get_or_default(), contract_package) { - (true, Some(contract_package)) => { - // TODO: Implement the following - // self.operators.get_or_default(&(token_owner, contract_package)); - true - } - _ => false, - } - } else { - false - }; - + // Revert if caller is not token_owner nor operator for the owner - if !is_owner && !is_operator && !is_package_operator { + if !is_owner && !is_operator { self.env().revert(CEP78Error::InvalidTokenOwner) }; - + // It makes sense to keep this token as owned by the caller. It just happens that the caller // owns a burnt token. That's all. Similarly, we should probably also not change the // owned_tokens dictionary. self.ensure_not_burned(&token_identifier); - + // Mark the token as burnt by adding the token_id to the burnt tokens dictionary. self.burnt_tokens.set(&token_identifier.to_string(), ()); self.token_count.subtract(&token_owner, 1); - - + // Emit Burn event. self.emit_ces_event(Burn::new(token_owner, token_identifier, caller)); } @@ -394,31 +331,34 @@ impl CEP78 { /// It looks up the owner of the supplied token_id arg. Reverts if the token is already burnt, /// `token_id` is invalid, or if caller is not owner nor an approved account nor operator. /// If token id is invalid it reverts with error [CEP78Error::InvalidTokenIdentifier]. - pub fn transfer(&mut self, token_id: Maybe, token_hash: Maybe, source_key: Address, target_key: Address) -> (String, Address) { + pub fn transfer( + &mut self, + token_id: Maybe, + token_hash: Maybe, + source_key: Address, + target_key: Address + ) -> (String, Address) { // If we are in minter or assigned mode we are not allowed to transfer ownership of token, hence // we revert. self.ensure_minter_or_assigned(); - let token_identifier = self.checked_token_identifier(token_id, token_hash); - + let token_id = token_identifier.to_string(); // We assume we cannot transfer burnt tokens self.ensure_not_burned(&token_identifier); - - self.ensure_not_owner(&token_identifier, &source_key); - let (caller, contract_package): (Address, Option) = (self.env().caller(), None); - + let caller = self.env().caller(); + let owner = self.owner_of_by_id(&token_identifier); // Check if caller is owner let is_owner = owner == caller; // Check if caller is approved to execute transfer let is_approved = !is_owner - && match self.approved.get(&token_identifier.to_string()) { + && match self.approved.get(&token_id) { Some(Some(maybe_approved)) => caller == maybe_approved, - Some(None) | None => false, + Some(None) | None => false }; // Check if caller is operator to execute transfer @@ -428,24 +368,9 @@ impl CEP78 { false }; - // With operator package mode check if caller's package is operator to let contract execute - // transfer - let is_package_operator = if !is_owner && !is_approved && !is_operator { - match (self.package_operator_mode.get_or_default(), contract_package) { - (true, Some(contract_package)) => { - // TODO: Implement the following - // self.operators.get_or_default(&(source_key, contract_package)) - self.operators.get_or_default(&(source_key, caller)) - } - _ => false, - } - } else { - false - }; - if let Some(filter_contract) = utils::get_transfer_filter_contract() { let result = TransferFilterContractContractRef::new(self.env(), filter_contract) - .can_transfer(source_key, target_key, token_identifier); + .can_transfer(source_key, target_key, token_identifier.clone()); if TransferFilterContractResult::DenyTransfer == result { self.env().revert(CEP78Error::TransferFilterContractDenied); @@ -453,148 +378,71 @@ impl CEP78 { } // Revert if caller is not owner nor approved nor an operator. - if !is_owner && !is_approved && !is_operator && !is_package_operator { + if !is_owner && !is_approved && !is_operator { self.env().revert(CEP78Error::InvalidTokenOwner); } + // Updated token_owners dictionary. Revert if token_owner not found. + match self.owners.get(&token_id) { + Some(token_actual_owner) => { + if token_actual_owner != source_key { + self.env().revert(CEP78Error::InvalidTokenOwner) + } + self.owners.set(&token_identifier.to_string(), target_key); + } + None => self + .env() + .revert(CEP78Error::MissingOwnerTokenIdentifierKey) + } + + self.token_count.subtract(&source_key, 1); + self.token_count.add(&target_key, 1); - // if NFTIdentifierMode::Hash == identifier_mode && runtime::get_key(OWNED_TOKENS).is_some() { - // if utils::should_migrate_token_hashes(source_key) { - // utils::migrate_token_hashes(source_key) - // } - - // if utils::should_migrate_token_hashes(target_key) { - // utils::migrate_token_hashes(target_key) - // } - // } - - // let target_owner_item_key = utils::encode_dictionary_item_key(target_owner_key); - - // // Updated token_owners dictionary. Revert if token_owner not found. - // match utils::get_dictionary_value_from_key::( - // TOKEN_OWNERS, - // &token_identifier.get_dictionary_item_key(), - // ) { - // Some(token_actual_owner) => { - // if token_actual_owner != source_owner_key { - // runtime::revert(NFTCoreError::InvalidTokenOwner) - // } - // utils::upsert_dictionary_value_from_key( - // TOKEN_OWNERS, - // &token_identifier.get_dictionary_item_key(), - // target_owner_key, - // ); - // } - // None => runtime::revert(NFTCoreError::MissingOwnerTokenIdentifierKey), - // } - - // let source_owner_item_key = utils::encode_dictionary_item_key(source_owner_key); - - // // Update the from_account balance - // let updated_from_account_balance = - // match utils::get_dictionary_value_from_key::(TOKEN_COUNT, &source_owner_item_key) { - // Some(balance) => { - // if balance > 0u64 { - // balance - 1u64 - // } else { - // // This should never happen... - // runtime::revert(NFTCoreError::FatalTokenIdDuplication); - // } - // } - // None => { - // // This should never happen... - // runtime::revert(NFTCoreError::FatalTokenIdDuplication); - // } - // }; - // utils::upsert_dictionary_value_from_key( - // TOKEN_COUNT, - // &source_owner_item_key, - // updated_from_account_balance, - // ); - - // // Update the to_account balance - // let updated_to_account_balance = - // match utils::get_dictionary_value_from_key::(TOKEN_COUNT, &target_owner_item_key) { - // Some(balance) => balance + 1u64, - // None => 1u64, - // }; - - // utils::upsert_dictionary_value_from_key( - // TOKEN_COUNT, - // &target_owner_item_key, - // updated_to_account_balance, - // ); - - // utils::upsert_dictionary_value_from_key( - // APPROVED, - // &token_identifier.get_dictionary_item_key(), - // Option::::None, - // ); - - // let events_mode = EventsMode::try_from(utils::get_stored_value_with_user_errors::( - // EVENTS_MODE, - // NFTCoreError::MissingEventsMode, - // NFTCoreError::InvalidEventsMode, - // )) - // .unwrap_or_revert(); - - // match events_mode { - // EventsMode::NoEvents => {} - // EventsMode::CEP47 => record_cep47_event_dictionary(CEP47Event::Transfer { - // sender: caller, - // recipient: target_owner_key, - // token_id: token_identifier.clone(), - // }), - // EventsMode::CES => { - // // Emit Transfer event. - // let spender = if caller == owner { None } else { Some(caller) }; - // casper_event_standard::emit(Transfer::new( - // owner, - // spender, - // target_owner_key, - // token_identifier.clone(), - // )); - // } - // } - - // let reporting_mode = utils::get_reporting_mode(); - - // if let OwnerReverseLookupMode::Complete | OwnerReverseLookupMode::TransfersOnly = reporting_mode - // { - // // Update to_account owned_tokens. Revert if owned_tokens list is not found - // let tokens_count = utils::get_token_index(&token_identifier); - // if OwnerReverseLookupMode::TransfersOnly == reporting_mode { - // utils::add_page_entry_and_page_record(tokens_count, &source_owner_item_key, false); - // } - - // let (page_table_entry, page_uref) = utils::update_page_entry_and_page_record( - // tokens_count, - // &source_owner_item_key, - // &target_owner_item_key, - // ); - - // let owned_tokens_actual_key = Key::dictionary(page_uref, source_owner_item_key.as_bytes()); - - // let receipt_string = utils::get_receipt_name(page_table_entry); - - // let receipt = CLValue::from_t((receipt_string, owned_tokens_actual_key)) - // .unwrap_or_revert_with(NFTCoreError::FailedToConvertToCLValue); - // runtime::ret(receipt) - // } + self.approved.set(&token_id, Option::
::None); + + let spender = if caller == owner { None } else { Some(caller) }; + self.emit_ces_event(Transfer::new( + owner, + spender, + target_key, + token_identifier.clone() + )); + + let reporting_mode = self.reverse_lookup.get_mode(); + + if let OwnerReverseLookupMode::Complete | OwnerReverseLookupMode::TransfersOnly = + reporting_mode + { + // Update to_account owned_tokens. Revert if owned_tokens list is not found + let tokens_count = self.reverse_lookup.get_token_index(&token_identifier); + if OwnerReverseLookupMode::TransfersOnly == reporting_mode { + self.pagination.add_page_entry_and_page_record(tokens_count, &source_key, false); + } + + let (page_table_entry, page_uref) = self.pagination.update_page_entry_and_page_record( + tokens_count, + &source_key, + &target_key, + ); + + let receipt_name = self.receipt_name.get_or_default(); + let receipt_string = format!("{receipt_name}_m_{PAGE_SIZE}_p_{page_table_entry}"); + // let receipt_address = Key::dictionary(page_uref, owned_tokens_item_key.as_bytes()); + // TODO: should not return `source_key` + return (receipt_string, source_key); + } todo!() } /// Approves another token holder (an approved account) to transfer tokens. It /// reverts if token_id is invalid, if caller is not the owner nor operator, if token has already /// been burnt, or if caller tries to approve themselves as an approved account. - pub fn approve(&mut self, spender: Address, token_id: Maybe, token_hash: Maybe, operator: Maybe
) { + pub fn approve(&mut self, spender: Address, token_id: Maybe, token_hash: Maybe) { // If we are in minter or assigned mode it makes no sense to approve an account. Hence we // revert. self.ensure_minter_or_assigned(); - - let (caller, contract_package): (Address, Option) = - (self.env().caller(), None); + let caller = self.env().caller(); let token_identifier = self.checked_token_identifier(token_id, token_hash); let owner = self.owner_of_by_id(&token_identifier); @@ -604,34 +452,17 @@ impl CEP78 { let is_owner = caller == owner; let is_operator = !is_owner && self.operators.get_or_default(&(owner, caller)); - let is_package_operator = if !is_owner && !is_operator { - match (self.package_operator_mode.get_or_default(), contract_package) { - (true, Some(contract_package)) => { - // TODO: Implement the following - // self.operators.get_or_default(&(owner, contract_package)) - true - } - _ => false, - } - } else { - false - }; - - if !is_owner && !is_operator && !is_package_operator { + if !is_owner && !is_operator { self.env().revert(CEP78Error::InvalidTokenOwner); } // We assume a burnt token cannot be approved self.ensure_not_burned(&token_identifier); - let spender = match operator { - Maybe::Some(deprecated_operator) => deprecated_operator, - Maybe::None => spender - }; - // If token owner or operator tries to approve itself that's probably a mistake and we revert. self.ensure_not_caller(spender); - self.approved.set(&token_identifier.to_string(), Some(spender)); + self.approved + .set(&token_identifier.to_string(), Some(spender)); self.emit_ces_event(Approval::new(owner, spender, token_identifier)); } @@ -644,38 +475,23 @@ impl CEP78 { // revert. self.ensure_minter_or_assigned(); - let (caller, contract_package): (Address, Option) = (env.caller(), None); + let caller = env.caller(); let token_identifier = self.checked_token_identifier(token_id, token_hash); - + // Revert if caller is not the token owner or an operator. Only the token owner / operators can // revoke an approved account let owner = self.owner_of_by_id(&token_identifier); let is_owner = caller == owner; let is_operator = !is_owner && self.operators.get_or_default(&(owner, caller)); - let is_package_operator = if !is_owner && !is_operator { - match ( - self.package_operator_mode.get_or_default(), - contract_package, - ) { - (true, Some(contract_package)) => { - // TODO: Implement the following - // self.operators.get_or_default(&(owner, contract_package)) - true - } - _ => false, - } - } else { - false - }; - - if !is_owner && !is_operator && !is_package_operator { + if !is_owner && !is_operator { env.revert(CEP78Error::InvalidTokenOwner); } // We assume a burnt token cannot be revoked self.ensure_not_burned(&token_identifier); - self.approved.set(&token_identifier.to_string(), Option::
::None); + self.approved + .set(&token_identifier.to_string(), Option::
::None); // Emit ApprovalRevoked event. self.emit_ces_event(ApprovalRevoked::new(owner, token_identifier)); } @@ -685,8 +501,7 @@ impl CEP78 { /// owner, if caller tries to approve itself as an operator. pub fn set_approval_for_all(&mut self, approve_all: bool, operator: Address) { let env = self.env(); - // If we are in minter or assigned mode it makes no sense to approve an operator. Hence we - // revert. + // If we are in minter or assigned mode it makes no sense to approve an operator. Hence we revert. self.ensure_minter_or_assigned(); // If caller tries to approve itself as operator that's probably a mistake and we revert. self.ensure_not_caller(operator); @@ -695,7 +510,7 @@ impl CEP78 { // Depending on approve_all we either approve all or disapprove all. self.operators.set(&(caller, operator), approve_all); - let events_mode: EventsMode = self.events_mode.get_as(&env); + let events_mode: EventsMode = self.events_mode.get_or_default(); if let EventsMode::CES = events_mode { if approve_all { env.emit_event(ApprovalForAll::new(caller, operator)); @@ -706,26 +521,30 @@ impl CEP78 { } /// Returns if an account is operator for a token owner - pub fn is_approved_for_all(&mut self, token_owner: Address, operator: Address) -> bool { + pub fn is_approved_for_all(&mut self, token_owner: Address, operator: Address) -> bool { self.operators.get_or_default(&(token_owner, operator)) } - + /// Returns the token owner given a token_id. It reverts if token_id /// is invalid. A burnt token still has an associated owner. pub fn owner_of(&mut self, token_id: Maybe, token_hash: Maybe) -> Address { let token_identifier = self.checked_token_identifier(token_id, token_hash); self.owner_of_by_id(&token_identifier) } - + /// Returns the approved account (if any) associated with the provided token_id /// Reverts if token has been burnt. - pub fn get_approved(&mut self, token_id: Maybe, token_hash: Maybe) -> Option
{ + pub fn get_approved( + &mut self, + token_id: Maybe, + token_hash: Maybe + ) -> Option
{ let token_identifier: TokenIdentifier = self.checked_token_identifier(token_id, token_hash); - + self.ensure_not_burned(&token_identifier); self.approved.get(&token_identifier.to_string()).flatten() } - + /// Returns the metadata associated with the provided token_id pub fn metadata(&mut self, token_id: Maybe, token_hash: Maybe) -> String { let token_identifier = self.checked_token_identifier(token_id, token_hash); @@ -733,16 +552,26 @@ impl CEP78 { } /// Updates the metadata if valid. - pub fn set_token_metadata(&mut self, token_id: Maybe, token_hash: Maybe, updated_token_metadata: String) { - self.metadata.ensure_mutability(CEP78Error::ForbiddenMetadataUpdate); - + pub fn set_token_metadata( + &mut self, + token_id: Maybe, + token_hash: Maybe, + updated_token_metadata: String + ) { + self.metadata + .ensure_mutability(CEP78Error::ForbiddenMetadataUpdate); + let token_identifier = self.checked_token_identifier(token_id, token_hash); self.ensure_owner_not_caller(&token_identifier); - self.metadata.update_or_revert(&updated_token_metadata, &token_identifier); - - self.emit_ces_event(MetadataUpdated::new(token_identifier, updated_token_metadata)); + self.metadata + .update_or_revert(&updated_token_metadata, &token_identifier); + + self.emit_ces_event(MetadataUpdated::new( + token_identifier, + updated_token_metadata + )); } - + /// Returns number of owned tokens associated with the provided token holder pub fn balance_of(&mut self, token_owner: Address) -> u64 { self.token_count.get(&token_owner).unwrap_or_default() @@ -754,15 +583,15 @@ impl CEP78 { /// It will also perform the necessary data transformations of historical /// data if needed pub fn migrate(&mut self, nft_package_key: String) { - todo!() + // no-op } - + /// This entrypoint will allow NFT owners to update their receipts from /// the previous owned_tokens list model to the current pagination model /// scheme. Calling the entrypoint will return a list of receipt names /// alongside the dictionary addressed to the relevant pages. pub fn updated_receipts(&mut self) -> Vec<(String, Address)> { - todo!() + vec![] } /// This entrypoint allows users to register with a give CEP-78 instance, @@ -771,17 +600,33 @@ impl CEP78 { /// resulting in more stable gas costs when minting and transferring /// Note: This entrypoint MUST be invoked if the reverse lookup is enabled /// in order to own NFTs. - pub fn register_owner(&mut self) -> (String, URef) { + pub fn register_owner(&mut self, token_owner: Maybe
) -> (String, URef) { + if vec![ + OwnerReverseLookupMode::Complete, + OwnerReverseLookupMode::TransfersOnly, + ] + .contains(&self.reverse_lookup.get_mode()) + { + let owner = match self.ownership_mode() { + OwnershipMode::Minter => self.env().caller(), + OwnershipMode::Assigned | OwnershipMode::Transferable => { + token_owner.unwrap(&self.env()) + } + }; + + self.pagination.register_owner(&owner); + } todo!() } } - impl CEP78 { #[inline] fn is_minter_or_assigned(&self) -> bool { - let ownership_mode: OwnershipMode = self.ownership_mode.get_as(&self.env()); - matches!(ownership_mode, OwnershipMode::Minter | OwnershipMode::Assigned) + matches!( + self.ownership_mode(), + OwnershipMode::Minter | OwnershipMode::Assigned + ) } #[inline] @@ -797,17 +642,21 @@ impl CEP78 { let identifier_mode: NFTIdentifierMode = self.metadata.get_identifier_mode(); match identifier_mode { NFTIdentifierMode::Ordinal => TokenIdentifier::Index(token_id.unwrap(&env)), - NFTIdentifierMode::Hash => TokenIdentifier::Hash(token_hash.unwrap(&env)), + NFTIdentifierMode::Hash => TokenIdentifier::Hash(token_hash.unwrap(&env)) } } #[inline] - fn checked_token_identifier(&self, token_id: Maybe, token_hash: Maybe) -> TokenIdentifier { + fn checked_token_identifier( + &self, + token_id: Maybe, + token_hash: Maybe + ) -> TokenIdentifier { let env = self.env(); let identifier_mode: NFTIdentifierMode = self.metadata.get_identifier_mode(); let token_identifier = match identifier_mode { NFTIdentifierMode::Ordinal => TokenIdentifier::Index(token_id.unwrap(&env)), - NFTIdentifierMode::Hash => TokenIdentifier::Hash(token_hash.unwrap(&env)), + NFTIdentifierMode::Hash => TokenIdentifier::Hash(token_hash.unwrap(&env)) }; let number_of_minted_tokens = self.counter.get_current_value(); @@ -824,16 +673,19 @@ impl CEP78 { fn owner_of_by_id(&self, id: &TokenIdentifier) -> Address { match self.owners.get(&id.to_string()) { Some(token_owner) => token_owner, - None => self.env().revert(CEP78Error::MissingOwnerTokenIdentifierKey), + None => self + .env() + .revert(CEP78Error::MissingOwnerTokenIdentifierKey) } } #[inline] fn is_token_burned(&self, token_identifier: &TokenIdentifier) -> bool { - self.burnt_tokens.get(&token_identifier.to_string()).is_some() + self.burnt_tokens + .get(&token_identifier.to_string()) + .is_some() } - #[inline] fn ensure_not_owner(&self, token_identifier: &TokenIdentifier, address: &Address) { let owner = self.owner_of_by_id(token_identifier); @@ -841,7 +693,7 @@ impl CEP78 { self.env().revert(CEP78Error::InvalidAccount); } } - + #[inline] fn ensure_owner_not_caller(&self, token_identifier: &TokenIdentifier) { let owner = self.owner_of_by_id(token_identifier); @@ -866,7 +718,7 @@ impl CEP78 { #[inline] fn emit_ces_event(&self, event: T) { - let events_mode: EventsMode = self.events_mode.get_as(&self.env()); + let events_mode: EventsMode = self.events_mode.get_or_default(); if let EventsMode::CES = events_mode { self.env().emit_event(event); } @@ -874,14 +726,23 @@ impl CEP78 { #[inline] fn ensure_burnable(&self) { - if let BurnMode::NonBurnable = self.burn_mode.get_as(&self.env()) { + if let BurnMode::NonBurnable = self.burn_mode.get_or_default() { self.env().revert(CEP78Error::InvalidBurnMode) } } -} + #[inline] + fn ownership_mode(&self) -> OwnershipMode { + self.ownership_mode.get_or_default() + } +} #[odra::external_contract] pub trait TransferFilterContract { - fn can_transfer(&self, source_key: Address, target_key: Address, token_id: TokenIdentifier) -> TransferFilterContractResult; -} \ No newline at end of file + fn can_transfer( + &self, + source_key: Address, + target_key: Address, + token_id: TokenIdentifier + ) -> TransferFilterContractResult; +} diff --git a/modules/src/cep78/utils.rs b/modules/src/cep78/utils.rs index 93f55673..7d5d4804 100644 --- a/modules/src/cep78/utils.rs +++ b/modules/src/cep78/utils.rs @@ -1,4 +1,7 @@ -use odra::{casper_types::{bytesrepr::FromBytes, ContractHash}, Address, ContractEnv, OdraError, UnwrapOrRevert, Var}; +use odra::{ + casper_types::bytesrepr::FromBytes, + Address, ContractEnv, OdraError, UnwrapOrRevert, Var +}; pub trait GetAs { fn get_as(&self, env: &ContractEnv) -> T; @@ -7,7 +10,7 @@ pub trait GetAs { impl GetAs for Var where R: TryInto + Default + FromBytes, - R::Error: Into, + R::Error: Into { fn get_as(&self, env: &ContractEnv) -> T { self.get_or_default().try_into().unwrap_or_revert(env) @@ -22,7 +25,7 @@ pub trait IntoOrRevert { impl IntoOrRevert for R where R: TryInto, - R::Error: Into, + R::Error: Into { type Error = R::Error; fn into_or_revert(self, env: &ContractEnv) -> T { @@ -32,4 +35,169 @@ where pub fn get_transfer_filter_contract() -> Option
{ None -} \ No newline at end of file +} + +// pub fn migrate_owned_tokens_in_ordinal_mode() { +// let current_number_of_minted_tokens = utils::get_stored_value_with_user_errors::( +// NUMBER_OF_MINTED_TOKENS, +// NFTCoreError::MissingTotalTokenSupply, +// NFTCoreError::InvalidTotalTokenSupply +// ); +// let page_table_uref = get_uref( +// PAGE_TABLE, +// NFTCoreError::MissingPageTableURef, +// NFTCoreError::InvalidPageTableURef +// ); +// let page_table_width = get_stored_value_with_user_errors::( +// PAGE_LIMIT, +// NFTCoreError::MissingPageLimit, +// NFTCoreError::InvalidPageLimit +// ); +// let mut searched_token_ids: Vec = vec![]; +// for token_id in 0..current_number_of_minted_tokens { +// if !searched_token_ids.contains(&token_id) { +// let token_identifier = TokenIdentifier::new_index(token_id); +// let token_owner_key = get_dictionary_value_from_key::( +// TOKEN_OWNERS, +// &token_identifier.get_dictionary_item_key() +// ) +// .unwrap_or_revert_with(NFTCoreError::MissingNftKind); +// let token_owner_item_key = encode_dictionary_item_key(token_owner_key); +// let owned_tokens_list = get_token_identifiers_from_dictionary( +// &NFTIdentifierMode::Ordinal, +// &token_owner_item_key +// ) +// .unwrap_or_revert(); +// for token_identifier in owned_tokens_list.into_iter() { +// let token_id = token_identifier.get_index().unwrap_or_revert(); +// let page_number = token_id / PAGE_SIZE; +// let page_index = token_id % PAGE_SIZE; +// let mut page_record = match storage::dictionary_get::>( +// page_table_uref, +// &token_owner_item_key +// ) +// .unwrap_or_revert() +// { +// Some(page_record) => page_record, +// None => vec![false; page_table_width as usize] +// }; +// let page_uref = get_uref( +// &format!("{PREFIX_PAGE_DICTIONARY}_{page_number}"), +// NFTCoreError::MissingStorageUref, +// NFTCoreError::InvalidStorageUref +// ); +// let _ = core::mem::replace(&mut page_record[page_number as usize], true); +// storage::dictionary_put(page_table_uref, &token_owner_item_key, page_record); +// let mut page = +// match storage::dictionary_get::>(page_uref, &token_owner_item_key) +// .unwrap_or_revert() +// { +// None => vec![false; PAGE_SIZE as usize], +// Some(single_page) => single_page +// }; +// let is_already_marked_as_owned = +// core::mem::replace(&mut page[page_index as usize], true); +// if is_already_marked_as_owned { +// runtime::revert(NFTCoreError::InvalidPageIndex) +// } +// storage::dictionary_put(page_uref, &token_owner_item_key, page); +// searched_token_ids.push(token_id) +// } +// } +// } +// } + +// pub fn should_migrate_token_hashes(token_owner: Address) -> bool { +// if get_token_identifiers_from_dictionary( +// &NFTIdentifierMode::Hash, +// &encode_dictionary_item_key(token_owner), +// ) +// .is_none() +// { +// return false; +// } +// let page_table_uref = get_uref( +// PAGE_TABLE, +// NFTCoreError::MissingPageTableURef, +// NFTCoreError::InvalidPageTableURef, +// ); +// // If the owner has registered, then they will have an page table entry +// // but it will contain no bits set. +// let page_table = storage::dictionary_get::>( +// page_table_uref, +// &encode_dictionary_item_key(token_owner), +// ) +// .unwrap_or_revert() +// .unwrap_or_revert_with(NFTCoreError::UnregisteredOwnerFromMigration); +// if page_table.contains(&true) { +// return false; +// } +// true +// } + +// pub fn migrate_token_hashes(token_owner: Key) { +// let mut unmatched_hash_count = get_stored_value_with_user_errors::( +// UNMATCHED_HASH_COUNT, +// NFTCoreError::MissingUnmatchedHashCount, +// NFTCoreError::InvalidUnmatchedHashCount +// ); + +// if unmatched_hash_count == 0 { +// runtime::revert(NFTCoreError::InvalidNumberOfMintedTokens) +// } + +// let token_owner_item_key = encode_dictionary_item_key(token_owner); +// let owned_tokens_list = +// get_token_identifiers_from_dictionary(&NFTIdentifierMode::Hash, &token_owner_item_key) +// .unwrap_or_revert_with(NFTCoreError::InvalidTokenOwner); + +// let page_table_uref = get_uref( +// PAGE_TABLE, +// NFTCoreError::MissingPageTableURef, +// NFTCoreError::InvalidPageTableURef +// ); + +// let page_table_width = get_stored_value_with_user_errors::( +// PAGE_LIMIT, +// NFTCoreError::MissingPageLimit, +// NFTCoreError::InvalidPageLimit +// ); + +// for token_identifier in owned_tokens_list.into_iter() { +// let token_address = unmatched_hash_count - 1; +// let page_table_entry = token_address / PAGE_SIZE; +// let page_address = token_address % PAGE_SIZE; +// let mut page_table = +// match storage::dictionary_get::>(page_table_uref, &token_owner_item_key) +// .unwrap_or_revert() +// { +// Some(page_record) => page_record, +// None => vec![false; page_table_width as usize] +// }; +// let _ = core::mem::replace(&mut page_table[page_table_entry as usize], true); +// storage::dictionary_put(page_table_uref, &token_owner_item_key, page_table); +// let page_uref = get_uref( +// &format!("{PREFIX_PAGE_DICTIONARY}_{page_table_entry}"), +// NFTCoreError::MissingStorageUref, +// NFTCoreError::InvalidStorageUref +// ); +// let mut page = match storage::dictionary_get::>(page_uref, &token_owner_item_key) +// .unwrap_or_revert() +// { +// Some(single_page) => single_page, +// None => vec![false; PAGE_SIZE as usize] +// }; +// let _ = core::mem::replace(&mut page[page_address as usize], true); +// storage::dictionary_put(page_uref, &token_owner_item_key, page); +// insert_hash_id_lookups(unmatched_hash_count - 1, token_identifier); +// unmatched_hash_count -= 1; +// } + +// let unmatched_hash_count_uref = get_uref( +// UNMATCHED_HASH_COUNT, +// NFTCoreError::MissingUnmatchedHashCount, +// NFTCoreError::InvalidUnmatchedHashCount +// ); + +// storage::write(unmatched_hash_count_uref, unmatched_hash_count); +// } diff --git a/modules/src/cep78/whitelist.rs b/modules/src/cep78/whitelist.rs index 1d620e78..abf66b9b 100644 --- a/modules/src/cep78/whitelist.rs +++ b/modules/src/cep78/whitelist.rs @@ -1,60 +1,46 @@ -use odra::{args::Maybe, casper_types::{ContractHash, Key}, prelude::*, Address, List, UnwrapOrRevert, Var}; +use odra::{args::Maybe, prelude::*, Address, List, Var}; -use super::{error::CEP78Error, modalities::WhitelistMode, utils::GetAs}; +use super::{error::CEP78Error, modalities::WhitelistMode}; #[odra::module] pub struct ACLWhitelist { addresses: List
, - mode: Var, - package_mode: Var, + mode: Var, + package_mode: Var } impl ACLWhitelist { - pub fn init(&mut self, addresses: Vec
, mode: u8, package_mode: bool) { + pub fn init(&mut self, addresses: Vec
, mode: WhitelistMode) { for address in addresses { self.addresses.push(address); } self.mode.set(mode); - self.package_mode.set(package_mode); + // Odra does not support version mode. + self.package_mode.set(true); } + #[inline] pub fn get_mode(&self) -> WhitelistMode { - self.mode.get_as(&self.env()) + self.mode.get_or_default() } - pub fn is_package_mode(&self) -> bool { - self.package_mode.get_or_default() + #[inline] + pub fn is_whitelisted(&self, address: &Address) -> bool { + self.addresses.iter().any(|a| &a == address) } - pub fn update_package_mode(&mut self, package_mode: Maybe) { - if let Maybe::Some(package_mode) = package_mode { - self.package_mode.set(package_mode); - } - } - - pub fn update_addresses(&mut self, addresses: Maybe>, contract_whitelist: Maybe>) { - let mut new_addresses = addresses.unwrap_or_default(); - - // Deprecated in 1.4 in favor of above ARG_ACL_WHITELIST - let new_contract_whitelist = contract_whitelist.unwrap_or_default(); - - for contract_hash in new_contract_whitelist.iter() { - let address = Address::try_from(Key::from(*contract_hash)).unwrap_or_revert(&self.env()); - new_addresses.push(address); - } - + pub fn update_addresses(&mut self, new_addresses: Maybe>) { + let new_addresses = new_addresses.unwrap_or_default(); if !new_addresses.is_empty() { match self.get_mode() { WhitelistMode::Unlocked => { - while let Some(_) = self.addresses.pop() { - - } + while let Some(_) = self.addresses.pop() {} for address in new_addresses { self.addresses.push(address); } } - WhitelistMode::Locked => self.env().revert(CEP78Error::InvalidWhitelistMode), + WhitelistMode::Locked => self.env().revert(CEP78Error::InvalidWhitelistMode) } } } -} \ No newline at end of file +} diff --git a/modules/src/lib.rs b/modules/src/lib.rs index 2abeace4..8c45fd9e 100644 --- a/modules/src/lib.rs +++ b/modules/src/lib.rs @@ -5,6 +5,7 @@ extern crate alloc; pub mod access; +pub mod cep78; pub mod erc1155; pub mod erc1155_receiver; pub mod erc1155_token; @@ -14,4 +15,3 @@ pub mod erc721_receiver; pub mod erc721_token; pub mod security; pub mod wrapped_native; -pub mod cep78; From 70bf5f2bb5957d5563b018442ffd102bddaed325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Tue, 16 Apr 2024 17:44:12 +0200 Subject: [PATCH 04/38] Skip args validation --- core/src/contract_container.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/contract_container.rs b/core/src/contract_container.rs index 4b47ac85..ef562ddb 100644 --- a/core/src/contract_container.rs +++ b/core/src/contract_container.rs @@ -32,7 +32,7 @@ impl ContractContainer { OdraError::VmError(VmError::NoSuchMethod(call_def.entry_point().to_string())) })?; // validate the args, return an error if the args are invalid - self.validate_args(&ep.args, call_def.args())?; + // self.validate_args(&ep.args, call_def.args())?; self.entry_points_caller.call(call_def) } From abc8b1d633e2ce1cb7300026569a757df1dd8da1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Tue, 16 Apr 2024 17:44:35 +0200 Subject: [PATCH 05/38] Impl Default for Maybe --- core/src/args.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/args.rs b/core/src/args.rs index 2d64cb01..1b8b390d 100644 --- a/core/src/args.rs +++ b/core/src/args.rs @@ -7,11 +7,12 @@ use casper_types::{ }; /// A type that represents an entrypoint arg that may or may not be present. -#[derive(Debug, Clone)] +#[derive(Default, Debug, Clone)] pub enum Maybe { /// A value is present. Some(T), /// No value is present. + #[default] None } From 116987ead3615fbe426414a6cb35c14f542f583a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Tue, 16 Apr 2024 17:46:01 +0200 Subject: [PATCH 06/38] update token impl --- modules/src/cep78/collection_info.rs | 68 +++++++++++++ modules/src/cep78/metadata.rs | 2 +- modules/src/cep78/mod.rs | 2 + modules/src/cep78/modalities.rs | 3 +- modules/src/cep78/pagination.rs | 81 ++++++++-------- modules/src/cep78/reverse_lookup.rs | 2 +- modules/src/cep78/settings.rs | 71 ++++++++++++++ modules/src/cep78/token.rs | 139 +++++++++++++-------------- modules/src/cep78/utils.rs | 3 +- modules/src/cep78/whitelist.rs | 18 ++-- 10 files changed, 261 insertions(+), 128 deletions(-) create mode 100644 modules/src/cep78/collection_info.rs create mode 100644 modules/src/cep78/settings.rs diff --git a/modules/src/cep78/collection_info.rs b/modules/src/cep78/collection_info.rs new file mode 100644 index 00000000..642b6539 --- /dev/null +++ b/modules/src/cep78/collection_info.rs @@ -0,0 +1,68 @@ +use odra::args::Maybe; +use odra::prelude::*; +use odra::Address; +use odra::Sequence; +use odra::Var; + +use super::constants; +use super::error::CEP78Error; + +#[odra::module] +pub struct CollectionInfo { + name: Var, + symbol: Var, + total_token_supply: Var, + counter: Sequence, + installer: Var
+} + +impl CollectionInfo { + pub fn init( + &mut self, + name: String, + symbol: String, + total_token_supply: u64, + installer: Address + ) { + if total_token_supply == 0 { + self.env().revert(CEP78Error::CannotInstallWithZeroSupply) + } + + if total_token_supply > constants::MAX_TOTAL_TOKEN_SUPPLY { + self.env().revert(CEP78Error::ExceededMaxTotalSupply) + } + + self.name.set(name); + self.symbol.set(symbol); + self.total_token_supply.set(total_token_supply); + self.installer.set(installer); + + self.counter.next_value(); + } + + #[inline] + pub fn installer(&self) -> Address { + self.installer + .get_or_revert_with(CEP78Error::MissingInstaller) + } + + #[inline] + pub fn total_token_supply(&self) -> u64 { + self.total_token_supply.get_or_default() + } + + #[inline] + pub fn increment_number_of_minted_tokens(&mut self) { + self.counter.next_value(); + } + + #[inline] + pub fn number_of_minted_tokens(&self) -> u64 { + self.counter.get_current_value() + } + + #[inline] + pub fn collection_name(&self) -> String { + self.name.get_or_default() + } +} diff --git a/modules/src/cep78/metadata.rs b/modules/src/cep78/metadata.rs index 7bb03902..26dae8a5 100644 --- a/modules/src/cep78/metadata.rs +++ b/modules/src/cep78/metadata.rs @@ -7,7 +7,7 @@ use super::{ modalities::{ MetadataMutability, MetadataRequirement, NFTIdentifierMode, NFTMetadataKind, Requirement, TokenIdentifier - }, + } }; #[odra::module] diff --git a/modules/src/cep78/mod.rs b/modules/src/cep78/mod.rs index 4692e7be..d51540eb 100644 --- a/modules/src/cep78/mod.rs +++ b/modules/src/cep78/mod.rs @@ -1,5 +1,6 @@ #![allow(missing_docs)] +mod collection_info; pub mod constants; pub mod error; pub mod events; @@ -7,6 +8,7 @@ mod metadata; pub mod modalities; mod pagination; mod reverse_lookup; +mod settings; #[cfg(test)] mod tests; pub mod token; diff --git a/modules/src/cep78/modalities.rs b/modules/src/cep78/modalities.rs index ec1f0504..6057c723 100644 --- a/modules/src/cep78/modalities.rs +++ b/modules/src/cep78/modalities.rs @@ -177,8 +177,9 @@ impl TryFrom for OwnershipMode { #[repr(u8)] #[odra::odra_type] -#[derive(PartialOrd, Ord, Copy)] +#[derive(Default, PartialOrd, Ord, Copy)] pub enum NFTIdentifierMode { + #[default] Ordinal = 0, Hash = 1 } diff --git a/modules/src/cep78/pagination.rs b/modules/src/cep78/pagination.rs index f19cabbd..df01e908 100644 --- a/modules/src/cep78/pagination.rs +++ b/modules/src/cep78/pagination.rs @@ -1,4 +1,8 @@ -use odra::{casper_types::{AccessRights, URef}, prelude::*, Address, Mapping, UnwrapOrRevert}; +use odra::{ + casper_types::{AccessRights, URef}, + prelude::*, + Address, Mapping, UnwrapOrRevert +}; use crate::cep78::constants::PREFIX_PAGE_DICTIONARY; @@ -111,8 +115,7 @@ impl Pagination { let _ = core::mem::replace(&mut target_page[page_address as usize], true); - self.pages - .set(&new_page_key, target_page); + self.pages.set(&new_page_key, target_page); // (page_table_entry, page_uref) let addr_array = [0u8; 32]; let uref_a = URef::new(addr_array, AccessRights::READ); @@ -122,42 +125,40 @@ impl Pagination { pub fn register_owner(&self, owner: &Address) { let page = self.page_tables.get_or_default(&owner); - - - - // let page_table_uref = utils::get_uref( - // PAGE_TABLE, - // NFTCoreError::MissingPageTableURef, - // NFTCoreError::InvalidPageTableURef, - // ); - - // let owner_item_key = utils::encode_dictionary_item_key(owner_key); - - // if storage::dictionary_get::>(page_table_uref, &owner_item_key) - // .unwrap_or_revert() - // .is_none() - // { - // let page_table_width = utils::get_stored_value_with_user_errors::( - // PAGE_LIMIT, - // NFTCoreError::MissingPageLimit, - // NFTCoreError::InvalidPageLimit, - // ); - // storage::dictionary_put( - // page_table_uref, - // &owner_item_key, - // vec![false; page_table_width as usize], - // ); - // } - // let collection_name = utils::get_stored_value_with_user_errors::( - // COLLECTION_NAME, - // NFTCoreError::MissingCollectionName, - // NFTCoreError::InvalidCollectionName, - // ); - // let package_uref = storage::new_uref(utils::get_stored_value_with_user_errors::( - // &format!("{PREFIX_CEP78}_{collection_name}"), - // NFTCoreError::MissingCep78PackageHash, - // NFTCoreError::InvalidCep78InvalidHash, - // )); - // runtime::ret(CLValue::from_t((collection_name, package_uref)).unwrap_or_revert()) + + // let page_table_uref = utils::get_uref( + // PAGE_TABLE, + // NFTCoreError::MissingPageTableURef, + // NFTCoreError::InvalidPageTableURef, + // ); + + // let owner_item_key = utils::encode_dictionary_item_key(owner_key); + + // if storage::dictionary_get::>(page_table_uref, &owner_item_key) + // .unwrap_or_revert() + // .is_none() + // { + // let page_table_width = utils::get_stored_value_with_user_errors::( + // PAGE_LIMIT, + // NFTCoreError::MissingPageLimit, + // NFTCoreError::InvalidPageLimit, + // ); + // storage::dictionary_put( + // page_table_uref, + // &owner_item_key, + // vec![false; page_table_width as usize], + // ); + // } + // let collection_name = utils::get_stored_value_with_user_errors::( + // COLLECTION_NAME, + // NFTCoreError::MissingCollectionName, + // NFTCoreError::InvalidCollectionName, + // ); + // let package_uref = storage::new_uref(utils::get_stored_value_with_user_errors::( + // &format!("{PREFIX_CEP78}_{collection_name}"), + // NFTCoreError::MissingCep78PackageHash, + // NFTCoreError::InvalidCep78InvalidHash, + // )); + // runtime::ret(CLValue::from_t((collection_name, package_uref)).unwrap_or_revert()) } } diff --git a/modules/src/cep78/reverse_lookup.rs b/modules/src/cep78/reverse_lookup.rs index 04ebbe2d..1ca6ce39 100644 --- a/modules/src/cep78/reverse_lookup.rs +++ b/modules/src/cep78/reverse_lookup.rs @@ -2,7 +2,7 @@ use odra::{prelude::*, Mapping, UnwrapOrRevert, Var}; use super::{ error::CEP78Error, - modalities::{OwnerReverseLookupMode, TokenIdentifier}, + modalities::{OwnerReverseLookupMode, TokenIdentifier} }; #[odra::module] diff --git a/modules/src/cep78/settings.rs b/modules/src/cep78/settings.rs new file mode 100644 index 00000000..5de6de90 --- /dev/null +++ b/modules/src/cep78/settings.rs @@ -0,0 +1,71 @@ +use odra::prelude::*; +use odra::Var; + +use super::modalities::BurnMode; +use super::modalities::EventsMode; +use super::modalities::MintingMode; +use super::modalities::NFTHolderMode; +use super::modalities::NFTKind; +use super::modalities::OwnershipMode; + +#[odra::module] +pub struct Settings { + allow_minting: Var, + minting_mode: Var, + ownership_mode: Var, + nft_kind: Var, + holder_mode: Var, + burn_mode: Var, + events_mode: Var +} + +impl Settings { + pub fn init( + &mut self, + allow_minting: bool, + minting_mode: MintingMode, + ownership_mode: OwnershipMode, + nft_kind: NFTKind, + holder_mode: NFTHolderMode, + burn_mode: BurnMode, + events_mode: EventsMode + ) { + self.allow_minting.set(allow_minting); + self.minting_mode.set(minting_mode); + self.ownership_mode.set(ownership_mode); + self.nft_kind.set(nft_kind); + self.holder_mode.set(holder_mode); + self.burn_mode.set(burn_mode); + self.events_mode.set(events_mode); + } + + #[inline] + pub fn allow_minting(&self) -> bool { + self.allow_minting.get_or_default() + } + + #[inline] + pub fn set_allow_minting(&mut self, value: bool) { + self.allow_minting.set(value) + } + + #[inline] + pub fn events_mode(&self) -> EventsMode { + self.events_mode.get_or_default() + } + + #[inline] + pub fn burn_mode(&self) -> BurnMode { + self.burn_mode.get_or_default() + } + + #[inline] + pub fn ownership_mode(&self) -> OwnershipMode { + self.ownership_mode.get_or_default() + } + + #[inline] + pub fn minting_mode(&self) -> MintingMode { + self.minting_mode.get_or_default() + } +} diff --git a/modules/src/cep78/token.rs b/modules/src/cep78/token.rs index bbe44433..45e8f907 100644 --- a/modules/src/cep78/token.rs +++ b/modules/src/cep78/token.rs @@ -1,53 +1,45 @@ use super::{ + collection_info::CollectionInfo, constants, error::CEP78Error, events::{ - Approval, ApprovalForAll, ApprovalRevoked, Burn, MetadataUpdated, Mint, RevokedForAll, Transfer, VariablesSet + Approval, ApprovalForAll, ApprovalRevoked, Burn, MetadataUpdated, Mint, RevokedForAll, + Transfer, VariablesSet }, metadata::Metadata, modalities::{ - BurnMode, EventsMode, MetadataMutability, MintingMode, NFTHolderMode, NFTIdentifierMode, NFTKind, NFTMetadataKind, OwnerReverseLookupMode, OwnershipMode, TokenIdentifier, TransferFilterContractResult, WhitelistMode + BurnMode, EventsMode, MetadataMutability, MintingMode, NFTHolderMode, NFTIdentifierMode, + NFTKind, NFTMetadataKind, OwnerReverseLookupMode, OwnershipMode, TokenIdentifier, + TransferFilterContractResult, WhitelistMode }, pagination::{Pagination, PAGE_SIZE}, reverse_lookup::ReverseLookup, - utils::{self, GetAs, IntoOrRevert}, + settings::Settings, + utils, whitelist::ACLWhitelist }; use odra::{ args::Maybe, - casper_types::{bytesrepr::ToBytes, Key, URef}, + casper_types::{bytesrepr::ToBytes, URef}, prelude::*, Address, Mapping, Sequence, SubModule, UnwrapOrRevert, Var }; -#[odra::module( - //package_hash_key = "cep78", - //access_key = "cep78", -)] +#[odra::module] pub struct CEP78 { - installer: Var
, - collection_name: Var, - collection_symbol: Var, - cap: Var, - allow_minting: Var, - minting_mode: Var, - ownership_mode: Var, - nft_kind: Var, - holder_mode: Var, - burn_mode: Var, - events_mode: Var, - counter: Sequence, + whitelist: SubModule, + metadata: SubModule, + reverse_lookup: SubModule, + pagination: SubModule, + info: SubModule, + settings: SubModule, owners: Mapping, issuers: Mapping, approved: Mapping>, token_count: Mapping, burnt_tokens: Mapping, operators: Mapping<(Address, Address), bool>, - receipt_name: Var, - whitelist: SubModule, - metadata: SubModule, - reverse_lookup: SubModule, - pagination: SubModule, + receipt_name: Var } #[odra::module] @@ -78,32 +70,29 @@ impl CEP78 { optional_metadata: Maybe> ) { let installer = self.env().caller(); - self.installer.set(installer); - self.collection_name.set(collection_name); - self.collection_symbol.set(collection_symbol); - - if total_token_supply == 0 { - self.env().revert(CEP78Error::CannotInstallWithZeroSupply) - } - - if total_token_supply > constants::MAX_TOTAL_TOKEN_SUPPLY { - self.env().revert(CEP78Error::ExceededMaxTotalSupply) - } + self.info.init( + collection_name, + collection_symbol, + total_token_supply, + installer + ); + self.settings.init( + allow_minting.unwrap_or(true), + minting_mode.clone().unwrap_or_default(), + ownership_mode, + nft_kind, + holder_mode.unwrap_or_default(), + burn_mode.unwrap_or_default(), + events_mode.unwrap_or_default() + ); - self.cap.set(total_token_supply); - self.allow_minting.set(allow_minting.unwrap_or(true)); - self.minting_mode.set(minting_mode.clone().unwrap_or_default()); - self.ownership_mode.set(ownership_mode); - self.nft_kind.set(nft_kind); - self.holder_mode.set(holder_mode.unwrap_or_default()); - self.burn_mode.set(burn_mode.unwrap_or_default()); - self.events_mode.set(events_mode.unwrap_or_default()); - self.reverse_lookup.init(owner_reverse_lookup_mode.clone().unwrap_or_default()); + self.reverse_lookup + .init(owner_reverse_lookup_mode.clone().unwrap_or_default()); self.receipt_name.set(receipt_name.unwrap_or_default()); self.whitelist.init( acl_white_list.unwrap_or_default(), - whitelist_mode.unwrap_or_default(), + whitelist_mode.unwrap_or_default() ); self.metadata.init( @@ -115,7 +104,9 @@ impl CEP78 { json_schema ); - if nft_identifier_mode == NFTIdentifierMode::Hash && metadata_mutability == MetadataMutability::Mutable { + if nft_identifier_mode == NFTIdentifierMode::Hash + && metadata_mutability == MetadataMutability::Mutable + { self.env().revert(CEP78Error::InvalidMetadataMutability) } @@ -125,8 +116,6 @@ impl CEP78 { { self.env().revert(CEP78Error::InvalidReportingMode) } - - self.counter.next_value(); } /// Exposes all variables that can be changed by managing account post @@ -136,17 +125,14 @@ impl CEP78 { pub fn set_variables( &mut self, allow_minting: Maybe, - acl_whitelist: Maybe>, + acl_whitelist: Maybe> ) { - let installer = self - .installer - .get_or_revert_with(CEP78Error::MissingInstaller); - + let installer = self.info.installer(); // Only the installing account can change the mutable variables. self.ensure_not_caller(installer); if let Maybe::Some(allow_minting) = allow_minting { - self.allow_minting.set(allow_minting); + self.settings.set_allow_minting(allow_minting); } self.whitelist.update_addresses(acl_whitelist); @@ -171,26 +157,24 @@ impl CEP78 { ) -> (String, Address, String) { // The contract owner can toggle the minting behavior on and off over time. // The contract is toggled on by default. - let allow_minting = self.allow_minting.get_or_default(); + let allow_minting = self.settings.allow_minting(); // If contract minting behavior is currently toggled off we revert. if !allow_minting { self.env().revert(CEP78Error::MintingIsPaused); } - let total_token_supply = self - .cap - .get_or_revert_with(CEP78Error::MissingTotalTokenSupply); + let total_token_supply = self.info.total_token_supply(); // The minted_tokens_count is the number of minted tokens so far. - let minted_tokens_count = self.counter.get_current_value(); + let minted_tokens_count = self.info.number_of_minted_tokens(); // Revert if the token supply has been exhausted. if minted_tokens_count >= total_token_supply { self.env().revert(CEP78Error::TokenSupplyDepleted); } - let minting_mode: MintingMode = self.minting_mode.get_or_default(); + let minting_mode: MintingMode = self.settings.minting_mode(); let caller = self.env().caller(); @@ -198,9 +182,7 @@ impl CEP78 { if MintingMode::Installer == minting_mode { match caller { Address::Account(_) => { - let installer_account = self - .installer - .get_or_revert_with(CEP78Error::MissingInstaller); + let installer_account = self.info.installer(); // Revert if private minting is required and caller is not installer. if caller != installer_account { self.env().revert(CEP78Error::InvalidMinter) @@ -259,7 +241,7 @@ impl CEP78 { self.token_count.add(&token_owner_key, 1); // Increment number_of_minted_tokens by one - self.counter.next_value(); + self.info.increment_number_of_minted_tokens(); // Emit Mint event. self.emit_ces_event(Mint::new( @@ -416,13 +398,14 @@ impl CEP78 { // Update to_account owned_tokens. Revert if owned_tokens list is not found let tokens_count = self.reverse_lookup.get_token_index(&token_identifier); if OwnerReverseLookupMode::TransfersOnly == reporting_mode { - self.pagination.add_page_entry_and_page_record(tokens_count, &source_key, false); + self.pagination + .add_page_entry_and_page_record(tokens_count, &source_key, false); } let (page_table_entry, page_uref) = self.pagination.update_page_entry_and_page_record( tokens_count, &source_key, - &target_key, + &target_key ); let receipt_name = self.receipt_name.get_or_default(); @@ -510,7 +493,7 @@ impl CEP78 { // Depending on approve_all we either approve all or disapprove all. self.operators.set(&(caller, operator), approve_all); - let events_mode: EventsMode = self.events_mode.get_or_default(); + let events_mode: EventsMode = self.settings.events_mode(); if let EventsMode::CES = events_mode { if approve_all { env.emit_event(ApprovalForAll::new(caller, operator)); @@ -618,6 +601,18 @@ impl CEP78 { } todo!() } + + pub fn is_whitelisted(&self, address: &Address) -> bool { + self.whitelist.is_whitelisted(address) + } + + pub fn get_whitelist_mode(&self) -> WhitelistMode { + self.whitelist.get_mode() + } + + pub fn get_collection_name(&self) -> String { + self.info.collection_name() + } } impl CEP78 { @@ -659,7 +654,7 @@ impl CEP78 { NFTIdentifierMode::Hash => TokenIdentifier::Hash(token_hash.unwrap(&env)) }; - let number_of_minted_tokens = self.counter.get_current_value(); + let number_of_minted_tokens = self.info.number_of_minted_tokens(); if let NFTIdentifierMode::Ordinal = identifier_mode { // Revert if token_id is out of bounds if token_identifier.get_index().unwrap_or_revert(&env) >= number_of_minted_tokens { @@ -718,7 +713,7 @@ impl CEP78 { #[inline] fn emit_ces_event(&self, event: T) { - let events_mode: EventsMode = self.events_mode.get_or_default(); + let events_mode: EventsMode = self.settings.events_mode(); if let EventsMode::CES = events_mode { self.env().emit_event(event); } @@ -726,14 +721,14 @@ impl CEP78 { #[inline] fn ensure_burnable(&self) { - if let BurnMode::NonBurnable = self.burn_mode.get_or_default() { + if let BurnMode::NonBurnable = self.settings.burn_mode() { self.env().revert(CEP78Error::InvalidBurnMode) } } #[inline] fn ownership_mode(&self) -> OwnershipMode { - self.ownership_mode.get_or_default() + self.settings.ownership_mode() } } diff --git a/modules/src/cep78/utils.rs b/modules/src/cep78/utils.rs index 7d5d4804..68281fb0 100644 --- a/modules/src/cep78/utils.rs +++ b/modules/src/cep78/utils.rs @@ -1,6 +1,5 @@ use odra::{ - casper_types::bytesrepr::FromBytes, - Address, ContractEnv, OdraError, UnwrapOrRevert, Var + casper_types::bytesrepr::FromBytes, Address, ContractEnv, OdraError, UnwrapOrRevert, Var }; pub trait GetAs { diff --git a/modules/src/cep78/whitelist.rs b/modules/src/cep78/whitelist.rs index abf66b9b..06cc95a6 100644 --- a/modules/src/cep78/whitelist.rs +++ b/modules/src/cep78/whitelist.rs @@ -1,19 +1,17 @@ -use odra::{args::Maybe, prelude::*, Address, List, Var}; +use odra::{args::Maybe, prelude::*, Address, List, Mapping, Var}; use super::{error::CEP78Error, modalities::WhitelistMode}; #[odra::module] pub struct ACLWhitelist { - addresses: List
, + addresses: Var>, mode: Var, package_mode: Var } impl ACLWhitelist { pub fn init(&mut self, addresses: Vec
, mode: WhitelistMode) { - for address in addresses { - self.addresses.push(address); - } + self.addresses.set(addresses); self.mode.set(mode); // Odra does not support version mode. self.package_mode.set(true); @@ -21,12 +19,13 @@ impl ACLWhitelist { #[inline] pub fn get_mode(&self) -> WhitelistMode { - self.mode.get_or_default() + // self.mode.get_or_default() + WhitelistMode::Locked } #[inline] pub fn is_whitelisted(&self, address: &Address) -> bool { - self.addresses.iter().any(|a| &a == address) + self.addresses.get_or_default().contains(address) } pub fn update_addresses(&mut self, new_addresses: Maybe>) { @@ -34,10 +33,7 @@ impl ACLWhitelist { if !new_addresses.is_empty() { match self.get_mode() { WhitelistMode::Unlocked => { - while let Some(_) = self.addresses.pop() {} - for address in new_addresses { - self.addresses.push(address); - } + self.addresses.set(new_addresses); } WhitelistMode::Locked => self.env().revert(CEP78Error::InvalidWhitelistMode) } From 4eccc45dbfa40e9b18a1740183384ce689b7ee8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Tue, 16 Apr 2024 17:46:15 +0200 Subject: [PATCH 07/38] ACL test wip --- modules/src/cep78/tests/acl.rs | 1039 ++++++++++++++++++++++ modules/src/cep78/tests/mod.rs | 16 + modules/src/cep78/tests/set_variables.rs | 7 + modules/src/cep78/tests/utils.rs | 217 ++++- 4 files changed, 1238 insertions(+), 41 deletions(-) create mode 100644 modules/src/cep78/tests/acl.rs create mode 100644 modules/src/cep78/tests/mod.rs create mode 100644 modules/src/cep78/tests/set_variables.rs diff --git a/modules/src/cep78/tests/acl.rs b/modules/src/cep78/tests/acl.rs new file mode 100644 index 00000000..58edf23b --- /dev/null +++ b/modules/src/cep78/tests/acl.rs @@ -0,0 +1,1039 @@ +use odra::{ + args::Maybe, + host::{Deployer, HostEnv, HostRef, NoArgs}, + prelude::*, + Address +}; + +use crate::cep78::{ + error::CEP78Error, + modalities::{ + MintingMode, NFTHolderMode, NFTMetadataKind, OwnershipMode, + WhitelistMode + }, + tests::utils::{InitArgsBuilder, TEST_PRETTY_721_META_DATA}, + token::CEP78HostRef +}; + +fn default_args_builder() -> InitArgsBuilder { + InitArgsBuilder::default() + .total_token_supply(100u64) + .nft_metadata_kind(NFTMetadataKind::NFT721) +} + +#[odra::module] +struct DummyContract; + +#[odra::module] +impl DummyContract {} + +#[odra::module] +struct TestContract; + +#[odra::module] +impl TestContract { + pub fn mint( + &mut self, + nft_contract_address: &Address, + token_metadata: String + ) -> (String, Address, String) { + NftContractContractRef::new(self.env(), *nft_contract_address) + .mint(self.env().self_address(), token_metadata) + } +} + +#[odra::external_contract] +trait NftContract { + fn mint(&mut self, token_owner: Address, token_metadata: String) -> (String, Address, String); +} + +#[test] +fn should_install_with_acl_whitelist() { + let env = odra_test::env(); + + let test_contract_address = TestContractHostRef::deploy(&env, NoArgs); + + let contract_whitelist = vec![test_contract_address.address().clone()]; + + let args = default_args_builder() + .holder_mode(NFTHolderMode::Contracts) + .whitelist_mode(WhitelistMode::Locked) + .minting_mode(MintingMode::Acl) + .acl_white_list(contract_whitelist) + .build(); + let contract = CEP78HostRef::deploy(&env, args); + + assert_eq!(WhitelistMode::Locked, contract.get_whitelist_mode()); + let is_whitelisted_contract = contract.is_whitelisted(test_contract_address.address()); + assert!(is_whitelisted_contract, "acl whitelist is incorrectly set"); +} + +#[test] +#[ignore = "No need to implement a contract whitelist"] +fn should_install_with_deprecated_contract_whitelist() {} + +#[test] +#[ignore = "No need to implement a contract whitelist"] +fn should_not_install_with_minting_mode_not_acl_if_acl_whitelist_provided() {} + +fn should_disallow_installation_of_contract_with_empty_locked_whitelist_in_public_mode_with_holder_mode( + env: &HostEnv, + nft_holder_mode: NFTHolderMode +) { + let args = default_args_builder() + .holder_mode(nft_holder_mode) + .whitelist_mode(WhitelistMode::Locked) + .minting_mode(MintingMode::Public) + .build(); + + CEP78HostRef::deploy(env, args); +} + +#[test] +fn should_allow_installation_of_contract_with_empty_locked_whitelist_in_public_mode() { + let env = odra_test::env(); + should_disallow_installation_of_contract_with_empty_locked_whitelist_in_public_mode_with_holder_mode( + &env, + NFTHolderMode::Accounts, + ); + should_disallow_installation_of_contract_with_empty_locked_whitelist_in_public_mode_with_holder_mode( + &env, + NFTHolderMode::Contracts, + ); + should_disallow_installation_of_contract_with_empty_locked_whitelist_in_public_mode_with_holder_mode( + &env, + NFTHolderMode::Mixed + ); +} + +// TODO: in Odra contract installation always succeeds, so this test is not applicable +#[test] +#[ignore = "in Odra contract installation always succeeds, so this test is not applicable"] +fn should_disallow_installation_with_contract_holder_mode_and_installer_mode() { + let env = odra_test::env(); + let contract_whitelist = vec![env.get_account(1), env.get_account(2), env.get_account(3)]; + + let args = default_args_builder() + .holder_mode(NFTHolderMode::Contracts) + .whitelist_mode(WhitelistMode::Locked) + .ownership_mode(OwnershipMode::Minter) + .minting_mode(MintingMode::Installer) + .acl_white_list(contract_whitelist) + .build(); + + CEP78HostRef::deploy(&env, args); + // builder.exec(install_request).expect_failure(); + // let error = builder.get_error().expect("should have an error"); + // assert_expected_error(error, 38, "Invalid MintingMode (not ACL) and NFTHolderMode"); +} + +#[test] +fn should_allow_whitelisted_account_to_mint() { + let env = odra_test::env(); + + let account_user_1 = env.get_account(1); + let account_whitelist = vec![account_user_1]; + + let args = default_args_builder() + .holder_mode(NFTHolderMode::Accounts) + .whitelist_mode(WhitelistMode::Locked) + .ownership_mode(OwnershipMode::Minter) + .minting_mode(MintingMode::Acl) + .acl_white_list(account_whitelist) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + assert!( + contract.is_whitelisted(&account_user_1), + "acl whitelist is incorrectly set" + ); + + env.set_caller(account_user_1); + contract.mint( + account_user_1, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let token_id = 0u64; + let actual_token_owner = contract.owner_of(Maybe::Some(token_id), Maybe::None); + + assert_eq!(actual_token_owner, account_user_1); +} + +#[test] +fn should_disallow_unlisted_account_from_minting() { + let env = odra_test::env(); + let account = env.get_account(0); + let account_whitelist = vec![account]; + + let args = default_args_builder() + .holder_mode(NFTHolderMode::Accounts) + .whitelist_mode(WhitelistMode::Locked) + .ownership_mode(OwnershipMode::Minter) + .minting_mode(MintingMode::Acl) + .acl_white_list(account_whitelist) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + assert!( + contract.is_whitelisted(&account), + "acl whitelist is incorrectly set" + ); + let account_user_1 = env.get_account(1); + + env.set_caller(account_user_1); + assert_eq!( + contract.try_mint( + account_user_1, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ), + Err(CEP78Error::InvalidMinter.into()), + "Unlisted account hash should not be permitted to mint" + ); +} + +#[test] +fn should_allow_whitelisted_contract_to_mint() { + let env = odra_test::env(); + + let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + + let contract_whitelist = vec![minting_contract.address().clone()]; + let args = default_args_builder() + .holder_mode(NFTHolderMode::Contracts) + .whitelist_mode(WhitelistMode::Locked) + .ownership_mode(OwnershipMode::Minter) + .minting_mode(MintingMode::Acl) + .acl_white_list(contract_whitelist) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + assert!( + contract.is_whitelisted(minting_contract.address()), + "acl whitelist is incorrectly set" + ); + + minting_contract.mint( + contract.address(), + TEST_PRETTY_721_META_DATA.to_string() + ); + + let token_id = 0u64; + let actual_token_owner = contract.owner_of(Maybe::Some(token_id), Maybe::None); + assert_eq!(&actual_token_owner, minting_contract.address()) +} + +#[test] +fn should_disallow_unlisted_contract_from_minting() { + let env = odra_test::env(); + + let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + + let contract_whitelist = vec![ + env.get_account(1), + env.get_account(2), + env.get_account(3), + ]; + let args = default_args_builder() + .holder_mode(NFTHolderMode::Contracts) + .whitelist_mode(WhitelistMode::Locked) + .ownership_mode(OwnershipMode::Minter) + .minting_mode(MintingMode::Acl) + .acl_white_list(contract_whitelist) + .build(); + let contract = CEP78HostRef::deploy(&env, args); + + assert_eq!( + minting_contract.try_mint( + contract.address(), + TEST_PRETTY_721_META_DATA.to_string(), + ), + Err(CEP78Error::UnlistedContractHash.into()), + "Unlisted account hash should not be permitted to mint" + ); +} + +#[test] +fn should_allow_mixed_account_contract_to_mint() { + let env = odra_test::env(); + + let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let account_user_1 = env.get_account(1); + let mixed_whitelist = vec![minting_contract.address().clone(), account_user_1]; + + let args = default_args_builder() + .holder_mode(NFTHolderMode::Mixed) + .whitelist_mode(WhitelistMode::Locked) + .ownership_mode(OwnershipMode::Minter) + .minting_mode(MintingMode::Acl) + .acl_white_list(mixed_whitelist) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + assert!( + contract.is_whitelisted(minting_contract.address()), + "acl whitelist is incorrectly set" + ); + + minting_contract.mint( + contract.address(), + TEST_PRETTY_721_META_DATA.to_string() + ); + + let token_id = 0u64; + let actual_token_owner = contract.owner_of(Maybe::Some(token_id), Maybe::None); + assert_eq!(&actual_token_owner, minting_contract.address()); + + assert!( + contract.is_whitelisted(&account_user_1), + "acl whitelist is incorrectly set" + ); + env.set_caller(account_user_1); + contract.mint(account_user_1, TEST_PRETTY_721_META_DATA.to_string(), Maybe::None); + + let token_id = 1u64; + let actual_token_owner = contract.owner_of(Maybe::Some(token_id), Maybe::None); + assert_eq!(actual_token_owner, account_user_1) +} + +#[test] +fn should_disallow_unlisted_contract_from_minting_with_mixed_account_contract() { + let env = odra_test::env(); + + let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let account_user_1 = env.get_account(1); + let mixed_whitelist = vec![ + DummyContractHostRef::deploy(&env, NoArgs).address().clone(), + account_user_1, + ]; + + let args = default_args_builder() + .holder_mode(NFTHolderMode::Mixed) + .whitelist_mode(WhitelistMode::Locked) + .ownership_mode(OwnershipMode::Minter) + .minting_mode(MintingMode::Acl) + .acl_white_list(mixed_whitelist) + .build(); + let contract = CEP78HostRef::deploy(&env, args); + + assert_eq!( + minting_contract.try_mint( + contract.address(), + TEST_PRETTY_721_META_DATA.to_string(), + ), + Err(CEP78Error::UnlistedContractHash.into()), + "Unlisted contract should not be permitted to mint" + ); +} + +#[test] +fn should_disallow_unlisted_account_from_minting_with_mixed_account_contract() { + let env = odra_test::env(); + + let minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let listed_account = env.get_account(0); + let unlisted_account = env.get_account(1); + let mixed_whitelist = vec![ + minting_contract.address().clone(), + listed_account, + ]; + + let args = default_args_builder() + .holder_mode(NFTHolderMode::Mixed) + .whitelist_mode(WhitelistMode::Locked) + .ownership_mode(OwnershipMode::Minter) + .minting_mode(MintingMode::Acl) + .acl_white_list(mixed_whitelist) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + env.set_caller(unlisted_account); + assert_eq!( + contract.try_mint( + unlisted_account, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ), + Err(CEP78Error::InvalidMinter.into()), + "Unlisted account should not be permitted to mint" + ); +} + +// #[test] +// fn should_disallow_listed_account_from_minting_with_nftholder_contract() { +// let mut builder = InMemoryWasmTestBuilder::default(); +// builder +// .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) +// .commit(); + +// let minting_contract_install_request = ExecuteRequestBuilder::standard( +// *DEFAULT_ACCOUNT_ADDR, +// MINTING_CONTRACT_WASM, +// runtime_args! {}, +// ) +// .build(); + +// builder +// .exec(minting_contract_install_request) +// .expect_success() +// .commit(); + +// let minting_contract_hash = get_minting_contract_hash(&builder); +// let mixed_whitelist = vec![ +// Key::from(minting_contract_hash), +// Key::from(*DEFAULT_ACCOUNT_ADDR), +// ]; + +// let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) +// .with_total_token_supply(100u64) +// .with_holder_mode(NFTHolderMode::Contracts) +// .with_whitelist_mode(WhitelistMode::Locked) +// .with_ownership_mode(OwnershipMode::Minter) +// .with_reporting_mode(OwnerReverseLookupMode::NoLookUp) +// .with_minting_mode(MintingMode::Acl) +// .with_acl_whitelist(mixed_whitelist) +// .build(); + +// builder.exec(install_request).expect_success().commit(); + +// let nft_contract_hash = get_nft_contract_hash(&builder); +// let nft_contract_key: Key = nft_contract_hash.into(); + +// let is_whitelisted_account = get_dictionary_value_from_key::( +// &builder, +// &nft_contract_key, +// ACL_WHITELIST, +// &DEFAULT_ACCOUNT_ADDR.to_string(), +// ); + +// assert!(is_whitelisted_account, "acl whitelist is incorrectly set"); + +// let account_user_1 = support::create_funded_dummy_account(&mut builder, Some(ACCOUNT_USER_1)); + +// let mint_runtime_args = runtime_args! { +// ARG_NFT_CONTRACT_HASH => nft_contract_key, +// ARG_TOKEN_OWNER => Key::Account(account_user_1), +// ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), +// ARG_REVERSE_LOOKUP => false +// }; + +// let mint_session_call = ExecuteRequestBuilder::contract_call_by_hash( +// account_user_1, +// nft_contract_hash, +// ENTRY_POINT_MINT, +// mint_runtime_args, +// ) +// .build(); + +// builder.exec(mint_session_call).expect_failure(); + +// let error = builder.get_error().expect("should have an error"); +// assert_expected_error(error, 76, "InvalidHolderMode(76) must have been raised"); +// } + +// #[test] +// fn should_disallow_contract_from_whitelisted_package_to_mint_without_acl_package_mode() { +// let mut builder = InMemoryWasmTestBuilder::default(); +// builder +// .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) +// .commit(); + +// let minting_contract_install_request = ExecuteRequestBuilder::standard( +// *DEFAULT_ACCOUNT_ADDR, +// MINTING_CONTRACT_WASM, +// runtime_args! {}, +// ) +// .build(); + +// builder +// .exec(minting_contract_install_request) +// .expect_success() +// .commit(); + +// let minting_contract_hash = get_minting_contract_hash(&builder); +// let minting_contract_package_hash = get_minting_contract_package_hash(&builder); + +// let contract_whitelist = vec![Key::from(minting_contract_package_hash)]; + +// let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) +// .with_total_token_supply(100u64) +// .with_holder_mode(NFTHolderMode::Contracts) +// .with_whitelist_mode(WhitelistMode::Locked) +// .with_ownership_mode(OwnershipMode::Minter) +// .with_reporting_mode(OwnerReverseLookupMode::NoLookUp) +// .with_minting_mode(MintingMode::Acl) +// .with_acl_whitelist(contract_whitelist) +// .build(); + +// builder.exec(install_request).expect_success().commit(); + +// let nft_contract_key: Key = get_nft_contract_hash(&builder).into(); + +// let is_whitelisted_contract_package = get_dictionary_value_from_key::( +// &builder, +// &nft_contract_key, +// ACL_WHITELIST, +// &minting_contract_package_hash.to_string(), +// ); + +// assert!( +// is_whitelisted_contract_package, +// "acl whitelist is incorrectly set" +// ); + +// let mint_runtime_args = runtime_args! { +// ARG_NFT_CONTRACT_HASH => nft_contract_key, +// ARG_TOKEN_OWNER => Key::from(minting_contract_hash), +// ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), +// ARG_REVERSE_LOOKUP => false +// }; + +// let mint_via_contract_call = ExecuteRequestBuilder::contract_call_by_hash( +// *DEFAULT_ACCOUNT_ADDR, +// minting_contract_hash, +// ENTRY_POINT_MINT, +// mint_runtime_args, +// ) +// .build(); + +// builder.exec(mint_via_contract_call).expect_failure(); + +// let error = builder.get_error().expect("should have an error"); +// assert_expected_error( +// error, +// 81, +// "Unlisted ContractHash from whitelisted ContractPackageHash can not mint without ACL package mode", +// ); +// } + +// #[test] +// fn should_allow_contract_from_whitelisted_package_to_mint_with_acl_package_mode() { +// let mut builder = InMemoryWasmTestBuilder::default(); +// builder +// .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) +// .commit(); + +// let minting_contract_install_request = ExecuteRequestBuilder::standard( +// *DEFAULT_ACCOUNT_ADDR, +// MINTING_CONTRACT_WASM, +// runtime_args! {}, +// ) +// .build(); + +// builder +// .exec(minting_contract_install_request) +// .expect_success() +// .commit(); + +// let minting_contract_hash = get_minting_contract_hash(&builder); +// let minting_contract_package_hash = get_minting_contract_package_hash(&builder); + +// let contract_whitelist = vec![Key::from(minting_contract_package_hash)]; +// let acl_package_mode = true; + +// let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) +// .with_total_token_supply(100u64) +// .with_holder_mode(NFTHolderMode::Contracts) +// .with_whitelist_mode(WhitelistMode::Locked) +// .with_ownership_mode(OwnershipMode::Minter) +// .with_reporting_mode(OwnerReverseLookupMode::NoLookUp) +// .with_minting_mode(MintingMode::Acl) +// .with_acl_whitelist(contract_whitelist) +// .with_acl_package_mode(acl_package_mode) +// .build(); + +// builder.exec(install_request).expect_success().commit(); + +// let nft_contract_key: Key = get_nft_contract_hash(&builder).into(); + +// let is_whitelisted_contract_package = get_dictionary_value_from_key::( +// &builder, +// &nft_contract_key, +// ACL_WHITELIST, +// &minting_contract_package_hash.to_string(), +// ); + +// assert!( +// is_whitelisted_contract_package, +// "acl whitelist is incorrectly set" +// ); + +// let mint_runtime_args = runtime_args! { +// ARG_NFT_CONTRACT_HASH => nft_contract_key, +// ARG_TOKEN_OWNER => Key::from(minting_contract_hash), +// ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), +// ARG_REVERSE_LOOKUP => false +// }; + +// let mint_via_contract_call = ExecuteRequestBuilder::contract_call_by_hash( +// *DEFAULT_ACCOUNT_ADDR, +// minting_contract_hash, +// ENTRY_POINT_MINT, +// mint_runtime_args, +// ) +// .build(); + +// builder +// .exec(mint_via_contract_call) +// .expect_success() +// .commit(); + +// let token_id = 0u64; + +// let actual_token_owner: Key = get_dictionary_value_from_key( +// &builder, +// &nft_contract_key, +// TOKEN_OWNERS, +// &token_id.to_string(), +// ); + +// let minting_contract_key: Key = minting_contract_hash.into(); + +// assert_eq!(actual_token_owner, minting_contract_key) +// } + +// #[test] +// fn should_allow_contract_from_whitelisted_package_to_mint_with_acl_package_mode_after_contract_upgrade( +// ) { +// let mut builder = InMemoryWasmTestBuilder::default(); +// builder +// .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) +// .commit(); + +// let minting_contract_install_request = ExecuteRequestBuilder::standard( +// *DEFAULT_ACCOUNT_ADDR, +// MINTING_CONTRACT_WASM, +// runtime_args! {}, +// ) +// .build(); + +// builder +// .exec(minting_contract_install_request) +// .expect_success() +// .commit(); + +// let minting_contract_hash = get_minting_contract_hash(&builder); +// let minting_contract_package_hash = get_minting_contract_package_hash(&builder); + +// let contract_whitelist = vec![Key::from(minting_contract_package_hash)]; +// let acl_package_mode = true; + +// let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) +// .with_total_token_supply(100u64) +// .with_holder_mode(NFTHolderMode::Contracts) +// .with_whitelist_mode(WhitelistMode::Locked) +// .with_ownership_mode(OwnershipMode::Minter) +// .with_reporting_mode(OwnerReverseLookupMode::NoLookUp) +// .with_minting_mode(MintingMode::Acl) +// .with_acl_whitelist(contract_whitelist) +// .with_acl_package_mode(acl_package_mode) +// .build(); + +// builder.exec(install_request).expect_success().commit(); + +// let nft_contract_key: Key = get_nft_contract_hash(&builder).into(); + +// let is_whitelisted_contract_package = get_dictionary_value_from_key::( +// &builder, +// &nft_contract_key, +// ACL_WHITELIST, +// &minting_contract_package_hash.to_string(), +// ); + +// assert!( +// is_whitelisted_contract_package, +// "acl whitelist is incorrectly set" +// ); + +// let version_minting_contract = support::query_stored_value::( +// &builder, +// Key::Account(*DEFAULT_ACCOUNT_ADDR), +// vec![MINTING_CONTRACT_VERSION.to_string()], +// ); + +// assert_eq!(version_minting_contract, 1u32); + +// let upgrade_request = ExecuteRequestBuilder::standard( +// *DEFAULT_ACCOUNT_ADDR, +// MINTING_CONTRACT_WASM, +// runtime_args! {}, +// ) +// .build(); + +// builder.exec(upgrade_request).expect_success().commit(); + +// let version_minting_contract = support::query_stored_value::( +// &builder, +// Key::Account(*DEFAULT_ACCOUNT_ADDR), +// vec![MINTING_CONTRACT_VERSION.to_string()], +// ); + +// assert_eq!(version_minting_contract, 2u32); + +// let minting_upgraded_contract_hash = get_minting_contract_hash(&builder); +// assert_ne!(minting_contract_hash, minting_upgraded_contract_hash); + +// let mint_runtime_args = runtime_args! { +// ARG_NFT_CONTRACT_HASH => nft_contract_key, +// ARG_TOKEN_OWNER => Key::from(minting_contract_hash), +// ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), +// ARG_REVERSE_LOOKUP => false +// }; + +// let mint_via_contract_call = ExecuteRequestBuilder::contract_call_by_hash( +// *DEFAULT_ACCOUNT_ADDR, +// minting_upgraded_contract_hash, +// ENTRY_POINT_MINT, +// mint_runtime_args, +// ) +// .build(); + +// builder +// .exec(mint_via_contract_call) +// .expect_success() +// .commit(); + +// let token_id = 0u64; + +// let actual_token_owner: Key = get_dictionary_value_from_key( +// &builder, +// &nft_contract_key, +// TOKEN_OWNERS, +// &token_id.to_string(), +// ); + +// let minting_contract_key: Key = minting_upgraded_contract_hash.into(); + +// assert_eq!(actual_token_owner, minting_contract_key) +// } + +// // Update + +// #[test] +// fn should_be_able_to_update_whitelist_for_minting_with_deprecated_arg_contract_whitelist() { +// let mut builder = InMemoryWasmTestBuilder::default(); +// builder +// .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) +// .commit(); + +// let minting_contract_install_request = ExecuteRequestBuilder::standard( +// *DEFAULT_ACCOUNT_ADDR, +// MINTING_CONTRACT_WASM, +// runtime_args! {}, +// ) +// .build(); + +// builder +// .exec(minting_contract_install_request) +// .expect_success() +// .commit(); + +// let minting_contract_hash = get_minting_contract_hash(&builder); + +// let contract_whitelist = vec![]; + +// let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) +// .with_total_token_supply(100u64) +// .with_holder_mode(NFTHolderMode::Contracts) +// .with_whitelist_mode(WhitelistMode::Unlocked) +// .with_ownership_mode(OwnershipMode::Minter) +// .with_reporting_mode(OwnerReverseLookupMode::NoLookUp) +// .with_minting_mode(MintingMode::Acl) +// .with_acl_whitelist(contract_whitelist) +// .build(); + +// builder.exec(install_request).expect_success().commit(); + +// let nft_contract_hash = get_nft_contract_hash(&builder); +// let nft_contract_key: Key = nft_contract_hash.into(); + +// let seed_uref = *builder +// .query(None, nft_contract_key, &[]) +// .expect("must have nft contract") +// .as_contract() +// .expect("must convert contract") +// .named_keys() +// .get(ACL_WHITELIST) +// .expect("must have key") +// .as_uref() +// .expect("must convert to seed uref"); + +// let is_whitelisted_account = +// builder.query_dictionary_item(None, seed_uref, &minting_contract_hash.to_string()); + +// assert!( +// is_whitelisted_account.is_err(), +// "acl whitelist is incorrectly set" +// ); + +// let mint_runtime_args = runtime_args! { +// ARG_NFT_CONTRACT_HASH => nft_contract_key, +// ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), +// ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), +// ARG_REVERSE_LOOKUP => false, +// }; + +// let mint_via_contract_call = ExecuteRequestBuilder::contract_call_by_hash( +// *DEFAULT_ACCOUNT_ADDR, +// minting_contract_hash, +// ENTRY_POINT_MINT, +// mint_runtime_args.clone(), +// ) +// .build(); + +// builder.exec(mint_via_contract_call).expect_failure(); + +// let error = builder.get_error().expect("should have an error"); +// assert_expected_error( +// error, +// 81, +// "Unlisted contract hash should not be permitted to mint", +// ); + +// let update_whitelist_request = ExecuteRequestBuilder::contract_call_by_hash( +// *DEFAULT_ACCOUNT_ADDR, +// nft_contract_hash, +// ENTRY_POINT_SET_VARIABLES, +// runtime_args! { +// ARG_CONTRACT_WHITELIST => vec![minting_contract_hash] +// }, +// ) +// .build(); + +// builder +// .exec(update_whitelist_request) +// .expect_success() +// .commit(); + +// let is_updated_acl_whitelist = get_dictionary_value_from_key::( +// &builder, +// &nft_contract_key, +// ACL_WHITELIST, +// &minting_contract_hash.to_string(), +// ); + +// assert!(is_updated_acl_whitelist, "acl whitelist is incorrectly set"); + +// let mint_via_contract_call = ExecuteRequestBuilder::contract_call_by_hash( +// *DEFAULT_ACCOUNT_ADDR, +// minting_contract_hash, +// ENTRY_POINT_MINT, +// mint_runtime_args, +// ) +// .build(); + +// builder +// .exec(mint_via_contract_call) +// .expect_success() +// .commit(); +// } + +// #[test] +// fn should_be_able_to_update_whitelist_for_minting() { +// let mut builder = InMemoryWasmTestBuilder::default(); +// builder +// .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) +// .commit(); + +// let minting_contract_install_request = ExecuteRequestBuilder::standard( +// *DEFAULT_ACCOUNT_ADDR, +// MINTING_CONTRACT_WASM, +// runtime_args! {}, +// ) +// .build(); + +// builder +// .exec(minting_contract_install_request) +// .expect_success() +// .commit(); + +// let minting_contract_hash = get_minting_contract_hash(&builder); + +// let contract_whitelist = vec![]; + +// let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) +// .with_total_token_supply(100u64) +// .with_holder_mode(NFTHolderMode::Contracts) +// .with_whitelist_mode(WhitelistMode::Unlocked) +// .with_ownership_mode(OwnershipMode::Minter) +// .with_reporting_mode(OwnerReverseLookupMode::NoLookUp) +// .with_minting_mode(MintingMode::Acl) +// .with_acl_whitelist(contract_whitelist) +// .build(); + +// builder.exec(install_request).expect_success().commit(); + +// let nft_contract_hash = get_nft_contract_hash(&builder); +// let nft_contract_key: Key = nft_contract_hash.into(); + +// let seed_uref = *builder +// .query(None, nft_contract_key, &[]) +// .expect("must have nft contract") +// .as_contract() +// .expect("must convert contract") +// .named_keys() +// .get(ACL_WHITELIST) +// .expect("must have key") +// .as_uref() +// .expect("must convert to seed uref"); + +// let is_whitelisted_account = +// builder.query_dictionary_item(None, seed_uref, &minting_contract_hash.to_string()); + +// assert!( +// is_whitelisted_account.is_err(), +// "acl whitelist is incorrectly set" +// ); + +// let mint_runtime_args = runtime_args! { +// ARG_NFT_CONTRACT_HASH => nft_contract_key, +// ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), +// ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), +// ARG_REVERSE_LOOKUP => false, +// }; + +// let mint_via_contract_call = ExecuteRequestBuilder::contract_call_by_hash( +// *DEFAULT_ACCOUNT_ADDR, +// minting_contract_hash, +// ENTRY_POINT_MINT, +// mint_runtime_args.clone(), +// ) +// .build(); + +// builder.exec(mint_via_contract_call).expect_failure(); + +// let error = builder.get_error().expect("should have an error"); +// assert_expected_error( +// error, +// 81, +// "Unlisted contract hash should not be permitted to mint", +// ); + +// let update_whitelist_request = ExecuteRequestBuilder::contract_call_by_hash( +// *DEFAULT_ACCOUNT_ADDR, +// nft_contract_hash, +// ENTRY_POINT_SET_VARIABLES, +// runtime_args! { +// ARG_ACL_WHITELIST => vec![Key::from(minting_contract_hash)] +// }, +// ) +// .build(); + +// builder +// .exec(update_whitelist_request) +// .expect_success() +// .commit(); + +// let is_updated_acl_whitelist = get_dictionary_value_from_key::( +// &builder, +// &nft_contract_key, +// ACL_WHITELIST, +// &minting_contract_hash.to_string(), +// ); + +// assert!(is_updated_acl_whitelist, "acl whitelist is incorrectly set"); + +// let mint_via_contract_call = ExecuteRequestBuilder::contract_call_by_hash( +// *DEFAULT_ACCOUNT_ADDR, +// minting_contract_hash, +// ENTRY_POINT_MINT, +// mint_runtime_args, +// ) +// .build(); + +// builder +// .exec(mint_via_contract_call) +// .expect_success() +// .commit(); +// } + +// // Upgrade + +// #[test] +// fn should_upgrade_from_named_keys_to_dict_and_acl_minting_mode() { +// let mut builder = InMemoryWasmTestBuilder::default(); +// builder +// .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) +// .commit(); + +// let minting_contract_install_request = ExecuteRequestBuilder::standard( +// *DEFAULT_ACCOUNT_ADDR, +// MINTING_CONTRACT_WASM, +// runtime_args! {}, +// ) +// .build(); + +// builder +// .exec(minting_contract_install_request) +// .expect_success() +// .commit(); + +// let minting_contract_hash = get_minting_contract_hash(&builder); +// let contract_whitelist = vec![minting_contract_hash]; + +// let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, CONTRACT_1_0_0_WASM) +// .with_collection_name(NFT_TEST_COLLECTION.to_string()) +// .with_collection_symbol(NFT_TEST_SYMBOL.to_string()) +// .with_total_token_supply(1000u64) +// .with_minting_mode(MintingMode::Installer) +// .with_holder_mode(NFTHolderMode::Contracts) +// .with_whitelist_mode(WhitelistMode::Locked) +// .with_ownership_mode(OwnershipMode::Transferable) +// .with_nft_metadata_kind(NFTMetadataKind::Raw) +// .with_reporting_mode(OwnerReverseLookupMode::NoLookUp) +// .with_contract_whitelist(contract_whitelist) +// .build(); + +// builder.exec(install_request).expect_success().commit(); + +// let nft_contract_hash_1_0_0 = support::get_nft_contract_hash_1_0_0(&builder); +// let nft_contract_key_1_0_0: Key = nft_contract_hash_1_0_0.into(); + +// let minting_mode = support::query_stored_value::( +// &builder, +// nft_contract_key_1_0_0, +// vec![ARG_MINTING_MODE.to_string()], +// ); + +// assert_eq!( +// minting_mode, +// MintingMode::Installer as u8, +// "minting mode should be set to public" +// ); + +// let upgrade_request = ExecuteRequestBuilder::standard( +// *DEFAULT_ACCOUNT_ADDR, +// NFT_CONTRACT_WASM, +// runtime_args! { +// ARG_NFT_CONTRACT_HASH => support::get_nft_contract_package_hash(&builder), +// ARG_COLLECTION_NAME => NFT_TEST_COLLECTION.to_string(), +// ARG_NAMED_KEY_CONVENTION => NamedKeyConventionMode::V1_0Standard as u8, +// }, +// ) +// .build(); + +// builder.exec(upgrade_request).expect_success().commit(); + +// let nft_contract_key: Key = support::get_nft_contract_hash(&builder).into(); + +// let is_updated_acl_whitelist = get_dictionary_value_from_key::( +// &builder, +// &nft_contract_key, +// ACL_WHITELIST, +// &minting_contract_hash.to_string(), +// ); + +// assert!(is_updated_acl_whitelist, "acl whitelist is incorrectly set"); + +// let minting_mode = support::query_stored_value::( +// &builder, +// nft_contract_key, +// vec![ARG_MINTING_MODE.to_string()], +// ); + +// assert_eq!( +// minting_mode, +// MintingMode::Acl as u8, +// "minting mode should be set to acl" +// ); +// } diff --git a/modules/src/cep78/tests/mod.rs b/modules/src/cep78/tests/mod.rs new file mode 100644 index 00000000..3fb75112 --- /dev/null +++ b/modules/src/cep78/tests/mod.rs @@ -0,0 +1,16 @@ +use odra::{ + args::Maybe, + host::{Deployer, HostRef} +}; + +mod acl; +mod set_variables; +mod utils; + +use crate::cep78::{error::CEP78Error, events::VariablesSet, modalities::OwnerReverseLookupMode}; + +use super::token::CEP78HostRef; + +pub(super) const COLLECTION_NAME: &str = "CEP78-Test-Collection"; +pub(super) const COLLECTION_SYMBOL: &str = "CEP78"; +pub(super) const TOTAL_TOKEN_SUPPLY: u64 = 100_000_000; diff --git a/modules/src/cep78/tests/set_variables.rs b/modules/src/cep78/tests/set_variables.rs new file mode 100644 index 00000000..ec9a9b69 --- /dev/null +++ b/modules/src/cep78/tests/set_variables.rs @@ -0,0 +1,7 @@ +use odra::{ + args::Maybe, + host::{Deployer, HostRef} +}; + +use super::{COLLECTION_NAME, COLLECTION_SYMBOL}; +use crate::cep78::{error::CEP78Error, events::VariablesSet, token::CEP78HostRef}; diff --git a/modules/src/cep78/tests/utils.rs b/modules/src/cep78/tests/utils.rs index f723fe5b..545a89ab 100644 --- a/modules/src/cep78/tests/utils.rs +++ b/modules/src/cep78/tests/utils.rs @@ -12,9 +12,8 @@ use odra::{ Address }; -#[derive(Default, Builder)] -#[builder(setter(into))] -pub struct InitArgs { +#[derive(Default)] +pub struct InitArgsBuilder { collection_name: String, collection_symbol: String, total_token_supply: u64, @@ -24,56 +23,192 @@ pub struct InitArgs { nft_kind: NFTKind, holder_mode: Maybe, whitelist_mode: Maybe, - acl_white_list: Maybe>, + acl_white_list: Maybe>, acl_package_mode: Maybe>, package_operator_mode: Maybe>, json_schema: Maybe, receipt_name: Maybe, - identifier_mode: u8, - burn_mode: Maybe, + identifier_mode: NFTIdentifierMode, + burn_mode: Maybe, operator_burn_mode: Maybe, nft_metadata_kind: NFTMetadataKind, metadata_mutability: MetadataMutability, owner_reverse_lookup_mode: Maybe, - events_mode: EventsMode, + events_mode: Maybe, transfer_filter_contract_contract_key: Maybe
, - additional_required_metadata: Maybe>, - optional_metadata: Maybe> + additional_required_metadata: Maybe>, + optional_metadata: Maybe> } -impl Into for InitArgs { - fn into(self) -> RuntimeArgs { - runtime_args! { - "collection_name" => self.collection_name, - "collection_symbol" => self.collection_symbol, - "total_token_supply" => self.total_token_supply, - "allow_minting" => self.allow_minting.unwrap_or_default(), - "minting_mode" => self.minting_mode.unwrap_or(MintingMode::Installer) as u8, - "ownership_mode" => self.ownership_mode as u8, - "nft_kind" => self.nft_kind as u8, - "holder_mode" => self.holder_mode.unwrap_or(NFTHolderMode::Accounts) as u8, - "whitelist_mode" => self.whitelist_mode.unwrap_or(WhitelistMode::Unlocked) as u8, - "acl_white_list" => self.acl_white_list.unwrap_or_default(), - "acl_package_mode" => self.acl_package_mode.unwrap_or_default(), - "package_operator_mode" => self.package_operator_mode.unwrap_or_default(), - "json_schema" => self.json_schema.unwrap_or_default(), - "receipt_name" => self.receipt_name.unwrap_or_default(), - "identifier_mode" => self.identifier_mode, - "burn_mode" => self.burn_mode.unwrap_or_default(), - "operator_burn_mode" => self.operator_burn_mode.unwrap_or_default(), - "nft_metadata_kind" => self.nft_metadata_kind as u8, - "metadata_mutability" => self.metadata_mutability as u8, - "owner_reverse_lookup_mode" => self.owner_reverse_lookup_mode.unwrap_or(OwnerReverseLookupMode::NoLookUp) as u8, - "events_mode" => self.events_mode as u8, - // "transfer_filter_contract_contract_key" => self.transfer_filter_contract_contract_key, - "additional_required_metadata" => self.additional_required_metadata.unwrap_or_default(), - "optional_metadata" => self.optional_metadata.unwrap_or_default(), - } +impl InitArgsBuilder { + pub fn collection_name(mut self, collection_name: String) -> Self { + self.collection_name = collection_name; + self + } + + pub fn collection_symbol(mut self, collection_symbol: String) -> Self { + self.collection_symbol = collection_symbol; + self + } + + pub fn total_token_supply(mut self, total_token_supply: u64) -> Self { + self.total_token_supply = total_token_supply; + self + } + + pub fn allow_minting(mut self, allow_minting: bool) -> Self { + self.allow_minting = Maybe::Some(allow_minting); + self + } + + pub fn minting_mode(mut self, minting_mode: MintingMode) -> Self { + self.minting_mode = Maybe::Some(minting_mode); + self + } + + pub fn ownership_mode(mut self, ownership_mode: OwnershipMode) -> Self { + self.ownership_mode = ownership_mode; + self + } + + pub fn nft_kind(mut self, nft_kind: NFTKind) -> Self { + self.nft_kind = nft_kind; + self + } + + pub fn holder_mode(mut self, holder_mode: NFTHolderMode) -> Self { + self.holder_mode = Maybe::Some(holder_mode); + self + } + + pub fn whitelist_mode(mut self, whitelist_mode: WhitelistMode) -> Self { + self.whitelist_mode = Maybe::Some(whitelist_mode); + self + } + + pub fn acl_white_list(mut self, acl_white_list: Vec
) -> Self { + self.acl_white_list = Maybe::Some(acl_white_list); + self + } + + pub fn acl_package_mode(mut self, acl_package_mode: Vec) -> Self { + self.acl_package_mode = Maybe::Some(acl_package_mode); + self + } + + pub fn package_operator_mode(mut self, package_operator_mode: Vec) -> Self { + self.package_operator_mode = Maybe::Some(package_operator_mode); + self + } + + pub fn json_schema(mut self, json_schema: String) -> Self { + self.json_schema = Maybe::Some(json_schema); + self + } + + pub fn receipt_name(mut self, receipt_name: String) -> Self { + self.receipt_name = Maybe::Some(receipt_name); + self + } + + pub fn identifier_mode(mut self, identifier_mode: NFTIdentifierMode) -> Self { + self.identifier_mode = identifier_mode; + self + } + + pub fn burn_mode(mut self, burn_mode: BurnMode) -> Self { + self.burn_mode = Maybe::Some(burn_mode); + self } -} -impl odra::host::InitArgs for InitArgs { - fn validate(_expected_ident: &str) -> bool { - true + pub fn operator_burn_mode(mut self, operator_burn_mode: bool) -> Self { + self.operator_burn_mode = Maybe::Some(operator_burn_mode); + self + } + + pub fn nft_metadata_kind(mut self, nft_metadata_kind: NFTMetadataKind) -> Self { + self.nft_metadata_kind = nft_metadata_kind; + self + } + + pub fn metadata_mutability(mut self, metadata_mutability: MetadataMutability) -> Self { + self.metadata_mutability = metadata_mutability; + self + } + + pub fn owner_reverse_lookup_mode( + mut self, + owner_reverse_lookup_mode: OwnerReverseLookupMode + ) -> Self { + self.owner_reverse_lookup_mode = Maybe::Some(owner_reverse_lookup_mode); + self + } + + pub fn events_mode(mut self, events_mode: EventsMode) -> Self { + self.events_mode = Maybe::Some(events_mode); + self + } + + pub fn transfer_filter_contract_contract_key( + mut self, + transfer_filter_contract_contract_key: Address + ) -> Self { + self.transfer_filter_contract_contract_key = + Maybe::Some(transfer_filter_contract_contract_key); + self + } + + pub fn build(self) -> CEP78InitArgs { + CEP78InitArgs { + collection_name: self.collection_name, + collection_symbol: self.collection_symbol, + total_token_supply: self.total_token_supply, + allow_minting: self.allow_minting, + minting_mode: self.minting_mode, + ownership_mode: self.ownership_mode, + nft_kind: self.nft_kind, + holder_mode: self.holder_mode, + whitelist_mode: self.whitelist_mode, + acl_white_list: self.acl_white_list, + json_schema: self.json_schema, + receipt_name: self.receipt_name, + nft_identifier_mode: self.identifier_mode, + burn_mode: self.burn_mode, + nft_metadata_kind: self.nft_metadata_kind, + metadata_mutability: self.metadata_mutability, + owner_reverse_lookup_mode: self.owner_reverse_lookup_mode, + events_mode: self.events_mode, + transfer_filter_contract_contract_key: self.transfer_filter_contract_contract_key, + additional_required_metadata: self.additional_required_metadata, + optional_metadata: self.optional_metadata + } } } + +pub const TEST_PRETTY_721_META_DATA: &str = r#"{ + "name": "John Doe", + "symbol": "abc", + "token_uri": "https://www.barfoo.com" + }"#; +pub const TEST_PRETTY_UPDATED_721_META_DATA: &str = r#"{ + "name": "John Doe", + "symbol": "abc", + "token_uri": "https://www.foobar.com" + }"#; +pub const TEST_PRETTY_CEP78_METADATA: &str = r#"{ + "name": "John Doe", + "token_uri": "https://www.barfoo.com", + "checksum": "940bffb3f2bba35f84313aa26da09ece3ad47045c6a1292c2bbd2df4ab1a55fb" + }"#; +pub const TEST_PRETTY_UPDATED_CEP78_METADATA: &str = r#"{ + "name": "John Doe", + "token_uri": "https://www.foobar.com", + "checksum": "fda4feaa137e83972db628e521c92159f5dc253da1565c9da697b8ad845a0788" + }"#; +pub const TEST_COMPACT_META_DATA: &str = + r#"{"name": "John Doe","symbol": "abc","token_uri": "https://www.barfoo.com"}"#; +pub const MALFORMED_META_DATA: &str = r#"{ + "name": "John Doe", + "symbol": abc, + "token_uri": "https://www.barfoo.com" + }"#; From f4b17b371e0883fc7d9c9047aa5046161bce298c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Wed, 17 Apr 2024 08:19:34 +0200 Subject: [PATCH 08/38] Add missing acl test --- modules/src/cep78/settings.rs | 5 + modules/src/cep78/tests/acl.rs | 873 ++++++--------------------------- modules/src/cep78/token.rs | 25 +- modules/src/cep78/whitelist.rs | 5 +- 4 files changed, 189 insertions(+), 719 deletions(-) diff --git a/modules/src/cep78/settings.rs b/modules/src/cep78/settings.rs index 5de6de90..94c95696 100644 --- a/modules/src/cep78/settings.rs +++ b/modules/src/cep78/settings.rs @@ -68,4 +68,9 @@ impl Settings { pub fn minting_mode(&self) -> MintingMode { self.minting_mode.get_or_default() } + + #[inline] + pub fn holder_mode(&self) -> NFTHolderMode { + self.holder_mode.get_or_default() + } } diff --git a/modules/src/cep78/tests/acl.rs b/modules/src/cep78/tests/acl.rs index 58edf23b..2ae965c6 100644 --- a/modules/src/cep78/tests/acl.rs +++ b/modules/src/cep78/tests/acl.rs @@ -7,10 +7,7 @@ use odra::{ use crate::cep78::{ error::CEP78Error, - modalities::{ - MintingMode, NFTHolderMode, NFTMetadataKind, OwnershipMode, - WhitelistMode - }, + modalities::{MintingMode, NFTHolderMode, NFTMetadataKind, OwnershipMode, WhitelistMode}, tests::utils::{InitArgsBuilder, TEST_PRETTY_721_META_DATA}, token::CEP78HostRef }; @@ -40,6 +37,16 @@ impl TestContract { NftContractContractRef::new(self.env(), *nft_contract_address) .mint(self.env().self_address(), token_metadata) } + + pub fn mint_for( + &mut self, + nft_contract_address: &Address, + token_owner: Address, + token_metadata: String + ) -> (String, Address, String) { + NftContractContractRef::new(self.env(), *nft_contract_address) + .mint(token_owner, token_metadata) + } } #[odra::external_contract] @@ -73,10 +80,33 @@ fn should_install_with_acl_whitelist() { fn should_install_with_deprecated_contract_whitelist() {} #[test] -#[ignore = "No need to implement a contract whitelist"] -fn should_not_install_with_minting_mode_not_acl_if_acl_whitelist_provided() {} +#[ignore = "Can't assert init errors in Odra"] +fn should_not_install_with_minting_mode_not_acl_if_acl_whitelist_provided() { + let env = odra_test::env(); + + let contract_whitelist = vec![env.get_account(0)]; + + let args = default_args_builder() + .holder_mode(NFTHolderMode::Contracts) + .whitelist_mode(WhitelistMode::Locked) + .ownership_mode(OwnershipMode::Transferable) + .minting_mode(MintingMode::Installer) // Not the right minting mode for acl + .acl_white_list(contract_whitelist) + .build(); + + CEP78HostRef::deploy(&env, args); + + // builder.exec(install_request).expect_failure(); + + // let actual_error = builder.get_error().expect("must have error"); + // support::assert_expected_error( + // actual_error, + // 38u16, + // "should disallow installing without acl minting mode if non empty acl list", + // ); +} -fn should_disallow_installation_of_contract_with_empty_locked_whitelist_in_public_mode_with_holder_mode( +fn should_allow_installation_of_contract_with_empty_locked_whitelist_in_public_mode_with_holder_mode( env: &HostEnv, nft_holder_mode: NFTHolderMode ) { @@ -92,15 +122,15 @@ fn should_disallow_installation_of_contract_with_empty_locked_whitelist_in_publi #[test] fn should_allow_installation_of_contract_with_empty_locked_whitelist_in_public_mode() { let env = odra_test::env(); - should_disallow_installation_of_contract_with_empty_locked_whitelist_in_public_mode_with_holder_mode( + should_allow_installation_of_contract_with_empty_locked_whitelist_in_public_mode_with_holder_mode( &env, NFTHolderMode::Accounts, ); - should_disallow_installation_of_contract_with_empty_locked_whitelist_in_public_mode_with_holder_mode( + should_allow_installation_of_contract_with_empty_locked_whitelist_in_public_mode_with_holder_mode( &env, NFTHolderMode::Contracts, ); - should_disallow_installation_of_contract_with_empty_locked_whitelist_in_public_mode_with_holder_mode( + should_allow_installation_of_contract_with_empty_locked_whitelist_in_public_mode_with_holder_mode( &env, NFTHolderMode::Mixed ); @@ -214,10 +244,7 @@ fn should_allow_whitelisted_contract_to_mint() { "acl whitelist is incorrectly set" ); - minting_contract.mint( - contract.address(), - TEST_PRETTY_721_META_DATA.to_string() - ); + minting_contract.mint(contract.address(), TEST_PRETTY_721_META_DATA.to_string()); let token_id = 0u64; let actual_token_owner = contract.owner_of(Maybe::Some(token_id), Maybe::None); @@ -230,11 +257,7 @@ fn should_disallow_unlisted_contract_from_minting() { let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); - let contract_whitelist = vec![ - env.get_account(1), - env.get_account(2), - env.get_account(3), - ]; + let contract_whitelist = vec![env.get_account(1), env.get_account(2), env.get_account(3)]; let args = default_args_builder() .holder_mode(NFTHolderMode::Contracts) .whitelist_mode(WhitelistMode::Locked) @@ -245,10 +268,7 @@ fn should_disallow_unlisted_contract_from_minting() { let contract = CEP78HostRef::deploy(&env, args); assert_eq!( - minting_contract.try_mint( - contract.address(), - TEST_PRETTY_721_META_DATA.to_string(), - ), + minting_contract.try_mint(contract.address(), TEST_PRETTY_721_META_DATA.to_string(),), Err(CEP78Error::UnlistedContractHash.into()), "Unlisted account hash should not be permitted to mint" ); @@ -276,10 +296,7 @@ fn should_allow_mixed_account_contract_to_mint() { "acl whitelist is incorrectly set" ); - minting_contract.mint( - contract.address(), - TEST_PRETTY_721_META_DATA.to_string() - ); + minting_contract.mint(contract.address(), TEST_PRETTY_721_META_DATA.to_string()); let token_id = 0u64; let actual_token_owner = contract.owner_of(Maybe::Some(token_id), Maybe::None); @@ -290,7 +307,11 @@ fn should_allow_mixed_account_contract_to_mint() { "acl whitelist is incorrectly set" ); env.set_caller(account_user_1); - contract.mint(account_user_1, TEST_PRETTY_721_META_DATA.to_string(), Maybe::None); + contract.mint( + account_user_1, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); let token_id = 1u64; let actual_token_owner = contract.owner_of(Maybe::Some(token_id), Maybe::None); @@ -318,10 +339,7 @@ fn should_disallow_unlisted_contract_from_minting_with_mixed_account_contract() let contract = CEP78HostRef::deploy(&env, args); assert_eq!( - minting_contract.try_mint( - contract.address(), - TEST_PRETTY_721_META_DATA.to_string(), - ), + minting_contract.try_mint(contract.address(), TEST_PRETTY_721_META_DATA.to_string(),), Err(CEP78Error::UnlistedContractHash.into()), "Unlisted contract should not be permitted to mint" ); @@ -334,10 +352,7 @@ fn should_disallow_unlisted_account_from_minting_with_mixed_account_contract() { let minting_contract = TestContractHostRef::deploy(&env, NoArgs); let listed_account = env.get_account(0); let unlisted_account = env.get_account(1); - let mixed_whitelist = vec![ - minting_contract.address().clone(), - listed_account, - ]; + let mixed_whitelist = vec![minting_contract.address().clone(), listed_account]; let args = default_args_builder() .holder_mode(NFTHolderMode::Mixed) @@ -359,681 +374,111 @@ fn should_disallow_unlisted_account_from_minting_with_mixed_account_contract() { ); } -// #[test] -// fn should_disallow_listed_account_from_minting_with_nftholder_contract() { -// let mut builder = InMemoryWasmTestBuilder::default(); -// builder -// .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) -// .commit(); - -// let minting_contract_install_request = ExecuteRequestBuilder::standard( -// *DEFAULT_ACCOUNT_ADDR, -// MINTING_CONTRACT_WASM, -// runtime_args! {}, -// ) -// .build(); - -// builder -// .exec(minting_contract_install_request) -// .expect_success() -// .commit(); - -// let minting_contract_hash = get_minting_contract_hash(&builder); -// let mixed_whitelist = vec![ -// Key::from(minting_contract_hash), -// Key::from(*DEFAULT_ACCOUNT_ADDR), -// ]; - -// let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) -// .with_total_token_supply(100u64) -// .with_holder_mode(NFTHolderMode::Contracts) -// .with_whitelist_mode(WhitelistMode::Locked) -// .with_ownership_mode(OwnershipMode::Minter) -// .with_reporting_mode(OwnerReverseLookupMode::NoLookUp) -// .with_minting_mode(MintingMode::Acl) -// .with_acl_whitelist(mixed_whitelist) -// .build(); - -// builder.exec(install_request).expect_success().commit(); - -// let nft_contract_hash = get_nft_contract_hash(&builder); -// let nft_contract_key: Key = nft_contract_hash.into(); - -// let is_whitelisted_account = get_dictionary_value_from_key::( -// &builder, -// &nft_contract_key, -// ACL_WHITELIST, -// &DEFAULT_ACCOUNT_ADDR.to_string(), -// ); - -// assert!(is_whitelisted_account, "acl whitelist is incorrectly set"); - -// let account_user_1 = support::create_funded_dummy_account(&mut builder, Some(ACCOUNT_USER_1)); - -// let mint_runtime_args = runtime_args! { -// ARG_NFT_CONTRACT_HASH => nft_contract_key, -// ARG_TOKEN_OWNER => Key::Account(account_user_1), -// ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), -// ARG_REVERSE_LOOKUP => false -// }; - -// let mint_session_call = ExecuteRequestBuilder::contract_call_by_hash( -// account_user_1, -// nft_contract_hash, -// ENTRY_POINT_MINT, -// mint_runtime_args, -// ) -// .build(); - -// builder.exec(mint_session_call).expect_failure(); - -// let error = builder.get_error().expect("should have an error"); -// assert_expected_error(error, 76, "InvalidHolderMode(76) must have been raised"); -// } - -// #[test] -// fn should_disallow_contract_from_whitelisted_package_to_mint_without_acl_package_mode() { -// let mut builder = InMemoryWasmTestBuilder::default(); -// builder -// .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) -// .commit(); - -// let minting_contract_install_request = ExecuteRequestBuilder::standard( -// *DEFAULT_ACCOUNT_ADDR, -// MINTING_CONTRACT_WASM, -// runtime_args! {}, -// ) -// .build(); - -// builder -// .exec(minting_contract_install_request) -// .expect_success() -// .commit(); - -// let minting_contract_hash = get_minting_contract_hash(&builder); -// let minting_contract_package_hash = get_minting_contract_package_hash(&builder); - -// let contract_whitelist = vec![Key::from(minting_contract_package_hash)]; - -// let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) -// .with_total_token_supply(100u64) -// .with_holder_mode(NFTHolderMode::Contracts) -// .with_whitelist_mode(WhitelistMode::Locked) -// .with_ownership_mode(OwnershipMode::Minter) -// .with_reporting_mode(OwnerReverseLookupMode::NoLookUp) -// .with_minting_mode(MintingMode::Acl) -// .with_acl_whitelist(contract_whitelist) -// .build(); - -// builder.exec(install_request).expect_success().commit(); - -// let nft_contract_key: Key = get_nft_contract_hash(&builder).into(); - -// let is_whitelisted_contract_package = get_dictionary_value_from_key::( -// &builder, -// &nft_contract_key, -// ACL_WHITELIST, -// &minting_contract_package_hash.to_string(), -// ); - -// assert!( -// is_whitelisted_contract_package, -// "acl whitelist is incorrectly set" -// ); - -// let mint_runtime_args = runtime_args! { -// ARG_NFT_CONTRACT_HASH => nft_contract_key, -// ARG_TOKEN_OWNER => Key::from(minting_contract_hash), -// ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), -// ARG_REVERSE_LOOKUP => false -// }; - -// let mint_via_contract_call = ExecuteRequestBuilder::contract_call_by_hash( -// *DEFAULT_ACCOUNT_ADDR, -// minting_contract_hash, -// ENTRY_POINT_MINT, -// mint_runtime_args, -// ) -// .build(); - -// builder.exec(mint_via_contract_call).expect_failure(); - -// let error = builder.get_error().expect("should have an error"); -// assert_expected_error( -// error, -// 81, -// "Unlisted ContractHash from whitelisted ContractPackageHash can not mint without ACL package mode", -// ); -// } - -// #[test] -// fn should_allow_contract_from_whitelisted_package_to_mint_with_acl_package_mode() { -// let mut builder = InMemoryWasmTestBuilder::default(); -// builder -// .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) -// .commit(); - -// let minting_contract_install_request = ExecuteRequestBuilder::standard( -// *DEFAULT_ACCOUNT_ADDR, -// MINTING_CONTRACT_WASM, -// runtime_args! {}, -// ) -// .build(); - -// builder -// .exec(minting_contract_install_request) -// .expect_success() -// .commit(); - -// let minting_contract_hash = get_minting_contract_hash(&builder); -// let minting_contract_package_hash = get_minting_contract_package_hash(&builder); - -// let contract_whitelist = vec![Key::from(minting_contract_package_hash)]; -// let acl_package_mode = true; - -// let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) -// .with_total_token_supply(100u64) -// .with_holder_mode(NFTHolderMode::Contracts) -// .with_whitelist_mode(WhitelistMode::Locked) -// .with_ownership_mode(OwnershipMode::Minter) -// .with_reporting_mode(OwnerReverseLookupMode::NoLookUp) -// .with_minting_mode(MintingMode::Acl) -// .with_acl_whitelist(contract_whitelist) -// .with_acl_package_mode(acl_package_mode) -// .build(); - -// builder.exec(install_request).expect_success().commit(); - -// let nft_contract_key: Key = get_nft_contract_hash(&builder).into(); - -// let is_whitelisted_contract_package = get_dictionary_value_from_key::( -// &builder, -// &nft_contract_key, -// ACL_WHITELIST, -// &minting_contract_package_hash.to_string(), -// ); - -// assert!( -// is_whitelisted_contract_package, -// "acl whitelist is incorrectly set" -// ); - -// let mint_runtime_args = runtime_args! { -// ARG_NFT_CONTRACT_HASH => nft_contract_key, -// ARG_TOKEN_OWNER => Key::from(minting_contract_hash), -// ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), -// ARG_REVERSE_LOOKUP => false -// }; - -// let mint_via_contract_call = ExecuteRequestBuilder::contract_call_by_hash( -// *DEFAULT_ACCOUNT_ADDR, -// minting_contract_hash, -// ENTRY_POINT_MINT, -// mint_runtime_args, -// ) -// .build(); - -// builder -// .exec(mint_via_contract_call) -// .expect_success() -// .commit(); - -// let token_id = 0u64; - -// let actual_token_owner: Key = get_dictionary_value_from_key( -// &builder, -// &nft_contract_key, -// TOKEN_OWNERS, -// &token_id.to_string(), -// ); - -// let minting_contract_key: Key = minting_contract_hash.into(); - -// assert_eq!(actual_token_owner, minting_contract_key) -// } - -// #[test] -// fn should_allow_contract_from_whitelisted_package_to_mint_with_acl_package_mode_after_contract_upgrade( -// ) { -// let mut builder = InMemoryWasmTestBuilder::default(); -// builder -// .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) -// .commit(); - -// let minting_contract_install_request = ExecuteRequestBuilder::standard( -// *DEFAULT_ACCOUNT_ADDR, -// MINTING_CONTRACT_WASM, -// runtime_args! {}, -// ) -// .build(); - -// builder -// .exec(minting_contract_install_request) -// .expect_success() -// .commit(); - -// let minting_contract_hash = get_minting_contract_hash(&builder); -// let minting_contract_package_hash = get_minting_contract_package_hash(&builder); - -// let contract_whitelist = vec![Key::from(minting_contract_package_hash)]; -// let acl_package_mode = true; - -// let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) -// .with_total_token_supply(100u64) -// .with_holder_mode(NFTHolderMode::Contracts) -// .with_whitelist_mode(WhitelistMode::Locked) -// .with_ownership_mode(OwnershipMode::Minter) -// .with_reporting_mode(OwnerReverseLookupMode::NoLookUp) -// .with_minting_mode(MintingMode::Acl) -// .with_acl_whitelist(contract_whitelist) -// .with_acl_package_mode(acl_package_mode) -// .build(); - -// builder.exec(install_request).expect_success().commit(); - -// let nft_contract_key: Key = get_nft_contract_hash(&builder).into(); - -// let is_whitelisted_contract_package = get_dictionary_value_from_key::( -// &builder, -// &nft_contract_key, -// ACL_WHITELIST, -// &minting_contract_package_hash.to_string(), -// ); - -// assert!( -// is_whitelisted_contract_package, -// "acl whitelist is incorrectly set" -// ); - -// let version_minting_contract = support::query_stored_value::( -// &builder, -// Key::Account(*DEFAULT_ACCOUNT_ADDR), -// vec![MINTING_CONTRACT_VERSION.to_string()], -// ); - -// assert_eq!(version_minting_contract, 1u32); - -// let upgrade_request = ExecuteRequestBuilder::standard( -// *DEFAULT_ACCOUNT_ADDR, -// MINTING_CONTRACT_WASM, -// runtime_args! {}, -// ) -// .build(); - -// builder.exec(upgrade_request).expect_success().commit(); - -// let version_minting_contract = support::query_stored_value::( -// &builder, -// Key::Account(*DEFAULT_ACCOUNT_ADDR), -// vec![MINTING_CONTRACT_VERSION.to_string()], -// ); - -// assert_eq!(version_minting_contract, 2u32); - -// let minting_upgraded_contract_hash = get_minting_contract_hash(&builder); -// assert_ne!(minting_contract_hash, minting_upgraded_contract_hash); - -// let mint_runtime_args = runtime_args! { -// ARG_NFT_CONTRACT_HASH => nft_contract_key, -// ARG_TOKEN_OWNER => Key::from(minting_contract_hash), -// ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), -// ARG_REVERSE_LOOKUP => false -// }; - -// let mint_via_contract_call = ExecuteRequestBuilder::contract_call_by_hash( -// *DEFAULT_ACCOUNT_ADDR, -// minting_upgraded_contract_hash, -// ENTRY_POINT_MINT, -// mint_runtime_args, -// ) -// .build(); - -// builder -// .exec(mint_via_contract_call) -// .expect_success() -// .commit(); - -// let token_id = 0u64; - -// let actual_token_owner: Key = get_dictionary_value_from_key( -// &builder, -// &nft_contract_key, -// TOKEN_OWNERS, -// &token_id.to_string(), -// ); - -// let minting_contract_key: Key = minting_upgraded_contract_hash.into(); - -// assert_eq!(actual_token_owner, minting_contract_key) -// } - -// // Update - -// #[test] -// fn should_be_able_to_update_whitelist_for_minting_with_deprecated_arg_contract_whitelist() { -// let mut builder = InMemoryWasmTestBuilder::default(); -// builder -// .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) -// .commit(); - -// let minting_contract_install_request = ExecuteRequestBuilder::standard( -// *DEFAULT_ACCOUNT_ADDR, -// MINTING_CONTRACT_WASM, -// runtime_args! {}, -// ) -// .build(); - -// builder -// .exec(minting_contract_install_request) -// .expect_success() -// .commit(); - -// let minting_contract_hash = get_minting_contract_hash(&builder); - -// let contract_whitelist = vec![]; - -// let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) -// .with_total_token_supply(100u64) -// .with_holder_mode(NFTHolderMode::Contracts) -// .with_whitelist_mode(WhitelistMode::Unlocked) -// .with_ownership_mode(OwnershipMode::Minter) -// .with_reporting_mode(OwnerReverseLookupMode::NoLookUp) -// .with_minting_mode(MintingMode::Acl) -// .with_acl_whitelist(contract_whitelist) -// .build(); - -// builder.exec(install_request).expect_success().commit(); - -// let nft_contract_hash = get_nft_contract_hash(&builder); -// let nft_contract_key: Key = nft_contract_hash.into(); - -// let seed_uref = *builder -// .query(None, nft_contract_key, &[]) -// .expect("must have nft contract") -// .as_contract() -// .expect("must convert contract") -// .named_keys() -// .get(ACL_WHITELIST) -// .expect("must have key") -// .as_uref() -// .expect("must convert to seed uref"); - -// let is_whitelisted_account = -// builder.query_dictionary_item(None, seed_uref, &minting_contract_hash.to_string()); - -// assert!( -// is_whitelisted_account.is_err(), -// "acl whitelist is incorrectly set" -// ); - -// let mint_runtime_args = runtime_args! { -// ARG_NFT_CONTRACT_HASH => nft_contract_key, -// ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), -// ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), -// ARG_REVERSE_LOOKUP => false, -// }; - -// let mint_via_contract_call = ExecuteRequestBuilder::contract_call_by_hash( -// *DEFAULT_ACCOUNT_ADDR, -// minting_contract_hash, -// ENTRY_POINT_MINT, -// mint_runtime_args.clone(), -// ) -// .build(); - -// builder.exec(mint_via_contract_call).expect_failure(); - -// let error = builder.get_error().expect("should have an error"); -// assert_expected_error( -// error, -// 81, -// "Unlisted contract hash should not be permitted to mint", -// ); - -// let update_whitelist_request = ExecuteRequestBuilder::contract_call_by_hash( -// *DEFAULT_ACCOUNT_ADDR, -// nft_contract_hash, -// ENTRY_POINT_SET_VARIABLES, -// runtime_args! { -// ARG_CONTRACT_WHITELIST => vec![minting_contract_hash] -// }, -// ) -// .build(); - -// builder -// .exec(update_whitelist_request) -// .expect_success() -// .commit(); - -// let is_updated_acl_whitelist = get_dictionary_value_from_key::( -// &builder, -// &nft_contract_key, -// ACL_WHITELIST, -// &minting_contract_hash.to_string(), -// ); - -// assert!(is_updated_acl_whitelist, "acl whitelist is incorrectly set"); - -// let mint_via_contract_call = ExecuteRequestBuilder::contract_call_by_hash( -// *DEFAULT_ACCOUNT_ADDR, -// minting_contract_hash, -// ENTRY_POINT_MINT, -// mint_runtime_args, -// ) -// .build(); - -// builder -// .exec(mint_via_contract_call) -// .expect_success() -// .commit(); -// } - -// #[test] -// fn should_be_able_to_update_whitelist_for_minting() { -// let mut builder = InMemoryWasmTestBuilder::default(); -// builder -// .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) -// .commit(); - -// let minting_contract_install_request = ExecuteRequestBuilder::standard( -// *DEFAULT_ACCOUNT_ADDR, -// MINTING_CONTRACT_WASM, -// runtime_args! {}, -// ) -// .build(); - -// builder -// .exec(minting_contract_install_request) -// .expect_success() -// .commit(); - -// let minting_contract_hash = get_minting_contract_hash(&builder); - -// let contract_whitelist = vec![]; - -// let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) -// .with_total_token_supply(100u64) -// .with_holder_mode(NFTHolderMode::Contracts) -// .with_whitelist_mode(WhitelistMode::Unlocked) -// .with_ownership_mode(OwnershipMode::Minter) -// .with_reporting_mode(OwnerReverseLookupMode::NoLookUp) -// .with_minting_mode(MintingMode::Acl) -// .with_acl_whitelist(contract_whitelist) -// .build(); - -// builder.exec(install_request).expect_success().commit(); - -// let nft_contract_hash = get_nft_contract_hash(&builder); -// let nft_contract_key: Key = nft_contract_hash.into(); - -// let seed_uref = *builder -// .query(None, nft_contract_key, &[]) -// .expect("must have nft contract") -// .as_contract() -// .expect("must convert contract") -// .named_keys() -// .get(ACL_WHITELIST) -// .expect("must have key") -// .as_uref() -// .expect("must convert to seed uref"); - -// let is_whitelisted_account = -// builder.query_dictionary_item(None, seed_uref, &minting_contract_hash.to_string()); - -// assert!( -// is_whitelisted_account.is_err(), -// "acl whitelist is incorrectly set" -// ); - -// let mint_runtime_args = runtime_args! { -// ARG_NFT_CONTRACT_HASH => nft_contract_key, -// ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), -// ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), -// ARG_REVERSE_LOOKUP => false, -// }; - -// let mint_via_contract_call = ExecuteRequestBuilder::contract_call_by_hash( -// *DEFAULT_ACCOUNT_ADDR, -// minting_contract_hash, -// ENTRY_POINT_MINT, -// mint_runtime_args.clone(), -// ) -// .build(); - -// builder.exec(mint_via_contract_call).expect_failure(); - -// let error = builder.get_error().expect("should have an error"); -// assert_expected_error( -// error, -// 81, -// "Unlisted contract hash should not be permitted to mint", -// ); - -// let update_whitelist_request = ExecuteRequestBuilder::contract_call_by_hash( -// *DEFAULT_ACCOUNT_ADDR, -// nft_contract_hash, -// ENTRY_POINT_SET_VARIABLES, -// runtime_args! { -// ARG_ACL_WHITELIST => vec![Key::from(minting_contract_hash)] -// }, -// ) -// .build(); - -// builder -// .exec(update_whitelist_request) -// .expect_success() -// .commit(); - -// let is_updated_acl_whitelist = get_dictionary_value_from_key::( -// &builder, -// &nft_contract_key, -// ACL_WHITELIST, -// &minting_contract_hash.to_string(), -// ); - -// assert!(is_updated_acl_whitelist, "acl whitelist is incorrectly set"); - -// let mint_via_contract_call = ExecuteRequestBuilder::contract_call_by_hash( -// *DEFAULT_ACCOUNT_ADDR, -// minting_contract_hash, -// ENTRY_POINT_MINT, -// mint_runtime_args, -// ) -// .build(); - -// builder -// .exec(mint_via_contract_call) -// .expect_success() -// .commit(); -// } - -// // Upgrade - -// #[test] -// fn should_upgrade_from_named_keys_to_dict_and_acl_minting_mode() { -// let mut builder = InMemoryWasmTestBuilder::default(); -// builder -// .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) -// .commit(); - -// let minting_contract_install_request = ExecuteRequestBuilder::standard( -// *DEFAULT_ACCOUNT_ADDR, -// MINTING_CONTRACT_WASM, -// runtime_args! {}, -// ) -// .build(); - -// builder -// .exec(minting_contract_install_request) -// .expect_success() -// .commit(); - -// let minting_contract_hash = get_minting_contract_hash(&builder); -// let contract_whitelist = vec![minting_contract_hash]; - -// let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, CONTRACT_1_0_0_WASM) -// .with_collection_name(NFT_TEST_COLLECTION.to_string()) -// .with_collection_symbol(NFT_TEST_SYMBOL.to_string()) -// .with_total_token_supply(1000u64) -// .with_minting_mode(MintingMode::Installer) -// .with_holder_mode(NFTHolderMode::Contracts) -// .with_whitelist_mode(WhitelistMode::Locked) -// .with_ownership_mode(OwnershipMode::Transferable) -// .with_nft_metadata_kind(NFTMetadataKind::Raw) -// .with_reporting_mode(OwnerReverseLookupMode::NoLookUp) -// .with_contract_whitelist(contract_whitelist) -// .build(); - -// builder.exec(install_request).expect_success().commit(); - -// let nft_contract_hash_1_0_0 = support::get_nft_contract_hash_1_0_0(&builder); -// let nft_contract_key_1_0_0: Key = nft_contract_hash_1_0_0.into(); - -// let minting_mode = support::query_stored_value::( -// &builder, -// nft_contract_key_1_0_0, -// vec![ARG_MINTING_MODE.to_string()], -// ); - -// assert_eq!( -// minting_mode, -// MintingMode::Installer as u8, -// "minting mode should be set to public" -// ); - -// let upgrade_request = ExecuteRequestBuilder::standard( -// *DEFAULT_ACCOUNT_ADDR, -// NFT_CONTRACT_WASM, -// runtime_args! { -// ARG_NFT_CONTRACT_HASH => support::get_nft_contract_package_hash(&builder), -// ARG_COLLECTION_NAME => NFT_TEST_COLLECTION.to_string(), -// ARG_NAMED_KEY_CONVENTION => NamedKeyConventionMode::V1_0Standard as u8, -// }, -// ) -// .build(); - -// builder.exec(upgrade_request).expect_success().commit(); - -// let nft_contract_key: Key = support::get_nft_contract_hash(&builder).into(); - -// let is_updated_acl_whitelist = get_dictionary_value_from_key::( -// &builder, -// &nft_contract_key, -// ACL_WHITELIST, -// &minting_contract_hash.to_string(), -// ); - -// assert!(is_updated_acl_whitelist, "acl whitelist is incorrectly set"); - -// let minting_mode = support::query_stored_value::( -// &builder, -// nft_contract_key, -// vec![ARG_MINTING_MODE.to_string()], -// ); - -// assert_eq!( -// minting_mode, -// MintingMode::Acl as u8, -// "minting mode should be set to acl" -// ); -// } +#[test] +fn should_disallow_listed_account_from_minting_with_nftholder_contract() { + let env = odra_test::env(); + + let minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let listed_account = env.get_account(0); + + let mixed_whitelist = vec![minting_contract.address().clone(), listed_account]; + + let args = default_args_builder() + .holder_mode(NFTHolderMode::Contracts) + .whitelist_mode(WhitelistMode::Locked) + .ownership_mode(OwnershipMode::Minter) + .minting_mode(MintingMode::Acl) + .acl_white_list(mixed_whitelist) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + assert!( + contract.is_whitelisted(&listed_account), + "acl whitelist is incorrectly set" + ); + + let unlisted_account = env.get_account(1); + env.set_caller(unlisted_account); + + assert_eq!( + contract.try_mint( + unlisted_account, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ), + Err(CEP78Error::InvalidHolderMode.into()) + ); +} + +#[test] +#[ignore = "ACL package mode package mode is switched on by default and can't be switched off"] +fn should_disallow_contract_from_whitelisted_package_to_mint_without_acl_package_mode() {} + +#[test] +#[ignore = "ACL package mode package mode is switched on by default and can't be switched off"] +fn should_allow_contract_from_whitelisted_package_to_mint_with_acl_package_mode() {} + +#[test] +#[ignore = "ACL package mode package mode is switched on by default and can't be switched off"] +fn should_allow_contract_from_whitelisted_package_to_mint_with_acl_package_mode_after_contract_upgrade( +) { +} + +// Update +#[test] +#[ignore = "Deprecated arg contract whitelist is not used in Odra"] +fn should_be_able_to_update_whitelist_for_minting_with_deprecated_arg_contract_whitelist() {} + +#[test] +fn should_be_able_to_update_whitelist_for_minting() { + let env = odra_test::env(); + + let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let contract_whitelist = vec![]; + + let args = default_args_builder() + .holder_mode(NFTHolderMode::Contracts) + .whitelist_mode(WhitelistMode::Unlocked) + .ownership_mode(OwnershipMode::Minter) + .minting_mode(MintingMode::Acl) + .acl_white_list(contract_whitelist) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + assert!( + !contract.is_whitelisted(minting_contract.address()), + "acl whitelist is incorrectly set" + ); + + assert_eq!( + minting_contract.try_mint_for( + contract.address(), + env.get_account(0), + TEST_PRETTY_721_META_DATA.to_string(), + ), + Err(CEP78Error::UnlistedContractHash.into()), + ); + + contract.set_variables( + Maybe::None, + Maybe::Some(vec![minting_contract.address().clone()]) + ); + + assert!( + contract.is_whitelisted(minting_contract.address()), + "acl whitelist is incorrectly set" + ); + + assert!(minting_contract + .try_mint_for( + contract.address(), + env.get_account(0), + TEST_PRETTY_721_META_DATA.to_string(), + ) + .is_ok()); +} + +// Upgrade +#[test] +#[ignore = "Odra implements v1.5.1, so this test is not applicable"] +fn should_upgrade_from_named_keys_to_dict_and_acl_minting_mode() {} diff --git a/modules/src/cep78/token.rs b/modules/src/cep78/token.rs index 45e8f907..53babd86 100644 --- a/modules/src/cep78/token.rs +++ b/modules/src/cep78/token.rs @@ -129,7 +129,7 @@ impl CEP78 { ) { let installer = self.info.installer(); // Only the installing account can change the mutable variables. - self.ensure_not_caller(installer); + self.ensure_caller(installer); if let Maybe::Some(allow_minting) = allow_minting { self.settings.set_allow_minting(allow_minting); @@ -176,7 +176,7 @@ impl CEP78 { let minting_mode: MintingMode = self.settings.minting_mode(); - let caller = self.env().caller(); + let caller = self.verified_caller(); // Revert if minting is private and caller is not installer. if MintingMode::Installer == minting_mode { @@ -711,6 +711,13 @@ impl CEP78 { } } + #[inline] + fn ensure_caller(&self, address: Address) { + if self.env().caller() != address { + self.env().revert(CEP78Error::InvalidAccount); + } + } + #[inline] fn emit_ces_event(&self, event: T) { let events_mode: EventsMode = self.settings.events_mode(); @@ -730,6 +737,20 @@ impl CEP78 { fn ownership_mode(&self) -> OwnershipMode { self.settings.ownership_mode() } + + #[inline] + fn verified_caller(&self) -> Address { + let holder_mode = self.settings.holder_mode(); + let caller = self.env().caller(); + + match (caller, holder_mode) { + (Address::Account(_), NFTHolderMode::Contracts) + | (Address::Contract(_), NFTHolderMode::Accounts) => { + self.env().revert(CEP78Error::InvalidHolderMode); + } + _ => caller + } + } } #[odra::external_contract] diff --git a/modules/src/cep78/whitelist.rs b/modules/src/cep78/whitelist.rs index 06cc95a6..f7f1bce0 100644 --- a/modules/src/cep78/whitelist.rs +++ b/modules/src/cep78/whitelist.rs @@ -1,4 +1,4 @@ -use odra::{args::Maybe, prelude::*, Address, List, Mapping, Var}; +use odra::{args::Maybe, prelude::*, Address, Var}; use super::{error::CEP78Error, modalities::WhitelistMode}; @@ -19,8 +19,7 @@ impl ACLWhitelist { #[inline] pub fn get_mode(&self) -> WhitelistMode { - // self.mode.get_or_default() - WhitelistMode::Locked + self.mode.get_or_default() } #[inline] From 4aca0695a4f8a8ca974347f6c11a47ca5391dd5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Wed, 17 Apr 2024 12:27:16 +0200 Subject: [PATCH 09/38] test set_varialbes endpoint --- modules/src/cep78/settings.rs | 17 ++++++- modules/src/cep78/tests/acl.rs | 13 ++--- modules/src/cep78/tests/mod.rs | 20 ++++---- modules/src/cep78/tests/set_variables.rs | 61 +++++++++++++++++++++++- modules/src/cep78/tests/utils.rs | 10 ++-- modules/src/cep78/token.rs | 22 +++++++-- 6 files changed, 111 insertions(+), 32 deletions(-) diff --git a/modules/src/cep78/settings.rs b/modules/src/cep78/settings.rs index 94c95696..10ac489a 100644 --- a/modules/src/cep78/settings.rs +++ b/modules/src/cep78/settings.rs @@ -16,7 +16,8 @@ pub struct Settings { nft_kind: Var, holder_mode: Var, burn_mode: Var, - events_mode: Var + events_mode: Var, + operator_burn_mode: Var } impl Settings { @@ -28,7 +29,8 @@ impl Settings { nft_kind: NFTKind, holder_mode: NFTHolderMode, burn_mode: BurnMode, - events_mode: EventsMode + events_mode: EventsMode, + operator_burn_mode: bool ) { self.allow_minting.set(allow_minting); self.minting_mode.set(minting_mode); @@ -37,6 +39,7 @@ impl Settings { self.holder_mode.set(holder_mode); self.burn_mode.set(burn_mode); self.events_mode.set(events_mode); + self.operator_burn_mode.set(operator_burn_mode); } #[inline] @@ -73,4 +76,14 @@ impl Settings { pub fn holder_mode(&self) -> NFTHolderMode { self.holder_mode.get_or_default() } + + #[inline] + pub fn operator_burn_mode(&self) -> bool { + self.operator_burn_mode.get_or_default() + } + + #[inline] + pub fn set_operator_burn_mode(&mut self, value: bool) { + self.operator_burn_mode.set(value) + } } diff --git a/modules/src/cep78/tests/acl.rs b/modules/src/cep78/tests/acl.rs index 2ae965c6..b86af333 100644 --- a/modules/src/cep78/tests/acl.rs +++ b/modules/src/cep78/tests/acl.rs @@ -7,16 +7,12 @@ use odra::{ use crate::cep78::{ error::CEP78Error, - modalities::{MintingMode, NFTHolderMode, NFTMetadataKind, OwnershipMode, WhitelistMode}, - tests::utils::{InitArgsBuilder, TEST_PRETTY_721_META_DATA}, + modalities::{MintingMode, NFTHolderMode, OwnershipMode, WhitelistMode}, + tests::utils::TEST_PRETTY_721_META_DATA, token::CEP78HostRef }; -fn default_args_builder() -> InitArgsBuilder { - InitArgsBuilder::default() - .total_token_supply(100u64) - .nft_metadata_kind(NFTMetadataKind::NFT721) -} +use super::default_args_builder; #[odra::module] struct DummyContract; @@ -461,7 +457,8 @@ fn should_be_able_to_update_whitelist_for_minting() { contract.set_variables( Maybe::None, - Maybe::Some(vec![minting_contract.address().clone()]) + Maybe::Some(vec![minting_contract.address().clone()]), + Maybe::None ); assert!( diff --git a/modules/src/cep78/tests/mod.rs b/modules/src/cep78/tests/mod.rs index 3fb75112..74fd2963 100644 --- a/modules/src/cep78/tests/mod.rs +++ b/modules/src/cep78/tests/mod.rs @@ -1,16 +1,18 @@ -use odra::{ - args::Maybe, - host::{Deployer, HostRef} -}; +use self::utils::InitArgsBuilder; + +use super::modalities::NFTMetadataKind; mod acl; mod set_variables; mod utils; -use crate::cep78::{error::CEP78Error, events::VariablesSet, modalities::OwnerReverseLookupMode}; - -use super::token::CEP78HostRef; - pub(super) const COLLECTION_NAME: &str = "CEP78-Test-Collection"; pub(super) const COLLECTION_SYMBOL: &str = "CEP78"; -pub(super) const TOTAL_TOKEN_SUPPLY: u64 = 100_000_000; + +pub(super) fn default_args_builder() -> InitArgsBuilder { + InitArgsBuilder::default() + .collection_name(COLLECTION_NAME.to_string()) + .collection_symbol(COLLECTION_SYMBOL.to_string()) + .total_token_supply(100u64) + .nft_metadata_kind(NFTMetadataKind::NFT721) +} diff --git a/modules/src/cep78/tests/set_variables.rs b/modules/src/cep78/tests/set_variables.rs index ec9a9b69..5c8d1fb2 100644 --- a/modules/src/cep78/tests/set_variables.rs +++ b/modules/src/cep78/tests/set_variables.rs @@ -3,5 +3,62 @@ use odra::{ host::{Deployer, HostRef} }; -use super::{COLLECTION_NAME, COLLECTION_SYMBOL}; -use crate::cep78::{error::CEP78Error, events::VariablesSet, token::CEP78HostRef}; +use crate::cep78::{ + error::CEP78Error, events::VariablesSet, modalities::EventsMode, tests::default_args_builder, + token::CEP78HostRef +}; + +#[test] +fn only_installer_should_be_able_to_toggle_allow_minting() { + let env = odra_test::env(); + let (installer, other_user) = (env.get_account(0), env.get_account(1)); + let args = default_args_builder() + .allow_minting(false) + .events_mode(EventsMode::CES) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + // Account other than installer account should not be able to change allow_minting + env.set_caller(other_user); + assert_eq!( + contract.try_set_variables(Maybe::Some(true), Maybe::None, Maybe::None), + Err(CEP78Error::InvalidAccount.into()) + ); + assert!(!contract.is_minting_allowed()); + + // Installer account should be able to change allow_minting + env.set_caller(installer); + assert_eq!( + contract.try_set_variables(Maybe::Some(true), Maybe::None, Maybe::None), + Ok(()) + ); + assert!(contract.is_minting_allowed()); + + // Expect VariablesSet event. + assert!(env.emitted_event(contract.address(), &VariablesSet {})); +} + +#[test] +#[ignore = "Acl package mode is true by default and immutable"] +fn installer_should_be_able_to_toggle_acl_package_mode() {} + +#[test] +#[ignore = "Package operator mode is true by default and immutable"] +fn installer_should_be_able_to_toggle_package_operator_mode() {} + +#[test] +fn installer_should_be_able_to_toggle_operator_burn_mode() { + let env = odra_test::env(); + let args = default_args_builder().events_mode(EventsMode::CES).build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + // Installer account should be able to change allow_minting + assert_eq!( + contract.try_set_variables(Maybe::None, Maybe::None, Maybe::Some(true)), + Ok(()) + ); + assert!(contract.is_operator_burn_mode()); + + // Expect VariablesSet event. + assert!(env.emitted(contract.address(), "VariablesSet")); +} diff --git a/modules/src/cep78/tests/utils.rs b/modules/src/cep78/tests/utils.rs index 545a89ab..e30e79be 100644 --- a/modules/src/cep78/tests/utils.rs +++ b/modules/src/cep78/tests/utils.rs @@ -3,14 +3,9 @@ use crate::cep78::{ BurnMode, EventsMode, MetadataMutability, MintingMode, NFTHolderMode, NFTIdentifierMode, NFTKind, NFTMetadataKind, OwnerReverseLookupMode, OwnershipMode, WhitelistMode }, - token::{CEP78HostRef, CEP78InitArgs} -}; -use derive_builder::Builder; -use odra::{ - args::Maybe, - casper_types::{runtime_args, RuntimeArgs}, - Address + token::CEP78InitArgs }; +use odra::{args::Maybe, Address}; #[derive(Default)] pub struct InitArgsBuilder { @@ -174,6 +169,7 @@ impl InitArgsBuilder { receipt_name: self.receipt_name, nft_identifier_mode: self.identifier_mode, burn_mode: self.burn_mode, + operator_burn_mode: self.operator_burn_mode, nft_metadata_kind: self.nft_metadata_kind, metadata_mutability: self.metadata_mutability, owner_reverse_lookup_mode: self.owner_reverse_lookup_mode, diff --git a/modules/src/cep78/token.rs b/modules/src/cep78/token.rs index 53babd86..fbb5f7da 100644 --- a/modules/src/cep78/token.rs +++ b/modules/src/cep78/token.rs @@ -63,6 +63,7 @@ impl CEP78 { json_schema: Maybe, receipt_name: Maybe, burn_mode: Maybe, + operator_burn_mode: Maybe, owner_reverse_lookup_mode: Maybe, events_mode: Maybe, transfer_filter_contract_contract_key: Maybe
, @@ -83,7 +84,8 @@ impl CEP78 { nft_kind, holder_mode.unwrap_or_default(), burn_mode.unwrap_or_default(), - events_mode.unwrap_or_default() + events_mode.unwrap_or_default(), + operator_burn_mode.unwrap_or_default() ); self.reverse_lookup @@ -112,7 +114,7 @@ impl CEP78 { if ownership_mode == OwnershipMode::Minter && minting_mode.unwrap_or_default() == MintingMode::Installer - && owner_reverse_lookup_mode.unwrap_or_default() == OwnerReverseLookupMode::NoLookUp + && owner_reverse_lookup_mode.unwrap_or_default() == OwnerReverseLookupMode::Complete { self.env().revert(CEP78Error::InvalidReportingMode) } @@ -125,7 +127,8 @@ impl CEP78 { pub fn set_variables( &mut self, allow_minting: Maybe, - acl_whitelist: Maybe> + acl_whitelist: Maybe>, + operator_burn_mode: Maybe ) { let installer = self.info.installer(); // Only the installing account can change the mutable variables. @@ -135,6 +138,10 @@ impl CEP78 { self.settings.set_allow_minting(allow_minting); } + if let Maybe::Some(operator_burn_mode) = operator_burn_mode { + self.settings.set_operator_burn_mode(operator_burn_mode); + } + self.whitelist.update_addresses(acl_whitelist); self.emit_ces_event(VariablesSet::new()); } @@ -613,6 +620,13 @@ impl CEP78 { pub fn get_collection_name(&self) -> String { self.info.collection_name() } + + pub fn is_minting_allowed(&self) -> bool { + self.settings.allow_minting() + } + pub fn is_operator_burn_mode(&self) -> bool { + self.settings.operator_burn_mode() + } } impl CEP78 { @@ -720,7 +734,7 @@ impl CEP78 { #[inline] fn emit_ces_event(&self, event: T) { - let events_mode: EventsMode = self.settings.events_mode(); + let events_mode = self.settings.events_mode(); if let EventsMode::CES = events_mode { self.env().emit_event(event); } From ca7e6a79e3c5d26191e52315967c67677476b6fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Thu, 18 Apr 2024 14:24:30 +0200 Subject: [PATCH 10/38] minting tests --- modules/Cargo.toml | 3 +- modules/src/cep78/collection_info.rs | 5 + modules/src/cep78/events.rs | 30 +- modules/src/cep78/metadata.rs | 28 +- modules/src/cep78/tests/acl.rs | 33 +- modules/src/cep78/tests/installer.rs | 325 +++++++++++ modules/src/cep78/tests/mint.rs | 837 +++++++++++++++++++++++++++ modules/src/cep78/tests/mod.rs | 38 +- modules/src/cep78/tests/utils.rs | 104 +++- modules/src/cep78/token.rs | 158 +++-- modules/src/lib.rs | 1 + 11 files changed, 1414 insertions(+), 148 deletions(-) create mode 100644 modules/src/cep78/tests/installer.rs create mode 100644 modules/src/cep78/tests/mint.rs diff --git a/modules/Cargo.toml b/modules/Cargo.toml index c4b4452b..6c1420c2 100644 --- a/modules/Cargo.toml +++ b/modules/Cargo.toml @@ -19,7 +19,8 @@ base16 = { version = "0.2.1", default-features = false } [dev-dependencies] odra-test = { path = "../odra-test", version = "0.9.1" } -derive_builder = "0.20.0" +once_cell = "1" +blake2 = "0.10.6" [build-dependencies] odra-build = { path = "../odra-build", version = "0.9.1" } diff --git a/modules/src/cep78/collection_info.rs b/modules/src/cep78/collection_info.rs index 642b6539..3462fb2e 100644 --- a/modules/src/cep78/collection_info.rs +++ b/modules/src/cep78/collection_info.rs @@ -65,4 +65,9 @@ impl CollectionInfo { pub fn collection_name(&self) -> String { self.name.get_or_default() } + + #[inline] + pub fn collection_symbol(&self) -> String { + self.symbol.get_or_default() + } } diff --git a/modules/src/cep78/events.rs b/modules/src/cep78/events.rs index 3486bc41..531872ab 100644 --- a/modules/src/cep78/events.rs +++ b/modules/src/cep78/events.rs @@ -9,10 +9,10 @@ pub struct Mint { } impl Mint { - pub fn new(recipient: Address, token_id: TokenIdentifier, data: String) -> Self { + pub fn new(recipient: Address, token_id: String, data: String) -> Self { Self { recipient, - token_id: token_id.to_string(), + token_id, data } } @@ -26,10 +26,10 @@ pub struct Burn { } impl Burn { - pub fn new(owner: Address, token_id: TokenIdentifier, burner: Address) -> Self { + pub fn new(owner: Address, token_id: String, burner: Address) -> Self { Self { owner, - token_id: token_id.to_string(), + token_id, burner } } @@ -43,11 +43,11 @@ pub struct Approval { } impl Approval { - pub fn new(owner: Address, spender: Address, token_id: TokenIdentifier) -> Self { + pub fn new(owner: Address, spender: Address, token_id: String) -> Self { Self { owner, spender, - token_id: token_id.to_string() + token_id } } } @@ -59,11 +59,8 @@ pub struct ApprovalRevoked { } impl ApprovalRevoked { - pub fn new(owner: Address, token_id: TokenIdentifier) -> Self { - Self { - owner, - token_id: token_id.to_string() - } + pub fn new(owner: Address, token_id: String) -> Self { + Self { owner, token_id } } } @@ -104,13 +101,13 @@ impl Transfer { owner: Address, spender: Option
, recipient: Address, - token_id: TokenIdentifier + token_id: String ) -> Self { Self { owner, spender, recipient, - token_id: token_id.to_string() + token_id } } } @@ -122,11 +119,8 @@ pub struct MetadataUpdated { } impl MetadataUpdated { - pub fn new(token_id: TokenIdentifier, data: String) -> Self { - Self { - token_id: token_id.to_string(), - data - } + pub fn new(token_id: String, data: String) -> Self { + Self { token_id, data } } } diff --git a/modules/src/cep78/metadata.rs b/modules/src/cep78/metadata.rs index 26dae8a5..4d7ca888 100644 --- a/modules/src/cep78/metadata.rs +++ b/modules/src/cep78/metadata.rs @@ -16,7 +16,7 @@ pub struct Metadata { identifier_mode: Var, mutability: Var, json_schema: Var, - validated_metadata: Mapping + validated_metadata: Mapping<(String, String), String> } impl Metadata { @@ -62,10 +62,9 @@ impl Metadata { Requirement::Required => { let id = token_identifier.to_string(); let kind = get_metadata_key(&metadata_kind); - let key = format!("{}{}", kind, id); let metadata = self .validated_metadata - .get(&key) + .get(&(kind, id)) .unwrap_or_revert_with(&env, CEP78Error::InvalidTokenIdentifier); return metadata; } @@ -75,6 +74,14 @@ impl Metadata { env.revert(CEP78Error::MissingTokenMetaData) } + // test only + pub fn get_metadata_by_kind(&self, token_identifier: String, kind: &NFTMetadataKind) -> String { + let kind = get_metadata_key(kind); + self.validated_metadata + .get(&(kind, token_identifier)) + .unwrap_or_default() + } + pub fn ensure_mutability(&self, error: CEP78Error) { let current_mutability = self .mutability @@ -84,7 +91,7 @@ impl Metadata { } } - pub fn update_or_revert(&mut self, token_metadata: &str, token_identifier: &TokenIdentifier) { + pub fn update_or_revert(&mut self, token_metadata: &str, token_id: &String) { let requirements = self.get_requirements(); for (metadata_kind, required) in requirements { if required == Requirement::Unneeded { @@ -93,10 +100,9 @@ impl Metadata { let token_metadata_validation = self.validate(&metadata_kind, token_metadata); match token_metadata_validation { Ok(validated_token_metadata) => { - let id = token_identifier.to_string(); let kind = get_metadata_key(&metadata_kind); - let key = format!("{}{}", kind, id); - self.validated_metadata.set(&key, validated_token_metadata); + self.validated_metadata + .set(&(kind, token_id.to_owned()), validated_token_metadata); } Err(err) => { self.env().revert(err); @@ -245,14 +251,14 @@ impl Metadata { #[derive(Serialize, Deserialize)] #[odra::odra_type] pub(crate) struct MetadataSchemaProperty { - name: String, - description: String, - required: bool + pub name: String, + pub description: String, + pub required: bool } #[derive(Serialize, Deserialize, Clone)] pub(crate) struct CustomMetadataSchema { - properties: BTreeMap + pub properties: BTreeMap } // Using a structure for the purposes of serialization formatting. diff --git a/modules/src/cep78/tests/acl.rs b/modules/src/cep78/tests/acl.rs index b86af333..443eea2e 100644 --- a/modules/src/cep78/tests/acl.rs +++ b/modules/src/cep78/tests/acl.rs @@ -8,43 +8,12 @@ use odra::{ use crate::cep78::{ error::CEP78Error, modalities::{MintingMode, NFTHolderMode, OwnershipMode, WhitelistMode}, - tests::utils::TEST_PRETTY_721_META_DATA, + tests::utils::{DummyContractHostRef, TestContractHostRef, TEST_PRETTY_721_META_DATA}, token::CEP78HostRef }; use super::default_args_builder; -#[odra::module] -struct DummyContract; - -#[odra::module] -impl DummyContract {} - -#[odra::module] -struct TestContract; - -#[odra::module] -impl TestContract { - pub fn mint( - &mut self, - nft_contract_address: &Address, - token_metadata: String - ) -> (String, Address, String) { - NftContractContractRef::new(self.env(), *nft_contract_address) - .mint(self.env().self_address(), token_metadata) - } - - pub fn mint_for( - &mut self, - nft_contract_address: &Address, - token_owner: Address, - token_metadata: String - ) -> (String, Address, String) { - NftContractContractRef::new(self.env(), *nft_contract_address) - .mint(token_owner, token_metadata) - } -} - #[odra::external_contract] trait NftContract { fn mint(&mut self, token_owner: Address, token_metadata: String) -> (String, Address, String); diff --git a/modules/src/cep78/tests/installer.rs b/modules/src/cep78/tests/installer.rs new file mode 100644 index 00000000..57069d3b --- /dev/null +++ b/modules/src/cep78/tests/installer.rs @@ -0,0 +1,325 @@ +use odra::host::Deployer; + +use crate::cep78::{ + modalities::MintingMode, + tests::{default_args_builder, COLLECTION_NAME, COLLECTION_SYMBOL}, + token::CEP78HostRef +}; + +#[test] +fn should_install_contract() { + let env = odra_test::env(); + + let args = default_args_builder() + .collection_name(COLLECTION_NAME.to_string()) + .collection_symbol(COLLECTION_SYMBOL.to_string()) + .total_token_supply(1u64) + .allow_minting(true) + .build(); + let contract = CEP78HostRef::deploy(&env, args); + + assert_eq!(&contract.get_collection_name(), COLLECTION_NAME); + assert_eq!(&contract.get_collection_symbol(), COLLECTION_SYMBOL); + assert_eq!(contract.get_total_supply(), 1u64); + assert_eq!(contract.is_minting_allowed(), true); + assert_eq!(contract.get_minting_mode(), MintingMode::Installer); + assert_eq!(contract.get_number_of_minted_tokens(), 0u64); + + // Expects Schemas to be registerd. + // let expected_schemas = Schemas::new() + // .with::() + // .with::() + // .with::() + // .with::() + // .with::() + // .with::() + // .with::() + // .with::() + // .with::(); + // let actual_schemas: Schemas = support::query_stored_value( + // &builder, + // nft_contract_key, + // vec![casper_event_standard::EVENTS_SCHEMA.to_string()], + // ); + // assert_eq!(actual_schemas, expected_schemas, "Schemas mismatch."); +} + +#[test] +#[ignore = "Not applicable in Odra, init is not allowed after installation by design"] +fn should_only_allow_init_during_installation_session() {} + +#[test] +fn should_install_with_allow_minting_set_to_false() { + let env = odra_test::env(); + + let args = default_args_builder().allow_minting(false).build(); + let contract = CEP78HostRef::deploy(&env, args); + assert_eq!(contract.is_minting_allowed(), false); +} + +#[test] +#[ignore = "Odra interface does not allow to pass a wrong type"] +fn should_reject_invalid_collection_name() {} +#[test] +#[ignore = "Odra interface does not allow to pass a wrong type"] +fn should_reject_invalid_collection_symbol() {} + +/* +#[test] +fn should_reject_non_numerical_total_token_supply_value() { + let install_request_builder = + InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) + .with_invalid_total_token_supply( + CLValue::from_t::("".to_string()).expect("expected CLValue"), + ); + support::assert_expected_invalid_installer_request( + install_request_builder, + 26, + "should reject installation when given an invalid total supply value", + ); +} + +#[test] +fn should_install_with_contract_holder_mode() { + let mut builder = InMemoryWasmTestBuilder::default(); + builder + .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) + .commit(); + + let contract_whitelist = vec![Key::from(ContractHash::default())]; + + let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) + .with_holder_mode(NFTHolderMode::Contracts) + .with_whitelist_mode(WhitelistMode::Unlocked) + .with_reporting_mode(OwnerReverseLookupMode::NoLookUp) + .with_minting_mode(MintingMode::Acl) + .with_acl_whitelist(contract_whitelist); + + builder + .exec(install_request.build()) + .expect_success() + .commit(); + + let nft_contract_key: Key = get_nft_contract_hash(&builder).into(); + + let actual_holder_mode: u8 = support::query_stored_value( + &builder, + nft_contract_key, + vec![ARG_HOLDER_MODE.to_string()], + ); + + assert_eq!( + actual_holder_mode, + NFTHolderMode::Contracts as u8, + "holder mode is not set to contracts" + ); + + let actual_whitelist_mode: u8 = support::query_stored_value( + &builder, + nft_contract_key, + vec![ARG_WHITELIST_MODE.to_string()], + ); + + assert_eq!( + actual_whitelist_mode, + WhitelistMode::Unlocked as u8, + "whitelist mode is not set to unlocked" + ); + + let is_whitelisted_account = get_dictionary_value_from_key::( + &builder, + &nft_contract_key, + ACL_WHITELIST, + &ContractHash::default().to_string(), + ); + + assert!(is_whitelisted_account, "acl whitelist is incorrectly set"); +} + +fn should_disallow_installation_of_contract_with_empty_locked_whitelist_with_holder_mode( + nft_holder_mode: NFTHolderMode, +) { + let mut builder = InMemoryWasmTestBuilder::default(); + builder + .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) + .commit(); + + let install_request_builder = + InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) + .with_holder_mode(nft_holder_mode) + .with_reporting_mode(OwnerReverseLookupMode::NoLookUp) + .with_whitelist_mode(WhitelistMode::Locked) + .with_minting_mode(MintingMode::Acl); + + support::assert_expected_invalid_installer_request( + install_request_builder, + 162, + "should fail execution since whitelist mode is locked and the provided whitelist is empty", + ); +} + +#[test] +fn should_disallow_installation_of_contract_with_empty_locked_whitelist() { + should_disallow_installation_of_contract_with_empty_locked_whitelist_with_holder_mode( + NFTHolderMode::Accounts, + ); + should_disallow_installation_of_contract_with_empty_locked_whitelist_with_holder_mode( + NFTHolderMode::Contracts, + ); + should_disallow_installation_of_contract_with_empty_locked_whitelist_with_holder_mode( + NFTHolderMode::Mixed, + ); +} + +#[test] +fn should_disallow_installation_with_zero_issuance() { + let mut builder = InMemoryWasmTestBuilder::default(); + builder + .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) + .commit(); + + let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) + .with_collection_name(NFT_TEST_COLLECTION.to_string()) + .with_collection_symbol(NFT_TEST_SYMBOL.to_string()) + .with_total_token_supply(0u64) + .with_ownership_mode(OwnershipMode::Minter) + .with_identifier_mode(NFTIdentifierMode::Ordinal) + .with_nft_metadata_kind(NFTMetadataKind::Raw) + .build(); + + builder.exec(install_request).expect_failure().commit(); + + let error = builder.get_error().expect("must have error"); + + support::assert_expected_error(error, 123u16, "cannot install when issuance is 0"); +} + +#[test] +fn should_disallow_installation_with_supply_exceeding_hard_cap() { + let mut builder = InMemoryWasmTestBuilder::default(); + builder + .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) + .commit(); + + let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) + .with_collection_name(NFT_TEST_COLLECTION.to_string()) + .with_collection_symbol(NFT_TEST_SYMBOL.to_string()) + .with_total_token_supply(1_000_001u64) + .with_ownership_mode(OwnershipMode::Minter) + .with_identifier_mode(NFTIdentifierMode::Ordinal) + .with_nft_metadata_kind(NFTMetadataKind::Raw) + .build(); + + builder.exec(install_request).expect_failure().commit(); + + let error = builder.get_error().expect("must have error"); + + support::assert_expected_error( + error, + 133u16, + "cannot install when issuance is more than 1_000_000", + ); +} + +#[test] +fn should_prevent_installation_with_ownership_and_minting_modality_conflict() { + let mut builder = InMemoryWasmTestBuilder::default(); + builder + .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) + .commit(); + + let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) + .with_collection_name(NFT_TEST_COLLECTION.to_string()) + .with_collection_symbol(NFT_TEST_SYMBOL.to_string()) + .with_total_token_supply(1_000u64) + .with_minting_mode(MintingMode::Installer) + .with_ownership_mode(OwnershipMode::Minter) + .with_reporting_mode(OwnerReverseLookupMode::Complete) + .build(); + + builder.exec(install_request).expect_failure().commit(); + + let error = builder.get_error().expect("must have error"); + + support::assert_expected_error( + error, + 130u16, + "cannot install when Ownership::Minter and MintingMode::Installer", + ); +} + +#[test] +fn should_prevent_installation_with_ownership_minter_and_owner_reverse_lookup_mode_transfer_only() { + let mut builder = InMemoryWasmTestBuilder::default(); + builder + .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) + .commit(); + + let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) + .with_collection_name(NFT_TEST_COLLECTION.to_string()) + .with_collection_symbol(NFT_TEST_SYMBOL.to_string()) + .with_total_token_supply(1_000u64) + .with_minting_mode(MintingMode::Installer) + .with_ownership_mode(OwnershipMode::Minter) + .with_reporting_mode(OwnerReverseLookupMode::TransfersOnly) + .build(); + + builder.exec(install_request).expect_failure().commit(); + + let error = builder.get_error().expect("must have error"); + + support::assert_expected_error( + error, + 140u16, + "cannot install when Ownership::Minter and OwnerReverseLookupMode::TransfersOnly", + ); +} + +#[test] +fn should_prevent_installation_with_ownership_assigned_and_owner_reverse_lookup_mode_transfer_only() +{ + let mut builder = InMemoryWasmTestBuilder::default(); + builder + .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) + .commit(); + + let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) + .with_collection_name(NFT_TEST_COLLECTION.to_string()) + .with_collection_symbol(NFT_TEST_SYMBOL.to_string()) + .with_total_token_supply(1_000u64) + .with_minting_mode(MintingMode::Installer) + .with_ownership_mode(OwnershipMode::Assigned) + .with_reporting_mode(OwnerReverseLookupMode::TransfersOnly) + .build(); + + builder.exec(install_request).expect_failure().commit(); + + let error = builder.get_error().expect("must have error"); + + support::assert_expected_error( + error, + 140u16, + "cannot install when Ownership::Assigned and OwnerReverseLookupMode::TransfersOnly", + ); +} + +#[test] +fn should_allow_installation_with_ownership_transferable_and_owner_reverse_lookup_mode_transfer_only( +) { + let mut builder = InMemoryWasmTestBuilder::default(); + builder + .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) + .commit(); + + let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) + .with_collection_name(NFT_TEST_COLLECTION.to_string()) + .with_collection_symbol(NFT_TEST_SYMBOL.to_string()) + .with_total_token_supply(1_000u64) + .with_minting_mode(MintingMode::Installer) + .with_ownership_mode(OwnershipMode::Transferable) + .with_reporting_mode(OwnerReverseLookupMode::TransfersOnly) + .build(); + + builder.exec(install_request).expect_success().commit(); +} +*/ diff --git a/modules/src/cep78/tests/mint.rs b/modules/src/cep78/tests/mint.rs new file mode 100644 index 00000000..30ced984 --- /dev/null +++ b/modules/src/cep78/tests/mint.rs @@ -0,0 +1,837 @@ +use odra::{ + args::Maybe, + casper_types::bytesrepr::ToBytes, + host::{Deployer, HostEnv, HostRef, NoArgs}, + DeployReport +}; +use serde::{de, Deserialize, Serialize}; + +use crate::cep78::{ + error::CEP78Error, + events::{ApprovalForAll, Mint, RevokedForAll}, + modalities::{ + EventsMode, MetadataMutability, MintingMode, NFTHolderMode, NFTIdentifierMode, + NFTMetadataKind, OwnerReverseLookupMode, OwnershipMode, TokenIdentifier, WhitelistMode + }, + tests::{ + utils::{ + self, TestContractHostRef, MALFORMED_META_DATA, TEST_COMPACT_META_DATA, + TEST_PRETTY_CEP78_METADATA, TEST_PRETTY_UPDATED_CEP78_METADATA + }, + TEST_CUSTOM_METADATA, TEST_CUSTOM_METADATA_SCHEMA + }, + token::CEP78HostRef +}; + +use super::{ + default_args_builder, + utils::{InitArgsBuilder, TEST_PRETTY_721_META_DATA}, + COLLECTION_NAME +}; + +#[derive(Serialize, Deserialize, Debug)] +struct Metadata { + name: String, + symbol: String, + token_uri: String +} + +fn default_token() -> (CEP78HostRef, HostEnv) { + let env = odra_test::env(); + let args = default_args_builder() + .total_token_supply(2u64) + .ownership_mode(OwnershipMode::Transferable) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .build(); + (CEP78HostRef::deploy(&env, args), env) +} + +#[test] +fn should_disallow_minting_when_allow_minting_is_set_to_false() { + let env = odra_test::env(); + let args = default_args_builder() + .nft_metadata_kind(NFTMetadataKind::NFT721) + .allow_minting(false) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + assert_eq!( + contract.try_mint( + env.get_account(0), + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ), + Err(CEP78Error::MintingIsPaused.into()), + "should now allow minting when minting is disabled", + ); +} + +#[test] +#[ignore = "Odra uses proxy pattern for contract calls, so this test is not applicable"] +fn entry_points_with_ret_should_return_correct_value() {} + +#[test] +fn should_mint() { + let env = odra_test::env(); + let args = default_args_builder() + .nft_metadata_kind(NFTMetadataKind::CEP78) + .events_mode(EventsMode::CES) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + let token_owner = env.get_account(0); + assert!(contract + .try_mint( + token_owner, + TEST_PRETTY_CEP78_METADATA.to_string(), + Maybe::None + ) + .is_ok()); + + // Expect Mint event. + let expected_event = Mint::new( + token_owner, + TokenIdentifier::Index(0).to_string(), + TEST_PRETTY_CEP78_METADATA.to_string() + ); + assert!(env.emitted_event(contract.address(), &expected_event)); +} + +#[test] +#[ignore = "Reverse lookup is not implemented yet"] +fn mint_should_return_dictionary_key_to_callers_owned_tokens() { + let env = odra_test::env(); + + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .allow_minting(true) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .build(); + let contract = CEP78HostRef::deploy(&env, args); + let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + + assert!(minting_contract + .try_mint_for( + contract.address(), + env.get_account(0), + TEST_PRETTY_721_META_DATA.to_string() + ) + .is_ok()); + + /* + let nft_receipt: String = + support::query_stored_value(&builder, nft_contract_key, vec![RECEIPT_NAME.to_string()]); + + let account_receipt = *account + .named_keys() + .get(&format!("{nft_receipt}_m_{PAGE_SIZE}_p_{}", 0)) + .expect("must have receipt"); + + let actual_page = builder + .query(None, account_receipt, &[]) + .expect("must have stored_value") + .as_cl_value() + .map(|page_cl_value| CLValue::into_t::>(page_cl_value.clone())) + .unwrap() + .unwrap(); + + let expected_page = { + let mut page = vec![false; PAGE_SIZE as usize]; + let _ = std::mem::replace(&mut page[0], true); + page + }; + + assert_eq!(actual_page, expected_page); + */ +} + +#[test] +fn mint_should_increment_number_of_minted_tokens_by_one_and_add_public_key_to_token_owners() { + let (mut contract, env) = default_token(); + let owner = env.get_account(0); + + // TODO: should register the owner first to create a page for the owner + contract.register_owner(Maybe::Some(owner)); + assert_eq!( + contract.try_mint(owner, TEST_PRETTY_721_META_DATA.to_string(), Maybe::None), + Ok(("".to_owned(), owner, "0".to_owned())) + ); + + assert_eq!( + contract.get_number_of_minted_tokens(), + 1u64, + "number_of_minted_tokens initialized at installation should have incremented by one" + ); + + let actual_token_meta_data = + contract.get_metadata_by_kind(NFTMetadataKind::NFT721, Maybe::Some(0u64), Maybe::None); + assert_eq!(actual_token_meta_data, TEST_PRETTY_721_META_DATA); + + let actual_token_owner = contract.owner_of(Maybe::Some(0u64), Maybe::None); + assert_eq!(actual_token_owner, owner); + + // Reverse lookup not implemented yet + /* let token_page = support::get_token_page_by_id( + &builder, + &nft_contract_key, + &Key::Account(*DEFAULT_ACCOUNT_ADDR), + token_id, + ); + + assert!(token_page[0]);*/ + + // If total_token_supply is initialized to 1 the following test should fail. + // If we set total_token_supply > 1 it should pass + assert!(contract + .try_mint(owner, TEST_PRETTY_721_META_DATA.to_string(), Maybe::None) + .is_ok()); +} + +#[test] +fn should_set_meta_data() { + let (mut contract, env) = default_token(); + + let token_owner = env.get_account(0); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let actual_token_meta_data = + contract.get_metadata_by_kind(NFTMetadataKind::NFT721, Maybe::Some(0u64), Maybe::None); + assert_eq!(actual_token_meta_data, TEST_PRETTY_721_META_DATA); +} +#[test] +fn should_set_issuer() { + let (mut contract, env) = default_token(); + + let token_owner = env.get_account(0); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let token_id = 0u64; + let actual_token_issuer = contract.get_token_issuer(Maybe::Some(token_id), Maybe::None); + assert_eq!(actual_token_issuer, token_owner); +} + +#[test] +fn should_set_issuer_with_different_owner() { + let (mut contract, env) = default_token(); + + let token_issuer = env.get_account(0); + let token_owner = env.get_account(1); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let token_id = 0u64; + let actual_token_issuer = contract.get_token_issuer(Maybe::Some(token_id), Maybe::None); + assert_eq!(actual_token_issuer, token_issuer); +} + +#[test] +fn should_track_token_balance_by_owner() { + let (mut contract, env) = default_token(); + + let token_owner = env.get_account(0); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let actual_minter_balance = contract.balance_of(token_owner); + let expected_minter_balance = 1u64; + assert_eq!(actual_minter_balance, expected_minter_balance); +} + +#[test] +fn should_allow_public_minting_with_flag_set_to_true() { + let env = odra_test::env(); + let args = default_args_builder() + .minting_mode(MintingMode::Public) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + let account_1 = env.get_account(1); + let minting_mode = contract.get_minting_mode(); + + assert_eq!( + minting_mode, + MintingMode::Public, + "public minting should be set to true" + ); + + let metadata = TEST_PRETTY_721_META_DATA.to_string(); + env.set_caller(account_1); + contract.mint(account_1, metadata, Maybe::None); + + let token_id = 0u64; + let minter_account_hash = contract.owner_of(Maybe::Some(token_id), Maybe::None); + assert_eq!(account_1, minter_account_hash); +} + +#[test] +fn should_disallow_public_minting_with_flag_set_to_false() { + let env = odra_test::env(); + let args = default_args_builder() + .minting_mode(MintingMode::Installer) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + let account_1 = env.get_account(1); + let metadata = TEST_PRETTY_721_META_DATA.to_string(); + + let minting_mode = contract.get_minting_mode(); + assert_eq!( + minting_mode, + MintingMode::Installer, + "public minting should be set to false" + ); + + env.set_caller(account_1); + assert_eq!( + contract.try_mint(account_1, metadata, Maybe::None), + Err(CEP78Error::InvalidMinter.into()), + "should not allow minting when minting is disabled" + ); +} + +#[test] +fn should_allow_minting_for_different_public_key_with_minting_mode_set_to_public() { + let env = odra_test::env(); + let args = default_args_builder() + .minting_mode(MintingMode::Public) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + let account_1 = env.get_account(1); + let account_2 = env.get_account(2); + + let minting_mode = contract.get_minting_mode(); + assert_eq!( + minting_mode, + MintingMode::Public, + "public minting should be set to true" + ); + + let metadata = TEST_PRETTY_721_META_DATA.to_string(); + assert!(contract + .try_mint(account_1, metadata.clone(), Maybe::None) + .is_ok()); + assert!(contract.try_mint(account_2, metadata, Maybe::None).is_ok()); +} + +#[test] +fn should_set_approval_for_all() { + let env = odra_test::env(); + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .events_mode(EventsMode::CES) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + let owner = env.get_account(0); + contract.mint(owner, TEST_PRETTY_721_META_DATA.to_string(), Maybe::None); + + let operator = env.get_account(1); + contract.set_approval_for_all(true, operator); + + let is_operator = contract.is_approved_for_all(owner, operator); + assert!(is_operator, "expected operator to be approved for all"); + + // Expect ApprovalForAll event. + let expected_event = ApprovalForAll::new(owner, operator); + assert!(env.emitted_event(contract.address(), &expected_event)); + + // Test if two minted tokens are transferable by operator + let token_receiver = env.get_account(1); + contract.register_owner(Maybe::Some(token_receiver)); + + let token_id = 0u64; + // Transfer first minted token by operator + let result = contract.try_transfer(Maybe::Some(token_id), Maybe::None, owner, token_receiver); + assert!(result.is_ok()); + + let actual_token_owner = contract.owner_of(Maybe::Some(token_id), Maybe::None); + assert_eq!(actual_token_owner, token_receiver); + + // Second mint by owner + contract.mint(owner, TEST_PRETTY_721_META_DATA.to_string(), Maybe::None); + + let token_id = 1u64; + // Transfer second minted token by operator + let result = contract.try_transfer(Maybe::Some(token_id), Maybe::None, owner, token_receiver); + assert!(result.is_ok()); + + let actual_token_owner = contract.owner_of(Maybe::Some(token_id), Maybe::None); + assert_eq!(actual_token_owner, token_receiver); +} + +#[test] +fn should_revoke_approval_for_all() { + let env = odra_test::env(); + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .events_mode(EventsMode::CES) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + let owner = env.get_account(0); + contract.mint(owner, TEST_PRETTY_721_META_DATA.to_string(), Maybe::None); + + let operator = env.get_account(1); + assert!(contract.try_set_approval_for_all(true, operator).is_ok()); + + let is_operator = contract.is_approved_for_all(owner, operator); + assert!(is_operator, "expected operator to be approved for all"); + + // Expect ApprovalForAll event. + let expected_event = ApprovalForAll::new(owner, operator); + assert!(env.emitted_event(contract.address(), &expected_event),); + + assert!(contract.try_set_approval_for_all(false, operator).is_ok()); + + let is_operator = contract.is_approved_for_all(owner, operator); + assert!(!is_operator, "expected operator not to be approved for all"); + + // Expect RevokedForAll event. + let expected_event = RevokedForAll::new(owner, operator); + assert!(env.emitted_event(contract.address(), &expected_event)); +} + +#[test] +fn should_not_mint_with_invalid_nft721_metadata() { + let (mut contract, env) = default_token(); + assert_eq!( + contract.try_mint( + env.get_account(0), + MALFORMED_META_DATA.to_string(), + Maybe::None + ), + Err(CEP78Error::FailedToParse721Metadata.into()), + "should not mint with invalid metadata" + ); +} + +#[test] +fn should_mint_with_compactified_metadata() { + let (mut contract, env) = default_token(); + contract.mint( + env.get_account(0), + TEST_COMPACT_META_DATA.to_string(), + Maybe::None + ); + + let token_id = 0u64; + let actual_metadata = + contract.get_metadata_by_kind(NFTMetadataKind::NFT721, Maybe::Some(token_id), Maybe::None); + assert_eq!(TEST_PRETTY_721_META_DATA, actual_metadata); +} + +#[test] +fn should_mint_with_valid_cep78_metadata() { + let env = odra_test::env(); + let args = default_args_builder() + .nft_metadata_kind(NFTMetadataKind::CEP78) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + contract.mint( + env.get_account(0), + TEST_PRETTY_CEP78_METADATA.to_string(), + Maybe::None + ); + + let token_id = 0u64; + let actual_metadata = + contract.get_metadata_by_kind(NFTMetadataKind::CEP78, Maybe::Some(token_id), Maybe::None); + assert_eq!(TEST_PRETTY_CEP78_METADATA, actual_metadata) +} + +#[test] +fn should_mint_with_custom_metadata_validation() { + let env = odra_test::env(); + let custom_json_schema = + serde_json::to_string(&*TEST_CUSTOM_METADATA_SCHEMA).expect("must convert to json schema"); + let args = default_args_builder() + .nft_metadata_kind(NFTMetadataKind::CustomValidated) + .json_schema(custom_json_schema) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + let metadata = + serde_json::to_string(&*TEST_CUSTOM_METADATA).expect("must convert to json metadata"); + contract.mint(env.get_account(0), metadata, Maybe::None); + + let token_id = 0u64; + let actual_metadata = contract.get_metadata_by_kind( + NFTMetadataKind::CustomValidated, + Maybe::Some(token_id), + Maybe::None + ); + let pretty_custom_metadata = serde_json::to_string_pretty(&*TEST_CUSTOM_METADATA) + .expect("must convert to json metadata"); + assert_eq!(pretty_custom_metadata, actual_metadata) +} + +#[test] +fn should_mint_with_raw_metadata() { + let env = odra_test::env(); + let args = default_args_builder() + .nft_metadata_kind(NFTMetadataKind::Raw) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + contract.mint(env.get_account(0), "raw_string".to_string(), Maybe::None); + + let token_id = 0u64; + let actual_metadata = + contract.get_metadata_by_kind(NFTMetadataKind::Raw, Maybe::Some(token_id), Maybe::None); + assert_eq!("raw_string".to_string(), actual_metadata) +} + +#[test] +fn should_mint_with_hash_identifier_mode() { + let env = odra_test::env(); + let args = default_args_builder() + .identifier_mode(NFTIdentifierMode::Hash) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + contract.mint( + env.get_account(0), + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let token_id_hash = + base16::encode_lower(&utils::create_blake2b_hash(TEST_PRETTY_721_META_DATA)); + + // TODO: Reverse lookup not implemented yet + /* + let token_page = get_token_page_by_hash( + &builder, + &nft_contract_key, + &Key::Account(*DEFAULT_ACCOUNT_ADDR), + token_id_hash, + ); + + assert!(token_page[0])*/ +} + +#[test] +fn should_fail_to_mint_when_immediate_caller_is_account_in_contract_mode() { + let env = odra_test::env(); + let args = default_args_builder() + .holder_mode(NFTHolderMode::Contracts) + .whitelist_mode(WhitelistMode::Unlocked) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + assert_eq!( + contract.try_mint( + env.get_account(0), + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ), + Err(CEP78Error::InvalidHolderMode.into()) + ); +} + +#[test] +fn should_approve_in_hash_identifier_mode() { + let env = odra_test::env(); + let args = default_args_builder() + .identifier_mode(NFTIdentifierMode::Hash) + .metadata_mutability(MetadataMutability::Immutable) + .ownership_mode(OwnershipMode::Transferable) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + contract.mint( + env.get_account(0), + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + let blake2b_hash = utils::create_blake2b_hash(TEST_PRETTY_721_META_DATA.to_bytes().unwrap()); + let token_hash = base16::encode_lower(&blake2b_hash); + let spender = env.get_account(1); + contract.approve(spender, Maybe::None, Maybe::Some(token_hash.clone())); + + let approved_account = contract.get_approved(Maybe::None, Maybe::Some(token_hash.clone())); + assert_eq!(approved_account, Some(spender)) +} + +#[test] +fn should_mint_without_returning_receipts_and_flat_gas_cost() { + let env = odra_test::env(); + let args = default_args_builder() + .identifier_mode(NFTIdentifierMode::Ordinal) + .nft_metadata_kind(NFTMetadataKind::Raw) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + contract.mint(env.get_account(0), "".to_string(), Maybe::None); + contract.mint(env.get_account(1), "".to_string(), Maybe::None); + + let costs = env + .gas_report() + .into_iter() + .filter_map(|r| match r { + DeployReport::WasmDeploy { .. } => None, + DeployReport::ContractCall { gas, .. } => Some(gas) + }) + .collect::>(); + + // In this case there is no first time allocation of a page. + // Therefore the second and first mints must have equivalent gas costs. + assert_eq!(costs.get(0), costs.get(1)) +} + +// A test to ensure that the page table allocation is preserved +// even if the "register_owner" is called twice. +#[test] +fn should_maintain_page_table_despite_invoking_register_owner() { + let env = odra_test::env(); + let args = default_args_builder() + .identifier_mode(NFTIdentifierMode::Ordinal) + .nft_metadata_kind(NFTMetadataKind::Raw) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + // TODO: register_owner is not implemented yet + contract.register_owner(Maybe::Some(token_owner)); + contract.mint(token_owner, "".to_string(), Maybe::None); + + // TODO: page table is not implemented yet + // let actual_page_table = contract.get_page_table(); + // assert_eq!(actual_page_table.len(), 1); + + // The mint WASM will register the owner, now we re-invoke the same entry point + // and ensure that the page table doesn't mutate. + contract.register_owner(Maybe::Some(token_owner)); + + // let table_post_register = contract.get_page_table(); + // assert_eq!(actual_page_table, table_post_register) +} + +#[test] +fn should_prevent_mint_to_unregistered_owner() { + let env = odra_test::env(); + let args = default_args_builder() + .identifier_mode(NFTIdentifierMode::Ordinal) + .nft_metadata_kind(NFTMetadataKind::Raw) + .ownership_mode(OwnershipMode::Transferable) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + assert_eq!( + contract.try_mint(env.get_account(0), "".to_string(), Maybe::None), + Err(CEP78Error::UnregisteredOwnerInMint.into()) + ); +} + +#[test] +fn should_mint_with_two_required_metadata_kind() { + let env = odra_test::env(); + let args = default_args_builder() + .identifier_mode(NFTIdentifierMode::Ordinal) + .nft_metadata_kind(NFTMetadataKind::CEP78) + .ownership_mode(OwnershipMode::Transferable) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .additional_required_metadata(vec![NFTMetadataKind::Raw]) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + // TODO: register_owner is not implemented yet + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_CEP78_METADATA.to_string(), + Maybe::None + ); + + let token_id = 0u64; + let meta_78 = + contract.get_metadata_by_kind(NFTMetadataKind::CEP78, Maybe::Some(token_id), Maybe::None); + let meta_raw = + contract.get_metadata_by_kind(NFTMetadataKind::Raw, Maybe::Some(token_id), Maybe::None); + + assert_eq!(meta_78, TEST_PRETTY_CEP78_METADATA); + assert_eq!(meta_raw, TEST_PRETTY_CEP78_METADATA); +} + +#[test] +fn should_mint_with_one_required_one_optional_metadata_kind_without_optional() { + let env = odra_test::env(); + let args = default_args_builder() + .identifier_mode(NFTIdentifierMode::Ordinal) + .nft_metadata_kind(NFTMetadataKind::CEP78) + .ownership_mode(OwnershipMode::Transferable) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .optional_metadata(vec![NFTMetadataKind::Raw]) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_CEP78_METADATA.to_string(), + Maybe::None + ); + + let token_id = 0u64; + let meta_78 = + contract.get_metadata_by_kind(NFTMetadataKind::CEP78, Maybe::Some(token_id), Maybe::None); + let meta_raw = + contract.get_metadata_by_kind(NFTMetadataKind::Raw, Maybe::Some(token_id), Maybe::None); + + assert_eq!(meta_78, TEST_PRETTY_CEP78_METADATA); + assert_eq!(meta_raw, TEST_PRETTY_CEP78_METADATA); + + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_CEP78_METADATA.to_string(), + Maybe::None + ); + + let token_id = 1u64; + let meta_78 = + contract.get_metadata_by_kind(NFTMetadataKind::CEP78, Maybe::Some(token_id), Maybe::None); + + assert_eq!(meta_78, TEST_PRETTY_CEP78_METADATA); +} + +#[test] +fn should_not_mint_with_missing_required_metadata() { + let env = odra_test::env(); + let args = default_args_builder() + .identifier_mode(NFTIdentifierMode::Ordinal) + .nft_metadata_kind(NFTMetadataKind::CEP78) + .ownership_mode(OwnershipMode::Transferable) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .additional_required_metadata(vec![NFTMetadataKind::NFT721]) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + + contract.register_owner(Maybe::Some(token_owner)); + assert_eq!( + contract.try_mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ), + Err(CEP78Error::FailedToParseCep99Metadata.into()) + ); +} + +#[test] +fn should_mint_with_transfer_only_reporting() { + let env = odra_test::env(); + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .nft_metadata_kind(NFTMetadataKind::CEP78) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::TransfersOnly) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + contract.mint( + token_owner, + TEST_PRETTY_CEP78_METADATA.to_string(), + Maybe::None + ); + + let actual_balance_after_mint = contract.balance_of(token_owner); + let expected_balance_after_mint = 1u64; + assert_eq!(actual_balance_after_mint, expected_balance_after_mint); +} + +#[test] +fn should_approve_all_in_hash_identifier_mode() { + let env = odra_test::env(); + let args = default_args_builder() + .identifier_mode(NFTIdentifierMode::Hash) + .ownership_mode(OwnershipMode::Transferable) + .nft_metadata_kind(NFTMetadataKind::CEP78) + .events_mode(EventsMode::CES) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + let operator = env.get_account(1); + contract.mint( + token_owner, + TEST_PRETTY_CEP78_METADATA.to_string(), + Maybe::None + ); + contract.mint( + token_owner, + TEST_PRETTY_UPDATED_CEP78_METADATA.to_string(), + Maybe::None + ); + + contract.set_approval_for_all(true, operator); + + let is_operator = contract.is_approved_for_all(token_owner, operator); + assert!(is_operator, "expected operator to be approved for all"); + + // Expect ApprovalForAll event. + let expected_event = ApprovalForAll::new(token_owner, operator); + assert!( + env.emitted_event(contract.address(), &expected_event), + "Expected ApprovalForAll event." + ); +} + +#[test] +fn should_approve_all_with_flat_gas_cost() { + let env = odra_test::env(); + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + let operator = env.get_account(1); + let other_operator = env.get_account(2); + + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + contract.set_approval_for_all(true, operator); + let is_operator = contract.is_approved_for_all(token_owner, operator); + assert!(is_operator, "expected operator to be approved for all"); + + contract.set_approval_for_all(true, other_operator); + let is_also_operator = contract.is_approved_for_all(token_owner, other_operator); + assert!( + is_also_operator, + "expected other operator to be approved for all" + ); + let gas_report = env.gas_report(); + let costs = gas_report + .into_iter() + .filter_map(|r| match r { + DeployReport::WasmDeploy { .. } => None, + DeployReport::ContractCall { gas, call_def, .. } => { + if call_def.entry_point() == "set_approval_for_all" { + Some(gas) + } else { + None + } + } + }) + .collect::>(); + + // Operator approval should have flat gas costs + // Therefore the second and first set_approve_for_all must have equivalent gas costs. + assert_eq!(costs.get(0), costs.get(1)); +} diff --git a/modules/src/cep78/tests/mod.rs b/modules/src/cep78/tests/mod.rs index 74fd2963..9622a05e 100644 --- a/modules/src/cep78/tests/mod.rs +++ b/modules/src/cep78/tests/mod.rs @@ -1,8 +1,16 @@ +use alloc::collections::BTreeMap; +use once_cell::sync::Lazy; + use self::utils::InitArgsBuilder; -use super::modalities::NFTMetadataKind; +use super::{ + metadata::{CustomMetadataSchema, MetadataSchemaProperty}, + modalities::NFTMetadataKind +}; mod acl; +mod installer; +mod mint; mod set_variables; mod utils; @@ -16,3 +24,31 @@ pub(super) fn default_args_builder() -> InitArgsBuilder { .total_token_supply(100u64) .nft_metadata_kind(NFTMetadataKind::NFT721) } + +pub(super) static TEST_CUSTOM_METADATA: Lazy> = Lazy::new(|| { + let mut attributes = BTreeMap::new(); + attributes.insert("deity_name".to_string(), "Baldur".to_string()); + attributes.insert("mythology".to_string(), "Nordic".to_string()); + attributes +}); + +pub(crate) static TEST_CUSTOM_METADATA_SCHEMA: Lazy = Lazy::new(|| { + let mut properties = BTreeMap::new(); + properties.insert( + "deity_name".to_string(), + MetadataSchemaProperty { + name: "deity_name".to_string(), + description: "The name of deity from a particular pantheon.".to_string(), + required: true + } + ); + properties.insert( + "mythology".to_string(), + MetadataSchemaProperty { + name: "mythology".to_string(), + description: "The mythology the deity belongs to.".to_string(), + required: true + } + ); + CustomMetadataSchema { properties } +}); diff --git a/modules/src/cep78/tests/utils.rs b/modules/src/cep78/tests/utils.rs index e30e79be..0f7ef153 100644 --- a/modules/src/cep78/tests/utils.rs +++ b/modules/src/cep78/tests/utils.rs @@ -5,7 +5,13 @@ use crate::cep78::{ }, token::CEP78InitArgs }; -use odra::{args::Maybe, Address}; +use blake2::{ + digest::{Update, VariableOutput}, + Blake2bVar +}; +use odra::{args::Maybe, casper_types::BLAKE2B_DIGEST_LENGTH, prelude::*, Address, ContractRef}; + +use super::acl::NftContractContractRef; #[derive(Default)] pub struct InitArgsBuilder { @@ -153,6 +159,19 @@ impl InitArgsBuilder { self } + pub fn additional_required_metadata( + mut self, + additional_required_metadata: Vec + ) -> Self { + self.additional_required_metadata = Maybe::Some(additional_required_metadata); + self + } + + pub fn optional_metadata(mut self, optional_metadata: Vec) -> Self { + self.optional_metadata = Maybe::Some(optional_metadata); + self + } + pub fn build(self) -> CEP78InitArgs { CEP78InitArgs { collection_name: self.collection_name, @@ -182,29 +201,72 @@ impl InitArgsBuilder { } pub const TEST_PRETTY_721_META_DATA: &str = r#"{ - "name": "John Doe", - "symbol": "abc", - "token_uri": "https://www.barfoo.com" - }"#; + "name": "John Doe", + "symbol": "abc", + "token_uri": "https://www.barfoo.com" +}"#; pub const TEST_PRETTY_UPDATED_721_META_DATA: &str = r#"{ - "name": "John Doe", - "symbol": "abc", - "token_uri": "https://www.foobar.com" - }"#; + "name": "John Doe", + "symbol": "abc", + "token_uri": "https://www.foobar.com" +}"#; pub const TEST_PRETTY_CEP78_METADATA: &str = r#"{ - "name": "John Doe", - "token_uri": "https://www.barfoo.com", - "checksum": "940bffb3f2bba35f84313aa26da09ece3ad47045c6a1292c2bbd2df4ab1a55fb" - }"#; + "name": "John Doe", + "token_uri": "https://www.barfoo.com", + "checksum": "940bffb3f2bba35f84313aa26da09ece3ad47045c6a1292c2bbd2df4ab1a55fb" +}"#; pub const TEST_PRETTY_UPDATED_CEP78_METADATA: &str = r#"{ - "name": "John Doe", - "token_uri": "https://www.foobar.com", - "checksum": "fda4feaa137e83972db628e521c92159f5dc253da1565c9da697b8ad845a0788" - }"#; + "name": "John Doe", + "token_uri": "https://www.foobar.com", + "checksum": "fda4feaa137e83972db628e521c92159f5dc253da1565c9da697b8ad845a0788" +}"#; pub const TEST_COMPACT_META_DATA: &str = r#"{"name": "John Doe","symbol": "abc","token_uri": "https://www.barfoo.com"}"#; pub const MALFORMED_META_DATA: &str = r#"{ - "name": "John Doe", - "symbol": abc, - "token_uri": "https://www.barfoo.com" - }"#; + "name": "John Doe", + "symbol": abc, + "token_uri": "https://www.barfoo.com" +}"#; + +#[odra::module] +struct DummyContract; + +#[odra::module] +impl DummyContract {} + +#[odra::module] +struct TestContract; + +#[odra::module] +impl TestContract { + pub fn mint( + &mut self, + nft_contract_address: &Address, + token_metadata: String + ) -> (String, Address, String) { + NftContractContractRef::new(self.env(), *nft_contract_address) + .mint(self.env().self_address(), token_metadata) + } + + pub fn mint_for( + &mut self, + nft_contract_address: &Address, + token_owner: Address, + token_metadata: String + ) -> (String, Address, String) { + NftContractContractRef::new(self.env(), *nft_contract_address) + .mint(token_owner, token_metadata) + } +} +use std::collections::hash_map::DefaultHasher; +use std::hash::Hasher; +use std::io::Write; +pub(crate) fn create_blake2b_hash>(data: T) -> [u8; BLAKE2B_DIGEST_LENGTH] { + let mut result = [0u8; 32]; + let mut hasher = ::new(32).expect("should create hasher"); + let _ = hasher.write(data.as_ref()); + hasher + .finalize_variable(&mut result) + .expect("should copy hash to the result array"); + result +} diff --git a/modules/src/cep78/token.rs b/modules/src/cep78/token.rs index fbb5f7da..2e2c488f 100644 --- a/modules/src/cep78/token.rs +++ b/modules/src/cep78/token.rs @@ -20,7 +20,7 @@ use super::{ }; use odra::{ args::Maybe, - casper_types::{bytesrepr::ToBytes, URef}, + casper_types::{bytesrepr::ToBytes, AccessRights, URef}, prelude::*, Address, Mapping, Sequence, SubModule, UnwrapOrRevert, Var }; @@ -216,27 +216,27 @@ impl CEP78 { let token_identifier: TokenIdentifier = match identifier_mode { NFTIdentifierMode::Ordinal => TokenIdentifier::Index(minted_tokens_count), NFTIdentifierMode::Hash => TokenIdentifier::Hash(if optional_token_hash.is_empty() { - base16::encode_lower(&self.env().hash(token_metadata.clone())) + let hash = self.env().hash(token_metadata.clone()); + base16::encode_lower(&hash) } else { optional_token_hash }) }; + let token_id = token_identifier.to_string(); - self.metadata - .update_or_revert(&token_metadata, &token_identifier); + self.metadata.update_or_revert(&token_metadata, &token_id); // The contract's ownership behavior (determined at installation) determines, // who owns the NFT we are about to mint.() - let token_owner_key = + let token_owner = if let OwnershipMode::Assigned | OwnershipMode::Transferable = self.ownership_mode() { token_owner } else { caller }; - let id = token_identifier.to_string(); - self.owners.set(&id, token_owner_key); - self.issuers.set(&id, caller); + self.owners.set(&token_id, token_owner); + self.issuers.set(&token_id, caller); if let NFTIdentifierMode::Hash = identifier_mode { // Update the forward and reverse trackers @@ -245,33 +245,35 @@ impl CEP78 { } //Increment the count of owned tokens. - self.token_count.add(&token_owner_key, 1); + self.token_count.add(&token_owner, 1); // Increment number_of_minted_tokens by one self.info.increment_number_of_minted_tokens(); // Emit Mint event. self.emit_ces_event(Mint::new( - token_owner_key, - token_identifier.clone(), + token_owner, + token_id.clone(), token_metadata.clone() )); if let OwnerReverseLookupMode::Complete = self.reverse_lookup.get_mode() { let (page_table_entry, page_uref) = self.pagination.add_page_entry_and_page_record( minted_tokens_count, - &token_owner_key, + &token_owner, true ); + /* let receipt_name = self.receipt_name.get_or_default(); let receipt_string = format!("{receipt_name}_m_{PAGE_SIZE}_p_{page_table_entry}"); // TODO: Implement the following - // let receipt_address = Key::dictionary(page_uref, owned_tokens_item_key.as_bytes()); + // let receipt_address = Key::dictionary(page_uref, owned_tokens_item.as_bytes()); let token_identifier_string = token_identifier.to_string(); // should not return `token_owner` return (receipt_string, token_owner, token_identifier_string); + */ } - (id, token_owner_key, token_metadata) + ("".to_string(), token_owner, token_id) } /// Burns the token with provided `token_id` argument, after which it is no @@ -284,8 +286,9 @@ impl CEP78 { self.ensure_burnable(); let token_identifier = self.token_identifier(token_id, token_hash); + let token_id = token_identifier.to_string(); - let token_owner = self.owner_of_by_id(&token_identifier); + let token_owner = self.owner_of_by_id(&token_id); let caller = self.env().caller(); // Check if caller is owner @@ -306,14 +309,14 @@ impl CEP78 { // It makes sense to keep this token as owned by the caller. It just happens that the caller // owns a burnt token. That's all. Similarly, we should probably also not change the // owned_tokens dictionary. - self.ensure_not_burned(&token_identifier); + self.ensure_not_burned(&token_id); // Mark the token as burnt by adding the token_id to the burnt tokens dictionary. - self.burnt_tokens.set(&token_identifier.to_string(), ()); + self.burnt_tokens.set(&token_id, ()); self.token_count.subtract(&token_owner, 1); // Emit Burn event. - self.emit_ces_event(Burn::new(token_owner, token_identifier, caller)); + self.emit_ces_event(Burn::new(token_owner, token_id, caller)); } /// Transfers ownership of the token from one account to another. @@ -334,12 +337,12 @@ impl CEP78 { let token_identifier = self.checked_token_identifier(token_id, token_hash); let token_id = token_identifier.to_string(); // We assume we cannot transfer burnt tokens - self.ensure_not_burned(&token_identifier); - self.ensure_not_owner(&token_identifier, &source_key); + self.ensure_not_burned(&token_id); + self.ensure_owner(&token_id, &source_key); let caller = self.env().caller(); - let owner = self.owner_of_by_id(&token_identifier); + let owner = self.owner_of_by_id(&token_id); // Check if caller is owner let is_owner = owner == caller; @@ -377,7 +380,7 @@ impl CEP78 { if token_actual_owner != source_key { self.env().revert(CEP78Error::InvalidTokenOwner) } - self.owners.set(&token_identifier.to_string(), target_key); + self.owners.set(&token_id, target_key); } None => self .env() @@ -390,12 +393,7 @@ impl CEP78 { self.approved.set(&token_id, Option::
::None); let spender = if caller == owner { None } else { Some(caller) }; - self.emit_ces_event(Transfer::new( - owner, - spender, - target_key, - token_identifier.clone() - )); + self.emit_ces_event(Transfer::new(owner, spender, target_key, token_id)); let reporting_mode = self.reverse_lookup.get_mode(); @@ -421,7 +419,7 @@ impl CEP78 { // TODO: should not return `source_key` return (receipt_string, source_key); } - todo!() + return ("".to_owned(), source_key); } /// Approves another token holder (an approved account) to transfer tokens. It @@ -434,8 +432,9 @@ impl CEP78 { let caller = self.env().caller(); let token_identifier = self.checked_token_identifier(token_id, token_hash); + let token_id = token_identifier.to_string(); - let owner = self.owner_of_by_id(&token_identifier); + let owner = self.owner_of_by_id(&token_id); // Revert if caller is not token owner nor operator. // Only the token owner or an operator can approve an account @@ -447,13 +446,12 @@ impl CEP78 { } // We assume a burnt token cannot be approved - self.ensure_not_burned(&token_identifier); + self.ensure_not_burned(&token_id); // If token owner or operator tries to approve itself that's probably a mistake and we revert. self.ensure_not_caller(spender); - self.approved - .set(&token_identifier.to_string(), Some(spender)); - self.emit_ces_event(Approval::new(owner, spender, token_identifier)); + self.approved.set(&token_id, Some(spender)); + self.emit_ces_event(Approval::new(owner, spender, token_id)); } /// Revokes an approved account to transfer tokens. It reverts @@ -467,10 +465,10 @@ impl CEP78 { let caller = env.caller(); let token_identifier = self.checked_token_identifier(token_id, token_hash); - + let token_id = token_identifier.to_string(); // Revert if caller is not the token owner or an operator. Only the token owner / operators can // revoke an approved account - let owner = self.owner_of_by_id(&token_identifier); + let owner = self.owner_of_by_id(&token_id); let is_owner = caller == owner; let is_operator = !is_owner && self.operators.get_or_default(&(owner, caller)); @@ -479,11 +477,10 @@ impl CEP78 { } // We assume a burnt token cannot be revoked - self.ensure_not_burned(&token_identifier); - self.approved - .set(&token_identifier.to_string(), Option::
::None); + self.ensure_not_burned(&token_id); + self.approved.set(&token_id, Option::
::None); // Emit ApprovalRevoked event. - self.emit_ces_event(ApprovalRevoked::new(owner, token_identifier)); + self.emit_ces_event(ApprovalRevoked::new(owner, token_id)); } /// Approves all tokens owned by the caller and future to another token holder @@ -517,9 +514,9 @@ impl CEP78 { /// Returns the token owner given a token_id. It reverts if token_id /// is invalid. A burnt token still has an associated owner. - pub fn owner_of(&mut self, token_id: Maybe, token_hash: Maybe) -> Address { + pub fn owner_of(&self, token_id: Maybe, token_hash: Maybe) -> Address { let token_identifier = self.checked_token_identifier(token_id, token_hash); - self.owner_of_by_id(&token_identifier) + self.owner_of_by_id(&token_identifier.to_string()) } /// Returns the approved account (if any) associated with the provided token_id @@ -530,9 +527,10 @@ impl CEP78 { token_hash: Maybe ) -> Option
{ let token_identifier: TokenIdentifier = self.checked_token_identifier(token_id, token_hash); + let token_id = token_identifier.to_string(); - self.ensure_not_burned(&token_identifier); - self.approved.get(&token_identifier.to_string()).flatten() + self.ensure_not_burned(&token_id); + self.approved.get(&token_id).flatten() } /// Returns the metadata associated with the provided token_id @@ -552,14 +550,12 @@ impl CEP78 { .ensure_mutability(CEP78Error::ForbiddenMetadataUpdate); let token_identifier = self.checked_token_identifier(token_id, token_hash); - self.ensure_owner_not_caller(&token_identifier); + let token_id = token_identifier.to_string(); + self.ensure_owner_not_caller(&token_id); self.metadata - .update_or_revert(&updated_token_metadata, &token_identifier); + .update_or_revert(&updated_token_metadata, &token_id); - self.emit_ces_event(MetadataUpdated::new( - token_identifier, - updated_token_metadata - )); + self.emit_ces_event(MetadataUpdated::new(token_id, updated_token_metadata)); } /// Returns number of owned tokens associated with the provided token holder @@ -606,7 +602,8 @@ impl CEP78 { self.pagination.register_owner(&owner); } - todo!() + // TODO: Implement the following + ("".to_string(), URef::new([0u8; 32], AccessRights::READ)) } pub fn is_whitelisted(&self, address: &Address) -> bool { @@ -621,12 +618,47 @@ impl CEP78 { self.info.collection_name() } + pub fn get_collection_symbol(&self) -> String { + self.info.collection_symbol() + } + pub fn is_minting_allowed(&self) -> bool { self.settings.allow_minting() } + pub fn is_operator_burn_mode(&self) -> bool { self.settings.operator_burn_mode() } + + pub fn get_total_supply(&self) -> u64 { + self.info.total_token_supply() + } + + pub fn get_minting_mode(&self) -> MintingMode { + self.settings.minting_mode() + } + + pub fn get_number_of_minted_tokens(&self) -> u64 { + self.info.number_of_minted_tokens() + } + + pub fn get_metadata_by_kind( + &self, + kind: NFTMetadataKind, + token_id: Maybe, + token_hash: Maybe + ) -> String { + let token_identifier = self.checked_token_identifier(token_id, token_hash); + self.metadata + .get_metadata_by_kind(token_identifier.to_string(), &kind) + } + + pub fn get_token_issuer(&self, token_id: Maybe, token_hash: Maybe) -> Address { + let token_identifier = self.checked_token_identifier(token_id, token_hash); + self.issuers + .get(&token_identifier.to_string()) + .unwrap_or_revert_with(&self.env(), CEP78Error::InvalidTokenIdentifier) + } } impl CEP78 { @@ -679,8 +711,8 @@ impl CEP78 { } #[inline] - fn owner_of_by_id(&self, id: &TokenIdentifier) -> Address { - match self.owners.get(&id.to_string()) { + fn owner_of_by_id(&self, id: &String) -> Address { + match self.owners.get(id) { Some(token_owner) => token_owner, None => self .env() @@ -689,31 +721,29 @@ impl CEP78 { } #[inline] - fn is_token_burned(&self, token_identifier: &TokenIdentifier) -> bool { - self.burnt_tokens - .get(&token_identifier.to_string()) - .is_some() + fn is_token_burned(&self, token_id: &String) -> bool { + self.burnt_tokens.get(token_id).is_some() } #[inline] - fn ensure_not_owner(&self, token_identifier: &TokenIdentifier, address: &Address) { - let owner = self.owner_of_by_id(token_identifier); - if address == &owner { + fn ensure_owner(&self, token_id: &String, address: &Address) { + let owner = self.owner_of_by_id(token_id); + if address != &owner { self.env().revert(CEP78Error::InvalidAccount); } } #[inline] - fn ensure_owner_not_caller(&self, token_identifier: &TokenIdentifier) { - let owner = self.owner_of_by_id(token_identifier); + fn ensure_owner_not_caller(&self, token_id: &String) { + let owner = self.owner_of_by_id(token_id); if self.env().caller() == owner { self.env().revert(CEP78Error::InvalidTokenOwner); } } #[inline] - fn ensure_not_burned(&self, token_identifier: &TokenIdentifier) { - if self.is_token_burned(token_identifier) { + fn ensure_not_burned(&self, token_id: &String) { + if self.is_token_burned(token_id) { self.env().revert(CEP78Error::PreviouslyBurntToken); } } diff --git a/modules/src/lib.rs b/modules/src/lib.rs index 8c45fd9e..595c2116 100644 --- a/modules/src/lib.rs +++ b/modules/src/lib.rs @@ -1,6 +1,7 @@ #![doc = "Odra's library of plug and play modules"] #![cfg_attr(not(test), no_std)] #![cfg_attr(not(test), no_main)] +#![recursion_limit = "256"] extern crate alloc; From 637fdbe0321acbfed487f4edf51140e0a588df20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Fri, 19 Apr 2024 09:13:47 +0200 Subject: [PATCH 11/38] Add burn tests --- modules/src/cep78/pagination.rs | 6 +- modules/src/cep78/tests/acl.rs | 8 +- modules/src/cep78/tests/burn.rs | 321 +++++++++++++++++++++++++++++++ modules/src/cep78/tests/mint.rs | 7 +- modules/src/cep78/tests/mod.rs | 1 + modules/src/cep78/tests/utils.rs | 23 ++- modules/src/cep78/token.rs | 6 + 7 files changed, 350 insertions(+), 22 deletions(-) create mode 100644 modules/src/cep78/tests/burn.rs diff --git a/modules/src/cep78/pagination.rs b/modules/src/cep78/pagination.rs index df01e908..8e324977 100644 --- a/modules/src/cep78/pagination.rs +++ b/modules/src/cep78/pagination.rs @@ -123,8 +123,10 @@ impl Pagination { (page_table_entry, uref_a) } - pub fn register_owner(&self, owner: &Address) { - let page = self.page_tables.get_or_default(&owner); + pub fn register_owner(&mut self, owner: &Address) { + if self.page_tables.get(&owner).is_none() { + self.page_tables.set(owner, vec![false; PAGE_SIZE as usize]); + } // let page_table_uref = utils::get_uref( // PAGE_TABLE, diff --git a/modules/src/cep78/tests/acl.rs b/modules/src/cep78/tests/acl.rs index 443eea2e..d5468e80 100644 --- a/modules/src/cep78/tests/acl.rs +++ b/modules/src/cep78/tests/acl.rs @@ -2,7 +2,6 @@ use odra::{ args::Maybe, host::{Deployer, HostEnv, HostRef, NoArgs}, prelude::*, - Address }; use crate::cep78::{ @@ -14,11 +13,6 @@ use crate::cep78::{ use super::default_args_builder; -#[odra::external_contract] -trait NftContract { - fn mint(&mut self, token_owner: Address, token_metadata: String) -> (String, Address, String); -} - #[test] fn should_install_with_acl_whitelist() { let env = odra_test::env(); @@ -203,7 +197,7 @@ fn should_allow_whitelisted_contract_to_mint() { .minting_mode(MintingMode::Acl) .acl_white_list(contract_whitelist) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let contract = CEP78HostRef::deploy(&env, args); assert!( contract.is_whitelisted(minting_contract.address()), "acl whitelist is incorrectly set" diff --git a/modules/src/cep78/tests/burn.rs b/modules/src/cep78/tests/burn.rs new file mode 100644 index 00000000..44ea4b1c --- /dev/null +++ b/modules/src/cep78/tests/burn.rs @@ -0,0 +1,321 @@ +use odra::{ + args::Maybe, + casper_types::bytesrepr::ToBytes, + host::{Deployer, HostRef, NoArgs}, + Address +}; + +use crate::cep78::{ + error::CEP78Error, + events::Burn, + modalities::{ + BurnMode, EventsMode, MetadataMutability, MintingMode, NFTHolderMode, NFTIdentifierMode, + OwnerReverseLookupMode, OwnershipMode, TokenIdentifier + }, + tests::{ + default_args_builder, + utils::{self, TestContractHostRef} + }, + token::CEP78HostRef +}; + +use super::utils::TEST_PRETTY_721_META_DATA; + +fn should_burn_minted_token(reporting: OwnerReverseLookupMode) { + let env = odra_test::env(); + let args = default_args_builder() + .owner_reverse_lookup_mode(reporting) + .ownership_mode(OwnershipMode::Transferable) + .events_mode(EventsMode::CES) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + let token_owner = env.get_account(0); + let token_id = 0u64; + let reverse_lookup_enabled = reporting == OwnerReverseLookupMode::Complete; + mint(&mut contract, reverse_lookup_enabled, token_owner); + + let actual_balance_before_burn = contract.balance_of(token_owner); + let expected_balance_before_burn = 1u64; + assert_eq!(actual_balance_before_burn, expected_balance_before_burn); + + let burn_result = contract.try_burn(Maybe::Some(token_id), Maybe::None); + assert!(burn_result.is_ok()); + + // This will error if token is not registered as burnt. + assert!(contract.token_burned(Maybe::Some(token_id), Maybe::None)); + + // This will error if token is not registered as burnt + let actual_balance = contract.balance_of(token_owner); + let expected_balance = 0u64; + assert_eq!(actual_balance, expected_balance); + + // Expect Burn event. + let expected_event = Burn::new( + token_owner, + TokenIdentifier::Index(token_id).to_string(), + token_owner + ); + assert!( + env.emitted_event(contract.address(), &expected_event), + "Expected Burn event." + ); +} + +fn mint(contract: &mut CEP78HostRef, reverse_lookup_enabled: bool, token_owner: Address) { + if reverse_lookup_enabled { + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + /*let token_page = contract.get_token_page(token_id); + assert!(token_page[0]);*/ + } else { + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + } +} + +#[test] +fn should_burn_minted_token_with_complete_reporting() { + should_burn_minted_token(OwnerReverseLookupMode::Complete); +} + +#[test] +fn should_burn_minted_token_with_transfer_only_reporting() { + should_burn_minted_token(OwnerReverseLookupMode::TransfersOnly); +} + +#[test] +fn should_not_burn_previously_burnt_token() { + let env = odra_test::env(); + let args = default_args_builder() + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .ownership_mode(OwnershipMode::Transferable) + .events_mode(EventsMode::CES) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + let token_owner = env.get_account(0); + mint(&mut contract, true, token_owner); + + let burn_result = contract.try_burn(Maybe::Some(0u64), Maybe::None); + assert!(burn_result.is_ok()); + + let re_burn_result = contract.try_burn(Maybe::Some(0u64), Maybe::None); + assert_eq!( + re_burn_result, + Err(CEP78Error::PreviouslyBurntToken.into()), + "should disallow burning of previously burnt token" + ); +} + +#[test] +fn should_return_expected_error_when_burning_non_existing_token() { + let env = odra_test::env(); + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + let token_id = 0u64; + assert_eq!( + contract.try_burn(Maybe::Some(token_id), Maybe::None), + Err(CEP78Error::MissingOwnerTokenIdentifierKey.into()), + "should return InvalidTokenID error when trying to burn a non_existing token", + ); +} + +#[test] +fn should_return_expected_error_burning_of_others_users_token() { + let env = odra_test::env(); + let args = default_args_builder() + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .ownership_mode(OwnershipMode::Transferable) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + let account_1 = env.get_account(1); + + mint(&mut contract, true, token_owner); + let token_id = 0u64; + /*let token_page = contract.get_token_page(token_id); + assert!(token_page[0]);*/ + env.set_caller(account_1); + assert_eq!( + contract.try_burn(Maybe::Some(token_id), Maybe::None), + Err(CEP78Error::InvalidTokenOwner.into()), + "should return InvalidTokenID error when trying to burn a non_existing token", + ); +} + +#[test] +fn should_allow_contract_to_burn_token() { + let env = odra_test::env(); + let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let contract_whitelist = vec![minting_contract.address().clone()]; + let args = default_args_builder() + .holder_mode(NFTHolderMode::Contracts) + .ownership_mode(OwnershipMode::Minter) + .minting_mode(MintingMode::Acl) + .ownership_mode(OwnershipMode::Transferable) + .acl_white_list(contract_whitelist) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + minting_contract.mint_for( + contract.address(), + token_owner, + TEST_PRETTY_721_META_DATA.to_string() + ); + + let current_token_balance = contract.balance_of(token_owner); + assert_eq!(1u64, current_token_balance); + + contract.burn(Maybe::Some(0u64), Maybe::None); + + let updated_token_balance = contract.balance_of(token_owner); + assert_eq!(updated_token_balance, 0u64) +} + +#[test] +fn should_not_burn_in_non_burn_mode() { + let env = odra_test::env(); + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .burn_mode(BurnMode::NonBurnable) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + mint(&mut contract, true, token_owner); + + assert_eq!( + contract.try_burn(Maybe::Some(0u64), Maybe::None), + Err(CEP78Error::InvalidBurnMode.into()) + ); +} + +#[test] +fn should_let_account_operator_burn_tokens_with_operator_burn_mode() { + let env = odra_test::env(); + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .operator_burn_mode(true) + .events_mode(EventsMode::CES) + .build(); + + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + mint(&mut contract, true, token_owner); + + let token_id = 0u64; + let operator = env.get_account(1); + + env.set_caller(operator); + assert_eq!( + contract.try_burn(Maybe::Some(token_id), Maybe::None), + Err(CEP78Error::InvalidTokenOwner.into()), + "InvalidTokenOwner should not allow burn by non operator" + ); + + env.set_caller(token_owner); + contract.set_approval_for_all(true, operator); + + env.set_caller(operator); + assert!(contract + .try_burn(Maybe::Some(token_id), Maybe::None) + .is_ok()); + assert!(contract.token_burned(Maybe::Some(token_id), Maybe::None)); + + let actual_balance = contract.balance_of(token_owner); + let expected_balance = 0u64; + assert_eq!(actual_balance, expected_balance); + + let expected_event = Burn::new( + token_owner, + TokenIdentifier::Index(token_id).to_string(), + operator + ); + assert!(env.emitted_event(contract.address(), &expected_event)); +} + +#[test] +fn should_let_contract_operator_burn_tokens_with_operator_burn_mode() { + let env = odra_test::env(); + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .operator_burn_mode(true) + .events_mode(EventsMode::CES) + .build(); + + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + mint(&mut contract, true, token_owner); + + let token_id = 0u64; + let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let operator = minting_contract.address().clone(); + let account_1 = env.get_account(1); + + env.set_caller(account_1); + assert_eq!( + minting_contract.try_burn(contract.address(), token_id), + Err(CEP78Error::InvalidTokenOwner.into()), + "InvalidTokenOwner should not allow burn by non operator" + ); + + env.set_caller(token_owner); + contract.set_approval_for_all(true, operator); + env.set_caller(account_1); + assert!(minting_contract + .try_burn(contract.address(), token_id) + .is_ok()); + + assert!(contract.token_burned(Maybe::Some(token_id), Maybe::None)); + + let actual_balance = contract.balance_of(token_owner); + let expected_balance = 0u64; + assert_eq!(actual_balance, expected_balance); + + let expected_event = Burn::new( + token_owner, + TokenIdentifier::Index(token_id).to_string(), + operator + ); + assert!(env.emitted_event(contract.address(), &expected_event)); +} + +#[test] +#[ignore = "package operator burn is on by default"] +fn should_let_package_operator_burn_tokens_with_contract_package_mode_and_operator_burn_mode() {} + +#[test] +fn should_burn_token_in_hash_identifier_mode() { + let env = odra_test::env(); + let args = default_args_builder() + .identifier_mode(NFTIdentifierMode::Hash) + .ownership_mode(OwnershipMode::Transferable) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .events_mode(EventsMode::CES) + .metadata_mutability(MetadataMutability::Immutable) + .build(); + + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + mint(&mut contract, true, token_owner); + + let blake2b_hash = utils::create_blake2b_hash(TEST_PRETTY_721_META_DATA.to_bytes().unwrap()); + let token_hash = base16::encode_lower(&blake2b_hash); + + assert!(contract + .try_burn(Maybe::None, Maybe::Some(token_hash)) + .is_ok()); +} diff --git a/modules/src/cep78/tests/mint.rs b/modules/src/cep78/tests/mint.rs index 30ced984..b81f380e 100644 --- a/modules/src/cep78/tests/mint.rs +++ b/modules/src/cep78/tests/mint.rs @@ -4,7 +4,7 @@ use odra::{ host::{Deployer, HostEnv, HostRef, NoArgs}, DeployReport }; -use serde::{de, Deserialize, Serialize}; +use serde::{Deserialize, Serialize}; use crate::cep78::{ error::CEP78Error, @@ -25,8 +25,7 @@ use crate::cep78::{ use super::{ default_args_builder, - utils::{InitArgsBuilder, TEST_PRETTY_721_META_DATA}, - COLLECTION_NAME + utils::TEST_PRETTY_721_META_DATA, }; #[derive(Serialize, Deserialize, Debug)] @@ -509,7 +508,7 @@ fn should_mint_with_hash_identifier_mode() { Maybe::None ); - let token_id_hash = + let _token_id_hash = base16::encode_lower(&utils::create_blake2b_hash(TEST_PRETTY_721_META_DATA)); // TODO: Reverse lookup not implemented yet diff --git a/modules/src/cep78/tests/mod.rs b/modules/src/cep78/tests/mod.rs index 9622a05e..afbf2e3f 100644 --- a/modules/src/cep78/tests/mod.rs +++ b/modules/src/cep78/tests/mod.rs @@ -9,6 +9,7 @@ use super::{ }; mod acl; +mod burn; mod installer; mod mint; mod set_variables; diff --git a/modules/src/cep78/tests/utils.rs b/modules/src/cep78/tests/utils.rs index 0f7ef153..953c6921 100644 --- a/modules/src/cep78/tests/utils.rs +++ b/modules/src/cep78/tests/utils.rs @@ -5,13 +5,9 @@ use crate::cep78::{ }, token::CEP78InitArgs }; -use blake2::{ - digest::{Update, VariableOutput}, - Blake2bVar -}; +use blake2::{digest::VariableOutput, Blake2bVar}; use odra::{args::Maybe, casper_types::BLAKE2B_DIGEST_LENGTH, prelude::*, Address, ContractRef}; - -use super::acl::NftContractContractRef; +use std::io::Write; #[derive(Default)] pub struct InitArgsBuilder { @@ -248,6 +244,11 @@ impl TestContract { .mint(self.env().self_address(), token_metadata) } + pub fn burn(&mut self, nft_contract_address: &Address, token_id: u64) { + NftContractContractRef::new(self.env(), *nft_contract_address) + .burn(Maybe::Some(token_id), Maybe::None) + } + pub fn mint_for( &mut self, nft_contract_address: &Address, @@ -258,9 +259,13 @@ impl TestContract { .mint(token_owner, token_metadata) } } -use std::collections::hash_map::DefaultHasher; -use std::hash::Hasher; -use std::io::Write; + +#[odra::external_contract] +trait NftContract { + fn mint(&mut self, token_owner: Address, token_metadata: String) -> (String, Address, String); + fn burn(&mut self, token_id: Maybe, token_hash: Maybe); +} + pub(crate) fn create_blake2b_hash>(data: T) -> [u8; BLAKE2B_DIGEST_LENGTH] { let mut result = [0u8; 32]; let mut hasher = ::new(32).expect("should create hasher"); diff --git a/modules/src/cep78/token.rs b/modules/src/cep78/token.rs index 2e2c488f..dc3aea5c 100644 --- a/modules/src/cep78/token.rs +++ b/modules/src/cep78/token.rs @@ -659,6 +659,12 @@ impl CEP78 { .get(&token_identifier.to_string()) .unwrap_or_revert_with(&self.env(), CEP78Error::InvalidTokenIdentifier) } + + pub fn token_burned(&self, token_id: Maybe, token_hash: Maybe) -> bool { + let token_identifier = self.token_identifier(token_id, token_hash); + let token_id = token_identifier.to_string(); + self.is_token_burned(&token_id) + } } impl CEP78 { From e5d71669e7ffcb8611aa7c7f6b82d1a663c14923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Fri, 19 Apr 2024 09:31:54 +0200 Subject: [PATCH 12/38] Add events tests --- modules/src/cep78/tests/acl.rs | 2 +- modules/src/cep78/tests/events.rs | 82 +++++++++++++++++++++++++++++++ modules/src/cep78/tests/mint.rs | 5 +- modules/src/cep78/tests/mod.rs | 1 + 4 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 modules/src/cep78/tests/events.rs diff --git a/modules/src/cep78/tests/acl.rs b/modules/src/cep78/tests/acl.rs index d5468e80..9ab01e4c 100644 --- a/modules/src/cep78/tests/acl.rs +++ b/modules/src/cep78/tests/acl.rs @@ -1,7 +1,7 @@ use odra::{ args::Maybe, host::{Deployer, HostEnv, HostRef, NoArgs}, - prelude::*, + prelude::* }; use crate::cep78::{ diff --git a/modules/src/cep78/tests/events.rs b/modules/src/cep78/tests/events.rs new file mode 100644 index 00000000..5588e780 --- /dev/null +++ b/modules/src/cep78/tests/events.rs @@ -0,0 +1,82 @@ +use odra::{ + args::Maybe, + host::{Deployer, HostRef} +}; + +use crate::cep78::{ + modalities::{EventsMode, OwnerReverseLookupMode, OwnershipMode}, + tests::{default_args_builder, utils::TEST_PRETTY_721_META_DATA}, + token::CEP78HostRef +}; + +// cep47 event style +#[test] +#[ignore = "Odra does not support cep47 events"] +fn should_record_cep47_dictionary_style_mint_event() {} + +#[test] +#[ignore = "Odra does not support cep47 events"] +fn should_record_cep47_dictionary_style_transfer_token_event_in_hash_identifier_mode() {} + +#[test] +#[ignore = "Odra does not support cep47 events"] +fn should_record_cep47_dictionary_style_metadata_update_event_for_nft721_using_token_id() {} + +#[test] +#[ignore = "Odra does not support cep47 events"] +fn should_cep47_dictionary_style_burn_event() {} + +#[test] +#[ignore = "Odra does not support cep47 events"] +fn should_cep47_dictionary_style_approve_event_in_hash_identifier_mode() {} + +#[test] +#[ignore = "Odra does not support cep47 events"] +fn should_cep47_dictionary_style_approvall_for_all_event() {} + +#[test] +#[ignore = "Odra does not support cep47 events"] +fn should_cep47_dictionary_style_revoked_for_all_event() {} + +#[test] +#[ignore = "Odra does not support cep47 events"] +fn should_record_migration_event_in_cep47() {} + +#[test] +#[ignore = "Named keys existence is not verifiable in Odra"] +fn should_not_have_events_dicts_in_no_events_mode() { + let env = odra_test::env(); + let args = default_args_builder() + .events_mode(EventsMode::NoEvents) + .build(); + let _contract = CEP78HostRef::deploy(&env, args); + + // Check dict from EventsMode::CES + // let events = named_keys.get(EVENTS_DICT); + // assert_eq!(events, None); +} + +#[test] +fn should_not_record_events_in_no_events_mode() { + let env = odra_test::env(); + let args = default_args_builder() + .events_mode(EventsMode::NoEvents) + .ownership_mode(OwnershipMode::Transferable) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + // This will error if token is not registered as burnt + let actual_balance = contract.balance_of(token_owner); + let expected_balance = 1u64; + assert_eq!(actual_balance, expected_balance); + + assert!(env.events_count(contract.address()) == 0); +} diff --git a/modules/src/cep78/tests/mint.rs b/modules/src/cep78/tests/mint.rs index b81f380e..f2699aa1 100644 --- a/modules/src/cep78/tests/mint.rs +++ b/modules/src/cep78/tests/mint.rs @@ -23,10 +23,7 @@ use crate::cep78::{ token::CEP78HostRef }; -use super::{ - default_args_builder, - utils::TEST_PRETTY_721_META_DATA, -}; +use super::{default_args_builder, utils::TEST_PRETTY_721_META_DATA}; #[derive(Serialize, Deserialize, Debug)] struct Metadata { diff --git a/modules/src/cep78/tests/mod.rs b/modules/src/cep78/tests/mod.rs index afbf2e3f..4761f279 100644 --- a/modules/src/cep78/tests/mod.rs +++ b/modules/src/cep78/tests/mod.rs @@ -10,6 +10,7 @@ use super::{ mod acl; mod burn; +mod events; mod installer; mod mint; mod set_variables; From 00cdaee44a3c5e52c48e0b1473ac9e1222d3b164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Fri, 19 Apr 2024 15:05:40 +0200 Subject: [PATCH 13/38] metadata tests --- modules/src/cep78/tests/metadata.rs | 491 ++++++++++++++++++++++++++++ modules/src/cep78/tests/mod.rs | 10 + modules/src/cep78/tests/utils.rs | 34 +- modules/src/cep78/token.rs | 6 +- 4 files changed, 533 insertions(+), 8 deletions(-) create mode 100644 modules/src/cep78/tests/metadata.rs diff --git a/modules/src/cep78/tests/metadata.rs b/modules/src/cep78/tests/metadata.rs new file mode 100644 index 00000000..0498bde5 --- /dev/null +++ b/modules/src/cep78/tests/metadata.rs @@ -0,0 +1,491 @@ +use odra::{ + args::Maybe, + casper_types::bytesrepr::ToBytes, + host::{Deployer, HostRef, NoArgs} +}; + +use crate::cep78::{ + error::CEP78Error, + events::MetadataUpdated, + modalities::{ + EventsMode, MetadataMutability, MintingMode, NFTHolderMode, NFTIdentifierMode, + NFTMetadataKind, OwnershipMode, TokenIdentifier, WhitelistMode + }, + tests::{ + utils::{ + TestContractHostRef, MALFORMED_META_DATA, TEST_PRETTY_CEP78_METADATA, + TEST_PRETTY_UPDATED_CEP78_METADATA + }, + TEST_CUSTOM_METADATA, TEST_CUSTOM_METADATA_SCHEMA, TEST_CUSTOM_UPDATED_METADATA, + TOKEN_HASH + }, + token::CEP78HostRef +}; + +use super::{ + default_args_builder, + utils::{self, TEST_PRETTY_721_META_DATA, TEST_PRETTY_UPDATED_721_META_DATA} +}; + +#[test] +fn should_prevent_update_in_immutable_mode() { + let env = odra_test::env(); + let args = default_args_builder() + .nft_metadata_kind(NFTMetadataKind::NFT721) + .identifier_mode(NFTIdentifierMode::Hash) + .metadata_mutability(MetadataMutability::Immutable) + .ownership_mode(OwnershipMode::Transferable) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + contract.mint( + env.get_account(0), + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let blake2b_hash = utils::create_blake2b_hash(TEST_PRETTY_721_META_DATA.to_bytes().unwrap()); + let token_hash = base16::encode_lower(&blake2b_hash); + + assert_eq!( + contract.try_set_token_metadata( + Maybe::None, + Maybe::Some(token_hash), + TEST_PRETTY_UPDATED_721_META_DATA.to_string() + ), + Err(CEP78Error::ForbiddenMetadataUpdate.into()) + ); +} + +#[test] +fn should_prevent_install_with_hash_identifier_in_mutable_mode() { + let env = odra_test::env(); + let args = default_args_builder() + .nft_metadata_kind(NFTMetadataKind::NFT721) + .identifier_mode(NFTIdentifierMode::Hash) + .metadata_mutability(MetadataMutability::Mutable) + .build(); + let _contract = CEP78HostRef::deploy(&env, args); + // Should be possible to verify errors at installation time + // assert_eq!(CEP78HostRef::deploy(&env, args), Err(CEP78Error::InvalidMetadataMutability)); +} + +#[test] +fn should_prevent_update_for_invalid_metadata() { + let env = odra_test::env(); + let args = default_args_builder() + .nft_metadata_kind(NFTMetadataKind::NFT721) + .identifier_mode(NFTIdentifierMode::Ordinal) + .metadata_mutability(MetadataMutability::Mutable) + .ownership_mode(OwnershipMode::Transferable) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + contract.mint( + env.get_account(0), + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let original_metadata = + contract.get_metadata_by_kind(NFTMetadataKind::NFT721, Maybe::Some(0u64), Maybe::None); + assert_eq!(TEST_PRETTY_721_META_DATA, original_metadata); + + assert_eq!( + contract.try_set_token_metadata( + Maybe::Some(0u64), + Maybe::None, + MALFORMED_META_DATA.to_string() + ), + Err(CEP78Error::FailedToParse721Metadata.into()) + ); +} + +#[test] +fn should_prevent_metadata_update_by_non_owner_key() { + let env = odra_test::env(); + let args = default_args_builder() + .nft_metadata_kind(NFTMetadataKind::NFT721) + .identifier_mode(NFTIdentifierMode::Ordinal) + .metadata_mutability(MetadataMutability::Mutable) + .ownership_mode(OwnershipMode::Transferable) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = contract.address().clone(); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let original_metadata = + contract.get_metadata_by_kind(NFTMetadataKind::NFT721, Maybe::Some(0u64), Maybe::None); + assert_eq!(TEST_PRETTY_721_META_DATA, original_metadata); + + let actual_token_owner = contract.owner_of(Maybe::Some(0u64), Maybe::None); + assert_eq!(actual_token_owner, token_owner); + + assert_eq!( + contract.try_set_token_metadata( + Maybe::Some(0u64), + Maybe::None, + TEST_PRETTY_UPDATED_721_META_DATA.to_string() + ), + Err(CEP78Error::InvalidTokenOwner.into()) + ); +} + +fn should_allow_update_for_valid_metadata_based_on_kind( + nft_metadata_kind: NFTMetadataKind, + identifier_mode: NFTIdentifierMode +) { + let env = odra_test::env(); + let json_schema = + serde_json::to_string(&*TEST_CUSTOM_METADATA_SCHEMA).expect("must convert to json schema"); + let args = default_args_builder() + .nft_metadata_kind(nft_metadata_kind.clone()) + .identifier_mode(identifier_mode) + .metadata_mutability(MetadataMutability::Mutable) + .ownership_mode(OwnershipMode::Transferable) + .json_schema(json_schema) + .events_mode(EventsMode::CES) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + + let custom_metadata = serde_json::to_string_pretty(&*TEST_CUSTOM_METADATA) + .expect("must convert to json metadata"); + + let original_metadata = match &nft_metadata_kind { + NFTMetadataKind::CEP78 => TEST_PRETTY_CEP78_METADATA, + NFTMetadataKind::NFT721 => TEST_PRETTY_721_META_DATA, + NFTMetadataKind::Raw => "", + NFTMetadataKind::CustomValidated => &custom_metadata + }; + + contract.mint(token_owner, original_metadata.to_string(), Maybe::None); + + let blake2b_hash = utils::create_blake2b_hash(original_metadata.to_bytes().unwrap()); + let token_hash = base16::encode_lower(&blake2b_hash); + let token_id = 0u64; + + let actual_metadata = match identifier_mode { + NFTIdentifierMode::Ordinal => contract.get_metadata_by_kind( + nft_metadata_kind.clone(), + Maybe::Some(token_id), + Maybe::None + ), + NFTIdentifierMode::Hash => contract.get_metadata_by_kind( + nft_metadata_kind.clone(), + Maybe::None, + Maybe::Some(token_hash.clone()) + ) + }; + + assert_eq!(actual_metadata, original_metadata.to_string()); + + let custom_updated_metadata = serde_json::to_string_pretty(&*TEST_CUSTOM_UPDATED_METADATA) + .expect("must convert to json metadata"); + + let updated_metadata = match &nft_metadata_kind { + NFTMetadataKind::CEP78 => TEST_PRETTY_UPDATED_CEP78_METADATA, + NFTMetadataKind::NFT721 => TEST_PRETTY_UPDATED_721_META_DATA, + NFTMetadataKind::Raw => "", + NFTMetadataKind::CustomValidated => &custom_updated_metadata + }; + + let update_result = match identifier_mode { + NFTIdentifierMode::Ordinal => contract.try_set_token_metadata( + Maybe::Some(token_id), + Maybe::None, + updated_metadata.to_string() + ), + NFTIdentifierMode::Hash => contract.try_set_token_metadata( + Maybe::None, + Maybe::Some(token_hash), + updated_metadata.to_string() + ) + }; + assert!(update_result.is_ok(), "failed to update metadata"); + + let blake2b_hash = utils::create_blake2b_hash(updated_metadata.to_bytes().unwrap()); + let token_hash = base16::encode_lower(&blake2b_hash); + + let actual_updated_metadata = match identifier_mode { + NFTIdentifierMode::Ordinal => { + contract.get_metadata_by_kind(nft_metadata_kind, Maybe::Some(token_id), Maybe::None) + } + NFTIdentifierMode::Hash => contract.get_metadata_by_kind( + nft_metadata_kind, + Maybe::None, + Maybe::Some(token_hash.clone()) + ) + }; + + assert_eq!(actual_updated_metadata, updated_metadata.to_string()); + + // Expect MetadataUpdated event. + let token_id = match identifier_mode { + NFTIdentifierMode::Ordinal => TokenIdentifier::Index(0), + NFTIdentifierMode::Hash => TokenIdentifier::Hash(token_hash) + } + .to_string(); + let expected_event = MetadataUpdated::new(token_id, updated_metadata.to_string()); + assert!(env.emitted_event(contract.address(), &expected_event)); +} + +#[test] +fn should_update_metadata_for_nft721_using_token_id() { + should_allow_update_for_valid_metadata_based_on_kind( + NFTMetadataKind::NFT721, + NFTIdentifierMode::Ordinal + ) +} + +#[test] +fn should_update_metadata_for_cep78_using_token_id() { + should_allow_update_for_valid_metadata_based_on_kind( + NFTMetadataKind::CEP78, + NFTIdentifierMode::Ordinal + ) +} + +#[test] +fn should_update_metadata_for_custom_validated_using_token_id() { + should_allow_update_for_valid_metadata_based_on_kind( + NFTMetadataKind::CustomValidated, + NFTIdentifierMode::Ordinal + ) +} + +#[test] +fn should_get_metadata_using_token_id() { + let env = odra_test::env(); + let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let minting_contract_address = minting_contract.address().clone(); + let contract_whitelist = vec![minting_contract_address]; + let args = default_args_builder() + .holder_mode(NFTHolderMode::Contracts) + .whitelist_mode(WhitelistMode::Locked) + .ownership_mode(OwnershipMode::Transferable) + .minting_mode(MintingMode::Acl) + .acl_white_list(contract_whitelist) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_id = 0u64; + + assert!( + contract.is_whitelisted(minting_contract.address()), + "acl whitelist is incorrectly set" + ); + + minting_contract.mint(contract.address(), TEST_PRETTY_721_META_DATA.to_string()); + + let minted_metadata = contract.metadata(Maybe::Some(token_id), Maybe::None); + assert_eq!(minted_metadata, TEST_PRETTY_721_META_DATA); +} + +#[test] +fn should_get_metadata_using_token_metadata_hash() { + let env = odra_test::env(); + let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let minting_contract_address = minting_contract.address().clone(); + let contract_whitelist = vec![minting_contract_address]; + let args = default_args_builder() + .identifier_mode(NFTIdentifierMode::Hash) + .holder_mode(NFTHolderMode::Contracts) + .whitelist_mode(WhitelistMode::Locked) + .ownership_mode(OwnershipMode::Transferable) + .metadata_mutability(MetadataMutability::Immutable) + .minting_mode(MintingMode::Acl) + .acl_white_list(contract_whitelist) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + assert!( + contract.is_whitelisted(minting_contract.address()), + "acl whitelist is incorrectly set" + ); + + minting_contract.mint(contract.address(), TEST_PRETTY_721_META_DATA.to_string()); + + let blake2b_hash = utils::create_blake2b_hash(TEST_PRETTY_721_META_DATA.to_bytes().unwrap()); + let token_hash = base16::encode_lower(&blake2b_hash); + + let minted_metadata = contract.metadata(Maybe::None, Maybe::Some(token_hash)); + assert_eq!(minted_metadata, TEST_PRETTY_721_META_DATA); +} + +#[test] +fn should_revert_minting_token_metadata_hash_twice() { + let env = odra_test::env(); + let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let minting_contract_address = minting_contract.address().clone(); + let contract_whitelist = vec![minting_contract_address]; + let args = default_args_builder() + .identifier_mode(NFTIdentifierMode::Hash) + .holder_mode(NFTHolderMode::Contracts) + .whitelist_mode(WhitelistMode::Locked) + .ownership_mode(OwnershipMode::Transferable) + .metadata_mutability(MetadataMutability::Immutable) + .minting_mode(MintingMode::Acl) + .acl_white_list(contract_whitelist) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + assert!( + contract.is_whitelisted(minting_contract.address()), + "acl whitelist is incorrectly set" + ); + minting_contract.mint(contract.address(), TEST_PRETTY_721_META_DATA.to_string()); + + let blake2b_hash = utils::create_blake2b_hash(TEST_PRETTY_721_META_DATA.to_bytes().unwrap()); + let token_hash = base16::encode_lower(&blake2b_hash); + + let minted_metadata = contract.metadata(Maybe::None, Maybe::Some(token_hash)); + assert_eq!(minted_metadata, TEST_PRETTY_721_META_DATA); + + assert_eq!( + minting_contract.try_mint(contract.address(), TEST_PRETTY_721_META_DATA.to_string(),), + Err(CEP78Error::DuplicateIdentifier.into()) + ); +} + +#[test] +fn should_get_metadata_using_custom_token_hash() { + let env = odra_test::env(); + let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let minting_contract_address = minting_contract.address().clone(); + let contract_whitelist = vec![minting_contract_address]; + let args = default_args_builder() + .identifier_mode(NFTIdentifierMode::Hash) + .holder_mode(NFTHolderMode::Contracts) + .whitelist_mode(WhitelistMode::Locked) + .ownership_mode(OwnershipMode::Transferable) + .metadata_mutability(MetadataMutability::Immutable) + .minting_mode(MintingMode::Acl) + .acl_white_list(contract_whitelist) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + assert!( + contract.is_whitelisted(minting_contract.address()), + "acl whitelist is incorrectly set" + ); + minting_contract.mint_with_hash( + contract.address(), + TEST_PRETTY_721_META_DATA.to_string(), + TOKEN_HASH.to_string() + ); + + let minted_metadata: String = + contract.metadata(Maybe::None, Maybe::Some(TOKEN_HASH.to_string())); + assert_eq!(minted_metadata, TEST_PRETTY_721_META_DATA); +} + +#[test] +fn should_revert_minting_custom_token_hash_identifier_twice() { + let env = odra_test::env(); + let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let minting_contract_address = minting_contract.address().clone(); + let contract_whitelist = vec![minting_contract_address]; + let args = default_args_builder() + .identifier_mode(NFTIdentifierMode::Hash) + .holder_mode(NFTHolderMode::Contracts) + .whitelist_mode(WhitelistMode::Locked) + .ownership_mode(OwnershipMode::Transferable) + .metadata_mutability(MetadataMutability::Immutable) + .minting_mode(MintingMode::Acl) + .acl_white_list(contract_whitelist) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + assert!( + contract.is_whitelisted(minting_contract.address()), + "acl whitelist is incorrectly set" + ); + minting_contract.mint_with_hash( + contract.address(), + TEST_PRETTY_721_META_DATA.to_string(), + TOKEN_HASH.to_string() + ); + + let minted_metadata: String = + contract.metadata(Maybe::None, Maybe::Some(TOKEN_HASH.to_string())); + assert_eq!(minted_metadata, TEST_PRETTY_721_META_DATA); + + assert_eq!( + minting_contract.try_mint_with_hash( + contract.address(), + TEST_PRETTY_721_META_DATA.to_string(), + TOKEN_HASH.to_string() + ), + Err(CEP78Error::DuplicateIdentifier.into()) + ); +} + +#[test] +fn should_require_valid_json_schema_when_kind_is_custom_validated() { + let _env = odra_test::env(); + let _args = default_args_builder() + .identifier_mode(NFTIdentifierMode::Ordinal) + .ownership_mode(OwnershipMode::Transferable) + .nft_metadata_kind(NFTMetadataKind::CustomValidated) + .build(); + + /*let error = builder.get_error().expect("must have error"); + support::assert_expected_error(error, 68, "valid json_schema is required")*/ +} + +#[test] +fn should_require_json_schema_when_kind_is_custom_validated() { + let env = odra_test::env(); + let nft_metadata_kind = NFTMetadataKind::CustomValidated; + + let args = default_args_builder() + .identifier_mode(NFTIdentifierMode::Ordinal) + .ownership_mode(OwnershipMode::Transferable) + .metadata_mutability(MetadataMutability::Mutable) + .nft_metadata_kind(nft_metadata_kind) + .json_schema("".to_string()) + .build(); + let _contract = CEP78HostRef::deploy(&env, args); + + /*let error = builder.get_error().expect("must have error"); + support::assert_expected_error(error, 67, "json_schema is required")*/ +} + +fn should_not_require_json_schema_when_kind_is(nft_metadata_kind: NFTMetadataKind) { + let env = odra_test::env(); + let args = default_args_builder() + .identifier_mode(NFTIdentifierMode::Ordinal) + .ownership_mode(OwnershipMode::Transferable) + .metadata_mutability(MetadataMutability::Mutable) + .nft_metadata_kind(nft_metadata_kind.clone()) + .json_schema("".to_string()) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + let original_metadata = match &nft_metadata_kind { + NFTMetadataKind::CEP78 => TEST_PRETTY_CEP78_METADATA, + NFTMetadataKind::NFT721 => TEST_PRETTY_721_META_DATA, + NFTMetadataKind::Raw => "", + _ => panic!( + "NFTMetadataKind {:?} not supported without json_schema", + nft_metadata_kind + ) + }; + + assert!(contract + .try_mint( + env.get_account(0), + original_metadata.to_string(), + Maybe::None + ) + .is_ok()); +} + +#[test] +fn should_not_require_json_schema_when_kind_is_not_custom_validated() { + should_not_require_json_schema_when_kind_is(NFTMetadataKind::Raw); + should_not_require_json_schema_when_kind_is(NFTMetadataKind::CEP78); + should_not_require_json_schema_when_kind_is(NFTMetadataKind::NFT721); +} diff --git a/modules/src/cep78/tests/mod.rs b/modules/src/cep78/tests/mod.rs index 4761f279..fb33b83e 100644 --- a/modules/src/cep78/tests/mod.rs +++ b/modules/src/cep78/tests/mod.rs @@ -12,12 +12,14 @@ mod acl; mod burn; mod events; mod installer; +mod metadata; mod mint; mod set_variables; mod utils; pub(super) const COLLECTION_NAME: &str = "CEP78-Test-Collection"; pub(super) const COLLECTION_SYMBOL: &str = "CEP78"; +pub const TOKEN_HASH: &str = "token_hash"; pub(super) fn default_args_builder() -> InitArgsBuilder { InitArgsBuilder::default() @@ -34,6 +36,14 @@ pub(super) static TEST_CUSTOM_METADATA: Lazy> = Lazy::n attributes }); +pub(crate) static TEST_CUSTOM_UPDATED_METADATA: Lazy> = Lazy::new(|| { + let mut attributes = BTreeMap::new(); + attributes.insert("deity_name".to_string(), "Baldur".to_string()); + attributes.insert("mythology".to_string(), "Nordic".to_string()); + attributes.insert("enemy".to_string(), "Loki".to_string()); + attributes +}); + pub(crate) static TEST_CUSTOM_METADATA_SCHEMA: Lazy = Lazy::new(|| { let mut properties = BTreeMap::new(); properties.insert( diff --git a/modules/src/cep78/tests/utils.rs b/modules/src/cep78/tests/utils.rs index 953c6921..45eb46dd 100644 --- a/modules/src/cep78/tests/utils.rs +++ b/modules/src/cep78/tests/utils.rs @@ -240,8 +240,24 @@ impl TestContract { nft_contract_address: &Address, token_metadata: String ) -> (String, Address, String) { - NftContractContractRef::new(self.env(), *nft_contract_address) - .mint(self.env().self_address(), token_metadata) + NftContractContractRef::new(self.env(), *nft_contract_address).mint( + self.env().self_address(), + token_metadata, + Maybe::None + ) + } + + pub fn mint_with_hash( + &mut self, + nft_contract_address: &Address, + token_metadata: String, + token_hash: String + ) -> (String, Address, String) { + NftContractContractRef::new(self.env(), *nft_contract_address).mint( + self.env().self_address(), + token_metadata, + Maybe::Some(token_hash) + ) } pub fn burn(&mut self, nft_contract_address: &Address, token_id: u64) { @@ -255,14 +271,22 @@ impl TestContract { token_owner: Address, token_metadata: String ) -> (String, Address, String) { - NftContractContractRef::new(self.env(), *nft_contract_address) - .mint(token_owner, token_metadata) + NftContractContractRef::new(self.env(), *nft_contract_address).mint( + token_owner, + token_metadata, + Maybe::None + ) } } #[odra::external_contract] trait NftContract { - fn mint(&mut self, token_owner: Address, token_metadata: String) -> (String, Address, String); + fn mint( + &mut self, + token_owner: Address, + token_metadata: String, + token_hash: Maybe + ) -> (String, Address, String); fn burn(&mut self, token_id: Maybe, token_hash: Maybe); } diff --git a/modules/src/cep78/token.rs b/modules/src/cep78/token.rs index dc3aea5c..982f960c 100644 --- a/modules/src/cep78/token.rs +++ b/modules/src/cep78/token.rs @@ -551,7 +551,7 @@ impl CEP78 { let token_identifier = self.checked_token_identifier(token_id, token_hash); let token_id = token_identifier.to_string(); - self.ensure_owner_not_caller(&token_id); + self.ensure_caller_is_owner(&token_id); self.metadata .update_or_revert(&updated_token_metadata, &token_id); @@ -740,9 +740,9 @@ impl CEP78 { } #[inline] - fn ensure_owner_not_caller(&self, token_id: &String) { + fn ensure_caller_is_owner(&self, token_id: &String) { let owner = self.owner_of_by_id(token_id); - if self.env().caller() == owner { + if self.env().caller() != owner { self.env().revert(CEP78Error::InvalidTokenOwner); } } From 2949e1b54fb5bdef6cf382a8c30a8e5acc79cd5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Tue, 23 Apr 2024 14:39:46 +0200 Subject: [PATCH 14/38] Add transfer tests --- modules/src/cep78/collection_info.rs | 1 - modules/src/cep78/events.rs | 1 - modules/src/cep78/tests/acl.rs | 26 +- modules/src/cep78/tests/burn.rs | 14 +- modules/src/cep78/tests/metadata.rs | 15 +- modules/src/cep78/tests/mint.rs | 7 +- modules/src/cep78/tests/mod.rs | 1 + modules/src/cep78/tests/transfer.rs | 1030 ++++++++++++++++++++++++++ modules/src/cep78/tests/utils.rs | 130 +++- modules/src/cep78/token.rs | 64 +- modules/src/cep78/utils.rs | 8 +- 11 files changed, 1188 insertions(+), 109 deletions(-) create mode 100644 modules/src/cep78/tests/transfer.rs diff --git a/modules/src/cep78/collection_info.rs b/modules/src/cep78/collection_info.rs index 3462fb2e..1a4bf7f9 100644 --- a/modules/src/cep78/collection_info.rs +++ b/modules/src/cep78/collection_info.rs @@ -1,4 +1,3 @@ -use odra::args::Maybe; use odra::prelude::*; use odra::Address; use odra::Sequence; diff --git a/modules/src/cep78/events.rs b/modules/src/cep78/events.rs index 531872ab..63ab6635 100644 --- a/modules/src/cep78/events.rs +++ b/modules/src/cep78/events.rs @@ -1,4 +1,3 @@ -use super::modalities::TokenIdentifier; use odra::{prelude::*, Address}; #[odra::event] diff --git a/modules/src/cep78/tests/acl.rs b/modules/src/cep78/tests/acl.rs index 9ab01e4c..09932a86 100644 --- a/modules/src/cep78/tests/acl.rs +++ b/modules/src/cep78/tests/acl.rs @@ -202,8 +202,8 @@ fn should_allow_whitelisted_contract_to_mint() { contract.is_whitelisted(minting_contract.address()), "acl whitelist is incorrectly set" ); - - minting_contract.mint(contract.address(), TEST_PRETTY_721_META_DATA.to_string()); + minting_contract.set_address(contract.address()); + minting_contract.mint(TEST_PRETTY_721_META_DATA.to_string(), false); let token_id = 0u64; let actual_token_owner = contract.owner_of(Maybe::Some(token_id), Maybe::None); @@ -225,9 +225,10 @@ fn should_disallow_unlisted_contract_from_minting() { .acl_white_list(contract_whitelist) .build(); let contract = CEP78HostRef::deploy(&env, args); + minting_contract.set_address(contract.address()); assert_eq!( - minting_contract.try_mint(contract.address(), TEST_PRETTY_721_META_DATA.to_string(),), + minting_contract.try_mint(TEST_PRETTY_721_META_DATA.to_string(), false), Err(CEP78Error::UnlistedContractHash.into()), "Unlisted account hash should not be permitted to mint" ); @@ -249,13 +250,14 @@ fn should_allow_mixed_account_contract_to_mint() { .acl_white_list(mixed_whitelist) .build(); let mut contract = CEP78HostRef::deploy(&env, args); + minting_contract.set_address(contract.address()); assert!( contract.is_whitelisted(minting_contract.address()), "acl whitelist is incorrectly set" ); - minting_contract.mint(contract.address(), TEST_PRETTY_721_META_DATA.to_string()); + minting_contract.mint(TEST_PRETTY_721_META_DATA.to_string(), false); let token_id = 0u64; let actual_token_owner = contract.owner_of(Maybe::Some(token_id), Maybe::None); @@ -296,9 +298,10 @@ fn should_disallow_unlisted_contract_from_minting_with_mixed_account_contract() .acl_white_list(mixed_whitelist) .build(); let contract = CEP78HostRef::deploy(&env, args); + minting_contract.set_address(contract.address()); assert_eq!( - minting_contract.try_mint(contract.address(), TEST_PRETTY_721_META_DATA.to_string(),), + minting_contract.try_mint(TEST_PRETTY_721_META_DATA.to_string(), false), Err(CEP78Error::UnlistedContractHash.into()), "Unlisted contract should not be permitted to mint" ); @@ -403,6 +406,7 @@ fn should_be_able_to_update_whitelist_for_minting() { .acl_white_list(contract_whitelist) .build(); let mut contract = CEP78HostRef::deploy(&env, args); + minting_contract.set_address(contract.address()); assert!( !contract.is_whitelisted(minting_contract.address()), @@ -410,11 +414,7 @@ fn should_be_able_to_update_whitelist_for_minting() { ); assert_eq!( - minting_contract.try_mint_for( - contract.address(), - env.get_account(0), - TEST_PRETTY_721_META_DATA.to_string(), - ), + minting_contract.try_mint_for(env.get_account(0), TEST_PRETTY_721_META_DATA.to_string(),), Err(CEP78Error::UnlistedContractHash.into()), ); @@ -430,11 +430,7 @@ fn should_be_able_to_update_whitelist_for_minting() { ); assert!(minting_contract - .try_mint_for( - contract.address(), - env.get_account(0), - TEST_PRETTY_721_META_DATA.to_string(), - ) + .try_mint_for(env.get_account(0), TEST_PRETTY_721_META_DATA.to_string(),) .is_ok()); } diff --git a/modules/src/cep78/tests/burn.rs b/modules/src/cep78/tests/burn.rs index 44ea4b1c..20d1ff31 100644 --- a/modules/src/cep78/tests/burn.rs +++ b/modules/src/cep78/tests/burn.rs @@ -167,12 +167,9 @@ fn should_allow_contract_to_burn_token() { .acl_white_list(contract_whitelist) .build(); let mut contract = CEP78HostRef::deploy(&env, args); + minting_contract.set_address(contract.address()); let token_owner = env.get_account(0); - minting_contract.mint_for( - contract.address(), - token_owner, - TEST_PRETTY_721_META_DATA.to_string() - ); + minting_contract.mint_for(token_owner, TEST_PRETTY_721_META_DATA.to_string()); let current_token_balance = contract.balance_of(token_owner); assert_eq!(1u64, current_token_balance); @@ -262,12 +259,13 @@ fn should_let_contract_operator_burn_tokens_with_operator_burn_mode() { let token_id = 0u64; let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + minting_contract.set_address(contract.address()); let operator = minting_contract.address().clone(); let account_1 = env.get_account(1); env.set_caller(account_1); assert_eq!( - minting_contract.try_burn(contract.address(), token_id), + minting_contract.try_burn(token_id), Err(CEP78Error::InvalidTokenOwner.into()), "InvalidTokenOwner should not allow burn by non operator" ); @@ -275,9 +273,7 @@ fn should_let_contract_operator_burn_tokens_with_operator_burn_mode() { env.set_caller(token_owner); contract.set_approval_for_all(true, operator); env.set_caller(account_1); - assert!(minting_contract - .try_burn(contract.address(), token_id) - .is_ok()); + assert!(minting_contract.try_burn(token_id).is_ok()); assert!(contract.token_burned(Maybe::Some(token_id), Maybe::None)); diff --git a/modules/src/cep78/tests/metadata.rs b/modules/src/cep78/tests/metadata.rs index 0498bde5..a4eb15b7 100644 --- a/modules/src/cep78/tests/metadata.rs +++ b/modules/src/cep78/tests/metadata.rs @@ -270,6 +270,7 @@ fn should_get_metadata_using_token_id() { .acl_white_list(contract_whitelist) .build(); let mut contract = CEP78HostRef::deploy(&env, args); + minting_contract.set_address(contract.address()); let token_id = 0u64; assert!( @@ -277,7 +278,7 @@ fn should_get_metadata_using_token_id() { "acl whitelist is incorrectly set" ); - minting_contract.mint(contract.address(), TEST_PRETTY_721_META_DATA.to_string()); + minting_contract.mint(TEST_PRETTY_721_META_DATA.to_string(), false); let minted_metadata = contract.metadata(Maybe::Some(token_id), Maybe::None); assert_eq!(minted_metadata, TEST_PRETTY_721_META_DATA); @@ -299,13 +300,14 @@ fn should_get_metadata_using_token_metadata_hash() { .acl_white_list(contract_whitelist) .build(); let mut contract = CEP78HostRef::deploy(&env, args); + minting_contract.set_address(contract.address()); assert!( contract.is_whitelisted(minting_contract.address()), "acl whitelist is incorrectly set" ); - minting_contract.mint(contract.address(), TEST_PRETTY_721_META_DATA.to_string()); + minting_contract.mint(TEST_PRETTY_721_META_DATA.to_string(), false); let blake2b_hash = utils::create_blake2b_hash(TEST_PRETTY_721_META_DATA.to_bytes().unwrap()); let token_hash = base16::encode_lower(&blake2b_hash); @@ -335,7 +337,7 @@ fn should_revert_minting_token_metadata_hash_twice() { contract.is_whitelisted(minting_contract.address()), "acl whitelist is incorrectly set" ); - minting_contract.mint(contract.address(), TEST_PRETTY_721_META_DATA.to_string()); + minting_contract.mint(TEST_PRETTY_721_META_DATA.to_string(), false); let blake2b_hash = utils::create_blake2b_hash(TEST_PRETTY_721_META_DATA.to_bytes().unwrap()); let token_hash = base16::encode_lower(&blake2b_hash); @@ -344,7 +346,7 @@ fn should_revert_minting_token_metadata_hash_twice() { assert_eq!(minted_metadata, TEST_PRETTY_721_META_DATA); assert_eq!( - minting_contract.try_mint(contract.address(), TEST_PRETTY_721_META_DATA.to_string(),), + minting_contract.try_mint(TEST_PRETTY_721_META_DATA.to_string(), false), Err(CEP78Error::DuplicateIdentifier.into()) ); } @@ -365,13 +367,13 @@ fn should_get_metadata_using_custom_token_hash() { .acl_white_list(contract_whitelist) .build(); let mut contract = CEP78HostRef::deploy(&env, args); + minting_contract.set_address(contract.address()); assert!( contract.is_whitelisted(minting_contract.address()), "acl whitelist is incorrectly set" ); minting_contract.mint_with_hash( - contract.address(), TEST_PRETTY_721_META_DATA.to_string(), TOKEN_HASH.to_string() ); @@ -397,13 +399,13 @@ fn should_revert_minting_custom_token_hash_identifier_twice() { .acl_white_list(contract_whitelist) .build(); let mut contract = CEP78HostRef::deploy(&env, args); + minting_contract.set_address(contract.address()); assert!( contract.is_whitelisted(minting_contract.address()), "acl whitelist is incorrectly set" ); minting_contract.mint_with_hash( - contract.address(), TEST_PRETTY_721_META_DATA.to_string(), TOKEN_HASH.to_string() ); @@ -414,7 +416,6 @@ fn should_revert_minting_custom_token_hash_identifier_twice() { assert_eq!( minting_contract.try_mint_with_hash( - contract.address(), TEST_PRETTY_721_META_DATA.to_string(), TOKEN_HASH.to_string() ), diff --git a/modules/src/cep78/tests/mint.rs b/modules/src/cep78/tests/mint.rs index f2699aa1..f89bb556 100644 --- a/modules/src/cep78/tests/mint.rs +++ b/modules/src/cep78/tests/mint.rs @@ -105,13 +105,10 @@ fn mint_should_return_dictionary_key_to_callers_owned_tokens() { .build(); let contract = CEP78HostRef::deploy(&env, args); let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + minting_contract.set_address(contract.address()); assert!(minting_contract - .try_mint_for( - contract.address(), - env.get_account(0), - TEST_PRETTY_721_META_DATA.to_string() - ) + .try_mint_for(env.get_account(0), TEST_PRETTY_721_META_DATA.to_string()) .is_ok()); /* diff --git a/modules/src/cep78/tests/mod.rs b/modules/src/cep78/tests/mod.rs index fb33b83e..59be6a9e 100644 --- a/modules/src/cep78/tests/mod.rs +++ b/modules/src/cep78/tests/mod.rs @@ -15,6 +15,7 @@ mod installer; mod metadata; mod mint; mod set_variables; +mod transfer; mod utils; pub(super) const COLLECTION_NAME: &str = "CEP78-Test-Collection"; diff --git a/modules/src/cep78/tests/transfer.rs b/modules/src/cep78/tests/transfer.rs new file mode 100644 index 00000000..a3b2b0aa --- /dev/null +++ b/modules/src/cep78/tests/transfer.rs @@ -0,0 +1,1030 @@ +use odra::{ + args::Maybe, + casper_types::bytesrepr::ToBytes, + host::{Deployer, HostEnv, HostRef, NoArgs}, + Address +}; + +use crate::cep78::{ + error::CEP78Error, + events::{Approval, ApprovalRevoked, Transfer}, + modalities::{ + EventsMode, MetadataMutability, MintingMode, NFTHolderMode, NFTIdentifierMode, + NFTMetadataKind, OwnerReverseLookupMode, OwnershipMode, TokenIdentifier, + TransferFilterContractResult, WhitelistMode + }, + tests::{ + default_args_builder, + utils::{TestContractHostRef, TEST_PRETTY_721_META_DATA} + }, + token::CEP78HostRef +}; + +use super::utils::{self, TransferFilterContractHostRef}; + +#[test] +fn should_disallow_transfer_with_minter_or_assigned_ownership_mode() { + let env = odra_test::env(); + let args = default_args_builder() + .ownership_mode(OwnershipMode::Assigned) + .minting_mode(MintingMode::Installer) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let actual_owner_balance = contract.balance_of(token_owner); + let expected_owner_balance = 1u64; + assert_eq!(actual_owner_balance, expected_owner_balance); + + let token_receiver = env.get_account(1); + contract.register_owner(Maybe::Some(token_receiver)); + + let token_id = 0u64; + assert_eq!( + contract.try_transfer( + Maybe::Some(token_id), + Maybe::None, + token_owner, + token_receiver + ), + Err(CEP78Error::InvalidOwnershipMode.into()) + ); +} + +#[test] +fn should_transfer_token_from_sender_to_receiver() { + let env = odra_test::env(); + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .events_mode(EventsMode::CES) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let actual_owner_balance = contract.balance_of(token_owner); + let expected_owner_balance = 1u64; + assert_eq!(actual_owner_balance, expected_owner_balance); + + let token_receiver = env.get_account(1); + contract.register_owner(Maybe::Some(token_receiver)); + + let token_id = 0u64; + contract.transfer( + Maybe::Some(token_id), + Maybe::None, + token_owner, + token_receiver + ); + + let actual_token_owner = contract.owner_of(Maybe::Some(token_id), Maybe::None); + assert_eq!(actual_token_owner, token_receiver); + + // TODO: not implemented yet + /*let token_receiver_page = support::get_token_page_by_id(&builder, &nft_contract_key, &token_receiver_key, token_id); + assert!(token_receiver_page[0]);*/ + + let actual_sender_balance = contract.balance_of(token_owner); + let expected_sender_balance = 0u64; + assert_eq!(actual_sender_balance, expected_sender_balance); + + let actual_receiver_balance = contract.balance_of(token_receiver); + let expected_receiver_balance = 1u64; + assert_eq!(actual_receiver_balance, expected_receiver_balance); + + // Expect Transfer event. + let expected_event = Transfer::new( + token_owner, + None, + token_receiver, + TokenIdentifier::Index(token_id).to_string() + ); + assert!(env.emitted_event(contract.address(), &expected_event)); +} + +fn approve_token_for_transfer_should_add_entry_to_approved_dictionary( + env: HostEnv, + operator: Option
+) { + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .events_mode(EventsMode::CES) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let spender = env.get_account(1); + let token_id = 0u64; + + if let Some(operator) = operator { + assert!(contract.try_set_approval_for_all(true, operator).is_ok()); + } + + let approving_account = match operator { + Some(operator) => operator, + None => token_owner + }; + env.set_caller(approving_account); + assert!(contract + .try_approve(spender, Maybe::Some(token_id), Maybe::None) + .is_ok()); + + let actual_approved_key = contract.get_approved(Maybe::Some(token_id), Maybe::None); + assert_eq!(actual_approved_key, Some(spender)); + + // Expect Approval event. + let expected_event = Approval::new( + token_owner, + spender, + TokenIdentifier::Index(token_id).to_string() + ); + assert!(env.emitted_event(contract.address(), &expected_event)); +} + +#[test] +fn approve_token_for_transfer_from_an_account_should_add_entry_to_approved_dictionary() { + let env = odra_test::env(); + approve_token_for_transfer_should_add_entry_to_approved_dictionary(env, None) +} + +#[test] +fn approve_token_for_transfer_from_an_operator_should_add_entry_to_approved_dictionary() { + let env = odra_test::env(); + let operator = env.get_account(10); + approve_token_for_transfer_should_add_entry_to_approved_dictionary(env, Some(operator)) +} + +fn revoke_token_for_transfer_should_remove_entry_to_approved_dictionary( + env: HostEnv, + operator: Option
+) { + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .events_mode(EventsMode::CES) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let spender = env.get_account(1); + let token_id = 0u64; + + if let Some(operator) = operator { + assert!(contract.try_set_approval_for_all(true, operator).is_ok()); + } + + let approving_account = match operator { + Some(operator) => operator, + None => token_owner + }; + env.set_caller(approving_account); + contract.approve(spender, Maybe::Some(token_id), Maybe::None); + + let actual_approved_key = contract.get_approved(Maybe::Some(token_id), Maybe::None); + assert_eq!(actual_approved_key, Some(spender)); + + env.set_caller(token_owner); + contract.revoke(Maybe::Some(token_id), Maybe::None); + + let actual_approved_key = contract.get_approved(Maybe::Some(token_id), Maybe::None); + assert_eq!(actual_approved_key, None); + + // Expect ApprovalRevoked event. + let expected_event = + ApprovalRevoked::new(token_owner, TokenIdentifier::Index(token_id).to_string()); + assert!(env.emitted_event(contract.address(), &expected_event)); +} + +#[test] +fn revoke_token_for_transfer_from_account_should_remove_entry_to_approved_dictionary() { + revoke_token_for_transfer_should_remove_entry_to_approved_dictionary(odra_test::env(), None) +} + +#[test] +fn revoke_token_for_transfer_from_operator_should_remove_entry_to_approved_dictionary() { + let env = odra_test::env(); + let operator = env.get_account(10); + revoke_token_for_transfer_should_remove_entry_to_approved_dictionary(env, Some(operator)) +} + +#[test] +fn should_disallow_approving_when_ownership_mode_is_minter_or_assigned() { + let env = odra_test::env(); + let args = default_args_builder() + .ownership_mode(OwnershipMode::Assigned) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .events_mode(EventsMode::CES) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let spender = env.get_account(1); + let token_id = 0u64; + + assert_eq!( + contract.try_approve(spender, Maybe::Some(token_id), Maybe::None), + Err(CEP78Error::InvalidOwnershipMode.into()) + ); +} + +fn should_be_able_to_transfer_token(env: HostEnv, operator: Option
) { + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .events_mode(EventsMode::CES) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + // Create a "to approve" spender account account and transfer funds + let spender = env.get_account(1); + let token_id = 0u64; + + if let Some(operator) = operator { + assert!(contract.try_set_approval_for_all(true, operator).is_ok()); + } + + let approving_account = match operator { + Some(operator) => operator, + None => token_owner + }; + env.set_caller(approving_account); + contract.approve(spender, Maybe::Some(token_id), Maybe::None); + + let actual_approved_key = contract.get_approved(Maybe::Some(token_id), Maybe::None); + assert_eq!(actual_approved_key, Some(spender)); + + // Create to_account and transfer minted token using spender + let to_account = env.get_account(2); + contract.register_owner(Maybe::Some(to_account)); + contract.transfer(Maybe::Some(token_id), Maybe::None, token_owner, to_account); + + let actual_approved_account_hash = contract.get_approved(Maybe::Some(token_id), Maybe::None); + assert_eq!( + actual_approved_account_hash, None, + "approved account should be set to none after a transfer" + ); +} + +#[test] +fn should_be_able_to_transfer_token_using_approved_account() { + should_be_able_to_transfer_token(odra_test::env(), None) +} + +#[test] +fn should_be_able_to_transfer_token_using_operator() { + let env = odra_test::env(); + let operator = env.get_account(11); + should_be_able_to_transfer_token(env, Some(operator)) +} + +#[test] +fn should_disallow_same_approved_account_to_transfer_token_twice() { + let env = odra_test::env(); + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .events_mode(EventsMode::CES) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let actual_owner_balance = contract.balance_of(token_owner); + let expected_owner_balance = 1u64; + assert_eq!(actual_owner_balance, expected_owner_balance); + + // Create a "to approve" spender account and transfer funds + let spender = env.get_account(1); + let token_id = 0u64; + + // Approve spender + contract.approve(spender, Maybe::Some(token_id), Maybe::None); + + let actual_approved_account = contract.get_approved(Maybe::Some(token_id), Maybe::None); + let expected_approved_account = Some(spender); + assert_eq!( + actual_approved_account, expected_approved_account, + "approved account should have been set in dictionary when approved" + ); + + // Create to_account and transfer minted token using spender + let to_account = env.get_account(2); + contract.register_owner(Maybe::Some(to_account)); + + env.set_caller(spender); + contract.transfer(Maybe::Some(token_id), Maybe::None, token_owner, to_account); + + // Create to_other_account and transfer minted token using spender + let to_other_account = env.get_account(3); + contract.register_owner(Maybe::Some(to_other_account)); + + env.set_caller(spender); + assert_eq!( + contract.try_transfer( + Maybe::Some(token_id), + Maybe::None, + to_account, + to_other_account + ), + Err(CEP78Error::InvalidTokenOwner.into()) + ); +} + +fn should_disallow_to_transfer_token_using_revoked_hash(env: HostEnv, operator: Option
) { + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let actual_owner_balance = contract.balance_of(token_owner); + let expected_owner_balance = 1u64; + assert_eq!(actual_owner_balance, expected_owner_balance); + + // Create a "to approve" spender account and transfer funds + let spender = env.get_account(1); + let token_id = 0u64; + + if let Some(operator) = operator { + assert!(contract.try_set_approval_for_all(true, operator).is_ok()); + } + + let approving_account = match operator { + Some(operator) => operator, + None => token_owner + }; + env.set_caller(approving_account); + contract.approve(spender, Maybe::Some(token_id), Maybe::None); + + let actual_approved_account = contract.get_approved(Maybe::Some(token_id), Maybe::None); + let expected_approved_account = Some(spender); + assert_eq!( + actual_approved_account, expected_approved_account, + "approved account should have been set in dictionary when approved" + ); + + // Create to_account and transfer minted token using account + let to_account = env.get_account(2); + contract.register_owner(Maybe::Some(to_account)); + + // Revoke approval + contract.revoke(Maybe::Some(token_id), Maybe::None); + + env.set_caller(spender); + assert_eq!( + contract.try_transfer(Maybe::Some(token_id), Maybe::None, token_owner, to_account), + Err(CEP78Error::InvalidTokenOwner.into()) + ); + + let actual_approved_account_hash = contract.get_approved(Maybe::Some(token_id), Maybe::None); + assert_eq!( + actual_approved_account_hash, None, + "approved account should be unset after revoke and a failed transfer" + ); +} + +#[test] +fn should_disallow_to_transfer_token_using_revoked_account() { + should_disallow_to_transfer_token_using_revoked_hash(odra_test::env(), None) +} + +#[test] +fn should_disallow_to_transfer_token_using_revoked_operator() { + let env = odra_test::env(); + let operator = env.get_account(11); + should_disallow_to_transfer_token_using_revoked_hash(env, Some(operator)) +} + +#[test] +#[ignore = "Odra does not support deprecated arguments"] +fn should_be_able_to_approve_with_deprecated_operator_argument() {} + +#[test] +fn should_transfer_between_contract_to_account() { + let env = odra_test::env(); + let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let contract_whitelist = vec![minting_contract.address().clone()]; + let args = default_args_builder() + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .events_mode(EventsMode::CES) + .holder_mode(NFTHolderMode::Contracts) + .whitelist_mode(WhitelistMode::Locked) + .ownership_mode(OwnershipMode::Transferable) + .minting_mode(MintingMode::Acl) + .acl_white_list(contract_whitelist) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + minting_contract.set_address(contract.address()); + + assert!( + contract.is_whitelisted(minting_contract.address()), + "acl whitelist is incorrectly set" + ); + minting_contract.mint(TEST_PRETTY_721_META_DATA.to_string(), true); + + let token_id = 0u64; + let actual_token_owner = contract.owner_of(Maybe::Some(token_id), Maybe::None); + assert_eq!( + &actual_token_owner, + minting_contract.address(), + "token owner is not minting contract" + ); + + let receiver = env.get_account(0); + contract.register_owner(Maybe::Some(receiver)); + minting_contract.transfer(token_id, receiver); + + let updated_token_owner = contract.owner_of(Maybe::Some(token_id), Maybe::None); + assert_eq!(updated_token_owner, receiver); +} + +#[test] +fn should_prevent_transfer_when_caller_is_not_owner() { + let env = odra_test::env(); + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .holder_mode(NFTHolderMode::Accounts) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + let token_owner = env.get_account(0); + let unauthorized_user = env.get_account(10); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let token_id = 0u64; + let actual_token_owner = contract.owner_of(Maybe::Some(token_id), Maybe::None); + assert_eq!(token_owner, actual_token_owner); + + env.set_caller(unauthorized_user); + assert_eq!( + contract.try_transfer( + Maybe::Some(token_id), + Maybe::None, + token_owner, + unauthorized_user + ), + Err(CEP78Error::InvalidTokenOwner.into()) + ); +} + +#[test] +fn should_transfer_token_in_hash_identifier_mode() { + let env = odra_test::env(); + let args = default_args_builder() + .identifier_mode(NFTIdentifierMode::Hash) + .ownership_mode(OwnershipMode::Transferable) + .metadata_mutability(MetadataMutability::Immutable) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + + let token_owner = env.get_account(0); + let new_owner = env.get_account(1); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let blake2b_hash = utils::create_blake2b_hash(TEST_PRETTY_721_META_DATA.to_bytes().unwrap()); + let token_hash = base16::encode_lower(&blake2b_hash); + contract.register_owner(Maybe::Some(new_owner)); + assert!(contract + .try_transfer(Maybe::None, Maybe::Some(token_hash), token_owner, new_owner) + .is_ok()); +} + +#[test] +fn should_not_allow_non_approved_contract_to_transfer() { + let env = odra_test::env(); + let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let args = default_args_builder() + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .holder_mode(NFTHolderMode::Mixed) + .ownership_mode(OwnershipMode::Transferable) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + minting_contract.set_address(contract.address()); + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let token_receiver = env.get_account(1); + contract.register_owner(Maybe::Some(token_receiver)); + + let token_id = 0u64; + assert_eq!( + minting_contract.try_transfer_from(token_id, token_owner, token_receiver), + Err(CEP78Error::InvalidTokenOwner.into()) + ); + + contract.approve( + *minting_contract.address(), + Maybe::Some(token_id), + Maybe::None + ); + assert!(minting_contract + .try_transfer_from(token_id, token_owner, token_receiver) + .is_ok()); +} + +#[test] +fn transfer_should_correctly_track_page_table_entries() { + let env = odra_test::env(); + let args = default_args_builder() + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .nft_metadata_kind(NFTMetadataKind::Raw) + .ownership_mode(OwnershipMode::Transferable) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + let new_owner = env.get_account(1); + + let number_of_tokens_pre_migration = 20usize; + for _ in 0..number_of_tokens_pre_migration { + contract.register_owner(Maybe::Some(token_owner)); + contract.mint(token_owner, "".to_string(), Maybe::None); + } + + contract.register_owner(Maybe::Some(new_owner)); + contract.transfer(Maybe::Some(11u64), Maybe::None, token_owner, new_owner); + + /* TODO: not implemented yet + let new_owner_page_table = support::get_dictionary_value_from_key::>( + &builder, + &nft_contract_key, + PAGE_TABLE, + &AccountHash::new(ACCOUNT_USER_1).to_string(), + ); + assert!(account_user_1_page_table[0])*/ +} + +#[test] +fn should_prevent_transfer_to_unregistered_owner() { + let env = odra_test::env(); + let args = default_args_builder() + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .nft_metadata_kind(NFTMetadataKind::Raw) + .ownership_mode(OwnershipMode::Transferable) + .identifier_mode(NFTIdentifierMode::Ordinal) + .metadata_mutability(MetadataMutability::Immutable) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint(token_owner, "".to_string(), Maybe::None); + + let _token_id = 0u64; + let _token_receiver = env.get_account(1); + /* TODO: not implemented yet + assert_eq!( + contract.try_transfer( + Maybe::Some(token_id), + Maybe::None, + token_owner, + token_receiver + ), + Err(CEP78Error::UnregisteredOwnerInTransfer.into()) + ); */ +} + +#[test] +fn should_transfer_token_from_sender_to_receiver_with_transfer_only_reporting() { + let env = odra_test::env(); + let args = default_args_builder() + .owner_reverse_lookup_mode(OwnerReverseLookupMode::TransfersOnly) + .ownership_mode(OwnershipMode::Transferable) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + env.set_caller(token_owner); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let actual_owner_balance = contract.balance_of(token_owner); + let expected_owner_balance = 1u64; + assert_eq!(actual_owner_balance, expected_owner_balance); + + let token_receiver = env.get_account(1); + env.set_caller(token_owner); + contract.register_owner(Maybe::Some(token_receiver)); + contract.transfer(Maybe::Some(0u64), Maybe::None, token_owner, token_receiver); + + let actual_token_owner = contract.owner_of(Maybe::Some(0u64), Maybe::None); + assert_eq!(actual_token_owner, token_receiver); + + /*let token_receiver_page = support::get_token_page_by_id( + &builder, + &nft_contract_key, + &Key::Account(token_receiver), + 0u64, + ); + assert!(token_receiver_page[0]);*/ + + let actual_sender_balance = contract.balance_of(token_owner); + let expected_sender_balance = 0u64; + assert_eq!(actual_sender_balance, expected_sender_balance); + + let actual_receiver_balance = contract.balance_of(token_receiver); + let expected_receiver_balance = 1u64; + assert_eq!(actual_receiver_balance, expected_receiver_balance); +} + +#[test] +fn disallow_owner_to_approve_itself() { + let env = odra_test::env(); + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + assert_eq!( + contract.try_approve(token_owner, Maybe::Some(0u64), Maybe::None), + Err(CEP78Error::InvalidAccount.into()) + ); +} + +#[test] +fn disallow_operator_to_approve_itself() { + let env = odra_test::env(); + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let token_id = 0u64; + let operator = env.get_account(1); + + assert!(contract.try_set_approval_for_all(true, operator).is_ok()); + env.set_caller(operator); + assert_eq!( + contract.try_approve(operator, Maybe::Some(token_id), Maybe::None), + Err(CEP78Error::InvalidAccount.into()) + ); +} + +#[test] +fn disallow_owner_to_approve_for_all_itself() { + let env = odra_test::env(); + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + assert_eq!( + contract.try_set_approval_for_all(true, token_owner), + Err(CEP78Error::InvalidAccount.into()) + ); +} + +#[test] +fn check_transfers_with_transfer_filter_contract_modes() { + let env = odra_test::env(); + + let mut transfer_filter_contract: TransferFilterContractHostRef = + TransferFilterContractHostRef::deploy(&env, NoArgs); + transfer_filter_contract.set_return_value(TransferFilterContractResult::DenyTransfer as u8); + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .transfer_filter_contract_contract_key(transfer_filter_contract.address().clone()) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let token_owner = env.get_account(0); + + for _i in 0..2 { + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + } + + let token_receiver = env.get_account(1); + contract.register_owner(Maybe::Some(token_receiver)); + + let token_id = 0u64; + assert_eq!( + contract.try_transfer( + Maybe::Some(token_id), + Maybe::None, + token_owner, + token_receiver + ), + Err(CEP78Error::TransferFilterContractDenied.into()) + ); + + transfer_filter_contract.set_return_value(TransferFilterContractResult::ProceedTransfer as u8); + assert!(contract + .try_transfer( + Maybe::Some(token_id), + Maybe::None, + token_owner, + token_receiver + ) + .is_ok()); + + assert_eq!( + contract.try_transfer( + Maybe::Some(token_id), + Maybe::None, + token_receiver, + token_owner + ), + Err(CEP78Error::InvalidTokenOwner.into()) + ); + + let token_id = 1u64; + assert!(contract + .try_transfer( + Maybe::Some(token_id), + Maybe::None, + token_owner, + token_receiver + ) + .is_ok()); +} + +#[test] +fn should_disallow_transfer_from_contract_with_package_operator_mode_without_operator() { + let env = odra_test::env(); + let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .holder_mode(NFTHolderMode::Mixed) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + minting_contract.set_address(contract.address()); + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let token_receiver = env.get_account(1); + contract.register_owner(Maybe::Some(token_receiver)); + + let token_id = 0u64; + assert_eq!( + minting_contract.try_transfer_from(token_id, token_owner, token_receiver), + Err(CEP78Error::InvalidTokenOwner.into()) + ); +} + +#[test] +#[ignore = "Odra does not support package operator mode - is always on"] +fn should_disallow_transfer_from_contract_without_package_operator_mode_with_package_as_operator() { +} + +#[test] +fn should_allow_transfer_from_contract_with_package_operator_mode_with_operator() { + let env = odra_test::env(); + let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .holder_mode(NFTHolderMode::Mixed) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + minting_contract.set_address(contract.address()); + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let token_receiver = env.get_account(1); + contract.register_owner(Maybe::Some(token_receiver)); + + let token_id = 0u64; + contract.set_approval_for_all(true, minting_contract.address().clone()); + assert!(minting_contract + .try_transfer_from(token_id, token_owner, token_receiver) + .is_ok()); + + let actual_token_owner = contract.owner_of(Maybe::Some(token_id), Maybe::None); + assert_eq!(actual_token_owner, token_receiver); +} + +#[test] +#[ignore = "Odra does not support package operator mode - is always on"] +fn should_disallow_package_operator_to_approve_without_package_operator_mode() {} + +#[test] +fn should_allow_package_operator_to_approve_with_package_operator_mode() { + let env = odra_test::env(); + let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .holder_mode(NFTHolderMode::Mixed) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + minting_contract.set_address(contract.address()); + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let token_receiver = env.get_account(1); + contract.register_owner(Maybe::Some(token_receiver)); + contract.set_approval_for_all(true, minting_contract.address().clone()); + + let token_id = 0u64; + let spender = env.get_account(2); + minting_contract.approve(spender, token_id); + + env.set_caller(spender); + contract.transfer( + Maybe::Some(token_id), + Maybe::None, + token_owner, + token_receiver + ); + + let actual_token_owner = contract.owner_of(Maybe::Some(token_id), Maybe::None); + assert_eq!(actual_token_owner, token_receiver); +} + +#[test] +fn should_allow_account_to_approve_spender_with_package_operator() { + let env = odra_test::env(); + let minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .holder_mode(NFTHolderMode::Mixed) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let token_receiver = env.get_account(1); + contract.register_owner(Maybe::Some(token_receiver)); + contract.set_approval_for_all(true, minting_contract.address().clone()); + + let token_id = 0u64; + let spender = env.get_account(2); + contract.approve(spender, Maybe::Some(token_id), Maybe::None); + + env.set_caller(spender); + contract.transfer( + Maybe::Some(token_id), + Maybe::None, + token_owner, + token_receiver + ); + + let actual_token_owner = contract.owner_of(Maybe::Some(token_id), Maybe::None); + assert_eq!(actual_token_owner, token_receiver); +} + +#[test] +fn should_allow_package_operator_to_revoke_with_package_operator_mode() { + let env = odra_test::env(); + let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let args = default_args_builder() + .ownership_mode(OwnershipMode::Transferable) + .holder_mode(NFTHolderMode::Mixed) + .build(); + let mut contract = CEP78HostRef::deploy(&env, args); + minting_contract.set_address(contract.address()); + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + contract.mint( + token_owner, + TEST_PRETTY_721_META_DATA.to_string(), + Maybe::None + ); + + let token_receiver = env.get_account(1); + contract.register_owner(Maybe::Some(token_receiver)); + contract.set_approval_for_all(true, minting_contract.address().clone()); + + let token_id = 0u64; + let spender = env.get_account(2); + contract.approve(spender, Maybe::Some(token_id), Maybe::None); + + minting_contract.revoke(token_id); + env.set_caller(spender); + + assert_eq!( + contract.try_transfer( + Maybe::Some(token_id), + Maybe::None, + token_owner, + token_receiver + ), + Err(CEP78Error::InvalidTokenOwner.into()) + ); + + let actual_token_owner = contract.owner_of(Maybe::Some(token_id), Maybe::None); + assert_eq!(actual_token_owner, token_owner); +} diff --git a/modules/src/cep78/tests/utils.rs b/modules/src/cep78/tests/utils.rs index 45eb46dd..378bad80 100644 --- a/modules/src/cep78/tests/utils.rs +++ b/modules/src/cep78/tests/utils.rs @@ -6,7 +6,12 @@ use crate::cep78::{ token::CEP78InitArgs }; use blake2::{digest::VariableOutput, Blake2bVar}; -use odra::{args::Maybe, casper_types::BLAKE2B_DIGEST_LENGTH, prelude::*, Address, ContractRef}; +use odra::{ + args::Maybe, + casper_types::{URef, BLAKE2B_DIGEST_LENGTH}, + prelude::*, + Address, ContractRef, Var +}; use std::io::Write; #[derive(Default)] @@ -21,8 +26,6 @@ pub struct InitArgsBuilder { holder_mode: Maybe, whitelist_mode: Maybe, acl_white_list: Maybe>, - acl_package_mode: Maybe>, - package_operator_mode: Maybe>, json_schema: Maybe, receipt_name: Maybe, identifier_mode: NFTIdentifierMode, @@ -68,11 +71,6 @@ impl InitArgsBuilder { self } - pub fn nft_kind(mut self, nft_kind: NFTKind) -> Self { - self.nft_kind = nft_kind; - self - } - pub fn holder_mode(mut self, holder_mode: NFTHolderMode) -> Self { self.holder_mode = Maybe::Some(holder_mode); self @@ -88,26 +86,11 @@ impl InitArgsBuilder { self } - pub fn acl_package_mode(mut self, acl_package_mode: Vec) -> Self { - self.acl_package_mode = Maybe::Some(acl_package_mode); - self - } - - pub fn package_operator_mode(mut self, package_operator_mode: Vec) -> Self { - self.package_operator_mode = Maybe::Some(package_operator_mode); - self - } - pub fn json_schema(mut self, json_schema: String) -> Self { self.json_schema = Maybe::Some(json_schema); self } - pub fn receipt_name(mut self, receipt_name: String) -> Self { - self.receipt_name = Maybe::Some(receipt_name); - self - } - pub fn identifier_mode(mut self, identifier_mode: NFTIdentifierMode) -> Self { self.identifier_mode = identifier_mode; self @@ -189,7 +172,7 @@ impl InitArgsBuilder { metadata_mutability: self.metadata_mutability, owner_reverse_lookup_mode: self.owner_reverse_lookup_mode, events_mode: self.events_mode, - transfer_filter_contract_contract_key: self.transfer_filter_contract_contract_key, + transfer_filter_contract_contract: self.transfer_filter_contract_contract_key, additional_required_metadata: self.additional_required_metadata, optional_metadata: self.optional_metadata } @@ -231,16 +214,28 @@ struct DummyContract; impl DummyContract {} #[odra::module] -struct TestContract; +struct TestContract { + nft_contract: Var
+} #[odra::module] impl TestContract { + pub fn set_address(&mut self, nft_contract: &Address) { + self.nft_contract.set(*nft_contract); + } + pub fn mint( &mut self, - nft_contract_address: &Address, - token_metadata: String + token_metadata: String, + is_reverse_lookup_enabled: bool ) -> (String, Address, String) { - NftContractContractRef::new(self.env(), *nft_contract_address).mint( + let nft_contract_address = self.nft_contract.get().unwrap(); + if is_reverse_lookup_enabled { + NftContractContractRef::new(self.env(), nft_contract_address) + .register_owner(Maybe::Some(self.env().self_address())); + } + + NftContractContractRef::new(self.env(), nft_contract_address).mint( self.env().self_address(), token_metadata, Maybe::None @@ -249,34 +244,75 @@ impl TestContract { pub fn mint_with_hash( &mut self, - nft_contract_address: &Address, token_metadata: String, token_hash: String ) -> (String, Address, String) { - NftContractContractRef::new(self.env(), *nft_contract_address).mint( + let nft_contract_address = self.nft_contract.get().unwrap(); + NftContractContractRef::new(self.env(), nft_contract_address).mint( self.env().self_address(), token_metadata, Maybe::Some(token_hash) ) } - pub fn burn(&mut self, nft_contract_address: &Address, token_id: u64) { - NftContractContractRef::new(self.env(), *nft_contract_address) + pub fn burn(&mut self, token_id: u64) { + let nft_contract_address = self.nft_contract.get().unwrap(); + NftContractContractRef::new(self.env(), nft_contract_address) .burn(Maybe::Some(token_id), Maybe::None) } pub fn mint_for( &mut self, - nft_contract_address: &Address, token_owner: Address, token_metadata: String ) -> (String, Address, String) { - NftContractContractRef::new(self.env(), *nft_contract_address).mint( + let nft_contract_address = self.nft_contract.get().unwrap(); + NftContractContractRef::new(self.env(), nft_contract_address).mint( token_owner, token_metadata, Maybe::None ) } + + pub fn transfer(&mut self, token_id: u64, target: Address) -> (String, Address) { + let address = self.env().self_address(); + let nft_contract_address = self.nft_contract.get().unwrap(); + NftContractContractRef::new(self.env(), nft_contract_address).transfer( + Maybe::Some(token_id), + Maybe::None, + address, + target + ) + } + pub fn transfer_from( + &mut self, + token_id: u64, + source: Address, + target: Address + ) -> (String, Address) { + let nft_contract_address = self.nft_contract.get().unwrap(); + NftContractContractRef::new(self.env(), nft_contract_address).transfer( + Maybe::Some(token_id), + Maybe::None, + source, + target + ) + } + + pub fn approve(&mut self, spender: Address, token_id: u64) { + let nft_contract_address = self.nft_contract.get().unwrap(); + NftContractContractRef::new(self.env(), nft_contract_address).approve( + spender, + Maybe::Some(token_id), + Maybe::None + ) + } + + pub fn revoke(&mut self, token_id: u64) { + let nft_contract_address = self.nft_contract.get().unwrap(); + NftContractContractRef::new(self.env(), nft_contract_address) + .revoke(Maybe::Some(token_id), Maybe::None) + } } #[odra::external_contract] @@ -288,6 +324,32 @@ trait NftContract { token_hash: Maybe ) -> (String, Address, String); fn burn(&mut self, token_id: Maybe, token_hash: Maybe); + fn register_owner(&mut self, token_owner: Maybe
) -> (String, URef); + fn transfer( + &mut self, + token_id: Maybe, + token_hash: Maybe, + source: Address, + target: Address + ) -> (String, Address); + fn approve(&mut self, spender: Address, token_id: Maybe, token_hash: Maybe); + fn revoke(&mut self, token_id: Maybe, token_hash: Maybe); +} + +#[odra::module] +pub struct TransferFilterContract { + value: Var +} + +#[odra::module] +impl TransferFilterContract { + pub fn set_return_value(&mut self, return_value: u8) { + self.value.set(return_value); + } + + pub fn can_transfer(&self) -> u8 { + self.value.get_or_default() + } } pub(crate) fn create_blake2b_hash>(data: T) -> [u8; BLAKE2B_DIGEST_LENGTH] { diff --git a/modules/src/cep78/token.rs b/modules/src/cep78/token.rs index 982f960c..9c95f0f4 100644 --- a/modules/src/cep78/token.rs +++ b/modules/src/cep78/token.rs @@ -39,7 +39,8 @@ pub struct CEP78 { token_count: Mapping, burnt_tokens: Mapping, operators: Mapping<(Address, Address), bool>, - receipt_name: Var + receipt_name: Var, + transfer_filter_contract: Var
} #[odra::module] @@ -66,7 +67,7 @@ impl CEP78 { operator_burn_mode: Maybe, owner_reverse_lookup_mode: Maybe, events_mode: Maybe, - transfer_filter_contract_contract_key: Maybe
, + transfer_filter_contract_contract: Maybe
, additional_required_metadata: Maybe>, optional_metadata: Maybe> ) { @@ -118,6 +119,11 @@ impl CEP78 { { self.env().revert(CEP78Error::InvalidReportingMode) } + + if let Maybe::Some(transfer_filter_contract_contract) = transfer_filter_contract_contract { + self.transfer_filter_contract + .set(transfer_filter_contract_contract); + } } /// Exposes all variables that can be changed by managing account post @@ -327,8 +333,8 @@ impl CEP78 { &mut self, token_id: Maybe, token_hash: Maybe, - source_key: Address, - target_key: Address + source: Address, + target: Address ) -> (String, Address) { // If we are in minter or assigned mode we are not allowed to transfer ownership of token, hence // we revert. @@ -338,7 +344,7 @@ impl CEP78 { let token_id = token_identifier.to_string(); // We assume we cannot transfer burnt tokens self.ensure_not_burned(&token_id); - self.ensure_owner(&token_id, &source_key); + self.ensure_owner(&token_id, &source); let caller = self.env().caller(); @@ -355,14 +361,14 @@ impl CEP78 { // Check if caller is operator to execute transfer let is_operator = if !is_owner && !is_approved { - self.operators.get_or_default(&(source_key, caller)) + self.operators.get_or_default(&(source, caller)) } else { false }; - if let Some(filter_contract) = utils::get_transfer_filter_contract() { + if let Some(filter_contract) = self.transfer_filter_contract.get() { let result = TransferFilterContractContractRef::new(self.env(), filter_contract) - .can_transfer(source_key, target_key, token_identifier.clone()); + .can_transfer(source, target, token_identifier.clone()); if TransferFilterContractResult::DenyTransfer == result { self.env().revert(CEP78Error::TransferFilterContractDenied); @@ -377,23 +383,23 @@ impl CEP78 { // Updated token_owners dictionary. Revert if token_owner not found. match self.owners.get(&token_id) { Some(token_actual_owner) => { - if token_actual_owner != source_key { + if token_actual_owner != source { self.env().revert(CEP78Error::InvalidTokenOwner) } - self.owners.set(&token_id, target_key); + self.owners.set(&token_id, target); } None => self .env() .revert(CEP78Error::MissingOwnerTokenIdentifierKey) } - self.token_count.subtract(&source_key, 1); - self.token_count.add(&target_key, 1); + self.token_count.subtract(&source, 1); + self.token_count.add(&target, 1); self.approved.set(&token_id, Option::
::None); let spender = if caller == owner { None } else { Some(caller) }; - self.emit_ces_event(Transfer::new(owner, spender, target_key, token_id)); + self.emit_ces_event(Transfer::new(owner, spender, target, token_id)); let reporting_mode = self.reverse_lookup.get_mode(); @@ -401,25 +407,23 @@ impl CEP78 { reporting_mode { // Update to_account owned_tokens. Revert if owned_tokens list is not found - let tokens_count = self.reverse_lookup.get_token_index(&token_identifier); - if OwnerReverseLookupMode::TransfersOnly == reporting_mode { - self.pagination - .add_page_entry_and_page_record(tokens_count, &source_key, false); - } - - let (page_table_entry, page_uref) = self.pagination.update_page_entry_and_page_record( - tokens_count, - &source_key, - &target_key - ); - - let receipt_name = self.receipt_name.get_or_default(); - let receipt_string = format!("{receipt_name}_m_{PAGE_SIZE}_p_{page_table_entry}"); + // let tokens_count = self.reverse_lookup.get_token_index(&token_identifier); + // if OwnerReverseLookupMode::TransfersOnly == reporting_mode { + // self.pagination + // .add_page_entry_and_page_record(tokens_count, &source, false); + // } + + // let (page_table_entry, page_uref) = + // self.pagination + // .update_page_entry_and_page_record(tokens_count, &source, &target); + + // let receipt_name = self.receipt_name.get_or_default(); + // let receipt_string = format!("{receipt_name}_m_{PAGE_SIZE}_p_{page_table_entry}"); // let receipt_address = Key::dictionary(page_uref, owned_tokens_item_key.as_bytes()); - // TODO: should not return `source_key` - return (receipt_string, source_key); + // TODO: should not return `source` + // return (receipt_string, source); } - return ("".to_owned(), source_key); + return ("".to_owned(), source); } /// Approves another token holder (an approved account) to transfer tokens. It diff --git a/modules/src/cep78/utils.rs b/modules/src/cep78/utils.rs index 68281fb0..7b4424ce 100644 --- a/modules/src/cep78/utils.rs +++ b/modules/src/cep78/utils.rs @@ -1,6 +1,4 @@ -use odra::{ - casper_types::bytesrepr::FromBytes, Address, ContractEnv, OdraError, UnwrapOrRevert, Var -}; +use odra::{casper_types::bytesrepr::FromBytes, ContractEnv, OdraError, UnwrapOrRevert, Var}; pub trait GetAs { fn get_as(&self, env: &ContractEnv) -> T; @@ -32,10 +30,6 @@ where } } -pub fn get_transfer_filter_contract() -> Option
{ - None -} - // pub fn migrate_owned_tokens_in_ordinal_mode() { // let current_number_of_minted_tokens = utils::get_stored_value_with_user_errors::( // NUMBER_OF_MINTED_TOKENS, From 039e933eef0f33671a2ea4d321754f1d4ef84c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Wed, 24 Apr 2024 09:16:59 +0200 Subject: [PATCH 15/38] refactor --- modules/src/cep78/collection_info.rs | 72 ----- modules/src/cep78/constants.rs | 76 ----- modules/src/cep78/data.rs | 152 ++++++++++ modules/src/cep78/error.rs | 6 +- modules/src/cep78/metadata.rs | 10 +- modules/src/cep78/mod.rs | 6 +- modules/src/cep78/modalities.rs | 134 +++++++-- modules/src/cep78/pagination.rs | 166 ----------- modules/src/cep78/reverse_lookup.rs | 252 ++++++++++++++-- modules/src/cep78/settings.rs | 7 +- modules/src/cep78/tests/metadata.rs | 2 +- modules/src/cep78/tests/mint.rs | 7 +- modules/src/cep78/token.rs | 420 ++++++++++----------------- modules/src/cep78/utils.rs | 196 ------------- modules/src/cep78/whitelist.rs | 2 +- 15 files changed, 672 insertions(+), 836 deletions(-) delete mode 100644 modules/src/cep78/collection_info.rs create mode 100644 modules/src/cep78/data.rs delete mode 100644 modules/src/cep78/pagination.rs delete mode 100644 modules/src/cep78/utils.rs diff --git a/modules/src/cep78/collection_info.rs b/modules/src/cep78/collection_info.rs deleted file mode 100644 index 1a4bf7f9..00000000 --- a/modules/src/cep78/collection_info.rs +++ /dev/null @@ -1,72 +0,0 @@ -use odra::prelude::*; -use odra::Address; -use odra::Sequence; -use odra::Var; - -use super::constants; -use super::error::CEP78Error; - -#[odra::module] -pub struct CollectionInfo { - name: Var, - symbol: Var, - total_token_supply: Var, - counter: Sequence, - installer: Var
-} - -impl CollectionInfo { - pub fn init( - &mut self, - name: String, - symbol: String, - total_token_supply: u64, - installer: Address - ) { - if total_token_supply == 0 { - self.env().revert(CEP78Error::CannotInstallWithZeroSupply) - } - - if total_token_supply > constants::MAX_TOTAL_TOKEN_SUPPLY { - self.env().revert(CEP78Error::ExceededMaxTotalSupply) - } - - self.name.set(name); - self.symbol.set(symbol); - self.total_token_supply.set(total_token_supply); - self.installer.set(installer); - - self.counter.next_value(); - } - - #[inline] - pub fn installer(&self) -> Address { - self.installer - .get_or_revert_with(CEP78Error::MissingInstaller) - } - - #[inline] - pub fn total_token_supply(&self) -> u64 { - self.total_token_supply.get_or_default() - } - - #[inline] - pub fn increment_number_of_minted_tokens(&mut self) { - self.counter.next_value(); - } - - #[inline] - pub fn number_of_minted_tokens(&self) -> u64 { - self.counter.get_current_value() - } - - #[inline] - pub fn collection_name(&self) -> String { - self.name.get_or_default() - } - - #[inline] - pub fn collection_symbol(&self) -> String { - self.symbol.get_or_default() - } -} diff --git a/modules/src/cep78/constants.rs b/modules/src/cep78/constants.rs index 94bc2b92..d87bded8 100644 --- a/modules/src/cep78/constants.rs +++ b/modules/src/cep78/constants.rs @@ -1,85 +1,9 @@ -pub const PREFIX_ACCESS_KEY_NAME: &str = "cep78_contract_package_access"; -pub const PREFIX_CEP78: &str = "cep78"; -pub const PREFIX_CONTRACT_NAME: &str = "cep78_contract_hash"; -pub const PREFIX_CONTRACT_VERSION: &str = "cep78_contract_version"; -pub const PREFIX_HASH_KEY_NAME: &str = "cep78_contract_package"; pub const PREFIX_PAGE_DICTIONARY: &str = "page"; -pub const ENTRY_POINT_APPROVE: &str = "approve"; -pub const ENTRY_POINT_BALANCE_OF: &str = "balance_of"; -pub const ENTRY_POINT_BURN: &str = "burn"; -pub const ENTRY_POINT_GET_APPROVED: &str = "get_approved"; -pub const ENTRY_POINT_INIT: &str = "init"; -pub const ENTRY_POINT_IS_APPROVED_FOR_ALL: &str = "is_approved_for_all"; -pub const ENTRY_POINT_METADATA: &str = "metadata"; -pub const ENTRY_POINT_MIGRATE: &str = "migrate"; -pub const ENTRY_POINT_MINT: &str = "mint"; -pub const ENTRY_POINT_OWNER_OF: &str = "owner_of"; -pub const ENTRY_POINT_REVOKE: &str = "revoke"; -pub const ENTRY_POINT_REGISTER_OWNER: &str = "register_owner"; -pub const ENTRY_POINT_SET_APPROVALL_FOR_ALL: &str = "set_approval_for_all"; -pub const ENTRY_POINT_SET_TOKEN_METADATA: &str = "set_token_metadata"; -pub const ENTRY_POINT_SET_VARIABLES: &str = "set_variables"; -pub const ENTRY_POINT_TRANSFER: &str = "transfer"; -pub const ENTRY_POINT_UPDATED_RECEIPTS: &str = "updated_receipts"; - -pub const ACL_PACKAGE_MODE: &str = "acl_package_mode"; -pub const ACL_WHITELIST: &str = "acl_whitelist"; -pub const ALLOW_MINTING: &str = "allow_minting"; -pub const APPROVED: &str = "approved"; -pub const BURN_MODE: &str = "burn_mode"; -pub const BURNT_TOKENS: &str = "burnt_tokens"; -pub const COLLECTION_NAME: &str = "collection_name"; -pub const COLLECTION_SYMBOL: &str = "collection_symbol"; -pub const CONTRACT_WHITELIST: &str = "contract_whitelist"; -pub const EVENT_TYPE: &str = "event_type"; -pub const EVENTS: &str = "events"; -pub const EVENTS_MODE: &str = "events_mode"; -pub const HASH_BY_INDEX: &str = "hash_by_index"; -pub const HOLDER_MODE: &str = "holder_mode"; -pub const IDENTIFIER_MODE: &str = "identifier_mode"; -pub const INDEX_BY_HASH: &str = "index_by_hash"; -pub const INSTALLER: &str = "installer"; -pub const JSON_SCHEMA: &str = "json_schema"; pub const METADATA_CEP78: &str = "metadata_cep78"; pub const METADATA_CUSTOM_VALIDATED: &str = "metadata_custom_validated"; -pub const METADATA_MUTABILITY: &str = "metadata_mutability"; pub const METADATA_NFT721: &str = "metadata_nft721"; pub const METADATA_RAW: &str = "metadata_raw"; -pub const MIGRATION_FLAG: &str = "migration_flag"; -pub const MINTING_MODE: &str = "minting_mode"; -pub const NFT_KIND: &str = "nft_kind"; -pub const NFT_METADATA_KIND: &str = "nft_metadata_kind"; -pub const NFT_METADATA_KINDS: &str = "nft_metadata_kinds"; -pub const NUMBER_OF_MINTED_TOKENS: &str = "number_of_minted_tokens"; -pub const OPERATOR: &str = "operator"; -pub const OPERATORS: &str = "operators"; -pub const OPERATOR_BURN_MODE: &str = "operator_burn_mode"; -pub const OWNED_TOKENS: &str = "owned_tokens"; -pub const OWNER: &str = "owner"; -pub const BURNER: &str = "burner"; -pub const OWNERSHIP_MODE: &str = "ownership_mode"; -pub const PACKAGE_OPERATOR_MODE: &str = "package_operator_mode"; -pub const PAGE_LIMIT: &str = "page_limit"; -pub const PAGE_TABLE: &str = "page_table"; -pub const RECEIPT_NAME: &str = "receipt_name"; -pub const RECIPIENT: &str = "recipient"; -pub const REPORTING_MODE: &str = "reporting_mode"; -pub const RLO_MFLAG: &str = "rlo_mflag"; -pub const SENDER: &str = "sender"; -pub const SPENDER: &str = "spender"; -pub const TOKEN_COUNT: &str = "balances"; -pub const TOKEN_ID: &str = "token_id"; -pub const TOKEN_ISSUERS: &str = "token_issuers"; -pub const TOKEN_OWNERS: &str = "token_owners"; -pub const TOTAL_TOKEN_SUPPLY: &str = "total_token_supply"; -pub const TRANSFER_FILTER_CONTRACT: &str = "transfer_filter_contract"; -pub const TRANSFER_FILTER_CONTRACT_METHOD: &str = "can_transfer"; -pub const UNMATCHED_HASH_COUNT: &str = "unmatched_hash_count"; -pub const WHITELIST_MODE: &str = "whitelist_mode"; // The cap on the amount of tokens within a given CEP-78 collection. pub const MAX_TOTAL_TOKEN_SUPPLY: u64 = 1_000_000u64; - -pub const ACCESS_KEY_NAME_1_0_0: &str = "nft_contract_package_access"; -pub const HASH_KEY_NAME_1_0_0: &str = "nft_contract_package"; diff --git a/modules/src/cep78/data.rs b/modules/src/cep78/data.rs new file mode 100644 index 00000000..5fb5b7aa --- /dev/null +++ b/modules/src/cep78/data.rs @@ -0,0 +1,152 @@ +use odra::prelude::*; +use odra::Address; +use odra::Mapping; +use odra::Sequence; +use odra::UnwrapOrRevert; +use odra::Var; + +use super::constants; +use super::error::CEP78Error; + +#[odra::module] +pub struct CollectionData { + name: Var, + symbol: Var, + total_token_supply: Var, + counter: Sequence, + installer: Var
, + owners: Mapping, + issuers: Mapping, + approved: Mapping>, + token_count: Mapping, + burnt_tokens: Mapping, + operators: Mapping<(Address, Address), bool> +} + +impl CollectionData { + pub fn init( + &mut self, + name: String, + symbol: String, + total_token_supply: u64, + installer: Address + ) { + if total_token_supply == 0 { + self.env().revert(CEP78Error::CannotInstallWithZeroSupply) + } + + if total_token_supply > constants::MAX_TOTAL_TOKEN_SUPPLY { + self.env().revert(CEP78Error::ExceededMaxTotalSupply) + } + + self.name.set(name); + self.symbol.set(symbol); + self.total_token_supply.set(total_token_supply); + self.installer.set(installer); + + self.counter.next_value(); + } + + #[inline] + pub fn installer(&self) -> Address { + self.installer + .get_or_revert_with(CEP78Error::MissingInstaller) + } + + #[inline] + pub fn total_token_supply(&self) -> u64 { + self.total_token_supply.get_or_default() + } + + #[inline] + pub fn increment_number_of_minted_tokens(&mut self) { + self.counter.next_value(); + } + + #[inline] + pub fn number_of_minted_tokens(&self) -> u64 { + self.counter.get_current_value() + } + + #[inline] + pub fn collection_name(&self) -> String { + self.name.get_or_default() + } + + #[inline] + pub fn collection_symbol(&self) -> String { + self.symbol.get_or_default() + } + + #[inline] + pub fn set_owner(&mut self, token_id: &String, token_owner: Address) { + self.owners.set(token_id, token_owner); + } + + #[inline] + pub fn set_issuer(&mut self, token_id: &String, issuer: Address) { + self.owners.set(token_id, issuer); + } + + #[inline] + pub fn increment_counter(&mut self, token_owner: &Address) { + self.token_count.add(token_owner, 1); + } + + #[inline] + pub fn decrement_counter(&mut self, token_owner: &Address) { + self.token_count.subtract(token_owner, 1); + } + + #[inline] + pub fn operator(&self, owner: Address, operator: Address) -> bool { + self.operators.get_or_default(&(owner, operator)) + } + + #[inline] + pub fn set_operator(&mut self, owner: Address, operator: Address, approved: bool) { + self.operators.set(&(owner, operator), approved); + } + + #[inline] + pub fn mark_burnt(&mut self, token_id: &String) { + self.burnt_tokens.set(token_id, ()); + } + + #[inline] + pub fn is_burnt(&self, token_id: &String) -> bool { + self.burnt_tokens.get(token_id).is_some() + } + + #[inline] + pub fn issuer(&self, token_id: &String) -> Address { + self.issuers + .get(token_id) + .unwrap_or_revert_with(&self.env(), CEP78Error::InvalidTokenIdentifier) + } + + #[inline] + pub fn approve(&mut self, token_id: &String, operator: Address) { + self.approved.set(token_id, Some(operator)); + } + + #[inline] + pub fn revoke(&mut self, token_id: &String) { + self.approved.set(token_id, None); + } + + #[inline] + pub fn approved(&self, token_id: &String) -> Option
{ + self.approved.get(token_id).flatten() + } + + #[inline] + pub fn owner_of(&self, token_id: &String) -> Option
{ + self.owners.get(token_id) + } + + #[inline] + pub fn token_count(&self, owner: &Address) -> u64 { + self.token_count.get(owner).unwrap_or_default() + } +} diff --git a/modules/src/cep78/error.rs b/modules/src/cep78/error.rs index 61bd1875..4b7a26b2 100644 --- a/modules/src/cep78/error.rs +++ b/modules/src/cep78/error.rs @@ -87,11 +87,11 @@ pub enum CEP78Error { InvalidReceiptName = 85, InvalidJsonMetadata = 86, InvalidJsonFormat = 87, - FailedToParseCep99Metadata = 88, + FailedToParseCep78Metadata = 88, FailedToParse721Metadata = 89, FailedToParseCustomMetadata = 90, - InvalidCEP99Metadata = 91, - FailedToJsonifyCEP99Metadata = 92, + InvalidCEP78Metadata = 91, + FailedToJsonifyCEP78Metadata = 92, InvalidNFT721Metadata = 93, FailedToJsonifyNFT721Metadata = 94, InvalidCustomMetadata = 95, diff --git a/modules/src/cep78/metadata.rs b/modules/src/cep78/metadata.rs index 4d7ca888..04e39975 100644 --- a/modules/src/cep78/metadata.rs +++ b/modules/src/cep78/metadata.rs @@ -116,25 +116,25 @@ impl Metadata { match kind { NFTMetadataKind::CEP78 => { let metadata = serde_json_wasm::from_str::(&metadata) - .map_err(|_| CEP78Error::FailedToParseCep99Metadata)?; + .map_err(|_| CEP78Error::FailedToParseCep78Metadata)?; if let Some(name_property) = token_schema.properties.get("name") { if name_property.required && metadata.name.is_empty() { - self.env().revert(CEP78Error::InvalidCEP99Metadata) + self.env().revert(CEP78Error::InvalidCEP78Metadata) } } if let Some(token_uri_property) = token_schema.properties.get("token_uri") { if token_uri_property.required && metadata.token_uri.is_empty() { - self.env().revert(CEP78Error::InvalidCEP99Metadata) + self.env().revert(CEP78Error::InvalidCEP78Metadata) } } if let Some(checksum_property) = token_schema.properties.get("checksum") { if checksum_property.required && metadata.checksum.is_empty() { - self.env().revert(CEP78Error::InvalidCEP99Metadata) + self.env().revert(CEP78Error::InvalidCEP78Metadata) } } serde_json::to_string_pretty(&metadata) - .map_err(|_| CEP78Error::FailedToJsonifyCEP99Metadata) + .map_err(|_| CEP78Error::FailedToJsonifyCEP78Metadata) } NFTMetadataKind::NFT721 => { let metadata = serde_json_wasm::from_str::(&metadata) diff --git a/modules/src/cep78/mod.rs b/modules/src/cep78/mod.rs index d51540eb..1d1cb010 100644 --- a/modules/src/cep78/mod.rs +++ b/modules/src/cep78/mod.rs @@ -1,16 +1,14 @@ #![allow(missing_docs)] -mod collection_info; -pub mod constants; +mod data; +mod constants; pub mod error; pub mod events; mod metadata; pub mod modalities; -mod pagination; mod reverse_lookup; mod settings; #[cfg(test)] mod tests; pub mod token; -mod utils; mod whitelist; diff --git a/modules/src/cep78/modalities.rs b/modules/src/cep78/modalities.rs index 6057c723..f8761eee 100644 --- a/modules/src/cep78/modalities.rs +++ b/modules/src/cep78/modalities.rs @@ -1,12 +1,16 @@ use super::error::CEP78Error; use odra::prelude::*; +/// The WhitelistMode dictates if the ACL whitelist restricting access to +/// the mint entry point can be updated. #[repr(u8)] #[odra::odra_type] #[derive(Default)] pub enum WhitelistMode { + /// The ACL whitelist is unlocked and can be updated via the `set_variables` endpoint. #[default] Unlocked = 0, + /// The ACL whitelist is locked and cannot be updated further. Locked = 1 } @@ -22,12 +26,23 @@ impl TryFrom for WhitelistMode { } } +/// The modality dictates which entities on a Casper network can own and mint NFTs. +/// +/// If the NFTHolderMode is set to Contracts a ContractHash whitelist must be provided. +/// This whitelist dictates which Contracts are allowed to mint NFTs in the restricted +/// Installer minting mode. +/// +/// This modality is an optional installation parameter and will default to the Mixed mode +/// if not provided. However, this mode cannot be changed once the contract has been installed. #[repr(u8)] #[odra::odra_type] #[derive(Copy, Default)] pub enum NFTHolderMode { + /// Only Accounts can own and mint NFTs. Accounts = 0, + /// Only Contracts can own and mint NFTs. Contracts = 1, + /// Both Accounts and Contracts can own and mint NFTs. #[default] Mixed = 2 } @@ -45,6 +60,11 @@ impl TryFrom for NFTHolderMode { } } +/// The minting mode governs the behavior of contract when minting new tokens. +/// +/// This modality is an optional installation parameter and will default +/// to the `Installer` mode if not provided. However, this mode cannot be changed +/// once the contract has been installed. #[odra::odra_type] #[repr(u8)] #[derive(Default)] @@ -124,14 +144,22 @@ impl TryFrom for Requirement { } } +/// This modality dictates the schema for the metadata for NFTs minted +/// by a given instance of an NFT contract. #[repr(u8)] #[derive(Default, PartialOrd, Ord)] #[odra::odra_type] pub enum NFTMetadataKind { + /// NFTs must have valid metadata conforming to the CEP-78 schema. #[default] CEP78 = 0, + /// NFTs must have valid metadata conforming to the NFT-721 metadata schema. NFT721 = 1, + /// Metadata validation will not occur and raw strings can be passed to + /// `token_metadata` runtime argument as part of the call to mint entrypoint. Raw = 2, + /// Custom schema provided at the time of install will be used when validating + /// the metadata as part of the call to mint entrypoint. CustomValidated = 3 } @@ -149,6 +177,11 @@ impl TryFrom for NFTMetadataKind { } } +/// This modality specifies the behavior regarding ownership of NFTs and whether +/// the owner of the NFT can change over the contract's lifetime. +/// +/// Ownership mode is a required installation parameter and cannot be changed +/// once the contract has been installed. #[repr(u8)] #[odra::odra_type] #[derive(Default, PartialOrd, Ord, Copy)] @@ -175,12 +208,30 @@ impl TryFrom for OwnershipMode { } } +/// The identifier mode governs the primary identifier for NFTs minted +/// for a given instance on an installed contract. +/// +/// Since the default primary identifier in the `Hash` mode is custom or derived by +/// hashing over the metadata, making it a content-addressed identifier, +/// the metadata for the minted NFT cannot be updated after the mint. +/// +/// Attempting to install the contract with the [MetadataMutability] modality set to +/// `Mutable` in the `Hash` identifier mode will raise an error. +/// +/// This modality is a required installation parameter and cannot be changed +/// once the contract has been installed. #[repr(u8)] #[odra::odra_type] #[derive(Default, PartialOrd, Ord, Copy)] pub enum NFTIdentifierMode { + /// NFTs minted in this modality are identified by a u64 value. + /// This value is determined by the number of NFTs minted by + /// the contract at the time the NFT is minted. #[default] Ordinal = 0, + /// NFTs minted in this modality are identified by an optional custom + /// string identifier or by default a base16 encoded representation of + /// the blake2b hash of the metadata provided at the time of mint. Hash = 1 } @@ -196,12 +247,22 @@ impl TryFrom for NFTIdentifierMode { } } +/// The metadata mutability mode governs the behavior around updates to a given NFTs metadata. +/// +/// The Mutable option cannot be used in conjunction with the Hash modality for the NFT identifier; +/// attempting to install the contract with this configuration raises +/// [super::error::CEP78Error::InvalidMetadataMutability] error. +/// +/// This modality is a required installation parameter and cannot be changed +/// once the contract has been installed. #[repr(u8)] #[derive(Default, PartialOrd, Ord, Copy)] #[odra::odra_type] pub enum MetadataMutability { + /// Metadata for NFTs minted in this mode cannot be updated once the NFT has been minted. #[default] Immutable = 0, + /// Metadata for NFTs minted in this mode can update the metadata via the `set_token_metadata` entrypoint. Mutable = 1 } @@ -263,12 +324,16 @@ impl ToString for TokenIdentifier { } } +/// The modality dictates whether tokens minted by a given instance of +/// an NFT contract can be burnt. #[repr(u8)] #[odra::odra_type] #[derive(Default)] pub enum BurnMode { + /// Minted tokens can be burnt. #[default] Burnable = 0, + /// Minted tokens cannot be burnt. NonBurnable = 1 } @@ -284,13 +349,46 @@ impl TryFrom for BurnMode { } } +/// This modality is set at install and determines if a given contract instance +/// writes necessary data to allow reverse lookup by owner in addition to by ID. +/// +/// This modality provides the following options: +/// +/// `NoLookup`: The reporting and receipt functionality is not supported. +/// In this option, the contract instance does not maintain a reverse lookup +/// database of ownership and therefore has more predictable gas costs and greater +/// scaling. +/// `Complete`: The reporting and receipt functionality is supported. Token +/// ownership will be tracked by the contract instance using the system described +/// [here](https://github.com/casper-ecosystem/cep-78-enhanced-nft/blob/dev/docs/reverse-lookup.md#owner-reverse-lookup-functionality). +/// `TransfersOnly`: The reporting and receipt functionality is supported like +/// `Complete`. However, it does not begin tracking until the first transfer. +/// This modality is for use cases where the majority of NFTs are owned by +/// a private minter and only NFT's that have been transferred benefit from +/// reverse lookup tracking. Token ownership will also be tracked by the contract +/// instance using the system described [here](https://github.com/casper-ecosystem/cep-78-enhanced-nft/blob/dev/docs/reverse-lookup.md#owner-reverse-lookup-functionality). +/// +/// Additionally, when set to Complete, causes a receipt to be returned by the mint +/// or transfer entrypoints, which the caller can store in their account or contract +/// context for later reference. +/// +/// Further, two special entrypoints are enabled in Complete mode. First, +/// `register_owner` which when called will allocate the necessary tracking +/// record for the imputed entity. This allows isolation of the one time gas cost +/// to do this per owner, which is convenient for accounting purposes. Second, +/// updated_receipts, which allows an owner of one or more NFTs held by the contract +/// instance to attain up to date receipt information for the NFTs they currently own. #[repr(u8)] #[derive(Default, PartialOrd, Ord, Copy)] #[odra::odra_type] pub enum OwnerReverseLookupMode { + /// The reporting and receipt functionality is not supported. #[default] NoLookUp = 0, + /// The reporting and receipt functionality is supported. Complete = 1, + /// The reporting and receipt functionality is supported, but the tracking + /// does not start until the first transfer. TransfersOnly = 2 } @@ -307,34 +405,20 @@ impl TryFrom for OwnerReverseLookupMode { } } -#[repr(u8)] -pub enum NamedKeyConventionMode { - DerivedFromCollectionName = 0, - V1_0Standard = 1, - V1_0Custom = 2 -} - -impl TryFrom for NamedKeyConventionMode { - type Error = CEP78Error; - - fn try_from(value: u8) -> Result { - match value { - 0 => Ok(NamedKeyConventionMode::DerivedFromCollectionName), - 1 => Ok(NamedKeyConventionMode::V1_0Standard), - 2 => Ok(NamedKeyConventionMode::V1_0Custom), - _ => Err(CEP78Error::InvalidNamedKeyConvention) - } - } -} - +/// The `EventsMode` modality determines how the installed instance of CEP-78 +/// will handle the recording of events that occur from interacting with +/// the contract. +/// +/// Odra does not allow to set the `CEP47` event schema. #[repr(u8)] #[odra::odra_type] #[derive(Copy, Default)] #[allow(clippy::upper_case_acronyms)] pub enum EventsMode { + /// Signals the contract to not record events at all. This is the default mode. #[default] NoEvents = 0, - CEP47 = 1, + /// Signals the contract to record events using the Casper Event Standard. CES = 2 } @@ -344,18 +428,24 @@ impl TryFrom for EventsMode { fn try_from(value: u8) -> Result { match value { 0 => Ok(EventsMode::NoEvents), - 1 => Ok(EventsMode::CEP47), 2 => Ok(EventsMode::CES), _ => Err(CEP78Error::InvalidEventsMode) } } } +/// The transfer filter modality, if enabled, specifies a contract package hash +/// pointing to a contract that will be called when the transfer method is +/// invoked on the contract. CEP-78 will call the `can_transfer` method on the +/// specified callback contract, which is expected to return a value of +/// `TransferFilterContractResult`, represented as a u8. #[repr(u8)] #[non_exhaustive] #[odra::odra_type] pub enum TransferFilterContractResult { + /// Blocks the transfer regardless of the outcome of other checks DenyTransfer = 0, + /// Allows the transfer to proceed if other checks also pass ProceedTransfer } diff --git a/modules/src/cep78/pagination.rs b/modules/src/cep78/pagination.rs deleted file mode 100644 index 8e324977..00000000 --- a/modules/src/cep78/pagination.rs +++ /dev/null @@ -1,166 +0,0 @@ -use odra::{ - casper_types::{AccessRights, URef}, - prelude::*, - Address, Mapping, UnwrapOrRevert -}; - -use crate::cep78::constants::PREFIX_PAGE_DICTIONARY; - -use super::error::CEP78Error; - -// The size of a given page, it is currently set to 1000 -// to ease the math around addressing newly minted tokens. -pub const PAGE_SIZE: u64 = 1000; - -#[odra::module] -pub struct Pagination { - page_tables: Mapping>, - pages: Mapping<(String, u64, Address), Vec> -} - -impl Pagination { - pub fn add_page_entry_and_page_record( - &mut self, - tokens_count: u64, - item_key: &Address, - on_mint: bool - ) -> (u64, URef) { - // there is an explicit page_table; - // this is the entry in that overall page table which maps to the underlying page - // upon which this mint's address will exist - let page_table_entry = tokens_count / PAGE_SIZE; - let page_address = tokens_count % PAGE_SIZE; - - let mut page_table = match self.page_tables.get(item_key) { - Some(page_table) => page_table, - None => self.env().revert(if on_mint { - CEP78Error::UnregisteredOwnerInMint - } else { - CEP78Error::UnregisteredOwnerInTransfer - }) - }; - - let page_key = ( - PREFIX_PAGE_DICTIONARY.to_string(), - page_table_entry, - *item_key - ); - let mut page = if !page_table[page_table_entry as usize] { - // We mark the page table entry to true to signal the allocation of a page. - let _ = core::mem::replace(&mut page_table[page_table_entry as usize], true); - self.pages.set(&page_key, page_table); - vec![false; PAGE_SIZE as usize] - } else { - self.pages - .get(&page_key) - .unwrap_or_revert_with(&self.env(), CEP78Error::MissingPage) - }; - - let _ = core::mem::replace(&mut page[page_address as usize], true); - - self.pages.set(&page_key, page); - // storage::dictionary_put(page_uref, item_key, page); - let addr_array = [0u8; 32]; - let uref_a = URef::new(addr_array, AccessRights::READ); - // (page_table_entry, page_uref) - (page_table_entry, uref_a) - } - - pub fn update_page_entry_and_page_record( - &mut self, - tokens_count: u64, - old_item_key: &Address, - new_item_key: &Address - ) -> (u64, URef) { - let page_table_entry = tokens_count / PAGE_SIZE; - let page_address = tokens_count % PAGE_SIZE; - - let old_page_key = ( - PREFIX_PAGE_DICTIONARY.to_string(), - page_table_entry, - *old_item_key - ); - let new_page_key = ( - PREFIX_PAGE_DICTIONARY.to_string(), - page_table_entry, - *new_item_key - ); - - let mut source_page = self - .pages - .get(&old_page_key) - .unwrap_or_revert_with(&self.env(), CEP78Error::InvalidPageNumber); - - if !source_page[page_address as usize] { - self.env().revert(CEP78Error::InvalidTokenIdentifier) - } - - let _ = core::mem::replace(&mut source_page[page_address as usize], false); - - self.pages.set(&old_page_key, source_page); - - let mut target_page_table = self - .page_tables - .get(&new_item_key) - .unwrap_or_revert_with(&self.env(), CEP78Error::UnregisteredOwnerInTransfer); - - let mut target_page = if !target_page_table[page_table_entry as usize] { - // Create a new page here - let _ = core::mem::replace(&mut target_page_table[page_table_entry as usize], true); - self.page_tables.set(new_item_key, target_page_table); - vec![false; PAGE_SIZE as usize] - } else { - self.pages.get(&new_page_key).unwrap_or_revert(&self.env()) - }; - - let _ = core::mem::replace(&mut target_page[page_address as usize], true); - - self.pages.set(&new_page_key, target_page); - // (page_table_entry, page_uref) - let addr_array = [0u8; 32]; - let uref_a = URef::new(addr_array, AccessRights::READ); - // (page_table_entry, page_uref) - (page_table_entry, uref_a) - } - - pub fn register_owner(&mut self, owner: &Address) { - if self.page_tables.get(&owner).is_none() { - self.page_tables.set(owner, vec![false; PAGE_SIZE as usize]); - } - - // let page_table_uref = utils::get_uref( - // PAGE_TABLE, - // NFTCoreError::MissingPageTableURef, - // NFTCoreError::InvalidPageTableURef, - // ); - - // let owner_item_key = utils::encode_dictionary_item_key(owner_key); - - // if storage::dictionary_get::>(page_table_uref, &owner_item_key) - // .unwrap_or_revert() - // .is_none() - // { - // let page_table_width = utils::get_stored_value_with_user_errors::( - // PAGE_LIMIT, - // NFTCoreError::MissingPageLimit, - // NFTCoreError::InvalidPageLimit, - // ); - // storage::dictionary_put( - // page_table_uref, - // &owner_item_key, - // vec![false; page_table_width as usize], - // ); - // } - // let collection_name = utils::get_stored_value_with_user_errors::( - // COLLECTION_NAME, - // NFTCoreError::MissingCollectionName, - // NFTCoreError::InvalidCollectionName, - // ); - // let package_uref = storage::new_uref(utils::get_stored_value_with_user_errors::( - // &format!("{PREFIX_CEP78}_{collection_name}"), - // NFTCoreError::MissingCep78PackageHash, - // NFTCoreError::InvalidCep78InvalidHash, - // )); - // runtime::ret(CLValue::from_t((collection_name, package_uref)).unwrap_or_revert()) - } -} diff --git a/modules/src/cep78/reverse_lookup.rs b/modules/src/cep78/reverse_lookup.rs index 1ca6ce39..781d9350 100644 --- a/modules/src/cep78/reverse_lookup.rs +++ b/modules/src/cep78/reverse_lookup.rs @@ -1,25 +1,33 @@ -use odra::{prelude::*, Mapping, UnwrapOrRevert, Var}; +use odra::{ + args::Maybe, + casper_types::{AccessRights, URef}, + prelude::*, + Address, Mapping, UnwrapOrRevert, Var +}; use super::{ + constants::PREFIX_PAGE_DICTIONARY, error::CEP78Error, - modalities::{OwnerReverseLookupMode, TokenIdentifier} + modalities::{OwnerReverseLookupMode, OwnershipMode, TokenIdentifier} }; +// The size of a given page, it is currently set to 1000 +// to ease the math around addressing newly minted tokens. +pub const PAGE_SIZE: u64 = 1000; #[odra::module] pub struct ReverseLookup { + mode: Var, hash_by_index: Mapping, index_by_hash: Mapping, - mode: Var + page_table: Mapping>, + pages: Mapping<(String, u64, Address), Vec>, + receipt_name: Var } impl ReverseLookup { - pub fn init(&mut self, mode: OwnerReverseLookupMode) { + pub fn init(&mut self, mode: OwnerReverseLookupMode, receipt_name: String) { self.mode.set(mode); - } - - #[inline] - pub fn get_mode(&self) -> OwnerReverseLookupMode { - self.mode.get_or_default() + self.receipt_name.set(receipt_name); } pub fn insert_hash( @@ -55,7 +63,215 @@ impl ReverseLookup { ); } - pub fn get_token_index(&self, token_identifier: &TokenIdentifier) -> u64 { + pub fn register_owner( + &mut self, + owner: Maybe
, + ownership_mode: OwnershipMode + ) -> (String, URef) { + let mode = self.get_mode(); + if vec![ + OwnerReverseLookupMode::Complete, + OwnerReverseLookupMode::TransfersOnly, + ] + .contains(&mode) + { + let owner = match ownership_mode { + OwnershipMode::Minter => self.env().caller(), + OwnershipMode::Assigned | OwnershipMode::Transferable => owner.unwrap(&self.env()) + }; + if self.page_table.get(&owner).is_none() { + self.page_table.set(&owner, vec![false; PAGE_SIZE as usize]); + } + + // let page_table_uref = utils::get_uref( + // PAGE_TABLE, + // NFTCoreError::MissingPageTableURef, + // NFTCoreError::InvalidPageTableURef, + // ); + + // let owner_item_key = utils::encode_dictionary_item_key(owner_key); + + // if storage::dictionary_get::>(page_table_uref, &owner_item_key) + // .unwrap_or_revert() + // .is_none() + // { + // let page_table_width = utils::get_stored_value_with_user_errors::( + // PAGE_LIMIT, + // NFTCoreError::MissingPageLimit, + // NFTCoreError::InvalidPageLimit, + // ); + // storage::dictionary_put( + // page_table_uref, + // &owner_item_key, + // vec![false; page_table_width as usize], + // ); + // } + // let collection_name = utils::get_stored_value_with_user_errors::( + // COLLECTION_NAME, + // NFTCoreError::MissingCollectionName, + // NFTCoreError::InvalidCollectionName, + // ); + // let package_uref = storage::new_uref(utils::get_stored_value_with_user_errors::( + // &format!("{PREFIX_CEP78}_{collection_name}"), + // NFTCoreError::MissingCep78PackageHash, + // NFTCoreError::InvalidCep78InvalidHash, + // )); + // runtime::ret(CLValue::from_t((collection_name, package_uref)).unwrap_or_revert()) + } + ("".to_string(), URef::new([0u8; 32], AccessRights::READ)) + } + + pub fn on_mint( + &mut self, + tokens_count: u64, + token_owner: Address, + token_id: String + ) -> (String, Address, String) { + if self.get_mode() == OwnerReverseLookupMode::Complete { + let (page_table_entry, _page_uref) = + self.add_page_entry_and_page_record(tokens_count, &token_owner, true); + + let receipt_name = self.receipt_name.get_or_default(); + let receipt_string = format!("{receipt_name}_m_{PAGE_SIZE}_p_{page_table_entry}"); + // TODO: Implement the following + // let receipt_address = Key::dictionary(page_uref, owned_tokens_item.as_bytes()); + // should not return `token_owner` + return (receipt_string, token_owner, token_id); + } + ("".to_string(), token_owner, token_id) + } + + pub fn on_transfer( + &mut self, + token_identifier: TokenIdentifier, + source: Address, + target: Address + ) -> (String, Address) { + let mode = self.get_mode(); + if let OwnerReverseLookupMode::Complete | OwnerReverseLookupMode::TransfersOnly = mode { + // Update to_account owned_tokens. Revert if owned_tokens list is not found + let tokens_count = self.get_token_index(&token_identifier); + if OwnerReverseLookupMode::TransfersOnly == mode { + self.add_page_entry_and_page_record(tokens_count, &source, false); + } + + let (page_table_entry, _page_uref) = + self.update_page_entry_and_page_record(tokens_count, &source, &target); + + let receipt_name = self.receipt_name.get_or_default(); + let _receipt_string = format!("{receipt_name}_m_{PAGE_SIZE}_p_{page_table_entry}"); + // TODO: Implement the following + // let receipt_address = Key::dictionary(page_uref, owned_tokens_item_key.as_bytes()); + // return (receipt_string, source); + } + return ("".to_owned(), source); + } + + fn add_page_entry_and_page_record( + &mut self, + tokens_count: u64, + item_key: &Address, + on_mint: bool + ) -> (u64, URef) { + // there is an explicit page_table; + // this is the entry in that overall page table which maps to the underlying page + // upon which this mint's address will exist + let page_table_entry = tokens_count / PAGE_SIZE; + let page_address = tokens_count % PAGE_SIZE; + + let mut page_table = match self.page_table.get(item_key) { + Some(page_table) => page_table, + None => self.env().revert(if on_mint { + CEP78Error::UnregisteredOwnerInMint + } else { + CEP78Error::UnregisteredOwnerInTransfer + }) + }; + + let page_key = ( + PREFIX_PAGE_DICTIONARY.to_string(), + page_table_entry, + *item_key + ); + let mut page = if !page_table[page_table_entry as usize] { + // We mark the page table entry to true to signal the allocation of a page. + let _ = core::mem::replace(&mut page_table[page_table_entry as usize], true); + self.pages.set(&page_key, page_table); + vec![false; PAGE_SIZE as usize] + } else { + self.pages + .get(&page_key) + .unwrap_or_revert_with(&self.env(), CEP78Error::MissingPage) + }; + + let _ = core::mem::replace(&mut page[page_address as usize], true); + + self.pages.set(&page_key, page); + // storage::dictionary_put(page_uref, item_key, page); + let addr_array = [0u8; 32]; + let uref_a = URef::new(addr_array, AccessRights::READ); + // (page_table_entry, page_uref) + (page_table_entry, uref_a) + } + + fn update_page_entry_and_page_record( + &mut self, + tokens_count: u64, + old_item_key: &Address, + new_item_key: &Address + ) -> (u64, URef) { + let page_table_entry = tokens_count / PAGE_SIZE; + let page_address = tokens_count % PAGE_SIZE; + + let old_page_key = ( + PREFIX_PAGE_DICTIONARY.to_string(), + page_table_entry, + *old_item_key + ); + let new_page_key = ( + PREFIX_PAGE_DICTIONARY.to_string(), + page_table_entry, + *new_item_key + ); + + let mut source_page = self + .pages + .get(&old_page_key) + .unwrap_or_revert_with(&self.env(), CEP78Error::InvalidPageNumber); + + if !source_page[page_address as usize] { + self.env().revert(CEP78Error::InvalidTokenIdentifier) + } + + let _ = core::mem::replace(&mut source_page[page_address as usize], false); + + self.pages.set(&old_page_key, source_page); + + let mut target_page_table = self + .page_table + .get(&new_item_key) + .unwrap_or_revert_with(&self.env(), CEP78Error::UnregisteredOwnerInTransfer); + + let mut target_page = if !target_page_table[page_table_entry as usize] { + // Create a new page here + let _ = core::mem::replace(&mut target_page_table[page_table_entry as usize], true); + self.page_table.set(new_item_key, target_page_table); + vec![false; PAGE_SIZE as usize] + } else { + self.pages.get(&new_page_key).unwrap_or_revert(&self.env()) + }; + + let _ = core::mem::replace(&mut target_page[page_address as usize], true); + + self.pages.set(&new_page_key, target_page); + // (page_table_entry, page_uref) + let addr_array = [0u8; 32]; + let uref_a = URef::new(addr_array, AccessRights::READ); + // (page_table_entry, page_uref) + (page_table_entry, uref_a) + } + + fn get_token_index(&self, token_identifier: &TokenIdentifier) -> u64 { match token_identifier { TokenIdentifier::Index(token_index) => *token_index, TokenIdentifier::Hash(_) => self @@ -65,16 +281,8 @@ impl ReverseLookup { } } - // pub fn remove(&mut self, index: u64, hash: String) { - // self.hash_by_index.remove(&index.to_string()); - // self.index_by_hash.remove(&hash); - // } - - // pub fn get_by_index(&self, index: u64) -> Option
{ - // self.hash_by_index.get(&index.to_string()).copied() - // } - - // pub fn get_by_hash(&self, hash: &str) -> Option
{ - // self.index_by_hash.get(hash).copied() - // } + #[inline] + fn get_mode(&self) -> OwnerReverseLookupMode { + self.mode.get_or_default() + } } diff --git a/modules/src/cep78/settings.rs b/modules/src/cep78/settings.rs index 10ac489a..fd6f2491 100644 --- a/modules/src/cep78/settings.rs +++ b/modules/src/cep78/settings.rs @@ -1,12 +1,7 @@ use odra::prelude::*; use odra::Var; -use super::modalities::BurnMode; -use super::modalities::EventsMode; -use super::modalities::MintingMode; -use super::modalities::NFTHolderMode; -use super::modalities::NFTKind; -use super::modalities::OwnershipMode; +use super::modalities::{BurnMode, EventsMode, MintingMode, NFTHolderMode, NFTKind, OwnershipMode}; #[odra::module] pub struct Settings { diff --git a/modules/src/cep78/tests/metadata.rs b/modules/src/cep78/tests/metadata.rs index a4eb15b7..0114c9d4 100644 --- a/modules/src/cep78/tests/metadata.rs +++ b/modules/src/cep78/tests/metadata.rs @@ -332,7 +332,7 @@ fn should_revert_minting_token_metadata_hash_twice() { .acl_white_list(contract_whitelist) .build(); let mut contract = CEP78HostRef::deploy(&env, args); - + minting_contract.set_address(contract.address()); assert!( contract.is_whitelisted(minting_contract.address()), "acl whitelist is incorrectly set" diff --git a/modules/src/cep78/tests/mint.rs b/modules/src/cep78/tests/mint.rs index f89bb556..175edc90 100644 --- a/modules/src/cep78/tests/mint.rs +++ b/modules/src/cep78/tests/mint.rs @@ -185,6 +185,7 @@ fn should_set_meta_data() { let (mut contract, env) = default_token(); let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); contract.mint( token_owner, TEST_PRETTY_721_META_DATA.to_string(), @@ -200,6 +201,7 @@ fn should_set_issuer() { let (mut contract, env) = default_token(); let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); contract.mint( token_owner, TEST_PRETTY_721_META_DATA.to_string(), @@ -217,6 +219,7 @@ fn should_set_issuer_with_different_owner() { let token_issuer = env.get_account(0); let token_owner = env.get_account(1); + contract.register_owner(Maybe::Some(token_owner)); contract.mint( token_owner, TEST_PRETTY_721_META_DATA.to_string(), @@ -233,6 +236,7 @@ fn should_track_token_balance_by_owner() { let (mut contract, env) = default_token(); let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); contract.mint( token_owner, TEST_PRETTY_721_META_DATA.to_string(), @@ -416,6 +420,7 @@ fn should_not_mint_with_invalid_nft721_metadata() { #[test] fn should_mint_with_compactified_metadata() { let (mut contract, env) = default_token(); + contract.register_owner(Maybe::Some(env.get_account(0))); contract.mint( env.get_account(0), TEST_COMPACT_META_DATA.to_string(), @@ -719,7 +724,7 @@ fn should_not_mint_with_missing_required_metadata() { TEST_PRETTY_721_META_DATA.to_string(), Maybe::None ), - Err(CEP78Error::FailedToParseCep99Metadata.into()) + Err(CEP78Error::FailedToParseCep78Metadata.into()) ); } diff --git a/modules/src/cep78/token.rs b/modules/src/cep78/token.rs index 9c95f0f4..ffbe5038 100644 --- a/modules/src/cep78/token.rs +++ b/modules/src/cep78/token.rs @@ -1,6 +1,5 @@ use super::{ - collection_info::CollectionInfo, - constants, + data::CollectionData, error::CEP78Error, events::{ Approval, ApprovalForAll, ApprovalRevoked, Burn, MetadataUpdated, Mint, RevokedForAll, @@ -12,34 +11,44 @@ use super::{ NFTKind, NFTMetadataKind, OwnerReverseLookupMode, OwnershipMode, TokenIdentifier, TransferFilterContractResult, WhitelistMode }, - pagination::{Pagination, PAGE_SIZE}, reverse_lookup::ReverseLookup, settings::Settings, - utils, whitelist::ACLWhitelist }; use odra::{ args::Maybe, - casper_types::{bytesrepr::ToBytes, AccessRights, URef}, + casper_types::{bytesrepr::ToBytes, URef}, prelude::*, - Address, Mapping, Sequence, SubModule, UnwrapOrRevert, Var + Address, OdraError, SubModule, UnwrapOrRevert, Var }; +type MintReceipt = (String, Address, String); +type TransferReceipt = (String, Address); + +/// CEP-78 is a standard for non-fungible tokens (NFTs) on the Casper network. +/// It defines a set of interfaces that allow for the creation, management, and +/// transfer of NFTs. The standard is designed to be flexible and modular, allowing +/// developers to customize the behavior of their NFTs to suit their specific needs. +/// The CEP-78 standard is inspired by the ERC-721 standard for NFTs on the Ethereum network. +/// The CEP-78 standard is designed to be simple and easy to use, while still providing +/// powerful features for developers to build on. +/// +/// A list of mandatory init arguments: +/// - `collection_name`: The name of the NFT collection. +/// - `collection_symbol`: The symbol of the NFT collection. +/// - `total_token_supply`: The total number of tokens that can be minted in the collection. +/// - `ownership_mode`: The ownership mode of the collection. See [OwnershipMode] for more details. +/// - `nft_kind`: The kind of NFTs in the collection. See [NFTKind] for more details. +/// - `nft_identifier_mode`: The identifier mode of the NFTs in the collection. See [NFTIdentifierMode] for more details. +/// - `nft_metadata_kind`: The kind of metadata associated with the NFTs in the collection. See [NFTMetadataKind] for more details. +/// - `metadata_mutability`: The mutability of the metadata associated with the NFTs in the collection. See [MetadataMutability] for more details. #[odra::module] pub struct CEP78 { - whitelist: SubModule, + data: SubModule, metadata: SubModule, - reverse_lookup: SubModule, - pagination: SubModule, - info: SubModule, settings: SubModule, - owners: Mapping, - issuers: Mapping, - approved: Mapping>, - token_count: Mapping, - burnt_tokens: Mapping, - operators: Mapping<(Address, Address), bool>, - receipt_name: Var, + whitelist: SubModule, + reverse_lookup: SubModule, transfer_filter_contract: Var
} @@ -71,8 +80,8 @@ impl CEP78 { additional_required_metadata: Maybe>, optional_metadata: Maybe> ) { - let installer = self.env().caller(); - self.info.init( + let installer = self.caller(); + self.data.init( collection_name, collection_symbol, total_token_supply, @@ -89,9 +98,10 @@ impl CEP78 { operator_burn_mode.unwrap_or_default() ); - self.reverse_lookup - .init(owner_reverse_lookup_mode.clone().unwrap_or_default()); - self.receipt_name.set(receipt_name.unwrap_or_default()); + self.reverse_lookup.init( + owner_reverse_lookup_mode.clone().unwrap_or_default(), + receipt_name.unwrap_or_default() + ); self.whitelist.init( acl_white_list.unwrap_or_default(), @@ -110,14 +120,14 @@ impl CEP78 { if nft_identifier_mode == NFTIdentifierMode::Hash && metadata_mutability == MetadataMutability::Mutable { - self.env().revert(CEP78Error::InvalidMetadataMutability) + self.revert(CEP78Error::InvalidMetadataMutability) } if ownership_mode == OwnershipMode::Minter && minting_mode.unwrap_or_default() == MintingMode::Installer && owner_reverse_lookup_mode.unwrap_or_default() == OwnerReverseLookupMode::Complete { - self.env().revert(CEP78Error::InvalidReportingMode) + self.revert(CEP78Error::InvalidReportingMode) } if let Maybe::Some(transfer_filter_contract_contract) = transfer_filter_contract_contract { @@ -127,17 +137,16 @@ impl CEP78 { } /// Exposes all variables that can be changed by managing account post - /// installation. Meant to be called by the managing account (INSTALLER) post - /// installation if a variable needs to be changed. - /// By switching allow_minting to false we pause minting. + /// installation. Meant to be called by the managing account (`Installer`) + /// if a variable needs to be changed. + /// By switching `allow_minting` to false minting is paused. pub fn set_variables( &mut self, allow_minting: Maybe, acl_whitelist: Maybe>, operator_burn_mode: Maybe ) { - let installer = self.info.installer(); - // Only the installing account can change the mutable variables. + let installer = self.data.installer(); self.ensure_caller(installer); if let Maybe::Some(allow_minting) = allow_minting { @@ -148,15 +157,15 @@ impl CEP78 { self.settings.set_operator_burn_mode(operator_burn_mode); } - self.whitelist.update_addresses(acl_whitelist); + self.whitelist.update(acl_whitelist); self.emit_ces_event(VariablesSet::new()); } /// Mints a new token with provided metadata. /// Reverts with [CEP78Error::MintingIsPaused] error if `allow_minting` is false. - /// When a token is minted the calling account is listed as its owner and the token is + /// When a token is minted, the calling account is listed as its owner and the token is /// automatically assigned an `u64` ID equal to the current `number_of_minted_tokens`. - /// Before minting the token, checks if `number_of_minted_tokens` + /// Before minting, the token checks if `number_of_minted_tokens` /// exceeds the `total_token_supply`. If so, it reverts the minting with an error /// [CEP78Error::TokenSupplyDepleted]. The `mint` function also checks whether the calling account /// is the managing account (the installer) If not, and if `public_minting` is set to @@ -167,62 +176,48 @@ impl CEP78 { token_owner: Address, token_metadata: String, token_hash: Maybe - ) -> (String, Address, String) { - // The contract owner can toggle the minting behavior on and off over time. - // The contract is toggled on by default. - let allow_minting = self.settings.allow_minting(); - - // If contract minting behavior is currently toggled off we revert. - if !allow_minting { - self.env().revert(CEP78Error::MintingIsPaused); + ) -> MintReceipt { + if !self.settings.allow_minting() { + self.revert(CEP78Error::MintingIsPaused); } - let total_token_supply = self.info.total_token_supply(); + let total_token_supply = self.data.total_token_supply(); + let minted_tokens_count = self.data.number_of_minted_tokens(); - // The minted_tokens_count is the number of minted tokens so far. - let minted_tokens_count = self.info.number_of_minted_tokens(); - - // Revert if the token supply has been exhausted. if minted_tokens_count >= total_token_supply { - self.env().revert(CEP78Error::TokenSupplyDepleted); + self.revert(CEP78Error::TokenSupplyDepleted); } - let minting_mode: MintingMode = self.settings.minting_mode(); - + let minting_mode = self.settings.minting_mode(); let caller = self.verified_caller(); - // Revert if minting is private and caller is not installer. if MintingMode::Installer == minting_mode { match caller { Address::Account(_) => { - let installer_account = self.info.installer(); - // Revert if private minting is required and caller is not installer. + let installer_account = self.data.installer(); if caller != installer_account { - self.env().revert(CEP78Error::InvalidMinter) + self.revert(CEP78Error::InvalidMinter) } } - _ => self.env().revert(CEP78Error::InvalidKey) + _ => self.revert(CEP78Error::InvalidKey) } } - // Revert if minting is acl and caller is not whitelisted. if MintingMode::Acl == minting_mode { - let is_whitelisted = self.whitelist.is_whitelisted(&caller); - if !is_whitelisted { + if !self.whitelist.is_whitelisted(&caller) { match caller { - Address::Contract(_) => self.env().revert(CEP78Error::UnlistedContractHash), - Address::Account(_) => self.env().revert(CEP78Error::InvalidMinter) + Address::Contract(_) => self.revert(CEP78Error::UnlistedContractHash), + Address::Account(_) => self.revert(CEP78Error::InvalidMinter) } } } let identifier_mode = self.metadata.get_identifier_mode(); - let optional_token_hash: String = token_hash.unwrap_or_default(); let token_identifier: TokenIdentifier = match identifier_mode { NFTIdentifierMode::Ordinal => TokenIdentifier::Index(minted_tokens_count), NFTIdentifierMode::Hash => TokenIdentifier::Hash(if optional_token_hash.is_empty() { - let hash = self.env().hash(token_metadata.clone()); + let hash = self.__env.hash(token_metadata.clone()); base16::encode_lower(&hash) } else { optional_token_hash @@ -232,54 +227,27 @@ impl CEP78 { self.metadata.update_or_revert(&token_metadata, &token_id); - // The contract's ownership behavior (determined at installation) determines, - // who owns the NFT we are about to mint.() - let token_owner = - if let OwnershipMode::Assigned | OwnershipMode::Transferable = self.ownership_mode() { - token_owner - } else { - caller - }; + let token_owner = if self.is_transferable_or_assigned() { + token_owner + } else { + caller + }; - self.owners.set(&token_id, token_owner); - self.issuers.set(&token_id, caller); + self.data.set_owner(&token_id, token_owner); + self.data.set_issuer(&token_id, caller); if let NFTIdentifierMode::Hash = identifier_mode { - // Update the forward and reverse trackers self.reverse_lookup .insert_hash(minted_tokens_count, &token_identifier); } - //Increment the count of owned tokens. - self.token_count.add(&token_owner, 1); - - // Increment number_of_minted_tokens by one - self.info.increment_number_of_minted_tokens(); - - // Emit Mint event. - self.emit_ces_event(Mint::new( - token_owner, - token_id.clone(), - token_metadata.clone() - )); - - if let OwnerReverseLookupMode::Complete = self.reverse_lookup.get_mode() { - let (page_table_entry, page_uref) = self.pagination.add_page_entry_and_page_record( - minted_tokens_count, - &token_owner, - true - ); - /* - let receipt_name = self.receipt_name.get_or_default(); - let receipt_string = format!("{receipt_name}_m_{PAGE_SIZE}_p_{page_table_entry}"); - // TODO: Implement the following - // let receipt_address = Key::dictionary(page_uref, owned_tokens_item.as_bytes()); - let token_identifier_string = token_identifier.to_string(); - // should not return `token_owner` - return (receipt_string, token_owner, token_identifier_string); - */ - } - ("".to_string(), token_owner, token_id) + self.data.increment_counter(&token_owner); + self.data.increment_number_of_minted_tokens(); + + self.emit_ces_event(Mint::new(token_owner, token_id.clone(), token_metadata)); + + self.reverse_lookup + .on_mint(minted_tokens_count, token_owner, token_id) } /// Burns the token with provided `token_id` argument, after which it is no @@ -295,33 +263,23 @@ impl CEP78 { let token_id = token_identifier.to_string(); let token_owner = self.owner_of_by_id(&token_id); - let caller = self.env().caller(); + let caller = self.__env.caller(); - // Check if caller is owner let is_owner = token_owner == caller; - - // Check if caller is operator to execute burn let is_operator = if !is_owner { - self.operators.get_or_default(&(token_owner, caller)) + self.data.operator(token_owner, caller) } else { false }; - // Revert if caller is not token_owner nor operator for the owner if !is_owner && !is_operator { - self.env().revert(CEP78Error::InvalidTokenOwner) + self.revert(CEP78Error::InvalidTokenOwner) }; - // It makes sense to keep this token as owned by the caller. It just happens that the caller - // owns a burnt token. That's all. Similarly, we should probably also not change the - // owned_tokens dictionary. self.ensure_not_burned(&token_id); + self.data.mark_burnt(&token_id); + self.data.decrement_counter(&token_owner); - // Mark the token as burnt by adding the token_id to the burnt tokens dictionary. - self.burnt_tokens.set(&token_id, ()); - self.token_count.subtract(&token_owner, 1); - - // Emit Burn event. self.emit_ces_event(Burn::new(token_owner, token_id, caller)); } @@ -335,33 +293,26 @@ impl CEP78 { token_hash: Maybe, source: Address, target: Address - ) -> (String, Address) { - // If we are in minter or assigned mode we are not allowed to transfer ownership of token, hence - // we revert. + ) -> TransferReceipt { self.ensure_minter_or_assigned(); let token_identifier = self.checked_token_identifier(token_id, token_hash); let token_id = token_identifier.to_string(); - // We assume we cannot transfer burnt tokens self.ensure_not_burned(&token_id); self.ensure_owner(&token_id, &source); - let caller = self.env().caller(); - + let caller = self.caller(); let owner = self.owner_of_by_id(&token_id); - // Check if caller is owner let is_owner = owner == caller; - // Check if caller is approved to execute transfer let is_approved = !is_owner - && match self.approved.get(&token_id) { - Some(Some(maybe_approved)) => caller == maybe_approved, - Some(None) | None => false + && match self.data.approved(&token_id) { + Some(maybe_approved) => caller == maybe_approved, + _ => false }; - // Check if caller is operator to execute transfer let is_operator = if !is_owner && !is_approved { - self.operators.get_or_default(&(source, caller)) + self.data.operator(source, caller) } else { false }; @@ -371,90 +322,58 @@ impl CEP78 { .can_transfer(source, target, token_identifier.clone()); if TransferFilterContractResult::DenyTransfer == result { - self.env().revert(CEP78Error::TransferFilterContractDenied); + self.revert(CEP78Error::TransferFilterContractDenied); } } - // Revert if caller is not owner nor approved nor an operator. if !is_owner && !is_approved && !is_operator { - self.env().revert(CEP78Error::InvalidTokenOwner); + self.revert(CEP78Error::InvalidTokenOwner); } - // Updated token_owners dictionary. Revert if token_owner not found. - match self.owners.get(&token_id) { + match self.data.owner_of(&token_id) { Some(token_actual_owner) => { if token_actual_owner != source { - self.env().revert(CEP78Error::InvalidTokenOwner) + self.revert(CEP78Error::InvalidTokenOwner) } - self.owners.set(&token_id, target); + self.data.set_owner(&token_id, target); } - None => self - .env() - .revert(CEP78Error::MissingOwnerTokenIdentifierKey) + None => self.revert(CEP78Error::MissingOwnerTokenIdentifierKey) } - self.token_count.subtract(&source, 1); - self.token_count.add(&target, 1); - - self.approved.set(&token_id, Option::
::None); + self.data.decrement_counter(&source); + self.data.increment_counter(&target); + self.data.revoke(&token_id); let spender = if caller == owner { None } else { Some(caller) }; self.emit_ces_event(Transfer::new(owner, spender, target, token_id)); - let reporting_mode = self.reverse_lookup.get_mode(); - - if let OwnerReverseLookupMode::Complete | OwnerReverseLookupMode::TransfersOnly = - reporting_mode - { - // Update to_account owned_tokens. Revert if owned_tokens list is not found - // let tokens_count = self.reverse_lookup.get_token_index(&token_identifier); - // if OwnerReverseLookupMode::TransfersOnly == reporting_mode { - // self.pagination - // .add_page_entry_and_page_record(tokens_count, &source, false); - // } - - // let (page_table_entry, page_uref) = - // self.pagination - // .update_page_entry_and_page_record(tokens_count, &source, &target); - - // let receipt_name = self.receipt_name.get_or_default(); - // let receipt_string = format!("{receipt_name}_m_{PAGE_SIZE}_p_{page_table_entry}"); - // let receipt_address = Key::dictionary(page_uref, owned_tokens_item_key.as_bytes()); - // TODO: should not return `source` - // return (receipt_string, source); - } - return ("".to_owned(), source); + self.reverse_lookup + .on_transfer(token_identifier, source, target) } /// Approves another token holder (an approved account) to transfer tokens. It /// reverts if token_id is invalid, if caller is not the owner nor operator, if token has already /// been burnt, or if caller tries to approve themselves as an approved account. pub fn approve(&mut self, spender: Address, token_id: Maybe, token_hash: Maybe) { - // If we are in minter or assigned mode it makes no sense to approve an account. Hence we - // revert. self.ensure_minter_or_assigned(); - let caller = self.env().caller(); + let caller = self.caller(); let token_identifier = self.checked_token_identifier(token_id, token_hash); let token_id = token_identifier.to_string(); let owner = self.owner_of_by_id(&token_id); - // Revert if caller is not token owner nor operator. - // Only the token owner or an operator can approve an account let is_owner = caller == owner; - let is_operator = !is_owner && self.operators.get_or_default(&(owner, caller)); + let is_operator = !is_owner && self.data.operator(owner, caller); if !is_owner && !is_operator { - self.env().revert(CEP78Error::InvalidTokenOwner); + self.revert(CEP78Error::InvalidTokenOwner); } - // We assume a burnt token cannot be approved self.ensure_not_burned(&token_id); - // If token owner or operator tries to approve itself that's probably a mistake and we revert. self.ensure_not_caller(spender); - self.approved.set(&token_id, Some(spender)); + self.data.approve(&token_id, spender); self.emit_ces_event(Approval::new(owner, spender, token_id)); } @@ -462,28 +381,23 @@ impl CEP78 { /// if token_id is invalid, if caller is not the owner, if token has already /// been burnt, if caller tries to approve itself. pub fn revoke(&mut self, token_id: Maybe, token_hash: Maybe) { - let env = self.env(); - // If we are in minter or assigned mode it makes no sense to approve an account. Hence we - // revert. self.ensure_minter_or_assigned(); - let caller = env.caller(); + let caller = self.caller(); let token_identifier = self.checked_token_identifier(token_id, token_hash); let token_id = token_identifier.to_string(); - // Revert if caller is not the token owner or an operator. Only the token owner / operators can - // revoke an approved account + let owner = self.owner_of_by_id(&token_id); let is_owner = caller == owner; - let is_operator = !is_owner && self.operators.get_or_default(&(owner, caller)); + let is_operator = !is_owner && self.data.operator(owner, caller); if !is_owner && !is_operator { - env.revert(CEP78Error::InvalidTokenOwner); + self.revert(CEP78Error::InvalidTokenOwner); } - // We assume a burnt token cannot be revoked self.ensure_not_burned(&token_id); - self.approved.set(&token_id, Option::
::None); - // Emit ApprovalRevoked event. + self.data.revoke(&token_id); + self.emit_ces_event(ApprovalRevoked::new(owner, token_id)); } @@ -491,29 +405,24 @@ impl CEP78 { /// (an operator) to transfer tokens. It reverts if token_id is invalid, if caller is not the /// owner, if caller tries to approve itself as an operator. pub fn set_approval_for_all(&mut self, approve_all: bool, operator: Address) { - let env = self.env(); - // If we are in minter or assigned mode it makes no sense to approve an operator. Hence we revert. self.ensure_minter_or_assigned(); - // If caller tries to approve itself as operator that's probably a mistake and we revert. self.ensure_not_caller(operator); - let caller = env.caller(); - // Depending on approve_all we either approve all or disapprove all. - self.operators.set(&(caller, operator), approve_all); + let caller = self.caller(); + self.data.set_operator(caller, operator, approve_all); - let events_mode: EventsMode = self.settings.events_mode(); - if let EventsMode::CES = events_mode { + if let EventsMode::CES = self.settings.events_mode() { if approve_all { - env.emit_event(ApprovalForAll::new(caller, operator)); + self.__env.emit_event(ApprovalForAll::new(caller, operator)); } else { - env.emit_event(RevokedForAll::new(caller, operator)); + self.__env.emit_event(RevokedForAll::new(caller, operator)); } } } /// Returns if an account is operator for a token owner pub fn is_approved_for_all(&mut self, token_owner: Address, operator: Address) -> bool { - self.operators.get_or_default(&(token_owner, operator)) + self.data.operator(token_owner, operator) } /// Returns the token owner given a token_id. It reverts if token_id @@ -534,7 +443,7 @@ impl CEP78 { let token_id = token_identifier.to_string(); self.ensure_not_burned(&token_id); - self.approved.get(&token_id).flatten() + self.data.approved(&token_id) } /// Returns the metadata associated with the provided token_id @@ -564,24 +473,7 @@ impl CEP78 { /// Returns number of owned tokens associated with the provided token holder pub fn balance_of(&mut self, token_owner: Address) -> u64 { - self.token_count.get(&token_owner).unwrap_or_default() - } - - /// This entrypoint will upgrade the contract from the 1_0 version to the - /// 1_1 version. The contract will insert any addition dictionaries and - /// sentinel values that were absent in the previous version of the contract. - /// It will also perform the necessary data transformations of historical - /// data if needed - pub fn migrate(&mut self, nft_package_key: String) { - // no-op - } - - /// This entrypoint will allow NFT owners to update their receipts from - /// the previous owned_tokens list model to the current pagination model - /// scheme. Calling the entrypoint will return a list of receipt names - /// alongside the dictionary addressed to the relevant pages. - pub fn updated_receipts(&mut self) -> Vec<(String, Address)> { - vec![] + self.data.token_count(&token_owner) } /// This entrypoint allows users to register with a give CEP-78 instance, @@ -591,25 +483,15 @@ impl CEP78 { /// Note: This entrypoint MUST be invoked if the reverse lookup is enabled /// in order to own NFTs. pub fn register_owner(&mut self, token_owner: Maybe
) -> (String, URef) { - if vec![ - OwnerReverseLookupMode::Complete, - OwnerReverseLookupMode::TransfersOnly, - ] - .contains(&self.reverse_lookup.get_mode()) - { - let owner = match self.ownership_mode() { - OwnershipMode::Minter => self.env().caller(), - OwnershipMode::Assigned | OwnershipMode::Transferable => { - token_owner.unwrap(&self.env()) - } - }; - - self.pagination.register_owner(&owner); - } - // TODO: Implement the following - ("".to_string(), URef::new([0u8; 32], AccessRights::READ)) + let ownership_mode = self.ownership_mode(); + self.reverse_lookup + .register_owner(token_owner, ownership_mode) } + /* + Test only getters + */ + pub fn is_whitelisted(&self, address: &Address) -> bool { self.whitelist.is_whitelisted(address) } @@ -619,11 +501,11 @@ impl CEP78 { } pub fn get_collection_name(&self) -> String { - self.info.collection_name() + self.data.collection_name() } pub fn get_collection_symbol(&self) -> String { - self.info.collection_symbol() + self.data.collection_symbol() } pub fn is_minting_allowed(&self) -> bool { @@ -635,7 +517,7 @@ impl CEP78 { } pub fn get_total_supply(&self) -> u64 { - self.info.total_token_supply() + self.data.total_token_supply() } pub fn get_minting_mode(&self) -> MintingMode { @@ -643,7 +525,7 @@ impl CEP78 { } pub fn get_number_of_minted_tokens(&self) -> u64 { - self.info.number_of_minted_tokens() + self.data.number_of_minted_tokens() } pub fn get_metadata_by_kind( @@ -659,9 +541,7 @@ impl CEP78 { pub fn get_token_issuer(&self, token_id: Maybe, token_hash: Maybe) -> Address { let token_identifier = self.checked_token_identifier(token_id, token_hash); - self.issuers - .get(&token_identifier.to_string()) - .unwrap_or_revert_with(&self.env(), CEP78Error::InvalidTokenIdentifier) + self.data.issuer(&token_identifier.to_string()) } pub fn token_burned(&self, token_id: Maybe, token_hash: Maybe) -> bool { @@ -672,6 +552,16 @@ impl CEP78 { } impl CEP78 { + #[inline] + fn caller(&self) -> Address { + self.__env.caller() + } + + #[inline] + fn revert>(&self, e: E) -> ! { + self.__env.revert(e) + } + #[inline] fn is_minter_or_assigned(&self) -> bool { matches!( @@ -680,10 +570,18 @@ impl CEP78 { ) } + #[inline] + fn is_transferable_or_assigned(&self) -> bool { + matches!( + self.ownership_mode(), + OwnershipMode::Transferable | OwnershipMode::Assigned + ) + } + #[inline] fn ensure_minter_or_assigned(&self) { if self.is_minter_or_assigned() { - self.env().revert(CEP78Error::InvalidOwnershipMode) + self.revert(CEP78Error::InvalidOwnershipMode) } } @@ -703,18 +601,18 @@ impl CEP78 { token_id: Maybe, token_hash: Maybe ) -> TokenIdentifier { - let env = self.env(); let identifier_mode: NFTIdentifierMode = self.metadata.get_identifier_mode(); let token_identifier = match identifier_mode { - NFTIdentifierMode::Ordinal => TokenIdentifier::Index(token_id.unwrap(&env)), - NFTIdentifierMode::Hash => TokenIdentifier::Hash(token_hash.unwrap(&env)) + NFTIdentifierMode::Ordinal => TokenIdentifier::Index(token_id.unwrap(&self.__env)), + NFTIdentifierMode::Hash => TokenIdentifier::Hash(token_hash.unwrap(&self.__env)) }; - let number_of_minted_tokens = self.info.number_of_minted_tokens(); + let number_of_minted_tokens = self.data.number_of_minted_tokens(); if let NFTIdentifierMode::Ordinal = identifier_mode { // Revert if token_id is out of bounds - if token_identifier.get_index().unwrap_or_revert(&env) >= number_of_minted_tokens { - env.revert(CEP78Error::InvalidTokenIdentifier); + if token_identifier.get_index().unwrap_or_revert(&self.__env) >= number_of_minted_tokens + { + self.revert(CEP78Error::InvalidTokenIdentifier); } } token_identifier @@ -722,7 +620,7 @@ impl CEP78 { #[inline] fn owner_of_by_id(&self, id: &String) -> Address { - match self.owners.get(id) { + match self.data.owner_of(id) { Some(token_owner) => token_owner, None => self .env() @@ -732,43 +630,43 @@ impl CEP78 { #[inline] fn is_token_burned(&self, token_id: &String) -> bool { - self.burnt_tokens.get(token_id).is_some() + self.data.is_burnt(token_id) } #[inline] fn ensure_owner(&self, token_id: &String, address: &Address) { let owner = self.owner_of_by_id(token_id); if address != &owner { - self.env().revert(CEP78Error::InvalidAccount); + self.revert(CEP78Error::InvalidAccount); } } #[inline] fn ensure_caller_is_owner(&self, token_id: &String) { let owner = self.owner_of_by_id(token_id); - if self.env().caller() != owner { - self.env().revert(CEP78Error::InvalidTokenOwner); + if self.caller() != owner { + self.revert(CEP78Error::InvalidTokenOwner); } } #[inline] fn ensure_not_burned(&self, token_id: &String) { if self.is_token_burned(token_id) { - self.env().revert(CEP78Error::PreviouslyBurntToken); + self.revert(CEP78Error::PreviouslyBurntToken); } } #[inline] fn ensure_not_caller(&self, address: Address) { - if self.env().caller() == address { - self.env().revert(CEP78Error::InvalidAccount); + if self.caller() == address { + self.revert(CEP78Error::InvalidAccount); } } #[inline] fn ensure_caller(&self, address: Address) { - if self.env().caller() != address { - self.env().revert(CEP78Error::InvalidAccount); + if self.caller() != address { + self.revert(CEP78Error::InvalidAccount); } } @@ -783,7 +681,7 @@ impl CEP78 { #[inline] fn ensure_burnable(&self) { if let BurnMode::NonBurnable = self.settings.burn_mode() { - self.env().revert(CEP78Error::InvalidBurnMode) + self.revert(CEP78Error::InvalidBurnMode) } } @@ -795,12 +693,12 @@ impl CEP78 { #[inline] fn verified_caller(&self) -> Address { let holder_mode = self.settings.holder_mode(); - let caller = self.env().caller(); + let caller = self.caller(); match (caller, holder_mode) { (Address::Account(_), NFTHolderMode::Contracts) | (Address::Contract(_), NFTHolderMode::Accounts) => { - self.env().revert(CEP78Error::InvalidHolderMode); + self.revert(CEP78Error::InvalidHolderMode); } _ => caller } diff --git a/modules/src/cep78/utils.rs b/modules/src/cep78/utils.rs deleted file mode 100644 index 7b4424ce..00000000 --- a/modules/src/cep78/utils.rs +++ /dev/null @@ -1,196 +0,0 @@ -use odra::{casper_types::bytesrepr::FromBytes, ContractEnv, OdraError, UnwrapOrRevert, Var}; - -pub trait GetAs { - fn get_as(&self, env: &ContractEnv) -> T; -} - -impl GetAs for Var -where - R: TryInto + Default + FromBytes, - R::Error: Into -{ - fn get_as(&self, env: &ContractEnv) -> T { - self.get_or_default().try_into().unwrap_or_revert(env) - } -} - -pub trait IntoOrRevert { - type Error; - fn into_or_revert(self, env: &ContractEnv) -> T; -} - -impl IntoOrRevert for R -where - R: TryInto, - R::Error: Into -{ - type Error = R::Error; - fn into_or_revert(self, env: &ContractEnv) -> T { - self.try_into().unwrap_or_revert(env) - } -} - -// pub fn migrate_owned_tokens_in_ordinal_mode() { -// let current_number_of_minted_tokens = utils::get_stored_value_with_user_errors::( -// NUMBER_OF_MINTED_TOKENS, -// NFTCoreError::MissingTotalTokenSupply, -// NFTCoreError::InvalidTotalTokenSupply -// ); -// let page_table_uref = get_uref( -// PAGE_TABLE, -// NFTCoreError::MissingPageTableURef, -// NFTCoreError::InvalidPageTableURef -// ); -// let page_table_width = get_stored_value_with_user_errors::( -// PAGE_LIMIT, -// NFTCoreError::MissingPageLimit, -// NFTCoreError::InvalidPageLimit -// ); -// let mut searched_token_ids: Vec = vec![]; -// for token_id in 0..current_number_of_minted_tokens { -// if !searched_token_ids.contains(&token_id) { -// let token_identifier = TokenIdentifier::new_index(token_id); -// let token_owner_key = get_dictionary_value_from_key::( -// TOKEN_OWNERS, -// &token_identifier.get_dictionary_item_key() -// ) -// .unwrap_or_revert_with(NFTCoreError::MissingNftKind); -// let token_owner_item_key = encode_dictionary_item_key(token_owner_key); -// let owned_tokens_list = get_token_identifiers_from_dictionary( -// &NFTIdentifierMode::Ordinal, -// &token_owner_item_key -// ) -// .unwrap_or_revert(); -// for token_identifier in owned_tokens_list.into_iter() { -// let token_id = token_identifier.get_index().unwrap_or_revert(); -// let page_number = token_id / PAGE_SIZE; -// let page_index = token_id % PAGE_SIZE; -// let mut page_record = match storage::dictionary_get::>( -// page_table_uref, -// &token_owner_item_key -// ) -// .unwrap_or_revert() -// { -// Some(page_record) => page_record, -// None => vec![false; page_table_width as usize] -// }; -// let page_uref = get_uref( -// &format!("{PREFIX_PAGE_DICTIONARY}_{page_number}"), -// NFTCoreError::MissingStorageUref, -// NFTCoreError::InvalidStorageUref -// ); -// let _ = core::mem::replace(&mut page_record[page_number as usize], true); -// storage::dictionary_put(page_table_uref, &token_owner_item_key, page_record); -// let mut page = -// match storage::dictionary_get::>(page_uref, &token_owner_item_key) -// .unwrap_or_revert() -// { -// None => vec![false; PAGE_SIZE as usize], -// Some(single_page) => single_page -// }; -// let is_already_marked_as_owned = -// core::mem::replace(&mut page[page_index as usize], true); -// if is_already_marked_as_owned { -// runtime::revert(NFTCoreError::InvalidPageIndex) -// } -// storage::dictionary_put(page_uref, &token_owner_item_key, page); -// searched_token_ids.push(token_id) -// } -// } -// } -// } - -// pub fn should_migrate_token_hashes(token_owner: Address) -> bool { -// if get_token_identifiers_from_dictionary( -// &NFTIdentifierMode::Hash, -// &encode_dictionary_item_key(token_owner), -// ) -// .is_none() -// { -// return false; -// } -// let page_table_uref = get_uref( -// PAGE_TABLE, -// NFTCoreError::MissingPageTableURef, -// NFTCoreError::InvalidPageTableURef, -// ); -// // If the owner has registered, then they will have an page table entry -// // but it will contain no bits set. -// let page_table = storage::dictionary_get::>( -// page_table_uref, -// &encode_dictionary_item_key(token_owner), -// ) -// .unwrap_or_revert() -// .unwrap_or_revert_with(NFTCoreError::UnregisteredOwnerFromMigration); -// if page_table.contains(&true) { -// return false; -// } -// true -// } - -// pub fn migrate_token_hashes(token_owner: Key) { -// let mut unmatched_hash_count = get_stored_value_with_user_errors::( -// UNMATCHED_HASH_COUNT, -// NFTCoreError::MissingUnmatchedHashCount, -// NFTCoreError::InvalidUnmatchedHashCount -// ); - -// if unmatched_hash_count == 0 { -// runtime::revert(NFTCoreError::InvalidNumberOfMintedTokens) -// } - -// let token_owner_item_key = encode_dictionary_item_key(token_owner); -// let owned_tokens_list = -// get_token_identifiers_from_dictionary(&NFTIdentifierMode::Hash, &token_owner_item_key) -// .unwrap_or_revert_with(NFTCoreError::InvalidTokenOwner); - -// let page_table_uref = get_uref( -// PAGE_TABLE, -// NFTCoreError::MissingPageTableURef, -// NFTCoreError::InvalidPageTableURef -// ); - -// let page_table_width = get_stored_value_with_user_errors::( -// PAGE_LIMIT, -// NFTCoreError::MissingPageLimit, -// NFTCoreError::InvalidPageLimit -// ); - -// for token_identifier in owned_tokens_list.into_iter() { -// let token_address = unmatched_hash_count - 1; -// let page_table_entry = token_address / PAGE_SIZE; -// let page_address = token_address % PAGE_SIZE; -// let mut page_table = -// match storage::dictionary_get::>(page_table_uref, &token_owner_item_key) -// .unwrap_or_revert() -// { -// Some(page_record) => page_record, -// None => vec![false; page_table_width as usize] -// }; -// let _ = core::mem::replace(&mut page_table[page_table_entry as usize], true); -// storage::dictionary_put(page_table_uref, &token_owner_item_key, page_table); -// let page_uref = get_uref( -// &format!("{PREFIX_PAGE_DICTIONARY}_{page_table_entry}"), -// NFTCoreError::MissingStorageUref, -// NFTCoreError::InvalidStorageUref -// ); -// let mut page = match storage::dictionary_get::>(page_uref, &token_owner_item_key) -// .unwrap_or_revert() -// { -// Some(single_page) => single_page, -// None => vec![false; PAGE_SIZE as usize] -// }; -// let _ = core::mem::replace(&mut page[page_address as usize], true); -// storage::dictionary_put(page_uref, &token_owner_item_key, page); -// insert_hash_id_lookups(unmatched_hash_count - 1, token_identifier); -// unmatched_hash_count -= 1; -// } - -// let unmatched_hash_count_uref = get_uref( -// UNMATCHED_HASH_COUNT, -// NFTCoreError::MissingUnmatchedHashCount, -// NFTCoreError::InvalidUnmatchedHashCount -// ); - -// storage::write(unmatched_hash_count_uref, unmatched_hash_count); -// } diff --git a/modules/src/cep78/whitelist.rs b/modules/src/cep78/whitelist.rs index f7f1bce0..a39a63ab 100644 --- a/modules/src/cep78/whitelist.rs +++ b/modules/src/cep78/whitelist.rs @@ -27,7 +27,7 @@ impl ACLWhitelist { self.addresses.get_or_default().contains(address) } - pub fn update_addresses(&mut self, new_addresses: Maybe>) { + pub fn update(&mut self, new_addresses: Maybe>) { let new_addresses = new_addresses.unwrap_or_default(); if !new_addresses.is_empty() { match self.get_mode() { From facb85c96a98d89072d388dd739932171fe6c999 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Wed, 24 Apr 2024 10:02:21 +0200 Subject: [PATCH 16/38] fix tests --- core/src/args.rs | 2 +- core/src/contract_container.rs | 41 +++-------------------------- modules/src/cep78/data.rs | 11 +++----- modules/src/cep78/reverse_lookup.rs | 15 +++++------ modules/src/cep78/tests/mint.rs | 5 ++-- 5 files changed, 17 insertions(+), 57 deletions(-) diff --git a/core/src/args.rs b/core/src/args.rs index 1b8b390d..d83f8176 100644 --- a/core/src/args.rs +++ b/core/src/args.rs @@ -1,6 +1,6 @@ //! This module provides types and traits for working with entrypoint arguments. -use crate::{contract_def::Argument, prelude::*, ContractEnv, ExecutionEnv, ExecutionError, OdraError}; +use crate::{contract_def::Argument, prelude::*, ContractEnv, ExecutionError}; use casper_types::{ bytesrepr::{FromBytes, ToBytes}, CLType, CLTyped, Parameter, RuntimeArgs diff --git a/core/src/contract_container.rs b/core/src/contract_container.rs index ef562ddb..70bdb7a4 100644 --- a/core/src/contract_container.rs +++ b/core/src/contract_container.rs @@ -1,8 +1,7 @@ -use crate::entry_point_callback::{Argument, EntryPointsCaller}; -use crate::{prelude::*, OdraResult}; -use crate::{CallDef, OdraError, VmError}; +use crate::entry_point_callback::EntryPointsCaller; +use crate::CallDef; +use crate::OdraResult; use casper_types::bytesrepr::Bytes; -use casper_types::RuntimeArgs; /// A wrapper struct for a EntryPointsCaller that is a layer of abstraction between the host and the entry points caller. /// @@ -22,42 +21,8 @@ impl ContractContainer { /// Calls the entry point with the given call definition. pub fn call(&self, call_def: CallDef) -> OdraResult { - // find the entry point - let ep = self - .entry_points_caller - .entry_points() - .iter() - .find(|ep| ep.name == call_def.entry_point()) - .ok_or_else(|| { - OdraError::VmError(VmError::NoSuchMethod(call_def.entry_point().to_string())) - })?; - // validate the args, return an error if the args are invalid - // self.validate_args(&ep.args, call_def.args())?; self.entry_points_caller.call(call_def) } - - fn validate_args(&self, args: &[Argument], input_args: &RuntimeArgs) -> OdraResult<()> { - for arg in args { - // check if the input args contain the arg - if let Some(input) = input_args - .named_args() - .find(|input| input.name() == arg.name.as_str()) - { - // check if the input arg has the expected type - let input_ty = input.cl_value().cl_type(); - let expected_ty = &arg.ty; - if input_ty != expected_ty { - return Err(OdraError::VmError(VmError::TypeMismatch { - expected: expected_ty.clone(), - found: input_ty.clone() - })); - } - } else { - return Err(OdraError::VmError(VmError::MissingArg)); - } - } - Ok(()) - } } #[cfg(test)] diff --git a/modules/src/cep78/data.rs b/modules/src/cep78/data.rs index 5fb5b7aa..d9834e89 100644 --- a/modules/src/cep78/data.rs +++ b/modules/src/cep78/data.rs @@ -1,7 +1,6 @@ use odra::prelude::*; use odra::Address; use odra::Mapping; -use odra::Sequence; use odra::UnwrapOrRevert; use odra::Var; @@ -13,7 +12,7 @@ pub struct CollectionData { name: Var, symbol: Var, total_token_supply: Var, - counter: Sequence, + counter: Var, installer: Var
, owners: Mapping, issuers: Mapping, @@ -43,8 +42,6 @@ impl CollectionData { self.symbol.set(symbol); self.total_token_supply.set(total_token_supply); self.installer.set(installer); - - self.counter.next_value(); } #[inline] @@ -60,12 +57,12 @@ impl CollectionData { #[inline] pub fn increment_number_of_minted_tokens(&mut self) { - self.counter.next_value(); + self.counter.add(1); } #[inline] pub fn number_of_minted_tokens(&self) -> u64 { - self.counter.get_current_value() + self.counter.get_or_default() } #[inline] @@ -85,7 +82,7 @@ impl CollectionData { #[inline] pub fn set_issuer(&mut self, token_id: &String, issuer: Address) { - self.owners.set(token_id, issuer); + self.issuers.set(token_id, issuer); } #[inline] diff --git a/modules/src/cep78/reverse_lookup.rs b/modules/src/cep78/reverse_lookup.rs index 781d9350..4c7ba2fe 100644 --- a/modules/src/cep78/reverse_lookup.rs +++ b/modules/src/cep78/reverse_lookup.rs @@ -145,7 +145,7 @@ impl ReverseLookup { &mut self, token_identifier: TokenIdentifier, source: Address, - target: Address + _target: Address ) -> (String, Address) { let mode = self.get_mode(); if let OwnerReverseLookupMode::Complete | OwnerReverseLookupMode::TransfersOnly = mode { @@ -154,13 +154,12 @@ impl ReverseLookup { if OwnerReverseLookupMode::TransfersOnly == mode { self.add_page_entry_and_page_record(tokens_count, &source, false); } - - let (page_table_entry, _page_uref) = - self.update_page_entry_and_page_record(tokens_count, &source, &target); - - let receipt_name = self.receipt_name.get_or_default(); - let _receipt_string = format!("{receipt_name}_m_{PAGE_SIZE}_p_{page_table_entry}"); // TODO: Implement the following + + // let (page_table_entry, _page_uref) = + // self.update_page_entry_and_page_record(tokens_count, &source, &target); + // let receipt_name = self.receipt_name.get_or_default(); + // let _receipt_string = format!("{receipt_name}_m_{PAGE_SIZE}_p_{page_table_entry}"); // let receipt_address = Key::dictionary(page_uref, owned_tokens_item_key.as_bytes()); // return (receipt_string, source); } @@ -214,7 +213,7 @@ impl ReverseLookup { (page_table_entry, uref_a) } - fn update_page_entry_and_page_record( + fn _update_page_entry_and_page_record( &mut self, tokens_count: u64, old_item_key: &Address, diff --git a/modules/src/cep78/tests/mint.rs b/modules/src/cep78/tests/mint.rs index 175edc90..f7028982 100644 --- a/modules/src/cep78/tests/mint.rs +++ b/modules/src/cep78/tests/mint.rs @@ -145,9 +145,8 @@ fn mint_should_increment_number_of_minted_tokens_by_one_and_add_public_key_to_to // TODO: should register the owner first to create a page for the owner contract.register_owner(Maybe::Some(owner)); - assert_eq!( - contract.try_mint(owner, TEST_PRETTY_721_META_DATA.to_string(), Maybe::None), - Ok(("".to_owned(), owner, "0".to_owned())) + assert!( + contract.try_mint(owner, TEST_PRETTY_721_META_DATA.to_string(), Maybe::None).is_ok() ); assert_eq!( From 961f3c13ab92f002929156eb85415763012fdb7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Wed, 24 Apr 2024 10:20:35 +0200 Subject: [PATCH 17/38] Refactor CEP78 module and fix tests --- core/src/contract_container.rs | 73 +++++----------------------- modules/src/cep78/events.rs | 2 + modules/src/cep78/metadata.rs | 8 +-- modules/src/cep78/mod.rs | 2 +- modules/src/cep78/reverse_lookup.rs | 8 +-- modules/src/cep78/settings.rs | 1 + modules/src/cep78/tests/acl.rs | 17 +++---- modules/src/cep78/tests/burn.rs | 4 +- modules/src/cep78/tests/installer.rs | 4 +- modules/src/cep78/tests/metadata.rs | 17 +++---- modules/src/cep78/tests/mint.rs | 10 ++-- modules/src/cep78/tests/transfer.rs | 12 ++--- modules/src/cep78/token.rs | 11 ++--- 13 files changed, 57 insertions(+), 112 deletions(-) diff --git a/core/src/contract_container.rs b/core/src/contract_container.rs index 70bdb7a4..ffba3e36 100644 --- a/core/src/contract_container.rs +++ b/core/src/contract_container.rs @@ -1,6 +1,8 @@ use crate::entry_point_callback::EntryPointsCaller; use crate::CallDef; +use crate::OdraError; use crate::OdraResult; +use crate::VmError; use casper_types::bytesrepr::Bytes; /// A wrapper struct for a EntryPointsCaller that is a layer of abstraction between the host and the entry points caller. @@ -21,20 +23,27 @@ impl ContractContainer { /// Calls the entry point with the given call definition. pub fn call(&self, call_def: CallDef) -> OdraResult { + // find the entry point + self + .entry_points_caller + .entry_points() + .iter() + .find(|ep| ep.name == call_def.entry_point()) + .ok_or_else(|| { + OdraError::VmError(VmError::NoSuchMethod(call_def.entry_point().to_string())) + })?; self.entry_points_caller.call(call_def) } } #[cfg(test)] mod tests { - use casper_types::CLType; - use super::ContractContainer; use crate::contract_context::MockContractContext; use crate::entry_point_callback::{Argument, EntryPoint, EntryPointsCaller}; use crate::host::{HostEnv, MockHostContext}; use crate::{ - casper_types::{runtime_args, RuntimeArgs}, + casper_types::RuntimeArgs, OdraError, VmError }; use crate::{prelude::*, CallDef, ContractEnv}; @@ -67,64 +76,6 @@ mod tests { assert!(result.is_ok()); } - #[test] - fn test_call_valid_entrypoint_with_wrong_arg_name() { - // Given an instance with a single entrypoint with one arg named "first". - let instance = ContractContainer::with_entrypoint(vec!["first"]); - - // When call the registered entrypoint with an arg named "second". - let call_def = CallDef::new(TEST_ENTRYPOINT, false, runtime_args! { "second" => 0u32 }); - let result = instance.call(call_def); - - // Then MissingArg error is returned. - assert_eq!(result.unwrap_err(), OdraError::VmError(VmError::MissingArg)); - } - - #[test] - fn test_call_valid_entrypoint_with_wrong_arg_type() { - // Given an instance with a single entrypoint with one arg named "first". - let instance = ContractContainer::with_entrypoint(vec!["first"]); - - // When call the registered entrypoint with an arg named "second". - let call_def = CallDef::new(TEST_ENTRYPOINT, false, runtime_args! { "first" => true }); - let result = instance.call(call_def); - - // Then MissingArg error is returned. - assert_eq!( - result.unwrap_err(), - OdraError::VmError(VmError::TypeMismatch { - expected: CLType::U32, - found: CLType::Bool - }) - ); - } - - #[test] - fn test_call_valid_entrypoint_with_missing_arg() { - // Given an instance with a single entrypoint with one arg named "first". - let instance = ContractContainer::with_entrypoint(vec!["first"]); - - // When call a valid entrypoint without args. - let call_def = CallDef::new(TEST_ENTRYPOINT, false, RuntimeArgs::new()); - let result = instance.call(call_def); - - // Then MissingArg error is returned. - assert_eq!(result.unwrap_err(), OdraError::VmError(VmError::MissingArg)); - } - - #[test] - fn test_many_missing_args() { - // Given an instance with a single entrypoint with "first", "second" and "third" args. - let instance = ContractContainer::with_entrypoint(vec!["first", "second", "third"]); - - // When call a valid entrypoint with a single valid args, - let call_def = CallDef::new(TEST_ENTRYPOINT, false, runtime_args! { "third" => 0u32 }); - let result = instance.call(call_def); - - // Then MissingArg error is returned. - assert_eq!(result.unwrap_err(), OdraError::VmError(VmError::MissingArg)); - } - impl ContractContainer { fn empty() -> Self { let ctx = Rc::new(RefCell::new(MockHostContext::new())); diff --git a/modules/src/cep78/events.rs b/modules/src/cep78/events.rs index 63ab6635..59ac386a 100644 --- a/modules/src/cep78/events.rs +++ b/modules/src/cep78/events.rs @@ -127,6 +127,7 @@ impl MetadataUpdated { pub struct VariablesSet {} impl VariablesSet { + #[allow(clippy::new_without_default)] pub fn new() -> Self { Self {} } @@ -136,6 +137,7 @@ impl VariablesSet { pub struct Migration {} impl Migration { + #[allow(clippy::new_without_default)] pub fn new() -> Self { Self {} } diff --git a/modules/src/cep78/metadata.rs b/modules/src/cep78/metadata.rs index 04e39975..78576fc5 100644 --- a/modules/src/cep78/metadata.rs +++ b/modules/src/cep78/metadata.rs @@ -112,10 +112,10 @@ impl Metadata { } fn validate(&self, kind: &NFTMetadataKind, metadata: &str) -> Result { - let token_schema = self.get_metadata_schema(&kind); + let token_schema = self.get_metadata_schema(kind); match kind { NFTMetadataKind::CEP78 => { - let metadata = serde_json_wasm::from_str::(&metadata) + let metadata = serde_json_wasm::from_str::(metadata) .map_err(|_| CEP78Error::FailedToParseCep78Metadata)?; if let Some(name_property) = token_schema.properties.get("name") { @@ -137,7 +137,7 @@ impl Metadata { .map_err(|_| CEP78Error::FailedToJsonifyCEP78Metadata) } NFTMetadataKind::NFT721 => { - let metadata = serde_json_wasm::from_str::(&metadata) + let metadata = serde_json_wasm::from_str::(metadata) .map_err(|_| CEP78Error::FailedToParse721Metadata)?; if let Some(name_property) = token_schema.properties.get("name") { @@ -161,7 +161,7 @@ impl Metadata { NFTMetadataKind::Raw => Ok(metadata.to_owned()), NFTMetadataKind::CustomValidated => { let custom_metadata = - serde_json_wasm::from_str::>(&metadata) + serde_json_wasm::from_str::>(metadata) .map(|attributes| CustomMetadata { attributes }) .map_err(|_| CEP78Error::FailedToParseCustomMetadata)?; diff --git a/modules/src/cep78/mod.rs b/modules/src/cep78/mod.rs index 1d1cb010..2306c56b 100644 --- a/modules/src/cep78/mod.rs +++ b/modules/src/cep78/mod.rs @@ -1,7 +1,7 @@ #![allow(missing_docs)] -mod data; mod constants; +mod data; pub mod error; pub mod events; mod metadata; diff --git a/modules/src/cep78/reverse_lookup.rs b/modules/src/cep78/reverse_lookup.rs index 4c7ba2fe..ec1ba17d 100644 --- a/modules/src/cep78/reverse_lookup.rs +++ b/modules/src/cep78/reverse_lookup.rs @@ -69,9 +69,9 @@ impl ReverseLookup { ownership_mode: OwnershipMode ) -> (String, URef) { let mode = self.get_mode(); - if vec![ + if [ OwnerReverseLookupMode::Complete, - OwnerReverseLookupMode::TransfersOnly, + OwnerReverseLookupMode::TransfersOnly ] .contains(&mode) { @@ -163,7 +163,7 @@ impl ReverseLookup { // let receipt_address = Key::dictionary(page_uref, owned_tokens_item_key.as_bytes()); // return (receipt_string, source); } - return ("".to_owned(), source); + ("".to_owned(), source) } fn add_page_entry_and_page_record( @@ -248,7 +248,7 @@ impl ReverseLookup { let mut target_page_table = self .page_table - .get(&new_item_key) + .get(new_item_key) .unwrap_or_revert_with(&self.env(), CEP78Error::UnregisteredOwnerInTransfer); let mut target_page = if !target_page_table[page_table_entry as usize] { diff --git a/modules/src/cep78/settings.rs b/modules/src/cep78/settings.rs index fd6f2491..d8ebf2a9 100644 --- a/modules/src/cep78/settings.rs +++ b/modules/src/cep78/settings.rs @@ -16,6 +16,7 @@ pub struct Settings { } impl Settings { + #[allow(clippy::too_many_arguments)] pub fn init( &mut self, allow_minting: bool, diff --git a/modules/src/cep78/tests/acl.rs b/modules/src/cep78/tests/acl.rs index 09932a86..ce6657e2 100644 --- a/modules/src/cep78/tests/acl.rs +++ b/modules/src/cep78/tests/acl.rs @@ -16,11 +16,8 @@ use super::default_args_builder; #[test] fn should_install_with_acl_whitelist() { let env = odra_test::env(); - let test_contract_address = TestContractHostRef::deploy(&env, NoArgs); - - let contract_whitelist = vec![test_contract_address.address().clone()]; - + let contract_whitelist = vec![*test_contract_address.address()]; let args = default_args_builder() .holder_mode(NFTHolderMode::Contracts) .whitelist_mode(WhitelistMode::Locked) @@ -189,7 +186,7 @@ fn should_allow_whitelisted_contract_to_mint() { let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); - let contract_whitelist = vec![minting_contract.address().clone()]; + let contract_whitelist = vec![*minting_contract.address()]; let args = default_args_builder() .holder_mode(NFTHolderMode::Contracts) .whitelist_mode(WhitelistMode::Locked) @@ -240,7 +237,7 @@ fn should_allow_mixed_account_contract_to_mint() { let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); let account_user_1 = env.get_account(1); - let mixed_whitelist = vec![minting_contract.address().clone(), account_user_1]; + let mixed_whitelist = vec![*minting_contract.address(), account_user_1]; let args = default_args_builder() .holder_mode(NFTHolderMode::Mixed) @@ -286,7 +283,7 @@ fn should_disallow_unlisted_contract_from_minting_with_mixed_account_contract() let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); let account_user_1 = env.get_account(1); let mixed_whitelist = vec![ - DummyContractHostRef::deploy(&env, NoArgs).address().clone(), + *DummyContractHostRef::deploy(&env, NoArgs).address(), account_user_1, ]; @@ -314,7 +311,7 @@ fn should_disallow_unlisted_account_from_minting_with_mixed_account_contract() { let minting_contract = TestContractHostRef::deploy(&env, NoArgs); let listed_account = env.get_account(0); let unlisted_account = env.get_account(1); - let mixed_whitelist = vec![minting_contract.address().clone(), listed_account]; + let mixed_whitelist = vec![*minting_contract.address(), listed_account]; let args = default_args_builder() .holder_mode(NFTHolderMode::Mixed) @@ -343,7 +340,7 @@ fn should_disallow_listed_account_from_minting_with_nftholder_contract() { let minting_contract = TestContractHostRef::deploy(&env, NoArgs); let listed_account = env.get_account(0); - let mixed_whitelist = vec![minting_contract.address().clone(), listed_account]; + let mixed_whitelist = vec![*minting_contract.address(), listed_account]; let args = default_args_builder() .holder_mode(NFTHolderMode::Contracts) @@ -420,7 +417,7 @@ fn should_be_able_to_update_whitelist_for_minting() { contract.set_variables( Maybe::None, - Maybe::Some(vec![minting_contract.address().clone()]), + Maybe::Some(vec![*minting_contract.address()]), Maybe::None ); diff --git a/modules/src/cep78/tests/burn.rs b/modules/src/cep78/tests/burn.rs index 20d1ff31..0c8f8b62 100644 --- a/modules/src/cep78/tests/burn.rs +++ b/modules/src/cep78/tests/burn.rs @@ -158,7 +158,7 @@ fn should_return_expected_error_burning_of_others_users_token() { fn should_allow_contract_to_burn_token() { let env = odra_test::env(); let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); - let contract_whitelist = vec![minting_contract.address().clone()]; + let contract_whitelist = vec![*minting_contract.address()]; let args = default_args_builder() .holder_mode(NFTHolderMode::Contracts) .ownership_mode(OwnershipMode::Minter) @@ -260,7 +260,7 @@ fn should_let_contract_operator_burn_tokens_with_operator_burn_mode() { let token_id = 0u64; let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); minting_contract.set_address(contract.address()); - let operator = minting_contract.address().clone(); + let operator = *minting_contract.address(); let account_1 = env.get_account(1); env.set_caller(account_1); diff --git a/modules/src/cep78/tests/installer.rs b/modules/src/cep78/tests/installer.rs index 57069d3b..2963456c 100644 --- a/modules/src/cep78/tests/installer.rs +++ b/modules/src/cep78/tests/installer.rs @@ -21,7 +21,7 @@ fn should_install_contract() { assert_eq!(&contract.get_collection_name(), COLLECTION_NAME); assert_eq!(&contract.get_collection_symbol(), COLLECTION_SYMBOL); assert_eq!(contract.get_total_supply(), 1u64); - assert_eq!(contract.is_minting_allowed(), true); + assert!(contract.is_minting_allowed()); assert_eq!(contract.get_minting_mode(), MintingMode::Installer); assert_eq!(contract.get_number_of_minted_tokens(), 0u64); @@ -54,7 +54,7 @@ fn should_install_with_allow_minting_set_to_false() { let args = default_args_builder().allow_minting(false).build(); let contract = CEP78HostRef::deploy(&env, args); - assert_eq!(contract.is_minting_allowed(), false); + assert!(!contract.is_minting_allowed()); } #[test] diff --git a/modules/src/cep78/tests/metadata.rs b/modules/src/cep78/tests/metadata.rs index 0114c9d4..c97ce6ae 100644 --- a/modules/src/cep78/tests/metadata.rs +++ b/modules/src/cep78/tests/metadata.rs @@ -109,7 +109,7 @@ fn should_prevent_metadata_update_by_non_owner_key() { .ownership_mode(OwnershipMode::Transferable) .build(); let mut contract = CEP78HostRef::deploy(&env, args); - let token_owner = contract.address().clone(); + let token_owner = *contract.address(); contract.mint( token_owner, TEST_PRETTY_721_META_DATA.to_string(), @@ -260,8 +260,7 @@ fn should_update_metadata_for_custom_validated_using_token_id() { fn should_get_metadata_using_token_id() { let env = odra_test::env(); let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); - let minting_contract_address = minting_contract.address().clone(); - let contract_whitelist = vec![minting_contract_address]; + let contract_whitelist = vec![*minting_contract.address()]; let args = default_args_builder() .holder_mode(NFTHolderMode::Contracts) .whitelist_mode(WhitelistMode::Locked) @@ -288,8 +287,7 @@ fn should_get_metadata_using_token_id() { fn should_get_metadata_using_token_metadata_hash() { let env = odra_test::env(); let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); - let minting_contract_address = minting_contract.address().clone(); - let contract_whitelist = vec![minting_contract_address]; + let contract_whitelist = vec![*minting_contract.address()]; let args = default_args_builder() .identifier_mode(NFTIdentifierMode::Hash) .holder_mode(NFTHolderMode::Contracts) @@ -320,8 +318,7 @@ fn should_get_metadata_using_token_metadata_hash() { fn should_revert_minting_token_metadata_hash_twice() { let env = odra_test::env(); let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); - let minting_contract_address = minting_contract.address().clone(); - let contract_whitelist = vec![minting_contract_address]; + let contract_whitelist = vec![*minting_contract.address()]; let args = default_args_builder() .identifier_mode(NFTIdentifierMode::Hash) .holder_mode(NFTHolderMode::Contracts) @@ -355,8 +352,7 @@ fn should_revert_minting_token_metadata_hash_twice() { fn should_get_metadata_using_custom_token_hash() { let env = odra_test::env(); let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); - let minting_contract_address = minting_contract.address().clone(); - let contract_whitelist = vec![minting_contract_address]; + let contract_whitelist = vec![*minting_contract.address()]; let args = default_args_builder() .identifier_mode(NFTIdentifierMode::Hash) .holder_mode(NFTHolderMode::Contracts) @@ -387,8 +383,7 @@ fn should_get_metadata_using_custom_token_hash() { fn should_revert_minting_custom_token_hash_identifier_twice() { let env = odra_test::env(); let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); - let minting_contract_address = minting_contract.address().clone(); - let contract_whitelist = vec![minting_contract_address]; + let contract_whitelist = vec![*minting_contract.address()]; let args = default_args_builder() .identifier_mode(NFTIdentifierMode::Hash) .holder_mode(NFTHolderMode::Contracts) diff --git a/modules/src/cep78/tests/mint.rs b/modules/src/cep78/tests/mint.rs index f7028982..9c6715c1 100644 --- a/modules/src/cep78/tests/mint.rs +++ b/modules/src/cep78/tests/mint.rs @@ -145,9 +145,9 @@ fn mint_should_increment_number_of_minted_tokens_by_one_and_add_public_key_to_to // TODO: should register the owner first to create a page for the owner contract.register_owner(Maybe::Some(owner)); - assert!( - contract.try_mint(owner, TEST_PRETTY_721_META_DATA.to_string(), Maybe::None).is_ok() - ); + assert!(contract + .try_mint(owner, TEST_PRETTY_721_META_DATA.to_string(), Maybe::None) + .is_ok()); assert_eq!( contract.get_number_of_minted_tokens(), @@ -585,7 +585,7 @@ fn should_mint_without_returning_receipts_and_flat_gas_cost() { // In this case there is no first time allocation of a page. // Therefore the second and first mints must have equivalent gas costs. - assert_eq!(costs.get(0), costs.get(1)) + assert_eq!(costs.first(), costs.get(1)) } // A test to ensure that the page table allocation is preserved @@ -830,5 +830,5 @@ fn should_approve_all_with_flat_gas_cost() { // Operator approval should have flat gas costs // Therefore the second and first set_approve_for_all must have equivalent gas costs. - assert_eq!(costs.get(0), costs.get(1)); + assert_eq!(costs.first(), costs.get(1)); } diff --git a/modules/src/cep78/tests/transfer.rs b/modules/src/cep78/tests/transfer.rs index a3b2b0aa..464d6cbe 100644 --- a/modules/src/cep78/tests/transfer.rs +++ b/modules/src/cep78/tests/transfer.rs @@ -460,7 +460,7 @@ fn should_be_able_to_approve_with_deprecated_operator_argument() {} fn should_transfer_between_contract_to_account() { let env = odra_test::env(); let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); - let contract_whitelist = vec![minting_contract.address().clone()]; + let contract_whitelist = vec![*minting_contract.address()]; let args = default_args_builder() .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) .events_mode(EventsMode::CES) @@ -777,7 +777,7 @@ fn check_transfers_with_transfer_filter_contract_modes() { transfer_filter_contract.set_return_value(TransferFilterContractResult::DenyTransfer as u8); let args = default_args_builder() .ownership_mode(OwnershipMode::Transferable) - .transfer_filter_contract_contract_key(transfer_filter_contract.address().clone()) + .transfer_filter_contract_contract_key(*transfer_filter_contract.address()) .build(); let mut contract = CEP78HostRef::deploy(&env, args); let token_owner = env.get_account(0); @@ -898,7 +898,7 @@ fn should_allow_transfer_from_contract_with_package_operator_mode_with_operator( contract.register_owner(Maybe::Some(token_receiver)); let token_id = 0u64; - contract.set_approval_for_all(true, minting_contract.address().clone()); + contract.set_approval_for_all(true, *minting_contract.address()); assert!(minting_contract .try_transfer_from(token_id, token_owner, token_receiver) .is_ok()); @@ -931,7 +931,7 @@ fn should_allow_package_operator_to_approve_with_package_operator_mode() { let token_receiver = env.get_account(1); contract.register_owner(Maybe::Some(token_receiver)); - contract.set_approval_for_all(true, minting_contract.address().clone()); + contract.set_approval_for_all(true, *minting_contract.address()); let token_id = 0u64; let spender = env.get_account(2); @@ -968,7 +968,7 @@ fn should_allow_account_to_approve_spender_with_package_operator() { let token_receiver = env.get_account(1); contract.register_owner(Maybe::Some(token_receiver)); - contract.set_approval_for_all(true, minting_contract.address().clone()); + contract.set_approval_for_all(true, *minting_contract.address()); let token_id = 0u64; let spender = env.get_account(2); @@ -1006,7 +1006,7 @@ fn should_allow_package_operator_to_revoke_with_package_operator_mode() { let token_receiver = env.get_account(1); contract.register_owner(Maybe::Some(token_receiver)); - contract.set_approval_for_all(true, minting_contract.address().clone()); + contract.set_approval_for_all(true, *minting_contract.address()); let token_id = 0u64; let spender = env.get_account(2); diff --git a/modules/src/cep78/token.rs b/modules/src/cep78/token.rs index ffbe5038..e6a60cf4 100644 --- a/modules/src/cep78/token.rs +++ b/modules/src/cep78/token.rs @@ -1,3 +1,4 @@ +#![allow(clippy::too_many_arguments)] use super::{ data::CollectionData, error::CEP78Error, @@ -203,12 +204,10 @@ impl CEP78 { } } - if MintingMode::Acl == minting_mode { - if !self.whitelist.is_whitelisted(&caller) { - match caller { - Address::Contract(_) => self.revert(CEP78Error::UnlistedContractHash), - Address::Account(_) => self.revert(CEP78Error::InvalidMinter) - } + if MintingMode::Acl == minting_mode && !self.whitelist.is_whitelisted(&caller) { + match caller { + Address::Contract(_) => self.revert(CEP78Error::UnlistedContractHash), + Address::Account(_) => self.revert(CEP78Error::InvalidMinter) } } From 24fa20eb08076af0701ab5b77ee88efbe5ed9a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Wed, 24 Apr 2024 10:27:26 +0200 Subject: [PATCH 18/38] fix linter issues --- core/src/contract_container.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/core/src/contract_container.rs b/core/src/contract_container.rs index ffba3e36..5ce0a7c2 100644 --- a/core/src/contract_container.rs +++ b/core/src/contract_container.rs @@ -1,4 +1,5 @@ use crate::entry_point_callback::EntryPointsCaller; +use crate::prelude::ToOwned; use crate::CallDef; use crate::OdraError; use crate::OdraResult; @@ -23,14 +24,13 @@ impl ContractContainer { /// Calls the entry point with the given call definition. pub fn call(&self, call_def: CallDef) -> OdraResult { - // find the entry point - self - .entry_points_caller + // find the entry point + self.entry_points_caller .entry_points() .iter() .find(|ep| ep.name == call_def.entry_point()) .ok_or_else(|| { - OdraError::VmError(VmError::NoSuchMethod(call_def.entry_point().to_string())) + OdraError::VmError(VmError::NoSuchMethod(call_def.entry_point().to_owned())) })?; self.entry_points_caller.call(call_def) } @@ -42,10 +42,7 @@ mod tests { use crate::contract_context::MockContractContext; use crate::entry_point_callback::{Argument, EntryPoint, EntryPointsCaller}; use crate::host::{HostEnv, MockHostContext}; - use crate::{ - casper_types::RuntimeArgs, - OdraError, VmError - }; + use crate::{casper_types::RuntimeArgs, OdraError, VmError}; use crate::{prelude::*, CallDef, ContractEnv}; const TEST_ENTRYPOINT: &str = "ep"; From af5e8e548539e54bb629ce7ee8a86f4618cc8bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Thu, 25 Apr 2024 07:51:31 +0200 Subject: [PATCH 19/38] Reorganize test contracts and rename CEP78 to Cep78 --- modules/Odra.toml | 12 ++ modules/src/cep78/mod.rs | 1 + modules/src/cep78/reverse_lookup.rs | 5 +- modules/src/cep78/tests/acl.rs | 49 +++---- modules/src/cep78/tests/burn.rs | 29 +++-- modules/src/cep78/tests/events.rs | 6 +- modules/src/cep78/tests/installer.rs | 6 +- modules/src/cep78/tests/metadata.rs | 53 ++++---- modules/src/cep78/tests/mint.rs | 63 ++++----- modules/src/cep78/tests/set_variables.rs | 6 +- modules/src/cep78/tests/transfer.rs | 71 +++++------ modules/src/cep78/tests/utils.rs | 155 +---------------------- modules/src/cep78/token.rs | 12 +- modules/src/cep78/utils.rs | 148 ++++++++++++++++++++++ 14 files changed, 320 insertions(+), 296 deletions(-) create mode 100644 modules/src/cep78/utils.rs diff --git a/modules/Odra.toml b/modules/Odra.toml index b0c5fa8d..d9a2e5ec 100644 --- a/modules/Odra.toml +++ b/modules/Odra.toml @@ -22,3 +22,15 @@ fqn = "access::Ownable" [[contracts]] fqn = "ownable_2step::Ownable2Step" +[[contracts]] +fqn = "cep78::token::Cep78" + +[[contracts]] +fqn = "cep78::utils::MockContract" + +[[contracts]] +fqn = "cep78::utils::MockDummyContract" + +[[contracts]] +fqn = "cep78::utils::MockTransferFilterContract" + diff --git a/modules/src/cep78/mod.rs b/modules/src/cep78/mod.rs index 2306c56b..ce0cfa78 100644 --- a/modules/src/cep78/mod.rs +++ b/modules/src/cep78/mod.rs @@ -12,3 +12,4 @@ mod settings; mod tests; pub mod token; mod whitelist; +mod utils; \ No newline at end of file diff --git a/modules/src/cep78/reverse_lookup.rs b/modules/src/cep78/reverse_lookup.rs index ec1ba17d..1c461740 100644 --- a/modules/src/cep78/reverse_lookup.rs +++ b/modules/src/cep78/reverse_lookup.rs @@ -67,7 +67,7 @@ impl ReverseLookup { &mut self, owner: Maybe
, ownership_mode: OwnershipMode - ) -> (String, URef) { + ) -> String { let mode = self.get_mode(); if [ OwnerReverseLookupMode::Complete, @@ -118,7 +118,8 @@ impl ReverseLookup { // )); // runtime::ret(CLValue::from_t((collection_name, package_uref)).unwrap_or_revert()) } - ("".to_string(), URef::new([0u8; 32], AccessRights::READ)) + // ("".to_string(), URef::new([255; 32], AccessRights::READ_ADD_WRITE)) + "".to_string() } pub fn on_mint( diff --git a/modules/src/cep78/tests/acl.rs b/modules/src/cep78/tests/acl.rs index ce6657e2..3db9ec87 100644 --- a/modules/src/cep78/tests/acl.rs +++ b/modules/src/cep78/tests/acl.rs @@ -7,8 +7,9 @@ use odra::{ use crate::cep78::{ error::CEP78Error, modalities::{MintingMode, NFTHolderMode, OwnershipMode, WhitelistMode}, - tests::utils::{DummyContractHostRef, TestContractHostRef, TEST_PRETTY_721_META_DATA}, - token::CEP78HostRef + tests::utils::TEST_PRETTY_721_META_DATA, + token::Cep78HostRef, + utils::{MockContractHostRef, MockDummyContractHostRef} }; use super::default_args_builder; @@ -16,7 +17,7 @@ use super::default_args_builder; #[test] fn should_install_with_acl_whitelist() { let env = odra_test::env(); - let test_contract_address = TestContractHostRef::deploy(&env, NoArgs); + let test_contract_address = MockContractHostRef::deploy(&env, NoArgs); let contract_whitelist = vec![*test_contract_address.address()]; let args = default_args_builder() .holder_mode(NFTHolderMode::Contracts) @@ -24,7 +25,7 @@ fn should_install_with_acl_whitelist() { .minting_mode(MintingMode::Acl) .acl_white_list(contract_whitelist) .build(); - let contract = CEP78HostRef::deploy(&env, args); + let contract = Cep78HostRef::deploy(&env, args); assert_eq!(WhitelistMode::Locked, contract.get_whitelist_mode()); let is_whitelisted_contract = contract.is_whitelisted(test_contract_address.address()); @@ -50,7 +51,7 @@ fn should_not_install_with_minting_mode_not_acl_if_acl_whitelist_provided() { .acl_white_list(contract_whitelist) .build(); - CEP78HostRef::deploy(&env, args); + Cep78HostRef::deploy(&env, args); // builder.exec(install_request).expect_failure(); @@ -72,7 +73,7 @@ fn should_allow_installation_of_contract_with_empty_locked_whitelist_in_public_m .minting_mode(MintingMode::Public) .build(); - CEP78HostRef::deploy(env, args); + Cep78HostRef::deploy(env, args); } #[test] @@ -107,7 +108,7 @@ fn should_disallow_installation_with_contract_holder_mode_and_installer_mode() { .acl_white_list(contract_whitelist) .build(); - CEP78HostRef::deploy(&env, args); + Cep78HostRef::deploy(&env, args); // builder.exec(install_request).expect_failure(); // let error = builder.get_error().expect("should have an error"); // assert_expected_error(error, 38, "Invalid MintingMode (not ACL) and NFTHolderMode"); @@ -127,7 +128,7 @@ fn should_allow_whitelisted_account_to_mint() { .minting_mode(MintingMode::Acl) .acl_white_list(account_whitelist) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); assert!( contract.is_whitelisted(&account_user_1), @@ -160,7 +161,7 @@ fn should_disallow_unlisted_account_from_minting() { .minting_mode(MintingMode::Acl) .acl_white_list(account_whitelist) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); assert!( contract.is_whitelisted(&account), @@ -184,7 +185,7 @@ fn should_disallow_unlisted_account_from_minting() { fn should_allow_whitelisted_contract_to_mint() { let env = odra_test::env(); - let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let mut minting_contract = MockContractHostRef::deploy(&env, NoArgs); let contract_whitelist = vec![*minting_contract.address()]; let args = default_args_builder() @@ -194,7 +195,7 @@ fn should_allow_whitelisted_contract_to_mint() { .minting_mode(MintingMode::Acl) .acl_white_list(contract_whitelist) .build(); - let contract = CEP78HostRef::deploy(&env, args); + let contract = Cep78HostRef::deploy(&env, args); assert!( contract.is_whitelisted(minting_contract.address()), "acl whitelist is incorrectly set" @@ -211,7 +212,7 @@ fn should_allow_whitelisted_contract_to_mint() { fn should_disallow_unlisted_contract_from_minting() { let env = odra_test::env(); - let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let mut minting_contract = MockContractHostRef::deploy(&env, NoArgs); let contract_whitelist = vec![env.get_account(1), env.get_account(2), env.get_account(3)]; let args = default_args_builder() @@ -221,7 +222,7 @@ fn should_disallow_unlisted_contract_from_minting() { .minting_mode(MintingMode::Acl) .acl_white_list(contract_whitelist) .build(); - let contract = CEP78HostRef::deploy(&env, args); + let contract = Cep78HostRef::deploy(&env, args); minting_contract.set_address(contract.address()); assert_eq!( @@ -235,7 +236,7 @@ fn should_disallow_unlisted_contract_from_minting() { fn should_allow_mixed_account_contract_to_mint() { let env = odra_test::env(); - let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let mut minting_contract = MockContractHostRef::deploy(&env, NoArgs); let account_user_1 = env.get_account(1); let mixed_whitelist = vec![*minting_contract.address(), account_user_1]; @@ -246,7 +247,7 @@ fn should_allow_mixed_account_contract_to_mint() { .minting_mode(MintingMode::Acl) .acl_white_list(mixed_whitelist) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); minting_contract.set_address(contract.address()); assert!( @@ -280,10 +281,10 @@ fn should_allow_mixed_account_contract_to_mint() { fn should_disallow_unlisted_contract_from_minting_with_mixed_account_contract() { let env = odra_test::env(); - let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let mut minting_contract = MockContractHostRef::deploy(&env, NoArgs); let account_user_1 = env.get_account(1); let mixed_whitelist = vec![ - *DummyContractHostRef::deploy(&env, NoArgs).address(), + *MockDummyContractHostRef::deploy(&env, NoArgs).address(), account_user_1, ]; @@ -294,7 +295,7 @@ fn should_disallow_unlisted_contract_from_minting_with_mixed_account_contract() .minting_mode(MintingMode::Acl) .acl_white_list(mixed_whitelist) .build(); - let contract = CEP78HostRef::deploy(&env, args); + let contract = Cep78HostRef::deploy(&env, args); minting_contract.set_address(contract.address()); assert_eq!( @@ -308,7 +309,7 @@ fn should_disallow_unlisted_contract_from_minting_with_mixed_account_contract() fn should_disallow_unlisted_account_from_minting_with_mixed_account_contract() { let env = odra_test::env(); - let minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let minting_contract = MockContractHostRef::deploy(&env, NoArgs); let listed_account = env.get_account(0); let unlisted_account = env.get_account(1); let mixed_whitelist = vec![*minting_contract.address(), listed_account]; @@ -320,7 +321,7 @@ fn should_disallow_unlisted_account_from_minting_with_mixed_account_contract() { .minting_mode(MintingMode::Acl) .acl_white_list(mixed_whitelist) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); env.set_caller(unlisted_account); assert_eq!( contract.try_mint( @@ -337,7 +338,7 @@ fn should_disallow_unlisted_account_from_minting_with_mixed_account_contract() { fn should_disallow_listed_account_from_minting_with_nftholder_contract() { let env = odra_test::env(); - let minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let minting_contract = MockContractHostRef::deploy(&env, NoArgs); let listed_account = env.get_account(0); let mixed_whitelist = vec![*minting_contract.address(), listed_account]; @@ -349,7 +350,7 @@ fn should_disallow_listed_account_from_minting_with_nftholder_contract() { .minting_mode(MintingMode::Acl) .acl_white_list(mixed_whitelist) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); assert!( contract.is_whitelisted(&listed_account), @@ -392,7 +393,7 @@ fn should_be_able_to_update_whitelist_for_minting_with_deprecated_arg_contract_w fn should_be_able_to_update_whitelist_for_minting() { let env = odra_test::env(); - let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let mut minting_contract = MockContractHostRef::deploy(&env, NoArgs); let contract_whitelist = vec![]; let args = default_args_builder() @@ -402,7 +403,7 @@ fn should_be_able_to_update_whitelist_for_minting() { .minting_mode(MintingMode::Acl) .acl_white_list(contract_whitelist) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); minting_contract.set_address(contract.address()); assert!( diff --git a/modules/src/cep78/tests/burn.rs b/modules/src/cep78/tests/burn.rs index 0c8f8b62..667990fa 100644 --- a/modules/src/cep78/tests/burn.rs +++ b/modules/src/cep78/tests/burn.rs @@ -14,9 +14,10 @@ use crate::cep78::{ }, tests::{ default_args_builder, - utils::{self, TestContractHostRef} + utils }, - token::CEP78HostRef + token::Cep78HostRef, + utils::MockContractHostRef }; use super::utils::TEST_PRETTY_721_META_DATA; @@ -28,7 +29,7 @@ fn should_burn_minted_token(reporting: OwnerReverseLookupMode) { .ownership_mode(OwnershipMode::Transferable) .events_mode(EventsMode::CES) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); let token_id = 0u64; @@ -62,7 +63,7 @@ fn should_burn_minted_token(reporting: OwnerReverseLookupMode) { ); } -fn mint(contract: &mut CEP78HostRef, reverse_lookup_enabled: bool, token_owner: Address) { +fn mint(contract: &mut Cep78HostRef, reverse_lookup_enabled: bool, token_owner: Address) { if reverse_lookup_enabled { contract.register_owner(Maybe::Some(token_owner)); contract.mint( @@ -99,7 +100,7 @@ fn should_not_burn_previously_burnt_token() { .ownership_mode(OwnershipMode::Transferable) .events_mode(EventsMode::CES) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); mint(&mut contract, true, token_owner); @@ -121,7 +122,7 @@ fn should_return_expected_error_when_burning_non_existing_token() { let args = default_args_builder() .ownership_mode(OwnershipMode::Transferable) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_id = 0u64; assert_eq!( @@ -138,7 +139,7 @@ fn should_return_expected_error_burning_of_others_users_token() { .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) .ownership_mode(OwnershipMode::Transferable) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); let account_1 = env.get_account(1); @@ -157,7 +158,7 @@ fn should_return_expected_error_burning_of_others_users_token() { #[test] fn should_allow_contract_to_burn_token() { let env = odra_test::env(); - let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let mut minting_contract = MockContractHostRef::deploy(&env, NoArgs); let contract_whitelist = vec![*minting_contract.address()]; let args = default_args_builder() .holder_mode(NFTHolderMode::Contracts) @@ -166,7 +167,7 @@ fn should_allow_contract_to_burn_token() { .ownership_mode(OwnershipMode::Transferable) .acl_white_list(contract_whitelist) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); minting_contract.set_address(contract.address()); let token_owner = env.get_account(0); minting_contract.mint_for(token_owner, TEST_PRETTY_721_META_DATA.to_string()); @@ -188,7 +189,7 @@ fn should_not_burn_in_non_burn_mode() { .burn_mode(BurnMode::NonBurnable) .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); mint(&mut contract, true, token_owner); @@ -208,7 +209,7 @@ fn should_let_account_operator_burn_tokens_with_operator_burn_mode() { .events_mode(EventsMode::CES) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); mint(&mut contract, true, token_owner); @@ -253,12 +254,12 @@ fn should_let_contract_operator_burn_tokens_with_operator_burn_mode() { .events_mode(EventsMode::CES) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); mint(&mut contract, true, token_owner); let token_id = 0u64; - let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let mut minting_contract = MockContractHostRef::deploy(&env, NoArgs); minting_contract.set_address(contract.address()); let operator = *minting_contract.address(); let account_1 = env.get_account(1); @@ -304,7 +305,7 @@ fn should_burn_token_in_hash_identifier_mode() { .metadata_mutability(MetadataMutability::Immutable) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); mint(&mut contract, true, token_owner); diff --git a/modules/src/cep78/tests/events.rs b/modules/src/cep78/tests/events.rs index 5588e780..f963bb05 100644 --- a/modules/src/cep78/tests/events.rs +++ b/modules/src/cep78/tests/events.rs @@ -6,7 +6,7 @@ use odra::{ use crate::cep78::{ modalities::{EventsMode, OwnerReverseLookupMode, OwnershipMode}, tests::{default_args_builder, utils::TEST_PRETTY_721_META_DATA}, - token::CEP78HostRef + token::Cep78HostRef }; // cep47 event style @@ -49,7 +49,7 @@ fn should_not_have_events_dicts_in_no_events_mode() { let args = default_args_builder() .events_mode(EventsMode::NoEvents) .build(); - let _contract = CEP78HostRef::deploy(&env, args); + let _contract = Cep78HostRef::deploy(&env, args); // Check dict from EventsMode::CES // let events = named_keys.get(EVENTS_DICT); @@ -64,7 +64,7 @@ fn should_not_record_events_in_no_events_mode() { .ownership_mode(OwnershipMode::Transferable) .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); contract.mint( diff --git a/modules/src/cep78/tests/installer.rs b/modules/src/cep78/tests/installer.rs index 2963456c..d73aad0b 100644 --- a/modules/src/cep78/tests/installer.rs +++ b/modules/src/cep78/tests/installer.rs @@ -3,7 +3,7 @@ use odra::host::Deployer; use crate::cep78::{ modalities::MintingMode, tests::{default_args_builder, COLLECTION_NAME, COLLECTION_SYMBOL}, - token::CEP78HostRef + token::Cep78HostRef }; #[test] @@ -16,7 +16,7 @@ fn should_install_contract() { .total_token_supply(1u64) .allow_minting(true) .build(); - let contract = CEP78HostRef::deploy(&env, args); + let contract = Cep78HostRef::deploy(&env, args); assert_eq!(&contract.get_collection_name(), COLLECTION_NAME); assert_eq!(&contract.get_collection_symbol(), COLLECTION_SYMBOL); @@ -53,7 +53,7 @@ fn should_install_with_allow_minting_set_to_false() { let env = odra_test::env(); let args = default_args_builder().allow_minting(false).build(); - let contract = CEP78HostRef::deploy(&env, args); + let contract = Cep78HostRef::deploy(&env, args); assert!(!contract.is_minting_allowed()); } diff --git a/modules/src/cep78/tests/metadata.rs b/modules/src/cep78/tests/metadata.rs index c97ce6ae..3b42bbd4 100644 --- a/modules/src/cep78/tests/metadata.rs +++ b/modules/src/cep78/tests/metadata.rs @@ -13,13 +13,14 @@ use crate::cep78::{ }, tests::{ utils::{ - TestContractHostRef, MALFORMED_META_DATA, TEST_PRETTY_CEP78_METADATA, + MALFORMED_META_DATA, TEST_PRETTY_CEP78_METADATA, TEST_PRETTY_UPDATED_CEP78_METADATA }, TEST_CUSTOM_METADATA, TEST_CUSTOM_METADATA_SCHEMA, TEST_CUSTOM_UPDATED_METADATA, TOKEN_HASH }, - token::CEP78HostRef + token::Cep78HostRef, + utils::MockContractHostRef }; use super::{ @@ -36,7 +37,7 @@ fn should_prevent_update_in_immutable_mode() { .metadata_mutability(MetadataMutability::Immutable) .ownership_mode(OwnershipMode::Transferable) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); contract.mint( env.get_account(0), TEST_PRETTY_721_META_DATA.to_string(), @@ -58,15 +59,15 @@ fn should_prevent_update_in_immutable_mode() { #[test] fn should_prevent_install_with_hash_identifier_in_mutable_mode() { - let env = odra_test::env(); - let args = default_args_builder() - .nft_metadata_kind(NFTMetadataKind::NFT721) - .identifier_mode(NFTIdentifierMode::Hash) - .metadata_mutability(MetadataMutability::Mutable) - .build(); - let _contract = CEP78HostRef::deploy(&env, args); + // let env = odra_test::env(); + // let args = default_args_builder() + // .nft_metadata_kind(NFTMetadataKind::NFT721) + // .identifier_mode(NFTIdentifierMode::Hash) + // .metadata_mutability(MetadataMutability::Mutable) + // .build(); + // let _contract = Cep78HostRef::deploy(&env, args); // Should be possible to verify errors at installation time - // assert_eq!(CEP78HostRef::deploy(&env, args), Err(CEP78Error::InvalidMetadataMutability)); + // assert_eq!(Cep78HostRef::deploy(&env, args), Err(CEP78Error::InvalidMetadataMutability)); } #[test] @@ -78,7 +79,7 @@ fn should_prevent_update_for_invalid_metadata() { .metadata_mutability(MetadataMutability::Mutable) .ownership_mode(OwnershipMode::Transferable) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); contract.mint( env.get_account(0), TEST_PRETTY_721_META_DATA.to_string(), @@ -108,7 +109,7 @@ fn should_prevent_metadata_update_by_non_owner_key() { .metadata_mutability(MetadataMutability::Mutable) .ownership_mode(OwnershipMode::Transferable) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = *contract.address(); contract.mint( token_owner, @@ -148,7 +149,7 @@ fn should_allow_update_for_valid_metadata_based_on_kind( .json_schema(json_schema) .events_mode(EventsMode::CES) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); let custom_metadata = serde_json::to_string_pretty(&*TEST_CUSTOM_METADATA) @@ -259,7 +260,7 @@ fn should_update_metadata_for_custom_validated_using_token_id() { #[test] fn should_get_metadata_using_token_id() { let env = odra_test::env(); - let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let mut minting_contract = MockContractHostRef::deploy(&env, NoArgs); let contract_whitelist = vec![*minting_contract.address()]; let args = default_args_builder() .holder_mode(NFTHolderMode::Contracts) @@ -268,7 +269,7 @@ fn should_get_metadata_using_token_id() { .minting_mode(MintingMode::Acl) .acl_white_list(contract_whitelist) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); minting_contract.set_address(contract.address()); let token_id = 0u64; @@ -286,7 +287,7 @@ fn should_get_metadata_using_token_id() { #[test] fn should_get_metadata_using_token_metadata_hash() { let env = odra_test::env(); - let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let mut minting_contract = MockContractHostRef::deploy(&env, NoArgs); let contract_whitelist = vec![*minting_contract.address()]; let args = default_args_builder() .identifier_mode(NFTIdentifierMode::Hash) @@ -297,7 +298,7 @@ fn should_get_metadata_using_token_metadata_hash() { .minting_mode(MintingMode::Acl) .acl_white_list(contract_whitelist) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); minting_contract.set_address(contract.address()); assert!( @@ -317,7 +318,7 @@ fn should_get_metadata_using_token_metadata_hash() { #[test] fn should_revert_minting_token_metadata_hash_twice() { let env = odra_test::env(); - let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let mut minting_contract = MockContractHostRef::deploy(&env, NoArgs); let contract_whitelist = vec![*minting_contract.address()]; let args = default_args_builder() .identifier_mode(NFTIdentifierMode::Hash) @@ -328,7 +329,7 @@ fn should_revert_minting_token_metadata_hash_twice() { .minting_mode(MintingMode::Acl) .acl_white_list(contract_whitelist) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); minting_contract.set_address(contract.address()); assert!( contract.is_whitelisted(minting_contract.address()), @@ -351,7 +352,7 @@ fn should_revert_minting_token_metadata_hash_twice() { #[test] fn should_get_metadata_using_custom_token_hash() { let env = odra_test::env(); - let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let mut minting_contract = MockContractHostRef::deploy(&env, NoArgs); let contract_whitelist = vec![*minting_contract.address()]; let args = default_args_builder() .identifier_mode(NFTIdentifierMode::Hash) @@ -362,7 +363,7 @@ fn should_get_metadata_using_custom_token_hash() { .minting_mode(MintingMode::Acl) .acl_white_list(contract_whitelist) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); minting_contract.set_address(contract.address()); assert!( @@ -382,7 +383,7 @@ fn should_get_metadata_using_custom_token_hash() { #[test] fn should_revert_minting_custom_token_hash_identifier_twice() { let env = odra_test::env(); - let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let mut minting_contract = MockContractHostRef::deploy(&env, NoArgs); let contract_whitelist = vec![*minting_contract.address()]; let args = default_args_builder() .identifier_mode(NFTIdentifierMode::Hash) @@ -393,7 +394,7 @@ fn should_revert_minting_custom_token_hash_identifier_twice() { .minting_mode(MintingMode::Acl) .acl_white_list(contract_whitelist) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); minting_contract.set_address(contract.address()); assert!( @@ -443,7 +444,7 @@ fn should_require_json_schema_when_kind_is_custom_validated() { .nft_metadata_kind(nft_metadata_kind) .json_schema("".to_string()) .build(); - let _contract = CEP78HostRef::deploy(&env, args); + let _contract = Cep78HostRef::deploy(&env, args); /*let error = builder.get_error().expect("must have error"); support::assert_expected_error(error, 67, "json_schema is required")*/ @@ -458,7 +459,7 @@ fn should_not_require_json_schema_when_kind_is(nft_metadata_kind: NFTMetadataKin .nft_metadata_kind(nft_metadata_kind.clone()) .json_schema("".to_string()) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let original_metadata = match &nft_metadata_kind { NFTMetadataKind::CEP78 => TEST_PRETTY_CEP78_METADATA, diff --git a/modules/src/cep78/tests/mint.rs b/modules/src/cep78/tests/mint.rs index 9c6715c1..340ece7e 100644 --- a/modules/src/cep78/tests/mint.rs +++ b/modules/src/cep78/tests/mint.rs @@ -15,12 +15,13 @@ use crate::cep78::{ }, tests::{ utils::{ - self, TestContractHostRef, MALFORMED_META_DATA, TEST_COMPACT_META_DATA, + self, MALFORMED_META_DATA, TEST_COMPACT_META_DATA, TEST_PRETTY_CEP78_METADATA, TEST_PRETTY_UPDATED_CEP78_METADATA }, TEST_CUSTOM_METADATA, TEST_CUSTOM_METADATA_SCHEMA }, - token::CEP78HostRef + token::Cep78HostRef, + utils::MockContractHostRef }; use super::{default_args_builder, utils::TEST_PRETTY_721_META_DATA}; @@ -32,14 +33,14 @@ struct Metadata { token_uri: String } -fn default_token() -> (CEP78HostRef, HostEnv) { +fn default_token() -> (Cep78HostRef, HostEnv) { let env = odra_test::env(); let args = default_args_builder() .total_token_supply(2u64) .ownership_mode(OwnershipMode::Transferable) .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) .build(); - (CEP78HostRef::deploy(&env, args), env) + (Cep78HostRef::deploy(&env, args), env) } #[test] @@ -49,7 +50,7 @@ fn should_disallow_minting_when_allow_minting_is_set_to_false() { .nft_metadata_kind(NFTMetadataKind::NFT721) .allow_minting(false) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); assert_eq!( contract.try_mint( @@ -73,7 +74,7 @@ fn should_mint() { .nft_metadata_kind(NFTMetadataKind::CEP78) .events_mode(EventsMode::CES) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); assert!(contract @@ -103,8 +104,8 @@ fn mint_should_return_dictionary_key_to_callers_owned_tokens() { .allow_minting(true) .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) .build(); - let contract = CEP78HostRef::deploy(&env, args); - let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let contract = Cep78HostRef::deploy(&env, args); + let mut minting_contract = MockContractHostRef::deploy(&env, NoArgs); minting_contract.set_address(contract.address()); assert!(minting_contract @@ -253,7 +254,7 @@ fn should_allow_public_minting_with_flag_set_to_true() { let args = default_args_builder() .minting_mode(MintingMode::Public) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let account_1 = env.get_account(1); let minting_mode = contract.get_minting_mode(); @@ -279,7 +280,7 @@ fn should_disallow_public_minting_with_flag_set_to_false() { let args = default_args_builder() .minting_mode(MintingMode::Installer) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let account_1 = env.get_account(1); let metadata = TEST_PRETTY_721_META_DATA.to_string(); @@ -305,7 +306,7 @@ fn should_allow_minting_for_different_public_key_with_minting_mode_set_to_public let args = default_args_builder() .minting_mode(MintingMode::Public) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let account_1 = env.get_account(1); let account_2 = env.get_account(2); @@ -331,7 +332,7 @@ fn should_set_approval_for_all() { .ownership_mode(OwnershipMode::Transferable) .events_mode(EventsMode::CES) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let owner = env.get_account(0); contract.mint(owner, TEST_PRETTY_721_META_DATA.to_string(), Maybe::None); @@ -377,7 +378,7 @@ fn should_revoke_approval_for_all() { .ownership_mode(OwnershipMode::Transferable) .events_mode(EventsMode::CES) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let owner = env.get_account(0); contract.mint(owner, TEST_PRETTY_721_META_DATA.to_string(), Maybe::None); @@ -438,7 +439,7 @@ fn should_mint_with_valid_cep78_metadata() { let args = default_args_builder() .nft_metadata_kind(NFTMetadataKind::CEP78) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); contract.mint( env.get_account(0), @@ -461,7 +462,7 @@ fn should_mint_with_custom_metadata_validation() { .nft_metadata_kind(NFTMetadataKind::CustomValidated) .json_schema(custom_json_schema) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let metadata = serde_json::to_string(&*TEST_CUSTOM_METADATA).expect("must convert to json metadata"); @@ -484,7 +485,7 @@ fn should_mint_with_raw_metadata() { let args = default_args_builder() .nft_metadata_kind(NFTMetadataKind::Raw) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); contract.mint(env.get_account(0), "raw_string".to_string(), Maybe::None); let token_id = 0u64; @@ -499,7 +500,7 @@ fn should_mint_with_hash_identifier_mode() { let args = default_args_builder() .identifier_mode(NFTIdentifierMode::Hash) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); contract.mint( env.get_account(0), TEST_PRETTY_721_META_DATA.to_string(), @@ -528,7 +529,7 @@ fn should_fail_to_mint_when_immediate_caller_is_account_in_contract_mode() { .holder_mode(NFTHolderMode::Contracts) .whitelist_mode(WhitelistMode::Unlocked) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); assert_eq!( contract.try_mint( @@ -548,7 +549,7 @@ fn should_approve_in_hash_identifier_mode() { .metadata_mutability(MetadataMutability::Immutable) .ownership_mode(OwnershipMode::Transferable) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); contract.mint( env.get_account(0), TEST_PRETTY_721_META_DATA.to_string(), @@ -569,10 +570,12 @@ fn should_mint_without_returning_receipts_and_flat_gas_cost() { let args = default_args_builder() .identifier_mode(NFTIdentifierMode::Ordinal) .nft_metadata_kind(NFTMetadataKind::Raw) + .events_mode(EventsMode::CES) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); contract.mint(env.get_account(0), "".to_string(), Maybe::None); contract.mint(env.get_account(1), "".to_string(), Maybe::None); + contract.mint(env.get_account(2), "".to_string(), Maybe::None); let costs = env .gas_report() @@ -585,7 +588,9 @@ fn should_mint_without_returning_receipts_and_flat_gas_cost() { // In this case there is no first time allocation of a page. // Therefore the second and first mints must have equivalent gas costs. - assert_eq!(costs.first(), costs.get(1)) + if let (Some(c1), Some(c2)) = (costs.get(0), costs.get(2)) { + assert_eq!(c1, c2); + } } // A test to ensure that the page table allocation is preserved @@ -597,7 +602,7 @@ fn should_maintain_page_table_despite_invoking_register_owner() { .identifier_mode(NFTIdentifierMode::Ordinal) .nft_metadata_kind(NFTMetadataKind::Raw) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); // TODO: register_owner is not implemented yet contract.register_owner(Maybe::Some(token_owner)); @@ -624,7 +629,7 @@ fn should_prevent_mint_to_unregistered_owner() { .ownership_mode(OwnershipMode::Transferable) .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); assert_eq!( contract.try_mint(env.get_account(0), "".to_string(), Maybe::None), Err(CEP78Error::UnregisteredOwnerInMint.into()) @@ -641,7 +646,7 @@ fn should_mint_with_two_required_metadata_kind() { .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) .additional_required_metadata(vec![NFTMetadataKind::Raw]) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); // TODO: register_owner is not implemented yet let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); @@ -671,7 +676,7 @@ fn should_mint_with_one_required_one_optional_metadata_kind_without_optional() { .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) .optional_metadata(vec![NFTMetadataKind::Raw]) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); contract.mint( @@ -713,7 +718,7 @@ fn should_not_mint_with_missing_required_metadata() { .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) .additional_required_metadata(vec![NFTMetadataKind::NFT721]) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); @@ -735,7 +740,7 @@ fn should_mint_with_transfer_only_reporting() { .nft_metadata_kind(NFTMetadataKind::CEP78) .owner_reverse_lookup_mode(OwnerReverseLookupMode::TransfersOnly) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); contract.mint( token_owner, @@ -757,7 +762,7 @@ fn should_approve_all_in_hash_identifier_mode() { .nft_metadata_kind(NFTMetadataKind::CEP78) .events_mode(EventsMode::CES) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); let operator = env.get_account(1); contract.mint( @@ -791,7 +796,7 @@ fn should_approve_all_with_flat_gas_cost() { .ownership_mode(OwnershipMode::Transferable) .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); let operator = env.get_account(1); let other_operator = env.get_account(2); diff --git a/modules/src/cep78/tests/set_variables.rs b/modules/src/cep78/tests/set_variables.rs index 5c8d1fb2..b85c26cf 100644 --- a/modules/src/cep78/tests/set_variables.rs +++ b/modules/src/cep78/tests/set_variables.rs @@ -5,7 +5,7 @@ use odra::{ use crate::cep78::{ error::CEP78Error, events::VariablesSet, modalities::EventsMode, tests::default_args_builder, - token::CEP78HostRef + token::Cep78HostRef }; #[test] @@ -16,7 +16,7 @@ fn only_installer_should_be_able_to_toggle_allow_minting() { .allow_minting(false) .events_mode(EventsMode::CES) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); // Account other than installer account should not be able to change allow_minting env.set_caller(other_user); @@ -50,7 +50,7 @@ fn installer_should_be_able_to_toggle_package_operator_mode() {} fn installer_should_be_able_to_toggle_operator_burn_mode() { let env = odra_test::env(); let args = default_args_builder().events_mode(EventsMode::CES).build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); // Installer account should be able to change allow_minting assert_eq!( diff --git a/modules/src/cep78/tests/transfer.rs b/modules/src/cep78/tests/transfer.rs index 464d6cbe..cf8e0710 100644 --- a/modules/src/cep78/tests/transfer.rs +++ b/modules/src/cep78/tests/transfer.rs @@ -15,12 +15,12 @@ use crate::cep78::{ }, tests::{ default_args_builder, - utils::{TestContractHostRef, TEST_PRETTY_721_META_DATA} + utils::TEST_PRETTY_721_META_DATA }, - token::CEP78HostRef + token::Cep78HostRef, utils::{MockContractHostRef, MockTransferFilterContractHostRef} }; -use super::utils::{self, TransferFilterContractHostRef}; +use super::utils; #[test] fn should_disallow_transfer_with_minter_or_assigned_ownership_mode() { @@ -30,7 +30,7 @@ fn should_disallow_transfer_with_minter_or_assigned_ownership_mode() { .minting_mode(MintingMode::Installer) .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); @@ -67,7 +67,7 @@ fn should_transfer_token_from_sender_to_receiver() { .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) .events_mode(EventsMode::CES) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); @@ -126,7 +126,7 @@ fn approve_token_for_transfer_should_add_entry_to_approved_dictionary( .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) .events_mode(EventsMode::CES) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); @@ -186,7 +186,7 @@ fn revoke_token_for_transfer_should_remove_entry_to_approved_dictionary( .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) .events_mode(EventsMode::CES) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); @@ -245,7 +245,7 @@ fn should_disallow_approving_when_ownership_mode_is_minter_or_assigned() { .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) .events_mode(EventsMode::CES) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); contract.mint( @@ -269,7 +269,7 @@ fn should_be_able_to_transfer_token(env: HostEnv, operator: Option
) { .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) .events_mode(EventsMode::CES) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); contract.mint( @@ -328,7 +328,7 @@ fn should_disallow_same_approved_account_to_transfer_token_twice() { .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) .events_mode(EventsMode::CES) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); @@ -384,7 +384,7 @@ fn should_disallow_to_transfer_token_using_revoked_hash(env: HostEnv, operator: .ownership_mode(OwnershipMode::Transferable) .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); @@ -459,7 +459,7 @@ fn should_be_able_to_approve_with_deprecated_operator_argument() {} #[test] fn should_transfer_between_contract_to_account() { let env = odra_test::env(); - let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let mut minting_contract = MockContractHostRef::deploy(&env, NoArgs); let contract_whitelist = vec![*minting_contract.address()]; let args = default_args_builder() .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) @@ -470,7 +470,7 @@ fn should_transfer_between_contract_to_account() { .minting_mode(MintingMode::Acl) .acl_white_list(contract_whitelist) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); minting_contract.set_address(contract.address()); assert!( @@ -502,7 +502,7 @@ fn should_prevent_transfer_when_caller_is_not_owner() { .ownership_mode(OwnershipMode::Transferable) .holder_mode(NFTHolderMode::Accounts) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); let unauthorized_user = env.get_account(10); @@ -537,7 +537,7 @@ fn should_transfer_token_in_hash_identifier_mode() { .ownership_mode(OwnershipMode::Transferable) .metadata_mutability(MetadataMutability::Immutable) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); let new_owner = env.get_account(1); @@ -559,13 +559,13 @@ fn should_transfer_token_in_hash_identifier_mode() { #[test] fn should_not_allow_non_approved_contract_to_transfer() { let env = odra_test::env(); - let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let mut minting_contract = MockContractHostRef::deploy(&env, NoArgs); let args = default_args_builder() .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) .holder_mode(NFTHolderMode::Mixed) .ownership_mode(OwnershipMode::Transferable) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); minting_contract.set_address(contract.address()); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); @@ -602,7 +602,7 @@ fn transfer_should_correctly_track_page_table_entries() { .nft_metadata_kind(NFTMetadataKind::Raw) .ownership_mode(OwnershipMode::Transferable) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); let new_owner = env.get_account(1); @@ -635,7 +635,7 @@ fn should_prevent_transfer_to_unregistered_owner() { .identifier_mode(NFTIdentifierMode::Ordinal) .metadata_mutability(MetadataMutability::Immutable) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); contract.mint(token_owner, "".to_string(), Maybe::None); @@ -661,7 +661,7 @@ fn should_transfer_token_from_sender_to_receiver_with_transfer_only_reporting() .owner_reverse_lookup_mode(OwnerReverseLookupMode::TransfersOnly) .ownership_mode(OwnershipMode::Transferable) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); env.set_caller(token_owner); @@ -706,7 +706,7 @@ fn disallow_owner_to_approve_itself() { let args = default_args_builder() .ownership_mode(OwnershipMode::Transferable) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); contract.mint( @@ -727,7 +727,7 @@ fn disallow_operator_to_approve_itself() { let args = default_args_builder() .ownership_mode(OwnershipMode::Transferable) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); contract.mint( @@ -753,7 +753,7 @@ fn disallow_owner_to_approve_for_all_itself() { let args = default_args_builder() .ownership_mode(OwnershipMode::Transferable) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); contract.mint( @@ -772,14 +772,13 @@ fn disallow_owner_to_approve_for_all_itself() { fn check_transfers_with_transfer_filter_contract_modes() { let env = odra_test::env(); - let mut transfer_filter_contract: TransferFilterContractHostRef = - TransferFilterContractHostRef::deploy(&env, NoArgs); + let mut transfer_filter_contract = MockTransferFilterContractHostRef::deploy(&env, NoArgs); transfer_filter_contract.set_return_value(TransferFilterContractResult::DenyTransfer as u8); let args = default_args_builder() .ownership_mode(OwnershipMode::Transferable) .transfer_filter_contract_contract_key(*transfer_filter_contract.address()) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); contract.mint( @@ -846,12 +845,12 @@ fn check_transfers_with_transfer_filter_contract_modes() { #[test] fn should_disallow_transfer_from_contract_with_package_operator_mode_without_operator() { let env = odra_test::env(); - let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let mut minting_contract = MockContractHostRef::deploy(&env, NoArgs); let args = default_args_builder() .ownership_mode(OwnershipMode::Transferable) .holder_mode(NFTHolderMode::Mixed) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); minting_contract.set_address(contract.address()); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); @@ -879,12 +878,12 @@ fn should_disallow_transfer_from_contract_without_package_operator_mode_with_pac #[test] fn should_allow_transfer_from_contract_with_package_operator_mode_with_operator() { let env = odra_test::env(); - let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let mut minting_contract = MockContractHostRef::deploy(&env, NoArgs); let args = default_args_builder() .ownership_mode(OwnershipMode::Transferable) .holder_mode(NFTHolderMode::Mixed) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); minting_contract.set_address(contract.address()); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); @@ -914,12 +913,12 @@ fn should_disallow_package_operator_to_approve_without_package_operator_mode() { #[test] fn should_allow_package_operator_to_approve_with_package_operator_mode() { let env = odra_test::env(); - let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let mut minting_contract = MockContractHostRef::deploy(&env, NoArgs); let args = default_args_builder() .ownership_mode(OwnershipMode::Transferable) .holder_mode(NFTHolderMode::Mixed) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); minting_contract.set_address(contract.address()); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); @@ -952,12 +951,12 @@ fn should_allow_package_operator_to_approve_with_package_operator_mode() { #[test] fn should_allow_account_to_approve_spender_with_package_operator() { let env = odra_test::env(); - let minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let minting_contract = MockContractHostRef::deploy(&env, NoArgs); let args = default_args_builder() .ownership_mode(OwnershipMode::Transferable) .holder_mode(NFTHolderMode::Mixed) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); contract.mint( @@ -989,12 +988,12 @@ fn should_allow_account_to_approve_spender_with_package_operator() { #[test] fn should_allow_package_operator_to_revoke_with_package_operator_mode() { let env = odra_test::env(); - let mut minting_contract = TestContractHostRef::deploy(&env, NoArgs); + let mut minting_contract = MockContractHostRef::deploy(&env, NoArgs); let args = default_args_builder() .ownership_mode(OwnershipMode::Transferable) .holder_mode(NFTHolderMode::Mixed) .build(); - let mut contract = CEP78HostRef::deploy(&env, args); + let mut contract = Cep78HostRef::deploy(&env, args); minting_contract.set_address(contract.address()); let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); diff --git a/modules/src/cep78/tests/utils.rs b/modules/src/cep78/tests/utils.rs index 378bad80..cade10f4 100644 --- a/modules/src/cep78/tests/utils.rs +++ b/modules/src/cep78/tests/utils.rs @@ -3,14 +3,14 @@ use crate::cep78::{ BurnMode, EventsMode, MetadataMutability, MintingMode, NFTHolderMode, NFTIdentifierMode, NFTKind, NFTMetadataKind, OwnerReverseLookupMode, OwnershipMode, WhitelistMode }, - token::CEP78InitArgs + token::Cep78InitArgs }; use blake2::{digest::VariableOutput, Blake2bVar}; use odra::{ args::Maybe, - casper_types::{URef, BLAKE2B_DIGEST_LENGTH}, + casper_types::BLAKE2B_DIGEST_LENGTH, prelude::*, - Address, ContractRef, Var + Address }; use std::io::Write; @@ -151,8 +151,8 @@ impl InitArgsBuilder { self } - pub fn build(self) -> CEP78InitArgs { - CEP78InitArgs { + pub fn build(self) -> Cep78InitArgs { + Cep78InitArgs { collection_name: self.collection_name, collection_symbol: self.collection_symbol, total_token_supply: self.total_token_supply, @@ -207,151 +207,6 @@ pub const MALFORMED_META_DATA: &str = r#"{ "token_uri": "https://www.barfoo.com" }"#; -#[odra::module] -struct DummyContract; - -#[odra::module] -impl DummyContract {} - -#[odra::module] -struct TestContract { - nft_contract: Var
-} - -#[odra::module] -impl TestContract { - pub fn set_address(&mut self, nft_contract: &Address) { - self.nft_contract.set(*nft_contract); - } - - pub fn mint( - &mut self, - token_metadata: String, - is_reverse_lookup_enabled: bool - ) -> (String, Address, String) { - let nft_contract_address = self.nft_contract.get().unwrap(); - if is_reverse_lookup_enabled { - NftContractContractRef::new(self.env(), nft_contract_address) - .register_owner(Maybe::Some(self.env().self_address())); - } - - NftContractContractRef::new(self.env(), nft_contract_address).mint( - self.env().self_address(), - token_metadata, - Maybe::None - ) - } - - pub fn mint_with_hash( - &mut self, - token_metadata: String, - token_hash: String - ) -> (String, Address, String) { - let nft_contract_address = self.nft_contract.get().unwrap(); - NftContractContractRef::new(self.env(), nft_contract_address).mint( - self.env().self_address(), - token_metadata, - Maybe::Some(token_hash) - ) - } - - pub fn burn(&mut self, token_id: u64) { - let nft_contract_address = self.nft_contract.get().unwrap(); - NftContractContractRef::new(self.env(), nft_contract_address) - .burn(Maybe::Some(token_id), Maybe::None) - } - - pub fn mint_for( - &mut self, - token_owner: Address, - token_metadata: String - ) -> (String, Address, String) { - let nft_contract_address = self.nft_contract.get().unwrap(); - NftContractContractRef::new(self.env(), nft_contract_address).mint( - token_owner, - token_metadata, - Maybe::None - ) - } - - pub fn transfer(&mut self, token_id: u64, target: Address) -> (String, Address) { - let address = self.env().self_address(); - let nft_contract_address = self.nft_contract.get().unwrap(); - NftContractContractRef::new(self.env(), nft_contract_address).transfer( - Maybe::Some(token_id), - Maybe::None, - address, - target - ) - } - pub fn transfer_from( - &mut self, - token_id: u64, - source: Address, - target: Address - ) -> (String, Address) { - let nft_contract_address = self.nft_contract.get().unwrap(); - NftContractContractRef::new(self.env(), nft_contract_address).transfer( - Maybe::Some(token_id), - Maybe::None, - source, - target - ) - } - - pub fn approve(&mut self, spender: Address, token_id: u64) { - let nft_contract_address = self.nft_contract.get().unwrap(); - NftContractContractRef::new(self.env(), nft_contract_address).approve( - spender, - Maybe::Some(token_id), - Maybe::None - ) - } - - pub fn revoke(&mut self, token_id: u64) { - let nft_contract_address = self.nft_contract.get().unwrap(); - NftContractContractRef::new(self.env(), nft_contract_address) - .revoke(Maybe::Some(token_id), Maybe::None) - } -} - -#[odra::external_contract] -trait NftContract { - fn mint( - &mut self, - token_owner: Address, - token_metadata: String, - token_hash: Maybe - ) -> (String, Address, String); - fn burn(&mut self, token_id: Maybe, token_hash: Maybe); - fn register_owner(&mut self, token_owner: Maybe
) -> (String, URef); - fn transfer( - &mut self, - token_id: Maybe, - token_hash: Maybe, - source: Address, - target: Address - ) -> (String, Address); - fn approve(&mut self, spender: Address, token_id: Maybe, token_hash: Maybe); - fn revoke(&mut self, token_id: Maybe, token_hash: Maybe); -} - -#[odra::module] -pub struct TransferFilterContract { - value: Var -} - -#[odra::module] -impl TransferFilterContract { - pub fn set_return_value(&mut self, return_value: u8) { - self.value.set(return_value); - } - - pub fn can_transfer(&self) -> u8 { - self.value.get_or_default() - } -} - pub(crate) fn create_blake2b_hash>(data: T) -> [u8; BLAKE2B_DIGEST_LENGTH] { let mut result = [0u8; 32]; let mut hasher = ::new(32).expect("should create hasher"); diff --git a/modules/src/cep78/token.rs b/modules/src/cep78/token.rs index e6a60cf4..b9b86c15 100644 --- a/modules/src/cep78/token.rs +++ b/modules/src/cep78/token.rs @@ -18,7 +18,7 @@ use super::{ }; use odra::{ args::Maybe, - casper_types::{bytesrepr::ToBytes, URef}, + casper_types::bytesrepr::ToBytes, prelude::*, Address, OdraError, SubModule, UnwrapOrRevert, Var }; @@ -44,7 +44,7 @@ type TransferReceipt = (String, Address); /// - `nft_metadata_kind`: The kind of metadata associated with the NFTs in the collection. See [NFTMetadataKind] for more details. /// - `metadata_mutability`: The mutability of the metadata associated with the NFTs in the collection. See [MetadataMutability] for more details. #[odra::module] -pub struct CEP78 { +pub struct Cep78 { data: SubModule, metadata: SubModule, settings: SubModule, @@ -54,7 +54,7 @@ pub struct CEP78 { } #[odra::module] -impl CEP78 { +impl Cep78 { /// Initializes the module. pub fn init( &mut self, @@ -481,7 +481,7 @@ impl CEP78 { /// resulting in more stable gas costs when minting and transferring /// Note: This entrypoint MUST be invoked if the reverse lookup is enabled /// in order to own NFTs. - pub fn register_owner(&mut self, token_owner: Maybe
) -> (String, URef) { + pub fn register_owner(&mut self, token_owner: Maybe
) -> String { let ownership_mode = self.ownership_mode(); self.reverse_lookup .register_owner(token_owner, ownership_mode) @@ -550,7 +550,7 @@ impl CEP78 { } } -impl CEP78 { +impl Cep78 { #[inline] fn caller(&self) -> Address { self.__env.caller() @@ -712,4 +712,4 @@ pub trait TransferFilterContract { target_key: Address, token_id: TokenIdentifier ) -> TransferFilterContractResult; -} +} \ No newline at end of file diff --git a/modules/src/cep78/utils.rs b/modules/src/cep78/utils.rs new file mode 100644 index 00000000..f4051383 --- /dev/null +++ b/modules/src/cep78/utils.rs @@ -0,0 +1,148 @@ +#![allow(dead_code)] +use odra::{args::Maybe, prelude::*, Address, Var}; + +#[odra::module] +struct MockDummyContract; + +#[odra::module] +impl MockDummyContract {} + + +#[odra::module] +pub struct MockTransferFilterContract { + value: Var +} + +#[odra::module] +impl MockTransferFilterContract { + pub fn set_return_value(&mut self, return_value: u8) { + self.value.set(return_value); + } + + pub fn can_transfer(&self) -> u8 { + self.value.get_or_default() + } +} + +#[odra::module] +struct MockContract { + nft_contract: Var
+} + +#[odra::module] +impl MockContract { + pub fn set_address(&mut self, nft_contract: &Address) { + self.nft_contract.set(*nft_contract); + } + + pub fn mint( + &mut self, + token_metadata: String, + is_reverse_lookup_enabled: bool + ) -> (String, Address, String) { + let nft_contract_address = self.nft_contract.get().unwrap(); + if is_reverse_lookup_enabled { + NftContractContractRef::new(self.env(), nft_contract_address) + .register_owner(Maybe::Some(self.env().self_address())); + } + + NftContractContractRef::new(self.env(), nft_contract_address).mint( + self.env().self_address(), + token_metadata, + Maybe::None + ) + } + + pub fn mint_with_hash( + &mut self, + token_metadata: String, + token_hash: String + ) -> (String, Address, String) { + let nft_contract_address = self.nft_contract.get().unwrap(); + NftContractContractRef::new(self.env(), nft_contract_address).mint( + self.env().self_address(), + token_metadata, + Maybe::Some(token_hash) + ) + } + + pub fn burn(&mut self, token_id: u64) { + let nft_contract_address = self.nft_contract.get().unwrap(); + NftContractContractRef::new(self.env(), nft_contract_address) + .burn(Maybe::Some(token_id), Maybe::None) + } + + pub fn mint_for( + &mut self, + token_owner: Address, + token_metadata: String + ) -> (String, Address, String) { + let nft_contract_address = self.nft_contract.get().unwrap(); + NftContractContractRef::new(self.env(), nft_contract_address).mint( + token_owner, + token_metadata, + Maybe::None + ) + } + + pub fn transfer(&mut self, token_id: u64, target: Address) -> (String, Address) { + let address = self.env().self_address(); + let nft_contract_address = self.nft_contract.get().unwrap(); + NftContractContractRef::new(self.env(), nft_contract_address).transfer( + Maybe::Some(token_id), + Maybe::None, + address, + target + ) + } + pub fn transfer_from( + &mut self, + token_id: u64, + source: Address, + target: Address + ) -> (String, Address) { + let nft_contract_address = self.nft_contract.get().unwrap(); + NftContractContractRef::new(self.env(), nft_contract_address).transfer( + Maybe::Some(token_id), + Maybe::None, + source, + target + ) + } + + pub fn approve(&mut self, spender: Address, token_id: u64) { + let nft_contract_address = self.nft_contract.get().unwrap(); + NftContractContractRef::new(self.env(), nft_contract_address).approve( + spender, + Maybe::Some(token_id), + Maybe::None + ) + } + + pub fn revoke(&mut self, token_id: u64) { + let nft_contract_address = self.nft_contract.get().unwrap(); + NftContractContractRef::new(self.env(), nft_contract_address) + .revoke(Maybe::Some(token_id), Maybe::None) + } +} + +#[odra::external_contract] +trait NftContract { + fn mint( + &mut self, + token_owner: Address, + token_metadata: String, + token_hash: Maybe + ) -> (String, Address, String); + fn burn(&mut self, token_id: Maybe, token_hash: Maybe); + fn register_owner(&mut self, token_owner: Maybe
) -> String; + fn transfer( + &mut self, + token_id: Maybe, + token_hash: Maybe, + source: Address, + target: Address + ) -> (String, Address); + fn approve(&mut self, spender: Address, token_id: Maybe, token_hash: Maybe); + fn revoke(&mut self, token_id: Maybe, token_hash: Maybe); +} From 622ac03697d3d6ace068861c9c90fa17fc6d4a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Thu, 25 Apr 2024 13:58:59 +0200 Subject: [PATCH 20/38] wip --- modules/src/cep78/tests/acl.rs | 10 +++++++--- modules/src/cep78/token.rs | 6 ++++++ modules/src/cep78/whitelist.rs | 5 +++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/modules/src/cep78/tests/acl.rs b/modules/src/cep78/tests/acl.rs index 3db9ec87..3a3c64e6 100644 --- a/modules/src/cep78/tests/acl.rs +++ b/modules/src/cep78/tests/acl.rs @@ -37,7 +37,7 @@ fn should_install_with_acl_whitelist() { fn should_install_with_deprecated_contract_whitelist() {} #[test] -#[ignore = "Can't assert init errors in Odra"] +// #[ignore = "Can't assert init errors in Odra"] fn should_not_install_with_minting_mode_not_acl_if_acl_whitelist_provided() { let env = odra_test::env(); @@ -51,8 +51,12 @@ fn should_not_install_with_minting_mode_not_acl_if_acl_whitelist_provided() { .acl_white_list(contract_whitelist) .build(); - Cep78HostRef::deploy(&env, args); - + let init_result = Cep78HostRef::try_deploy(&env, args); + assert_eq!( + init_result.err(), + Some(CEP78Error::InvalidMintingMode.into()), + "should disallow installing with minting mode not acl if acl whitelist provided" + ); // builder.exec(install_request).expect_failure(); // let actual_error = builder.get_error().expect("must have error"); diff --git a/modules/src/cep78/token.rs b/modules/src/cep78/token.rs index b9b86c15..9dc5236b 100644 --- a/modules/src/cep78/token.rs +++ b/modules/src/cep78/token.rs @@ -118,6 +118,12 @@ impl Cep78 { json_schema ); + // Revert if minting mode is not ACL and acl list is not empty + if MintingMode::Acl != self.settings.minting_mode() && !self.whitelist.is_empty() { + self.revert(CEP78Error::InvalidMintingMode) + } + + if nft_identifier_mode == NFTIdentifierMode::Hash && metadata_mutability == MetadataMutability::Mutable { diff --git a/modules/src/cep78/whitelist.rs b/modules/src/cep78/whitelist.rs index a39a63ab..58d43d97 100644 --- a/modules/src/cep78/whitelist.rs +++ b/modules/src/cep78/whitelist.rs @@ -27,6 +27,11 @@ impl ACLWhitelist { self.addresses.get_or_default().contains(address) } + #[inline] + pub fn is_empty(&self) -> bool { + self.addresses.get_or_default().is_empty() + } + pub fn update(&mut self, new_addresses: Maybe>) { let new_addresses = new_addresses.unwrap_or_default(); if !new_addresses.is_empty() { From 38659821aea1061bc8bd3ef0b7bb6ce238c655eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Thu, 25 Apr 2024 14:03:42 +0200 Subject: [PATCH 21/38] Refactor contract deployment functions to return Result types --- core/src/host.rs | 36 +++++++++++++++------ odra-casper/livenet-env/src/livenet_host.rs | 4 +-- odra-casper/test-vm/src/casper_host.rs | 21 +++++++++--- odra-casper/test-vm/src/vm/casper_vm.rs | 22 ++++++++++--- odra-vm/src/odra_vm_host.rs | 12 +++---- 5 files changed, 68 insertions(+), 27 deletions(-) diff --git a/core/src/host.rs b/core/src/host.rs index 2035427c..59ae9d5a 100644 --- a/core/src/host.rs +++ b/core/src/host.rs @@ -55,7 +55,7 @@ pub trait EntryPointsCallerProvider { /// on a virtual machine or on a real blockchain. /// /// The `Deployer` trait provides a simple way to deploy a contract. -pub trait Deployer { +pub trait Deployer: Sized { /// Deploys a contract with given init args. /// /// If the init args are provided, the contract is deployed and initialized @@ -64,6 +64,11 @@ pub trait Deployer { /// /// Returns a host reference to the deployed contract. fn deploy(env: &HostEnv, init_args: T) -> Self; + + /// Tries to deploy a contract with given init args. + /// + /// Similar to `deploy`, but returns a result instead of panicking. + fn try_deploy(env: &HostEnv, init_args: T) -> OdraResult; } /// A type which can be used as initialization arguments for a contract. @@ -95,14 +100,25 @@ impl From for RuntimeArgs { impl Deployer for R { fn deploy(env: &HostEnv, init_args: T) -> Self { + let contract_ident = R::ident(); + match Self::try_deploy(env, init_args) { + Ok(contract) => contract, + Err(OdraError::VmError(VmError::MissingArg)) => { + core::panic!("Invalid init args for contract {}.", contract_ident) + } + Err(_) => core::panic!("Contract init failed") + } + } + + fn try_deploy(env: &HostEnv, init_args: T) -> OdraResult { let contract_ident = R::ident(); if !T::validate(&contract_ident) { - core::panic!("Invalid init args for contract {}.", contract_ident); + return Err(OdraError::VmError(VmError::MissingArg)); } let caller = R::entry_points_caller(env); - let address = env.new_contract(&contract_ident, init_args.into(), caller); - R::new(address, env.clone()) + let address = env.new_contract(&contract_ident, init_args.into(), caller)?; + Ok(R::new(address, env.clone())) } } @@ -155,7 +171,7 @@ pub trait HostContext { name: &str, init_args: RuntimeArgs, entry_points_caller: EntryPointsCaller - ) -> Address; + ) -> OdraResult
; /// Registers an existing contract with the specified address and entry points caller. fn register_contract(&self, address: Address, entry_points_caller: EntryPointsCaller); @@ -223,7 +239,7 @@ impl HostEnv { name: &str, init_args: RuntimeArgs, entry_points_caller: EntryPointsCaller - ) -> Address { + ) -> OdraResult
{ let backend = self.backend.borrow(); let mut init_args = init_args; @@ -238,11 +254,11 @@ impl HostEnv { ) .unwrap(); - let deployed_contract = backend.new_contract(name, init_args, entry_points_caller); + let deployed_contract = backend.new_contract(name, init_args, entry_points_caller)?; self.deployed_contracts.borrow_mut().push(deployed_contract); self.events_count.borrow_mut().insert(deployed_contract, 0); - deployed_contract + Ok(deployed_contract) } /// Registers an existing contract with the specified address and entry points caller. @@ -532,7 +548,7 @@ mod test { let mut ctx = MockHostContext::new(); ctx.expect_new_contract() - .returning(|_, _, _| Address::Account(AccountHash::new([0; 32]))); + .returning(|_, _, _| Ok(Address::Account(AccountHash::new([0; 32])))); let env = HostEnv::new(Rc::new(RefCell::new(ctx))); ::deploy(&env, NoArgs); } @@ -581,7 +597,7 @@ mod test { fn test_host_env() { let mut ctx = MockHostContext::new(); ctx.expect_new_contract() - .returning(|_, _, _| Address::Account(AccountHash::new([0; 32]))); + .returning(|_, _, _| Ok(Address::Account(AccountHash::new([0; 32])))); ctx.expect_caller() .returning(|| Address::Account(AccountHash::new([2; 32]))) .times(1); diff --git a/odra-casper/livenet-env/src/livenet_host.rs b/odra-casper/livenet-env/src/livenet_host.rs index 8331c33a..5ae10ec9 100644 --- a/odra-casper/livenet-env/src/livenet_host.rs +++ b/odra-casper/livenet-env/src/livenet_host.rs @@ -143,10 +143,10 @@ impl HostContext for LivenetHost { name: &str, init_args: RuntimeArgs, entry_points_caller: EntryPointsCaller - ) -> Address { + ) -> Result { let address = self.casper_client.borrow_mut().deploy_wasm(name, init_args); self.register_contract(address, entry_points_caller); - address + Ok(address) } fn contract_env(&self) -> ContractEnv { diff --git a/odra-casper/test-vm/src/casper_host.rs b/odra-casper/test-vm/src/casper_host.rs index e386754b..24088c7d 100644 --- a/odra-casper/test-vm/src/casper_host.rs +++ b/odra-casper/test-vm/src/casper_host.rs @@ -96,10 +96,23 @@ impl HostContext for CasperHost { name: &str, init_args: RuntimeArgs, entry_points_caller: EntryPointsCaller - ) -> Address { - self.vm - .borrow_mut() - .new_contract(name, init_args, entry_points_caller) + ) -> OdraResult
{ + let mut opt_result: Option
= None; + let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + opt_result = Some(self.vm.borrow_mut().new_contract( + name, + init_args, + entry_points_caller + )); + })); + + match opt_result { + Some(result) => Ok(result), + None => { + let error = self.vm.borrow().error(); + Err(error.unwrap_or(OdraError::VmError(VmError::Panic))) + } + } } fn register_contract(&self, address: Address, entry_points_caller: EntryPointsCaller) { diff --git a/odra-casper/test-vm/src/vm/casper_vm.rs b/odra-casper/test-vm/src/vm/casper_vm.rs index 7648a665..44a0fb31 100644 --- a/odra-casper/test-vm/src/vm/casper_vm.rs +++ b/odra-casper/test-vm/src/vm/casper_vm.rs @@ -219,9 +219,16 @@ impl CasperVm { .into_t() .unwrap(); - self.deploy_contract(&wasm_path, &init_args); - let contract_package_hash = self.contract_package_hash_from_name(&package_hash_key_name); - contract_package_hash.try_into().unwrap() + let result = self.deploy_contract(&wasm_path, &init_args); + if let Some(error) = result { + let odra_error = parse_error(error.clone()); + self.error = Some(odra_error.clone()); + panic!("Revert: Contract deploy failed {:?}", error); + } else { + let contract_package_hash = + self.contract_package_hash_from_name(&package_hash_key_name); + contract_package_hash.try_into().unwrap() + } } /// Create a new instance with predefined accounts. @@ -399,7 +406,11 @@ impl CasperVm { } } - fn deploy_contract(&mut self, wasm_path: &str, args: &RuntimeArgs) { + fn deploy_contract( + &mut self, + wasm_path: &str, + args: &RuntimeArgs + ) -> Option { self.error = None; let session_code = PathBuf::from(wasm_path); @@ -414,12 +425,13 @@ impl CasperVm { let execute_request = ExecuteRequestBuilder::from_deploy_item(deploy_item) .with_block_time(self.block_time) .build(); - self.context.exec(execute_request).commit().expect_success(); + let result = self.context.exec(execute_request).commit(); self.collect_gas(); self.gas_report.push(DeployReport::WasmDeploy { gas: self.last_call_contract_gas_cost(), file_name: wasm_path.to_string() }); + self.context.get_error() } } diff --git a/odra-vm/src/odra_vm_host.rs b/odra-vm/src/odra_vm_host.rs index bcd967fe..c4109e13 100644 --- a/odra-vm/src/odra_vm_host.rs +++ b/odra-vm/src/odra_vm_host.rs @@ -2,11 +2,11 @@ use crate::odra_vm_contract_env::OdraVmContractEnv; use crate::OdraVm; use odra_core::casper_types::{bytesrepr::Bytes, PublicKey, RuntimeArgs, U512}; use odra_core::entry_point_callback::EntryPointsCaller; -use odra_core::prelude::*; use odra_core::{ host::{HostContext, HostEnv}, CallDef, ContractContext, ContractEnv }; +use odra_core::{prelude::*, OdraResult}; use odra_core::{Address, OdraError, VmError}; use odra_core::{EventError, GasReport}; @@ -54,7 +54,7 @@ impl HostContext for OdraVmHost { address: &Address, call_def: CallDef, _use_proxy: bool - ) -> Result { + ) -> OdraResult { let mut opt_result: Option = None; let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { opt_result = Some(self.vm.borrow().call_contract(*address, call_def)); @@ -74,7 +74,7 @@ impl HostContext for OdraVmHost { name: &str, init_args: RuntimeArgs, entry_points_caller: EntryPointsCaller - ) -> Address { + ) -> OdraResult
{ let address = self .vm .borrow() @@ -85,14 +85,14 @@ impl HostContext for OdraVmHost { .iter() .any(|ep| ep.name == "init") { - let _ = self.call_contract( + self.call_contract( &address, CallDef::new(String::from("init"), true, init_args), false - ); + )?; } - address + Ok(address) } fn register_contract(&self, address: Address, entry_points_caller: EntryPointsCaller) { From f9f740edced90ca75da8e6aa1bac4594ae59f2cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Thu, 25 Apr 2024 15:31:54 +0200 Subject: [PATCH 22/38] Add missing installation time checks and tests --- modules/src/cep78/metadata.rs | 14 +- modules/src/cep78/mod.rs | 2 +- modules/src/cep78/tests/acl.rs | 11 +- modules/src/cep78/tests/burn.rs | 5 +- modules/src/cep78/tests/installer.rs | 293 ++++++++------------------- modules/src/cep78/tests/metadata.rs | 30 +-- modules/src/cep78/tests/mint.rs | 8 +- modules/src/cep78/tests/transfer.rs | 8 +- modules/src/cep78/tests/utils.rs | 7 +- modules/src/cep78/token.rs | 95 ++++++--- modules/src/cep78/utils.rs | 1 - modules/src/cep78/whitelist.rs | 5 - 12 files changed, 183 insertions(+), 296 deletions(-) diff --git a/modules/src/cep78/metadata.rs b/modules/src/cep78/metadata.rs index 78576fc5..141f39c6 100644 --- a/modules/src/cep78/metadata.rs +++ b/modules/src/cep78/metadata.rs @@ -27,7 +27,7 @@ impl Metadata { optional_metadata: Maybe>, metadata_mutability: MetadataMutability, identifier_mode: NFTIdentifierMode, - json_schema: Maybe + json_schema: String ) { let mut requirements = MetadataRequirement::new(); for optional in optional_metadata.unwrap_or_default() { @@ -38,10 +38,20 @@ impl Metadata { } requirements.insert(base_metadata_kind, Requirement::Required); + // Attempt to parse the provided schema if the `CustomValidated` metadata kind is required or + // optional and fail installation if the schema cannot be parsed. + if let Some(req) = requirements.get(&NFTMetadataKind::CustomValidated) { + if req == &Requirement::Required || req == &Requirement::Optional { + serde_json_wasm::from_str::(&json_schema) + .map_err(|_| CEP78Error::InvalidJsonSchema) + .unwrap_or_revert(&self.env()); + } + } + self.requirements.set(requirements); self.identifier_mode.set(identifier_mode); self.mutability.set(metadata_mutability); - self.json_schema.set(json_schema.unwrap_or_default()); + self.json_schema.set(json_schema); } pub fn get_requirements(&self) -> MetadataRequirement { diff --git a/modules/src/cep78/mod.rs b/modules/src/cep78/mod.rs index ce0cfa78..c89f0516 100644 --- a/modules/src/cep78/mod.rs +++ b/modules/src/cep78/mod.rs @@ -11,5 +11,5 @@ mod settings; #[cfg(test)] mod tests; pub mod token; +mod utils; mod whitelist; -mod utils; \ No newline at end of file diff --git a/modules/src/cep78/tests/acl.rs b/modules/src/cep78/tests/acl.rs index 3a3c64e6..c50b0492 100644 --- a/modules/src/cep78/tests/acl.rs +++ b/modules/src/cep78/tests/acl.rs @@ -37,7 +37,6 @@ fn should_install_with_acl_whitelist() { fn should_install_with_deprecated_contract_whitelist() {} #[test] -// #[ignore = "Can't assert init errors in Odra"] fn should_not_install_with_minting_mode_not_acl_if_acl_whitelist_provided() { let env = odra_test::env(); @@ -56,15 +55,7 @@ fn should_not_install_with_minting_mode_not_acl_if_acl_whitelist_provided() { init_result.err(), Some(CEP78Error::InvalidMintingMode.into()), "should disallow installing with minting mode not acl if acl whitelist provided" - ); - // builder.exec(install_request).expect_failure(); - - // let actual_error = builder.get_error().expect("must have error"); - // support::assert_expected_error( - // actual_error, - // 38u16, - // "should disallow installing without acl minting mode if non empty acl list", - // ); + ); } fn should_allow_installation_of_contract_with_empty_locked_whitelist_in_public_mode_with_holder_mode( diff --git a/modules/src/cep78/tests/burn.rs b/modules/src/cep78/tests/burn.rs index 667990fa..f6c642bf 100644 --- a/modules/src/cep78/tests/burn.rs +++ b/modules/src/cep78/tests/burn.rs @@ -12,10 +12,7 @@ use crate::cep78::{ BurnMode, EventsMode, MetadataMutability, MintingMode, NFTHolderMode, NFTIdentifierMode, OwnerReverseLookupMode, OwnershipMode, TokenIdentifier }, - tests::{ - default_args_builder, - utils - }, + tests::{default_args_builder, utils}, token::Cep78HostRef, utils::MockContractHostRef }; diff --git a/modules/src/cep78/tests/installer.rs b/modules/src/cep78/tests/installer.rs index d73aad0b..6e00e38d 100644 --- a/modules/src/cep78/tests/installer.rs +++ b/modules/src/cep78/tests/installer.rs @@ -1,9 +1,13 @@ -use odra::host::Deployer; +use odra::host::{Deployer, HostRef, NoArgs}; use crate::cep78::{ - modalities::MintingMode, + error::CEP78Error, + modalities::{ + MintingMode, NFTHolderMode, OwnerReverseLookupMode, OwnershipMode, WhitelistMode + }, tests::{default_args_builder, COLLECTION_NAME, COLLECTION_SYMBOL}, - token::Cep78HostRef + token::Cep78HostRef, + utils::MockDummyContractHostRef }; #[test] @@ -24,24 +28,6 @@ fn should_install_contract() { assert!(contract.is_minting_allowed()); assert_eq!(contract.get_minting_mode(), MintingMode::Installer); assert_eq!(contract.get_number_of_minted_tokens(), 0u64); - - // Expects Schemas to be registerd. - // let expected_schemas = Schemas::new() - // .with::() - // .with::() - // .with::() - // .with::() - // .with::() - // .with::() - // .with::() - // .with::() - // .with::(); - // let actual_schemas: Schemas = support::query_stored_value( - // &builder, - // nft_contract_key, - // vec![casper_event_standard::EVENTS_SCHEMA.to_string()], - // ); - // assert_eq!(actual_schemas, expected_schemas, "Schemas mismatch."); } #[test] @@ -64,96 +50,54 @@ fn should_reject_invalid_collection_name() {} #[ignore = "Odra interface does not allow to pass a wrong type"] fn should_reject_invalid_collection_symbol() {} -/* #[test] -fn should_reject_non_numerical_total_token_supply_value() { - let install_request_builder = - InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_invalid_total_token_supply( - CLValue::from_t::("".to_string()).expect("expected CLValue"), - ); - support::assert_expected_invalid_installer_request( - install_request_builder, - 26, - "should reject installation when given an invalid total supply value", - ); -} +#[ignore = "Odra interface does not allow to pass a wrong type"] +fn should_reject_non_numerical_total_token_supply_value() {} #[test] fn should_install_with_contract_holder_mode() { - let mut builder = InMemoryWasmTestBuilder::default(); - builder - .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) - .commit(); - - let contract_whitelist = vec![Key::from(ContractHash::default())]; - - let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_holder_mode(NFTHolderMode::Contracts) - .with_whitelist_mode(WhitelistMode::Unlocked) - .with_reporting_mode(OwnerReverseLookupMode::NoLookUp) - .with_minting_mode(MintingMode::Acl) - .with_acl_whitelist(contract_whitelist); - - builder - .exec(install_request.build()) - .expect_success() - .commit(); - - let nft_contract_key: Key = get_nft_contract_hash(&builder).into(); - - let actual_holder_mode: u8 = support::query_stored_value( - &builder, - nft_contract_key, - vec![ARG_HOLDER_MODE.to_string()], - ); + let env = odra_test::env(); + let whitelisted_contract = MockDummyContractHostRef::deploy(&env, NoArgs); + let contract_whitelist = vec![whitelisted_contract.address().clone()]; + let args = default_args_builder() + .holder_mode(NFTHolderMode::Contracts) + .whitelist_mode(WhitelistMode::Unlocked) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::NoLookUp) + .minting_mode(MintingMode::Acl) + .acl_white_list(contract_whitelist) + .build(); + let contract = Cep78HostRef::deploy(&env, args); assert_eq!( - actual_holder_mode, - NFTHolderMode::Contracts as u8, + contract.get_holder_mode(), + NFTHolderMode::Contracts, "holder mode is not set to contracts" ); - let actual_whitelist_mode: u8 = support::query_stored_value( - &builder, - nft_contract_key, - vec![ARG_WHITELIST_MODE.to_string()], - ); - assert_eq!( - actual_whitelist_mode, - WhitelistMode::Unlocked as u8, + contract.get_whitelist_mode(), + WhitelistMode::Unlocked, "whitelist mode is not set to unlocked" ); - let is_whitelisted_account = get_dictionary_value_from_key::( - &builder, - &nft_contract_key, - ACL_WHITELIST, - &ContractHash::default().to_string(), - ); - + let is_whitelisted_account = contract.is_whitelisted(whitelisted_contract.address()); assert!(is_whitelisted_account, "acl whitelist is incorrectly set"); } fn should_disallow_installation_of_contract_with_empty_locked_whitelist_with_holder_mode( - nft_holder_mode: NFTHolderMode, + nft_holder_mode: NFTHolderMode ) { - let mut builder = InMemoryWasmTestBuilder::default(); - builder - .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) - .commit(); - - let install_request_builder = - InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_holder_mode(nft_holder_mode) - .with_reporting_mode(OwnerReverseLookupMode::NoLookUp) - .with_whitelist_mode(WhitelistMode::Locked) - .with_minting_mode(MintingMode::Acl); + let env = odra_test::env(); + let args = default_args_builder() + .holder_mode(nft_holder_mode) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::NoLookUp) + .whitelist_mode(WhitelistMode::Locked) + .minting_mode(MintingMode::Acl) + .build(); - support::assert_expected_invalid_installer_request( - install_request_builder, - 162, + assert_eq!( + Cep78HostRef::try_deploy(&env, args).err(), + Some(CEP78Error::EmptyACLWhitelist.into()), "should fail execution since whitelist mode is locked and the provided whitelist is empty", ); } @@ -161,116 +105,66 @@ fn should_disallow_installation_of_contract_with_empty_locked_whitelist_with_hol #[test] fn should_disallow_installation_of_contract_with_empty_locked_whitelist() { should_disallow_installation_of_contract_with_empty_locked_whitelist_with_holder_mode( - NFTHolderMode::Accounts, + NFTHolderMode::Accounts ); should_disallow_installation_of_contract_with_empty_locked_whitelist_with_holder_mode( - NFTHolderMode::Contracts, + NFTHolderMode::Contracts ); should_disallow_installation_of_contract_with_empty_locked_whitelist_with_holder_mode( - NFTHolderMode::Mixed, + NFTHolderMode::Mixed ); } #[test] fn should_disallow_installation_with_zero_issuance() { - let mut builder = InMemoryWasmTestBuilder::default(); - builder - .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) - .commit(); - - let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_collection_name(NFT_TEST_COLLECTION.to_string()) - .with_collection_symbol(NFT_TEST_SYMBOL.to_string()) - .with_total_token_supply(0u64) - .with_ownership_mode(OwnershipMode::Minter) - .with_identifier_mode(NFTIdentifierMode::Ordinal) - .with_nft_metadata_kind(NFTMetadataKind::Raw) - .build(); - - builder.exec(install_request).expect_failure().commit(); - - let error = builder.get_error().expect("must have error"); - - support::assert_expected_error(error, 123u16, "cannot install when issuance is 0"); + let env = odra_test::env(); + let args = default_args_builder().total_token_supply(0).build(); + assert_eq!( + Cep78HostRef::try_deploy(&env, args).err(), + Some(CEP78Error::CannotInstallWithZeroSupply.into()), + "cannot install when issuance is equal 0", + ); } #[test] fn should_disallow_installation_with_supply_exceeding_hard_cap() { - let mut builder = InMemoryWasmTestBuilder::default(); - builder - .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) - .commit(); - - let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_collection_name(NFT_TEST_COLLECTION.to_string()) - .with_collection_symbol(NFT_TEST_SYMBOL.to_string()) - .with_total_token_supply(1_000_001u64) - .with_ownership_mode(OwnershipMode::Minter) - .with_identifier_mode(NFTIdentifierMode::Ordinal) - .with_nft_metadata_kind(NFTMetadataKind::Raw) + let env = odra_test::env(); + let args = default_args_builder() + .total_token_supply(1_000_001u64) .build(); - - builder.exec(install_request).expect_failure().commit(); - - let error = builder.get_error().expect("must have error"); - - support::assert_expected_error( - error, - 133u16, + assert_eq!( + Cep78HostRef::try_deploy(&env, args).err(), + Some(CEP78Error::ExceededMaxTotalSupply.into()), "cannot install when issuance is more than 1_000_000", ); } #[test] fn should_prevent_installation_with_ownership_and_minting_modality_conflict() { - let mut builder = InMemoryWasmTestBuilder::default(); - builder - .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) - .commit(); - - let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_collection_name(NFT_TEST_COLLECTION.to_string()) - .with_collection_symbol(NFT_TEST_SYMBOL.to_string()) - .with_total_token_supply(1_000u64) - .with_minting_mode(MintingMode::Installer) - .with_ownership_mode(OwnershipMode::Minter) - .with_reporting_mode(OwnerReverseLookupMode::Complete) + let env = odra_test::env(); + let args = default_args_builder() + .minting_mode(MintingMode::Installer) + .ownership_mode(OwnershipMode::Minter) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) .build(); - - builder.exec(install_request).expect_failure().commit(); - - let error = builder.get_error().expect("must have error"); - - support::assert_expected_error( - error, - 130u16, + assert_eq!( + Cep78HostRef::try_deploy(&env, args).err(), + Some(CEP78Error::InvalidReportingMode.into()), "cannot install when Ownership::Minter and MintingMode::Installer", ); } #[test] fn should_prevent_installation_with_ownership_minter_and_owner_reverse_lookup_mode_transfer_only() { - let mut builder = InMemoryWasmTestBuilder::default(); - builder - .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) - .commit(); - - let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_collection_name(NFT_TEST_COLLECTION.to_string()) - .with_collection_symbol(NFT_TEST_SYMBOL.to_string()) - .with_total_token_supply(1_000u64) - .with_minting_mode(MintingMode::Installer) - .with_ownership_mode(OwnershipMode::Minter) - .with_reporting_mode(OwnerReverseLookupMode::TransfersOnly) + let env = odra_test::env(); + let args = default_args_builder() + .minting_mode(MintingMode::Installer) + .ownership_mode(OwnershipMode::Minter) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::TransfersOnly) .build(); - - builder.exec(install_request).expect_failure().commit(); - - let error = builder.get_error().expect("must have error"); - - support::assert_expected_error( - error, - 140u16, + assert_eq!( + Cep78HostRef::try_deploy(&env, args).err(), + Some(CEP78Error::OwnerReverseLookupModeNotTransferable.into()), "cannot install when Ownership::Minter and OwnerReverseLookupMode::TransfersOnly", ); } @@ -278,48 +172,27 @@ fn should_prevent_installation_with_ownership_minter_and_owner_reverse_lookup_mo #[test] fn should_prevent_installation_with_ownership_assigned_and_owner_reverse_lookup_mode_transfer_only() { - let mut builder = InMemoryWasmTestBuilder::default(); - builder - .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) - .commit(); - - let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_collection_name(NFT_TEST_COLLECTION.to_string()) - .with_collection_symbol(NFT_TEST_SYMBOL.to_string()) - .with_total_token_supply(1_000u64) - .with_minting_mode(MintingMode::Installer) - .with_ownership_mode(OwnershipMode::Assigned) - .with_reporting_mode(OwnerReverseLookupMode::TransfersOnly) + let env = odra_test::env(); + let args = default_args_builder() + .minting_mode(MintingMode::Installer) + .ownership_mode(OwnershipMode::Assigned) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::TransfersOnly) .build(); - - builder.exec(install_request).expect_failure().commit(); - - let error = builder.get_error().expect("must have error"); - - support::assert_expected_error( - error, - 140u16, - "cannot install when Ownership::Assigned and OwnerReverseLookupMode::TransfersOnly", + assert_eq!( + Cep78HostRef::try_deploy(&env, args).err(), + Some(CEP78Error::OwnerReverseLookupModeNotTransferable.into()), + "cannot install when Ownership::Minter and OwnerReverseLookupMode::TransfersOnly", ); } #[test] fn should_allow_installation_with_ownership_transferable_and_owner_reverse_lookup_mode_transfer_only( ) { - let mut builder = InMemoryWasmTestBuilder::default(); - builder - .run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST) - .commit(); - - let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_collection_name(NFT_TEST_COLLECTION.to_string()) - .with_collection_symbol(NFT_TEST_SYMBOL.to_string()) - .with_total_token_supply(1_000u64) - .with_minting_mode(MintingMode::Installer) - .with_ownership_mode(OwnershipMode::Transferable) - .with_reporting_mode(OwnerReverseLookupMode::TransfersOnly) + let env = odra_test::env(); + let args = default_args_builder() + .minting_mode(MintingMode::Installer) + .ownership_mode(OwnershipMode::Transferable) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::TransfersOnly) .build(); - - builder.exec(install_request).expect_success().commit(); + assert_eq!(Cep78HostRef::try_deploy(&env, args).err(), None); } -*/ diff --git a/modules/src/cep78/tests/metadata.rs b/modules/src/cep78/tests/metadata.rs index 3b42bbd4..9b03111d 100644 --- a/modules/src/cep78/tests/metadata.rs +++ b/modules/src/cep78/tests/metadata.rs @@ -13,8 +13,7 @@ use crate::cep78::{ }, tests::{ utils::{ - MALFORMED_META_DATA, TEST_PRETTY_CEP78_METADATA, - TEST_PRETTY_UPDATED_CEP78_METADATA + MALFORMED_META_DATA, TEST_PRETTY_CEP78_METADATA, TEST_PRETTY_UPDATED_CEP78_METADATA }, TEST_CUSTOM_METADATA, TEST_CUSTOM_METADATA_SCHEMA, TEST_CUSTOM_UPDATED_METADATA, TOKEN_HASH @@ -59,15 +58,16 @@ fn should_prevent_update_in_immutable_mode() { #[test] fn should_prevent_install_with_hash_identifier_in_mutable_mode() { - // let env = odra_test::env(); - // let args = default_args_builder() - // .nft_metadata_kind(NFTMetadataKind::NFT721) - // .identifier_mode(NFTIdentifierMode::Hash) - // .metadata_mutability(MetadataMutability::Mutable) - // .build(); - // let _contract = Cep78HostRef::deploy(&env, args); - // Should be possible to verify errors at installation time - // assert_eq!(Cep78HostRef::deploy(&env, args), Err(CEP78Error::InvalidMetadataMutability)); + let env = odra_test::env(); + let args = default_args_builder() + .nft_metadata_kind(NFTMetadataKind::NFT721) + .identifier_mode(NFTIdentifierMode::Hash) + .metadata_mutability(MetadataMutability::Mutable) + .build(); + assert_eq!( + Cep78HostRef::try_deploy(&env, args).err(), + Some(CEP78Error::InvalidMetadataMutability.into()) + ); } #[test] @@ -444,10 +444,12 @@ fn should_require_json_schema_when_kind_is_custom_validated() { .nft_metadata_kind(nft_metadata_kind) .json_schema("".to_string()) .build(); - let _contract = Cep78HostRef::deploy(&env, args); - /*let error = builder.get_error().expect("must have error"); - support::assert_expected_error(error, 67, "json_schema is required")*/ + assert_eq!( + Cep78HostRef::try_deploy(&env, args).err(), + Some(CEP78Error::MissingJsonSchema.into()), + "should fail execution since json_schema is required" + ); } fn should_not_require_json_schema_when_kind_is(nft_metadata_kind: NFTMetadataKind) { diff --git a/modules/src/cep78/tests/mint.rs b/modules/src/cep78/tests/mint.rs index 340ece7e..796ecb96 100644 --- a/modules/src/cep78/tests/mint.rs +++ b/modules/src/cep78/tests/mint.rs @@ -15,8 +15,8 @@ use crate::cep78::{ }, tests::{ utils::{ - self, MALFORMED_META_DATA, TEST_COMPACT_META_DATA, - TEST_PRETTY_CEP78_METADATA, TEST_PRETTY_UPDATED_CEP78_METADATA + self, MALFORMED_META_DATA, TEST_COMPACT_META_DATA, TEST_PRETTY_CEP78_METADATA, + TEST_PRETTY_UPDATED_CEP78_METADATA }, TEST_CUSTOM_METADATA, TEST_CUSTOM_METADATA_SCHEMA }, @@ -588,7 +588,7 @@ fn should_mint_without_returning_receipts_and_flat_gas_cost() { // In this case there is no first time allocation of a page. // Therefore the second and first mints must have equivalent gas costs. - if let (Some(c1), Some(c2)) = (costs.get(0), costs.get(2)) { + if let (Some(c1), Some(c2)) = (costs.get(1), costs.get(2)) { assert_eq!(c1, c2); } } @@ -604,7 +604,6 @@ fn should_maintain_page_table_despite_invoking_register_owner() { .build(); let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); - // TODO: register_owner is not implemented yet contract.register_owner(Maybe::Some(token_owner)); contract.mint(token_owner, "".to_string(), Maybe::None); @@ -647,7 +646,6 @@ fn should_mint_with_two_required_metadata_kind() { .additional_required_metadata(vec![NFTMetadataKind::Raw]) .build(); let mut contract = Cep78HostRef::deploy(&env, args); - // TODO: register_owner is not implemented yet let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); contract.mint( diff --git a/modules/src/cep78/tests/transfer.rs b/modules/src/cep78/tests/transfer.rs index cf8e0710..9456356d 100644 --- a/modules/src/cep78/tests/transfer.rs +++ b/modules/src/cep78/tests/transfer.rs @@ -13,11 +13,9 @@ use crate::cep78::{ NFTMetadataKind, OwnerReverseLookupMode, OwnershipMode, TokenIdentifier, TransferFilterContractResult, WhitelistMode }, - tests::{ - default_args_builder, - utils::TEST_PRETTY_721_META_DATA - }, - token::Cep78HostRef, utils::{MockContractHostRef, MockTransferFilterContractHostRef} + tests::{default_args_builder, utils::TEST_PRETTY_721_META_DATA}, + token::Cep78HostRef, + utils::{MockContractHostRef, MockTransferFilterContractHostRef} }; use super::utils; diff --git a/modules/src/cep78/tests/utils.rs b/modules/src/cep78/tests/utils.rs index cade10f4..0a0a7153 100644 --- a/modules/src/cep78/tests/utils.rs +++ b/modules/src/cep78/tests/utils.rs @@ -6,12 +6,7 @@ use crate::cep78::{ token::Cep78InitArgs }; use blake2::{digest::VariableOutput, Blake2bVar}; -use odra::{ - args::Maybe, - casper_types::BLAKE2B_DIGEST_LENGTH, - prelude::*, - Address -}; +use odra::{args::Maybe, casper_types::BLAKE2B_DIGEST_LENGTH, prelude::*, Address}; use std::io::Write; #[derive(Default)] diff --git a/modules/src/cep78/token.rs b/modules/src/cep78/token.rs index 9dc5236b..811fce51 100644 --- a/modules/src/cep78/token.rs +++ b/modules/src/cep78/token.rs @@ -17,10 +17,8 @@ use super::{ whitelist::ACLWhitelist }; use odra::{ - args::Maybe, - casper_types::bytesrepr::ToBytes, - prelude::*, - Address, OdraError, SubModule, UnwrapOrRevert, Var + args::Maybe, casper_types::bytesrepr::ToBytes, prelude::*, Address, OdraError, SubModule, + UnwrapOrRevert, Var }; type MintReceipt = (String, Address, String); @@ -82,6 +80,57 @@ impl Cep78 { optional_metadata: Maybe> ) { let installer = self.caller(); + let minting_mode = minting_mode.unwrap_or_default(); + let owner_reverse_lookup_mode = owner_reverse_lookup_mode.unwrap_or_default(); + let acl_white_list = acl_white_list.unwrap_or_default(); + let whitelist_mode = whitelist_mode.unwrap_or_default(); + let json_schema = json_schema.unwrap_or_default(); + let is_whitelist_empty = acl_white_list.is_empty(); + + // Revert if minting mode is not ACL and acl list is not empty + if MintingMode::Acl != minting_mode && !is_whitelist_empty { + self.revert(CEP78Error::InvalidMintingMode) + } + + // Revert if minting mode is ACL or holder_mode is contracts and acl list is locked and empty + if MintingMode::Acl == minting_mode + && is_whitelist_empty + && WhitelistMode::Locked == whitelist_mode + { + self.revert(CEP78Error::EmptyACLWhitelist) + } + + if nft_identifier_mode == NFTIdentifierMode::Hash + && metadata_mutability == MetadataMutability::Mutable + { + self.revert(CEP78Error::InvalidMetadataMutability) + } + + if ownership_mode == OwnershipMode::Minter + && minting_mode == MintingMode::Installer + && owner_reverse_lookup_mode == OwnerReverseLookupMode::Complete + { + self.revert(CEP78Error::InvalidReportingMode) + } + + // Check if schema is missing before checking its validity + if nft_metadata_kind == NFTMetadataKind::CustomValidated && json_schema.is_empty() { + self.revert(CEP78Error::MissingJsonSchema) + } + + // OwnerReverseLookup TransfersOnly mode should be Transferable + if OwnerReverseLookupMode::TransfersOnly == owner_reverse_lookup_mode + && OwnershipMode::Transferable != ownership_mode + { + self.revert(CEP78Error::OwnerReverseLookupModeNotTransferable) + } + + if ownership_mode != OwnershipMode::Transferable + && transfer_filter_contract_contract.is_some() + { + self.revert(CEP78Error::TransferFilterContractNeedsTransferableMode) + } + self.data.init( collection_name, collection_symbol, @@ -90,7 +139,7 @@ impl Cep78 { ); self.settings.init( allow_minting.unwrap_or(true), - minting_mode.clone().unwrap_or_default(), + minting_mode, ownership_mode, nft_kind, holder_mode.unwrap_or_default(), @@ -99,15 +148,10 @@ impl Cep78 { operator_burn_mode.unwrap_or_default() ); - self.reverse_lookup.init( - owner_reverse_lookup_mode.clone().unwrap_or_default(), - receipt_name.unwrap_or_default() - ); + self.reverse_lookup + .init(owner_reverse_lookup_mode, receipt_name.unwrap_or_default()); - self.whitelist.init( - acl_white_list.unwrap_or_default(), - whitelist_mode.unwrap_or_default() - ); + self.whitelist.init(acl_white_list.clone(), whitelist_mode); self.metadata.init( nft_metadata_kind, @@ -118,25 +162,6 @@ impl Cep78 { json_schema ); - // Revert if minting mode is not ACL and acl list is not empty - if MintingMode::Acl != self.settings.minting_mode() && !self.whitelist.is_empty() { - self.revert(CEP78Error::InvalidMintingMode) - } - - - if nft_identifier_mode == NFTIdentifierMode::Hash - && metadata_mutability == MetadataMutability::Mutable - { - self.revert(CEP78Error::InvalidMetadataMutability) - } - - if ownership_mode == OwnershipMode::Minter - && minting_mode.unwrap_or_default() == MintingMode::Installer - && owner_reverse_lookup_mode.unwrap_or_default() == OwnerReverseLookupMode::Complete - { - self.revert(CEP78Error::InvalidReportingMode) - } - if let Maybe::Some(transfer_filter_contract_contract) = transfer_filter_contract_contract { self.transfer_filter_contract .set(transfer_filter_contract_contract); @@ -529,6 +554,10 @@ impl Cep78 { self.settings.minting_mode() } + pub fn get_holder_mode(&self) -> NFTHolderMode { + self.settings.holder_mode() + } + pub fn get_number_of_minted_tokens(&self) -> u64 { self.data.number_of_minted_tokens() } @@ -718,4 +747,4 @@ pub trait TransferFilterContract { target_key: Address, token_id: TokenIdentifier ) -> TransferFilterContractResult; -} \ No newline at end of file +} diff --git a/modules/src/cep78/utils.rs b/modules/src/cep78/utils.rs index f4051383..735917e9 100644 --- a/modules/src/cep78/utils.rs +++ b/modules/src/cep78/utils.rs @@ -7,7 +7,6 @@ struct MockDummyContract; #[odra::module] impl MockDummyContract {} - #[odra::module] pub struct MockTransferFilterContract { value: Var diff --git a/modules/src/cep78/whitelist.rs b/modules/src/cep78/whitelist.rs index 58d43d97..a39a63ab 100644 --- a/modules/src/cep78/whitelist.rs +++ b/modules/src/cep78/whitelist.rs @@ -27,11 +27,6 @@ impl ACLWhitelist { self.addresses.get_or_default().contains(address) } - #[inline] - pub fn is_empty(&self) -> bool { - self.addresses.get_or_default().is_empty() - } - pub fn update(&mut self, new_addresses: Maybe>) { let new_addresses = new_addresses.unwrap_or_default(); if !new_addresses.is_empty() { From 32489d4f0c242f3410c283541b2f35f07810b099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Thu, 25 Apr 2024 16:49:32 +0200 Subject: [PATCH 23/38] Add missing tests --- modules/src/cep78/tests/costs.rs | 89 ++++++++++++++++++++++++++++++++ modules/src/cep78/tests/mint.rs | 25 +-------- modules/src/cep78/tests/mod.rs | 1 + modules/src/cep78/tests/utils.rs | 31 ++++++++++- 4 files changed, 122 insertions(+), 24 deletions(-) create mode 100644 modules/src/cep78/tests/costs.rs diff --git a/modules/src/cep78/tests/costs.rs b/modules/src/cep78/tests/costs.rs new file mode 100644 index 00000000..20fc5672 --- /dev/null +++ b/modules/src/cep78/tests/costs.rs @@ -0,0 +1,89 @@ +use odra::{args::Maybe, host::Deployer}; + +use crate::cep78::{modalities::{NFTHolderMode, NFTMetadataKind, OwnerReverseLookupMode, OwnershipMode, WhitelistMode}, tests::{default_args_builder, utils}, token::Cep78HostRef}; + +#[test] +#[ignore = "Reverse lookup is not implemented yet"] +fn mint_cost_should_remain_stable() { + let env = odra_test::env(); + let args = default_args_builder() + .holder_mode(NFTHolderMode::Contracts) + .whitelist_mode(WhitelistMode::Unlocked) + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .nft_metadata_kind(NFTMetadataKind::Raw) + .ownership_mode(OwnershipMode::Transferable) + .build(); + + let mut _contract = Cep78HostRef::deploy(&env, args); +} + +#[test] +fn transfer_costs_should_remain_stable() { + let env = odra_test::env(); + let args = default_args_builder() + .owner_reverse_lookup_mode(OwnerReverseLookupMode::Complete) + .nft_metadata_kind(NFTMetadataKind::Raw) + .ownership_mode(OwnershipMode::Transferable) + .build(); + let mut contract = Cep78HostRef::deploy(&env, args); + let token_owner = env.get_account(0); + contract.register_owner(Maybe::Some(token_owner)); + let receiver = env.get_account(1); + + for _ in 0..3 { + contract.mint(token_owner, "".to_string(), Maybe::None); + } + + contract.register_owner(Maybe::Some(receiver)); + contract.transfer(Maybe::Some(0u64), Maybe::None, token_owner, receiver); + contract.transfer(Maybe::Some(1u64), Maybe::None, token_owner, receiver); + contract.transfer(Maybe::Some(2u64), Maybe::None, token_owner, receiver); + + // We check only the second and third gas costs as the first transfer cost + // has the additional gas of allocating a whole new page. Thus we ensure + // that costs once a page has been allocated remain stable. + let costs = utils::get_gas_cost_of(&env, "transfer"); + assert_eq!(costs.get(1), costs.get(2)); +} + +fn should_cost_less_when_installing_without_reverse_lookup(reporting: OwnerReverseLookupMode) { + let env = odra_test::env(); + + let args = default_args_builder() + .owner_reverse_lookup_mode(OwnerReverseLookupMode::NoLookUp) + .nft_metadata_kind(NFTMetadataKind::Raw) + .ownership_mode(OwnershipMode::Transferable) + .build(); + Cep78HostRef::deploy(&env, args); + // let page_dictionary_lookup = builder.query(None, no_lookup_hash, &["page_0".to_string()]); + // assert!(page_dictionary_lookup.is_err()); + + let args = default_args_builder() + .owner_reverse_lookup_mode(reporting) + .nft_metadata_kind(NFTMetadataKind::Raw) + .ownership_mode(OwnershipMode::Transferable) + .build(); + Cep78HostRef::deploy(&env, args); + + // let page_dictionary_lookup = builder.query(None, reverse_lookup_hash, &["page_0".to_string()]); + // assert!(page_dictionary_lookup.is_ok()); + + let costs = utils::get_deploy_gas_cost(&env); + if let Some(no_lookup_gas_cost) = costs.get(0) { + if let Some(reverse_lookup_gas_cost) = costs.get(1) { + assert!(no_lookup_gas_cost < reverse_lookup_gas_cost); + } + } +} + +#[test] +#[ignore = "Reverse lookup is not implemented yet"] +fn should_cost_less_when_installing_without_reverse_lookup_but_complete() { + should_cost_less_when_installing_without_reverse_lookup(OwnerReverseLookupMode::Complete); +} + +#[test] +#[ignore = "Reverse lookup is not implemented yet"] +fn should_cost_less_when_installing_without_reverse_lookup_but_transfer_only() { + should_cost_less_when_installing_without_reverse_lookup(OwnerReverseLookupMode::TransfersOnly); +} diff --git a/modules/src/cep78/tests/mint.rs b/modules/src/cep78/tests/mint.rs index 796ecb96..ac45bc82 100644 --- a/modules/src/cep78/tests/mint.rs +++ b/modules/src/cep78/tests/mint.rs @@ -2,7 +2,6 @@ use odra::{ args::Maybe, casper_types::bytesrepr::ToBytes, host::{Deployer, HostEnv, HostRef, NoArgs}, - DeployReport }; use serde::{Deserialize, Serialize}; @@ -577,14 +576,7 @@ fn should_mint_without_returning_receipts_and_flat_gas_cost() { contract.mint(env.get_account(1), "".to_string(), Maybe::None); contract.mint(env.get_account(2), "".to_string(), Maybe::None); - let costs = env - .gas_report() - .into_iter() - .filter_map(|r| match r { - DeployReport::WasmDeploy { .. } => None, - DeployReport::ContractCall { gas, .. } => Some(gas) - }) - .collect::>(); + let costs = utils::get_gas_cost_of(&env, "mint"); // In this case there is no first time allocation of a page. // Therefore the second and first mints must have equivalent gas costs. @@ -816,20 +808,7 @@ fn should_approve_all_with_flat_gas_cost() { is_also_operator, "expected other operator to be approved for all" ); - let gas_report = env.gas_report(); - let costs = gas_report - .into_iter() - .filter_map(|r| match r { - DeployReport::WasmDeploy { .. } => None, - DeployReport::ContractCall { gas, call_def, .. } => { - if call_def.entry_point() == "set_approval_for_all" { - Some(gas) - } else { - None - } - } - }) - .collect::>(); + let costs = utils::get_gas_cost_of(&env, "set_approval_for_all"); // Operator approval should have flat gas costs // Therefore the second and first set_approve_for_all must have equivalent gas costs. diff --git a/modules/src/cep78/tests/mod.rs b/modules/src/cep78/tests/mod.rs index 59be6a9e..c652ca8b 100644 --- a/modules/src/cep78/tests/mod.rs +++ b/modules/src/cep78/tests/mod.rs @@ -10,6 +10,7 @@ use super::{ mod acl; mod burn; +mod costs; mod events; mod installer; mod metadata; diff --git a/modules/src/cep78/tests/utils.rs b/modules/src/cep78/tests/utils.rs index 0a0a7153..642d0cb9 100644 --- a/modules/src/cep78/tests/utils.rs +++ b/modules/src/cep78/tests/utils.rs @@ -6,7 +6,7 @@ use crate::cep78::{ token::Cep78InitArgs }; use blake2::{digest::VariableOutput, Blake2bVar}; -use odra::{args::Maybe, casper_types::BLAKE2B_DIGEST_LENGTH, prelude::*, Address}; +use odra::{args::Maybe, casper_types::{BLAKE2B_DIGEST_LENGTH, U512}, host::HostEnv, prelude::*, Address, DeployReport}; use std::io::Write; #[derive(Default)] @@ -211,3 +211,32 @@ pub(crate) fn create_blake2b_hash>(data: T) -> [u8; BLAKE2B_DIGES .expect("should copy hash to the result array"); result } + + +pub(crate) fn get_gas_cost_of(env: &HostEnv, entry_point: &str) -> Vec { + let gas_report = env.gas_report(); + gas_report + .into_iter() + .filter_map(|r| match r { + DeployReport::WasmDeploy { .. } => None, + DeployReport::ContractCall { gas, call_def, .. } => { + if call_def.entry_point() == entry_point { + Some(gas) + } else { + None + } + } + }) + .collect::>() +} + +pub(crate) fn get_deploy_gas_cost(env: &HostEnv) -> Vec { + let gas_report = env.gas_report(); + gas_report + .into_iter() + .filter_map(|r| match r { + DeployReport::WasmDeploy { gas, .. } => Some(gas), + DeployReport::ContractCall { .. } => None + }) + .collect::>() +} \ No newline at end of file From dd33780081ef3f228d904094e2e11d96cd20f424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Thu, 25 Apr 2024 17:27:14 +0200 Subject: [PATCH 24/38] make clippy happy again --- modules/src/cep78/tests/costs.rs | 14 ++++++++++---- modules/src/cep78/tests/installer.rs | 2 +- modules/src/cep78/tests/mint.rs | 2 +- modules/src/cep78/tests/utils.rs | 11 ++++++++--- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/modules/src/cep78/tests/costs.rs b/modules/src/cep78/tests/costs.rs index 20fc5672..3f982918 100644 --- a/modules/src/cep78/tests/costs.rs +++ b/modules/src/cep78/tests/costs.rs @@ -1,6 +1,12 @@ use odra::{args::Maybe, host::Deployer}; -use crate::cep78::{modalities::{NFTHolderMode, NFTMetadataKind, OwnerReverseLookupMode, OwnershipMode, WhitelistMode}, tests::{default_args_builder, utils}, token::Cep78HostRef}; +use crate::cep78::{ + modalities::{ + NFTHolderMode, NFTMetadataKind, OwnerReverseLookupMode, OwnershipMode, WhitelistMode + }, + tests::{default_args_builder, utils}, + token::Cep78HostRef +}; #[test] #[ignore = "Reverse lookup is not implemented yet"] @@ -29,7 +35,7 @@ fn transfer_costs_should_remain_stable() { let token_owner = env.get_account(0); contract.register_owner(Maybe::Some(token_owner)); let receiver = env.get_account(1); - + for _ in 0..3 { contract.mint(token_owner, "".to_string(), Maybe::None); } @@ -57,7 +63,7 @@ fn should_cost_less_when_installing_without_reverse_lookup(reporting: OwnerRever Cep78HostRef::deploy(&env, args); // let page_dictionary_lookup = builder.query(None, no_lookup_hash, &["page_0".to_string()]); // assert!(page_dictionary_lookup.is_err()); - + let args = default_args_builder() .owner_reverse_lookup_mode(reporting) .nft_metadata_kind(NFTMetadataKind::Raw) @@ -69,7 +75,7 @@ fn should_cost_less_when_installing_without_reverse_lookup(reporting: OwnerRever // assert!(page_dictionary_lookup.is_ok()); let costs = utils::get_deploy_gas_cost(&env); - if let Some(no_lookup_gas_cost) = costs.get(0) { + if let Some(no_lookup_gas_cost) = costs.first() { if let Some(reverse_lookup_gas_cost) = costs.get(1) { assert!(no_lookup_gas_cost < reverse_lookup_gas_cost); } diff --git a/modules/src/cep78/tests/installer.rs b/modules/src/cep78/tests/installer.rs index 6e00e38d..1ab0988e 100644 --- a/modules/src/cep78/tests/installer.rs +++ b/modules/src/cep78/tests/installer.rs @@ -58,7 +58,7 @@ fn should_reject_non_numerical_total_token_supply_value() {} fn should_install_with_contract_holder_mode() { let env = odra_test::env(); let whitelisted_contract = MockDummyContractHostRef::deploy(&env, NoArgs); - let contract_whitelist = vec![whitelisted_contract.address().clone()]; + let contract_whitelist = vec![*whitelisted_contract.address()]; let args = default_args_builder() .holder_mode(NFTHolderMode::Contracts) .whitelist_mode(WhitelistMode::Unlocked) diff --git a/modules/src/cep78/tests/mint.rs b/modules/src/cep78/tests/mint.rs index ac45bc82..868c7bba 100644 --- a/modules/src/cep78/tests/mint.rs +++ b/modules/src/cep78/tests/mint.rs @@ -1,7 +1,7 @@ use odra::{ args::Maybe, casper_types::bytesrepr::ToBytes, - host::{Deployer, HostEnv, HostRef, NoArgs}, + host::{Deployer, HostEnv, HostRef, NoArgs} }; use serde::{Deserialize, Serialize}; diff --git a/modules/src/cep78/tests/utils.rs b/modules/src/cep78/tests/utils.rs index 642d0cb9..2e29662d 100644 --- a/modules/src/cep78/tests/utils.rs +++ b/modules/src/cep78/tests/utils.rs @@ -6,7 +6,13 @@ use crate::cep78::{ token::Cep78InitArgs }; use blake2::{digest::VariableOutput, Blake2bVar}; -use odra::{args::Maybe, casper_types::{BLAKE2B_DIGEST_LENGTH, U512}, host::HostEnv, prelude::*, Address, DeployReport}; +use odra::{ + args::Maybe, + casper_types::{BLAKE2B_DIGEST_LENGTH, U512}, + host::HostEnv, + prelude::*, + Address, DeployReport +}; use std::io::Write; #[derive(Default)] @@ -212,7 +218,6 @@ pub(crate) fn create_blake2b_hash>(data: T) -> [u8; BLAKE2B_DIGES result } - pub(crate) fn get_gas_cost_of(env: &HostEnv, entry_point: &str) -> Vec { let gas_report = env.gas_report(); gas_report @@ -239,4 +244,4 @@ pub(crate) fn get_deploy_gas_cost(env: &HostEnv) -> Vec { DeployReport::ContractCall { .. } => None }) .collect::>() -} \ No newline at end of file +} From 0aef4cbfa8eed39f15a2879a2d86ee3b984e2168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Fri, 26 Apr 2024 19:59:41 +0200 Subject: [PATCH 25/38] cep78 using named keys - wip --- core/src/contract_context.rs | 4 +- core/src/contract_env.rs | 9 +- modules/Cargo.toml | 1 - modules/src/cep18/storage.rs | 14 +- modules/src/cep78/data.rs | 76 +++++--- modules/src/cep78/mod.rs | 1 + modules/src/cep78/settings.rs | 47 +++-- modules/src/cep78/storage/mod.rs | 184 ++++++++++++++++++ odra-casper/wasm-env/src/host_functions.rs | 11 +- odra-casper/wasm-env/src/wasm_contract_env.rs | 4 +- odra-vm/src/odra_vm_contract_env.rs | 4 +- odra-vm/src/vm/odra_vm.rs | 8 +- odra/src/lib.rs | 2 +- 13 files changed, 296 insertions(+), 69 deletions(-) create mode 100644 modules/src/cep78/storage/mod.rs diff --git a/core/src/contract_context.rs b/core/src/contract_context.rs index d421b3d1..fe459966 100644 --- a/core/src/contract_context.rs +++ b/core/src/contract_context.rs @@ -49,7 +49,7 @@ pub trait ContractContext { /// /// * `dictionary_name` - The name of the dictionary. /// * `key` - The key to retrieve the value for. - fn get_dictionary_value(&self, dictionary_name: &str, key: &str) -> Option; + fn get_dictionary_value(&self, dictionary_name: &str, key: &[u8]) -> Option; /// Sets the key value behind a named dictionary. /// @@ -58,7 +58,7 @@ pub trait ContractContext { /// * `dictionary_name` - The name of the dictionary. /// * `key` - The key to set the value for. /// * `value` - The value to set. - fn set_dictionary_value(&self, dictionary_name: &str, key: &str, value: CLValue); + fn set_dictionary_value(&self, dictionary_name: &str, key: &[u8], value: CLValue); /// Retrieves the address of the caller. fn caller(&self) -> Address; diff --git a/core/src/contract_env.rs b/core/src/contract_env.rs index 22c12234..b3a6732e 100644 --- a/core/src/contract_env.rs +++ b/core/src/contract_env.rs @@ -104,13 +104,12 @@ impl ContractEnv { } /// Retrieves the value associated with the given named key from the named dictionary in the contract storage. - pub fn get_dictionary_value, V: AsRef>( + pub fn get_dictionary_value>( &self, dictionary_name: U, - key: V + key: &[u8] ) -> Option { let dictionary_name = dictionary_name.as_ref(); - let key = key.as_ref(); let bytes = self .backend .borrow() @@ -123,10 +122,10 @@ impl ContractEnv { } /// Sets the value associated with the given named key in the named dictionary in the contract storage. - pub fn set_dictionary_value, V: AsRef>( + pub fn set_dictionary_value>( &self, dictionary_name: U, - key: V, + key: &[u8], value: T ) { let dictionary_name = dictionary_name.as_ref(); diff --git a/modules/Cargo.toml b/modules/Cargo.toml index 194ba69d..916f1844 100644 --- a/modules/Cargo.toml +++ b/modules/Cargo.toml @@ -11,7 +11,6 @@ categories = ["wasm"] [dependencies] odra = { path = "../odra", version = "0.9.1", default-features = false } -hex = { version = "0.4.3", default-features = false } serde = { version = "1.0.80", default-features = false } serde_json = { version = "1.0.59", default-features = false } serde-json-wasm = { version = "1.0.1", default-features = false } diff --git a/modules/src/cep18/storage.rs b/modules/src/cep18/storage.rs index bdc995da..c519d30f 100644 --- a/modules/src/cep18/storage.rs +++ b/modules/src/cep18/storage.rs @@ -120,13 +120,13 @@ impl Cep18BalancesStorage { /// Sets the balance of the given account. pub fn set(&self, account: &Address, balance: U256) { self.env() - .set_dictionary_value(BALANCES_KEY, self.key(account), balance); + .set_dictionary_value(BALANCES_KEY, self.key(account).as_bytes(), balance); } /// Gets the balance of the given account. pub fn get_or_default(&self, account: &Address) -> U256 { self.env() - .get_dictionary_value(BALANCES_KEY, self.key(account)) + .get_dictionary_value(BALANCES_KEY, self.key(account).as_bytes()) .unwrap_or_default() } @@ -168,13 +168,13 @@ impl Cep18AllowancesStorage { /// Sets the allowance of the given owner and spender. pub fn set(&self, owner: &Address, spender: &Address, amount: U256) { self.env() - .set_dictionary_value(ALLOWANCES_KEY, self.key(owner, spender), amount); + .set_dictionary_value(ALLOWANCES_KEY, &self.key(owner, spender), amount); } /// Gets the allowance of the given owner and spender. pub fn get_or_default(&self, owner: &Address, spender: &Address) -> U256 { self.env() - .get_dictionary_value(ALLOWANCES_KEY, self.key(owner, spender)) + .get_dictionary_value(ALLOWANCES_KEY, &self.key(owner, spender)) .unwrap_or_default() } @@ -196,12 +196,14 @@ impl Cep18AllowancesStorage { self.set(owner, spender, new_allowance); } - fn key(&self, owner: &Address, spender: &Address) -> String { + fn key(&self, owner: &Address, spender: &Address) -> [u8; 64] { + let mut result = [0u8; 64]; let mut preimage = Vec::new(); preimage.append(&mut owner.to_bytes().unwrap_or_revert(&self.env())); preimage.append(&mut spender.to_bytes().unwrap_or_revert(&self.env())); let key_bytes = self.env().hash(&preimage); - hex::encode(key_bytes) + odra::utils::hex_to_slice(&key_bytes, &mut result); + result } } diff --git a/modules/src/cep78/data.rs b/modules/src/cep78/data.rs index d9834e89..43a802cc 100644 --- a/modules/src/cep78/data.rs +++ b/modules/src/cep78/data.rs @@ -1,25 +1,52 @@ +use odra::casper_types::bytesrepr::ToBytes; use odra::prelude::*; +use base64::prelude::*; use odra::Address; -use odra::Mapping; +use odra::SubModule; use odra::UnwrapOrRevert; -use odra::Var; +use crate::basic_key_value_storage; +use crate::compound_key_storage; +use crate::encoded_key_value_storage; +use crate::simple_storage; use super::constants; use super::error::CEP78Error; +use super::storage::*; + +simple_storage!(Cep78CollectionName, String, COLLECTION_NAME, CEP78Error::MissingCollectionName); +simple_storage!(Cep78CollectionSymbol, String, COLLECTION_SYMBOL, CEP78Error::MissingCollectionName); +simple_storage!(Cep78TotalSupply, u64, TOTAL_TOKEN_SUPPLY, CEP78Error::MissingTotalTokenSupply); +simple_storage!(Cep78TokenCounter, u64, NUMBER_OF_MINTED_TOKENS); + +impl Cep78TokenCounter { + pub fn add(&mut self, value: u64) { + match self.get() { + Some(current_value) => self.set(value + current_value), + None => self.set(value) + } + } +} +simple_storage!(Cep78Installer, Address, INSTALLER, CEP78Error::MissingInstaller); +compound_key_storage!(Cep78Operators, OPERATORS, Address, bool); +basic_key_value_storage!(Cep78Owners, TOKEN_OWNERS, Address); +basic_key_value_storage!(Cep78Issuers, TOKEN_ISSUERS, Address); +basic_key_value_storage!(Cep78BurntTokens, BURNT_TOKENS, ()); +encoded_key_value_storage!(Cep78TokenCount, TOKEN_COUNT, Address, u64); +encoded_key_value_storage!(Cep78Approved, APPROVED, String, Option
); #[odra::module] pub struct CollectionData { - name: Var, - symbol: Var, - total_token_supply: Var, - counter: Var, - installer: Var
, - owners: Mapping, - issuers: Mapping, - approved: Mapping>, - token_count: Mapping, - burnt_tokens: Mapping, - operators: Mapping<(Address, Address), bool> + name: SubModule, + symbol: SubModule, + total_token_supply: SubModule, + minted_tokens_count: SubModule, + installer: SubModule, + owners: SubModule, + issuers: SubModule, + approved: SubModule, + token_count: SubModule, + burnt_tokens: SubModule, + operators: SubModule } impl CollectionData { @@ -46,33 +73,32 @@ impl CollectionData { #[inline] pub fn installer(&self) -> Address { - self.installer - .get_or_revert_with(CEP78Error::MissingInstaller) + self.installer.get() } #[inline] pub fn total_token_supply(&self) -> u64 { - self.total_token_supply.get_or_default() + self.total_token_supply.get() } #[inline] pub fn increment_number_of_minted_tokens(&mut self) { - self.counter.add(1); + self.minted_tokens_count.add(1); } #[inline] pub fn number_of_minted_tokens(&self) -> u64 { - self.counter.get_or_default() + self.minted_tokens_count.get().unwrap_or_default() } #[inline] pub fn collection_name(&self) -> String { - self.name.get_or_default() + self.name.get() } #[inline] pub fn collection_symbol(&self) -> String { - self.symbol.get_or_default() + self.symbol.get() } #[inline] @@ -87,22 +113,24 @@ impl CollectionData { #[inline] pub fn increment_counter(&mut self, token_owner: &Address) { - self.token_count.add(token_owner, 1); + let value = self.token_count.get(token_owner).unwrap_or_default(); + self.token_count.set(token_owner, value + 1); } #[inline] pub fn decrement_counter(&mut self, token_owner: &Address) { - self.token_count.subtract(token_owner, 1); + let value = self.token_count.get(token_owner).unwrap_or_default(); + self.token_count.set(token_owner, value - 1); } #[inline] pub fn operator(&self, owner: Address, operator: Address) -> bool { - self.operators.get_or_default(&(owner, operator)) + self.operators.get_or_default(&owner, &operator) } #[inline] pub fn set_operator(&mut self, owner: Address, operator: Address, approved: bool) { - self.operators.set(&(owner, operator), approved); + self.operators.set(&owner, &operator, approved); } #[inline] diff --git a/modules/src/cep78/mod.rs b/modules/src/cep78/mod.rs index c89f0516..43414c4c 100644 --- a/modules/src/cep78/mod.rs +++ b/modules/src/cep78/mod.rs @@ -13,3 +13,4 @@ mod tests; pub mod token; mod utils; mod whitelist; +mod storage; \ No newline at end of file diff --git a/modules/src/cep78/settings.rs b/modules/src/cep78/settings.rs index d8ebf2a9..19cd0833 100644 --- a/modules/src/cep78/settings.rs +++ b/modules/src/cep78/settings.rs @@ -1,18 +1,33 @@ use odra::prelude::*; -use odra::Var; +use odra::SubModule; +use odra::UnwrapOrRevert; +use crate::simple_storage; + +use super::error::CEP78Error; use super::modalities::{BurnMode, EventsMode, MintingMode, NFTHolderMode, NFTKind, OwnershipMode}; +use super::storage::*; + +simple_storage!(Cep78AllowMinting, bool, ALLOW_MINTING); +simple_storage!(Cep78MintingMode, MintingMode, MINTING_MODE, CEP78Error::MissingMintingMode); +simple_storage!(Cep78OwnershipMode, OwnershipMode, OWNERSHIP_MODE , CEP78Error::MissingOwnershipMode); +simple_storage!(Cep78NFTKind, NFTKind, NFT_KIND, CEP78Error::MissingNftKind); +simple_storage!(Cep78HolderMode, NFTHolderMode, HOLDER_MODE, CEP78Error::MissingHolderMode); +simple_storage!(Cep78BurnMode, BurnMode, BURN_MODE, CEP78Error::MissingBurnMode); +simple_storage!(Cep78EventsMode, EventsMode, EVENTS_MODE, CEP78Error::MissingEventsMode); +simple_storage!(Cep78OperatorBurnMode, bool, OPERATOR_BURN_MODE, CEP78Error::MissingOperatorBurnMode); + #[odra::module] pub struct Settings { - allow_minting: Var, - minting_mode: Var, - ownership_mode: Var, - nft_kind: Var, - holder_mode: Var, - burn_mode: Var, - events_mode: Var, - operator_burn_mode: Var + allow_minting: SubModule, + minting_mode: SubModule, + ownership_mode: SubModule, + nft_kind: SubModule, + holder_mode: SubModule, + burn_mode: SubModule, + events_mode: SubModule, + operator_burn_mode: SubModule } impl Settings { @@ -40,7 +55,7 @@ impl Settings { #[inline] pub fn allow_minting(&self) -> bool { - self.allow_minting.get_or_default() + self.allow_minting.get().unwrap_or_default() } #[inline] @@ -50,32 +65,32 @@ impl Settings { #[inline] pub fn events_mode(&self) -> EventsMode { - self.events_mode.get_or_default() + self.events_mode.get() } #[inline] pub fn burn_mode(&self) -> BurnMode { - self.burn_mode.get_or_default() + self.burn_mode.get() } #[inline] pub fn ownership_mode(&self) -> OwnershipMode { - self.ownership_mode.get_or_default() + self.ownership_mode.get() } #[inline] pub fn minting_mode(&self) -> MintingMode { - self.minting_mode.get_or_default() + self.minting_mode.get() } #[inline] pub fn holder_mode(&self) -> NFTHolderMode { - self.holder_mode.get_or_default() + self.holder_mode.get() } #[inline] pub fn operator_burn_mode(&self) -> bool { - self.operator_burn_mode.get_or_default() + self.operator_burn_mode.get() } #[inline] diff --git a/modules/src/cep78/storage/mod.rs b/modules/src/cep78/storage/mod.rs new file mode 100644 index 00000000..65798a04 --- /dev/null +++ b/modules/src/cep78/storage/mod.rs @@ -0,0 +1,184 @@ +pub const ACL_PACKAGE_MODE: &str = "acl_package_mode"; +pub const ACL_WHITELIST: &str = "acl_whitelist"; +pub const ALLOW_MINTING: &str = "allow_minting"; +pub const APPROVED: &str = "approved"; +pub const BURN_MODE: &str = "burn_mode"; +pub const BURNT_TOKENS: &str = "burnt_tokens"; +pub const COLLECTION_NAME: &str = "collection_name"; +pub const COLLECTION_SYMBOL: &str = "collection_symbol"; +pub const CONTRACT_WHITELIST: &str = "contract_whitelist"; +pub const EVENT_TYPE: &str = "event_type"; +pub const EVENTS: &str = "events"; +pub const EVENTS_MODE: &str = "events_mode"; +pub const HASH_BY_INDEX: &str = "hash_by_index"; +pub const HOLDER_MODE: &str = "holder_mode"; +pub const IDENTIFIER_MODE: &str = "identifier_mode"; +pub const INDEX_BY_HASH: &str = "index_by_hash"; +pub const INSTALLER: &str = "installer"; +pub const JSON_SCHEMA: &str = "json_schema"; +pub const METADATA_CEP78: &str = "metadata_cep78"; +pub const METADATA_CUSTOM_VALIDATED: &str = "metadata_custom_validated"; +pub const METADATA_MUTABILITY: &str = "metadata_mutability"; +pub const METADATA_NFT721: &str = "metadata_nft721"; +pub const METADATA_RAW: &str = "metadata_raw"; +pub const MIGRATION_FLAG: &str = "migration_flag"; +pub const MINTING_MODE: &str = "minting_mode"; +pub const NFT_KIND: &str = "nft_kind"; +pub const NFT_METADATA_KIND: &str = "nft_metadata_kind"; +pub const NFT_METADATA_KINDS: &str = "nft_metadata_kinds"; +pub const NUMBER_OF_MINTED_TOKENS: &str = "number_of_minted_tokens"; +pub const OPERATOR: &str = "operator"; +pub const OPERATORS: &str = "operators"; +pub const OPERATOR_BURN_MODE: &str = "operator_burn_mode"; +pub const OWNED_TOKENS: &str = "owned_tokens"; +pub const OWNER: &str = "owner"; +pub const BURNER: &str = "burner"; +pub const OWNERSHIP_MODE: &str = "ownership_mode"; +pub const PACKAGE_OPERATOR_MODE: &str = "package_operator_mode"; +pub const PAGE_LIMIT: &str = "page_limit"; +pub const PAGE_TABLE: &str = "page_table"; +pub const RECEIPT_NAME: &str = "receipt_name"; +pub const RECIPIENT: &str = "recipient"; +pub const REPORTING_MODE: &str = "reporting_mode"; +pub const RLO_MFLAG: &str = "rlo_mflag"; +pub const SENDER: &str = "sender"; +pub const SPENDER: &str = "spender"; +pub const TOKEN_COUNT: &str = "balances"; +pub const TOKEN_ID: &str = "token_id"; +pub const TOKEN_ISSUERS: &str = "token_issuers"; +pub const TOKEN_OWNERS: &str = "token_owners"; +pub const TOTAL_TOKEN_SUPPLY: &str = "total_token_supply"; +pub const TRANSFER_FILTER_CONTRACT: &str = "transfer_filter_contract"; +pub const TRANSFER_FILTER_CONTRACT_METHOD: &str = "can_transfer"; +pub const UNMATCHED_HASH_COUNT: &str = "unmatched_hash_count"; +pub const WHITELIST_MODE: &str = "whitelist_mode"; + +#[macro_export] +macro_rules! simple_storage { + ($name:ident, $value_ty:ty, $key:ident, $err:expr) => { + #[odra::module] + pub struct $name; + + impl $name { + pub fn set(&self, value: $value_ty) { + self.env().set_named_value($key, value); + } + + pub fn get(&self) -> $value_ty { + self.env() + .get_named_value($key) + .unwrap_or_revert_with(&self.env(), $err) + } + } + }; + ($name:ident, $value_ty:ty, $key:ident) => { + #[odra::module] + pub struct $name; + + impl $name { + pub fn set(&self, value: $value_ty) { + self.env().set_named_value($key, value); + } + + pub fn get(&self) -> Option<$value_ty> { + self.env().get_named_value($key) + } + } + }; +} + +#[macro_export] +macro_rules! compound_key_storage { + ($name:ident, $dict:expr, $k1_type:ty, $k2_type:ty, $value_type:ty) => { + #[odra::module] + pub struct $name; + + impl $name { + pub fn set(&self, key1: &$k1_type, key2: &$k2_type, value: $value_type) { + let env = self.env(); + let parts = [ + key1.to_bytes().unwrap_or_revert(&env), + key2.to_bytes().unwrap_or_revert(&env), + ]; + let key = crate::cep78::storage::compound_key(&env, &parts); + env.set_dictionary_value($dict, &key, value); + } + + pub fn get_or_default(&self, key1: &$k1_type, key2: &$k2_type) -> $value_type { + let env = self.env(); + let parts = [ + key1.to_bytes().unwrap_or_revert(&env), + key2.to_bytes().unwrap_or_revert(&env), + ]; + let key = crate::cep78::storage::compound_key(&env, &parts); + env.get_dictionary_value($dict, &key).unwrap_or_default() + } + } + }; + ($name:ident, $dict:expr, $k1_type:ty, $value_type:ty) => { + compound_key_storage!( + $name, + $dict, + $k1_type, + $k1_type, + $value_type + ); + } +} + +#[macro_export] +macro_rules! encoded_key_value_storage { + ($name:ident, $dict:expr, $key:ty, $value_type:ty) => { + #[odra::module] + pub struct $name; + + impl $name { + pub fn set(&self, key: &$key, value: $value_type) { + let env = self.env(); + let preimage = key.to_bytes().unwrap_or_revert(&env); + let key = BASE64_STANDARD.encode(preimage); + env.set_dictionary_value($dict, key.as_bytes(), value); + } + + pub fn get(&self, key: &$key) -> Option<$value_type> { + let env = self.env(); + let preimage = key.to_bytes().unwrap_or_revert(&env); + let key = BASE64_STANDARD.encode(preimage); + env.get_dictionary_value($dict, key.as_bytes()) + } + } + }; +} + +#[macro_export] +macro_rules! basic_key_value_storage { + ($name:ident, $dict:expr, $value_type:ty) => { + #[odra::module] + pub struct $name; + + impl $name { + pub fn set(&self, key: &str, value: $value_type) { + self.env().set_dictionary_value($dict, key.as_bytes(), value); + } + + pub fn get(&self, key: &str) -> Option<$value_type> { + self.env().get_dictionary_value($dict, key.as_bytes()) + } + } + }; +} + +pub fn compound_key(env: &odra::ContractEnv, parts: &[odra::prelude::Vec]) -> [u8; 64] { + use odra::casper_types::bytesrepr::ToBytes; + use odra::UnwrapOrRevert; + + let mut result = [0u8; 64]; + let mut preimage = odra::prelude::Vec::new(); + for part in parts { + preimage.append(&mut part.to_bytes().unwrap_or_revert(env)); + } + + let key_bytes = env.hash(&preimage); + odra::utils::hex_to_slice(&key_bytes, &mut result); + result +} \ No newline at end of file diff --git a/odra-casper/wasm-env/src/host_functions.rs b/odra-casper/wasm-env/src/host_functions.rs index 7f9388c1..3f6627ff 100644 --- a/odra-casper/wasm-env/src/host_functions.rs +++ b/odra-casper/wasm-env/src/host_functions.rs @@ -238,7 +238,7 @@ pub fn get_named_key(name: &str) -> Option { } /// Writes a value under a key in a dictionary to a contract's storage. -pub fn set_dictionary_value(dictionary_name: &str, key: &str, value: CLValue) { +pub fn set_dictionary_value(dictionary_name: &str, key: &[u8], value: CLValue) { let dictionary_uref = get_dictionary(dictionary_name); let (uref_ptr, uref_size, _bytes1) = to_ptr(dictionary_uref); let (dictionary_item_key_ptr, dictionary_item_key_size) = dictionary_item_key_to_ptr(key); @@ -265,7 +265,7 @@ pub fn set_dictionary_value(dictionary_name: &str, key: &str, value: CLValue) { } /// Gets a value under a key in a dictionary from the contract's storage. -pub fn get_dictionary_value(dictionary_name: &str, key: &str) -> Option { +pub fn get_dictionary_value(dictionary_name: &str, key: &[u8]) -> Option { let dictionary_uref = get_dictionary(dictionary_name); let (uref_ptr, uref_size, _bytes1) = to_ptr(dictionary_uref); let (dictionary_item_key_ptr, dictionary_item_key_size) = dictionary_item_key_to_ptr(key); @@ -629,10 +629,9 @@ fn to_ptr(t: T) -> (*const u8, usize, Vec) { (ptr, size, bytes) } -fn dictionary_item_key_to_ptr(dictionary_item_key: &str) -> (*const u8, usize) { - let bytes = dictionary_item_key.as_bytes(); - let ptr = bytes.as_ptr(); - let size = bytes.len(); +fn dictionary_item_key_to_ptr(dictionary_item_key: &[u8]) -> (*const u8, usize) { + let ptr = dictionary_item_key.as_ptr(); + let size = dictionary_item_key.len(); (ptr, size) } diff --git a/odra-casper/wasm-env/src/wasm_contract_env.rs b/odra-casper/wasm-env/src/wasm_contract_env.rs index a32d2bb5..6953089a 100644 --- a/odra-casper/wasm-env/src/wasm_contract_env.rs +++ b/odra-casper/wasm-env/src/wasm_contract_env.rs @@ -30,11 +30,11 @@ impl ContractContext for WasmContractEnv { host_functions::set_named_key(name, value); } - fn get_dictionary_value(&self, dictionary_name: &str, key: &str) -> Option { + fn get_dictionary_value(&self, dictionary_name: &str, key: &[u8]) -> Option { host_functions::get_dictionary_value(dictionary_name, key) } - fn set_dictionary_value(&self, dictionary_name: &str, key: &str, value: CLValue) { + fn set_dictionary_value(&self, dictionary_name: &str, key: &[u8], value: CLValue) { host_functions::set_dictionary_value(dictionary_name, key, value); } diff --git a/odra-vm/src/odra_vm_contract_env.rs b/odra-vm/src/odra_vm_contract_env.rs index e0edff20..62c1cb26 100644 --- a/odra-vm/src/odra_vm_contract_env.rs +++ b/odra-vm/src/odra_vm_contract_env.rs @@ -34,11 +34,11 @@ impl ContractContext for OdraVmContractEnv { self.vm.borrow().set_named_key(name, value) } - fn get_dictionary_value(&self, dictionary_name: &str, key: &str) -> Option { + fn get_dictionary_value(&self, dictionary_name: &str, key: &[u8]) -> Option { self.vm.borrow().get_dict_value(dictionary_name, key) } - fn set_dictionary_value(&self, dictionary_name: &str, key: &str, value: CLValue) { + fn set_dictionary_value(&self, dictionary_name: &str, key: &[u8], value: CLValue) { self.vm.borrow().set_dict_value(dictionary_name, key, value) } diff --git a/odra-vm/src/vm/odra_vm.rs b/odra-vm/src/vm/odra_vm.rs index 02a8f959..c6e41bba 100644 --- a/odra-vm/src/vm/odra_vm.rs +++ b/odra-vm/src/vm/odra_vm.rs @@ -175,10 +175,10 @@ impl OdraVm { } /// Sets the value of the dictionary item. - pub fn set_dict_value(&self, dict: &str, key: &str, value: CLValue) { + pub fn set_dict_value(&self, dict: &str, key: &[u8], value: CLValue) { self.state.write().unwrap().set_dict_value( dict.as_bytes(), - key.as_bytes(), + key, Bytes::from(value.inner_bytes().as_slice()) ); } @@ -187,12 +187,12 @@ impl OdraVm { /// /// Returns `None` if the dictionary or the key does not exist. /// If the dictionary or the key does not exist, the virtual machine is in error state. - pub fn get_dict_value(&self, dict: &str, key: &str) -> Option { + pub fn get_dict_value(&self, dict: &str, key: &[u8]) -> Option { let result = { self.state .read() .unwrap() - .get_dict_value(dict.as_bytes(), key.as_bytes()) + .get_dict_value(dict.as_bytes(), key) }; match result { Ok(result) => result, diff --git a/odra/src/lib.rs b/odra/src/lib.rs index c92bc3c0..e4a97af7 100644 --- a/odra/src/lib.rs +++ b/odra/src/lib.rs @@ -38,7 +38,7 @@ #![no_std] pub use odra_core::{ - args, arithmetic, contract_def, entry_point_callback, host, module, prelude, uints + args, arithmetic, contract_def, entry_point_callback, host, module, prelude, uints, utils }; pub use odra_core::{casper_event_standard, casper_event_standard::Event, casper_types}; pub use odra_core::{ From 297bb729431447914bf5e6e7558e6672d1e631bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Mon, 6 May 2024 13:35:29 +0200 Subject: [PATCH 26/38] Use named keys instead of Var and Mappings --- modules/src/cep78/constants.rs | 30 +++ modules/src/cep78/data.rs | 44 +++-- modules/src/cep78/metadata.rs | 105 +++++++--- modules/src/cep78/mod.rs | 3 +- modules/src/cep78/reverse_lookup.rs | 26 ++- modules/src/cep78/settings.rs | 50 +++-- modules/src/cep78/storage/mod.rs | 184 ----------------- modules/src/cep78/tests/mod.rs | 5 +- modules/src/cep78/tests/utils.rs | 175 +---------------- modules/src/cep78/token.rs | 23 ++- modules/src/cep78/utils.rs | 185 ++++++++++++++++++ modules/src/cep78/whitelist.rs | 42 +++- modules/src/lib.rs | 3 +- modules/src/storage.rs | 135 +++++++++++++ .../livenet-env/src/livenet_contract_env.rs | 4 +- odra-casper/rpc-client/src/casper_client.rs | 5 +- 16 files changed, 573 insertions(+), 446 deletions(-) delete mode 100644 modules/src/cep78/storage/mod.rs create mode 100644 modules/src/storage.rs diff --git a/modules/src/cep78/constants.rs b/modules/src/cep78/constants.rs index d87bded8..aa270b1f 100644 --- a/modules/src/cep78/constants.rs +++ b/modules/src/cep78/constants.rs @@ -1,3 +1,33 @@ +pub const ACL_PACKAGE_MODE: &str = "acl_package_mode"; +pub const ACL_WHITELIST: &str = "acl_whitelist"; +pub const ALLOW_MINTING: &str = "allow_minting"; +pub const APPROVED: &str = "approved"; +pub const BURN_MODE: &str = "burn_mode"; +pub const BURNT_TOKENS: &str = "burnt_tokens"; +pub const COLLECTION_NAME: &str = "collection_name"; +pub const COLLECTION_SYMBOL: &str = "collection_symbol"; +pub const EVENTS_MODE: &str = "events_mode"; +pub const HOLDER_MODE: &str = "holder_mode"; +pub const IDENTIFIER_MODE: &str = "identifier_mode"; +pub const INSTALLER: &str = "installer"; +pub const JSON_SCHEMA: &str = "json_schema"; +pub const METADATA_MUTABILITY: &str = "metadata_mutability"; +pub const MINTING_MODE: &str = "minting_mode"; +pub const NFT_KIND: &str = "nft_kind"; +pub const NFT_METADATA_KIND: &str = "nft_metadata_kind"; +pub const NFT_METADATA_KINDS: &str = "nft_metadata_kinds"; +pub const NUMBER_OF_MINTED_TOKENS: &str = "number_of_minted_tokens"; +pub const OPERATORS: &str = "operators"; +pub const OPERATOR_BURN_MODE: &str = "operator_burn_mode"; +pub const OWNERSHIP_MODE: &str = "ownership_mode"; +pub const RECEIPT_NAME: &str = "receipt_name"; +pub const REPORTING_MODE: &str = "reporting_mode"; +pub const TOKEN_COUNT: &str = "balances"; +pub const TOKEN_ISSUERS: &str = "token_issuers"; +pub const TOKEN_OWNERS: &str = "token_owners"; +pub const TOTAL_TOKEN_SUPPLY: &str = "total_token_supply"; +pub const TRANSFER_FILTER_CONTRACT: &str = "transfer_filter_contract"; +pub const WHITELIST_MODE: &str = "whitelist_mode"; pub const PREFIX_PAGE_DICTIONARY: &str = "page"; pub const METADATA_CEP78: &str = "metadata_cep78"; diff --git a/modules/src/cep78/data.rs b/modules/src/cep78/data.rs index 43a802cc..e3b94cf0 100644 --- a/modules/src/cep78/data.rs +++ b/modules/src/cep78/data.rs @@ -1,23 +1,32 @@ -use odra::casper_types::bytesrepr::ToBytes; -use odra::prelude::*; -use base64::prelude::*; -use odra::Address; -use odra::SubModule; -use odra::UnwrapOrRevert; use crate::basic_key_value_storage; -use crate::compound_key_storage; +use crate::compound_key_value_storage; use crate::encoded_key_value_storage; use crate::simple_storage; +use odra::{casper_types::bytesrepr::ToBytes, prelude::*, Address, SubModule, UnwrapOrRevert}; use super::constants; +use super::constants::*; use super::error::CEP78Error; -use super::storage::*; -simple_storage!(Cep78CollectionName, String, COLLECTION_NAME, CEP78Error::MissingCollectionName); -simple_storage!(Cep78CollectionSymbol, String, COLLECTION_SYMBOL, CEP78Error::MissingCollectionName); -simple_storage!(Cep78TotalSupply, u64, TOTAL_TOKEN_SUPPLY, CEP78Error::MissingTotalTokenSupply); +simple_storage!( + Cep78CollectionName, + String, + COLLECTION_NAME, + CEP78Error::MissingCollectionName +); +simple_storage!( + Cep78CollectionSymbol, + String, + COLLECTION_SYMBOL, + CEP78Error::MissingCollectionName +); +simple_storage!( + Cep78TotalSupply, + u64, + TOTAL_TOKEN_SUPPLY, + CEP78Error::MissingTotalTokenSupply +); simple_storage!(Cep78TokenCounter, u64, NUMBER_OF_MINTED_TOKENS); - impl Cep78TokenCounter { pub fn add(&mut self, value: u64) { match self.get() { @@ -26,13 +35,18 @@ impl Cep78TokenCounter { } } } -simple_storage!(Cep78Installer, Address, INSTALLER, CEP78Error::MissingInstaller); -compound_key_storage!(Cep78Operators, OPERATORS, Address, bool); +simple_storage!( + Cep78Installer, + Address, + INSTALLER, + CEP78Error::MissingInstaller +); +compound_key_value_storage!(Cep78Operators, OPERATORS, Address, bool); basic_key_value_storage!(Cep78Owners, TOKEN_OWNERS, Address); basic_key_value_storage!(Cep78Issuers, TOKEN_ISSUERS, Address); basic_key_value_storage!(Cep78BurntTokens, BURNT_TOKENS, ()); encoded_key_value_storage!(Cep78TokenCount, TOKEN_COUNT, Address, u64); -encoded_key_value_storage!(Cep78Approved, APPROVED, String, Option
); +basic_key_value_storage!(Cep78Approved, APPROVED, Option
); #[odra::module] pub struct CollectionData { diff --git a/modules/src/cep78/metadata.rs b/modules/src/cep78/metadata.rs index 141f39c6..ba68bf27 100644 --- a/modules/src/cep78/metadata.rs +++ b/modules/src/cep78/metadata.rs @@ -1,7 +1,12 @@ -use odra::{args::Maybe, prelude::*, Mapping, UnwrapOrRevert, Var}; +use odra::{args::Maybe, prelude::*, SubModule, UnwrapOrRevert}; use serde::{Deserialize, Serialize}; +use crate::simple_storage; + use super::{ + constants::{ + IDENTIFIER_MODE, JSON_SCHEMA, METADATA_MUTABILITY, NFT_METADATA_KIND, NFT_METADATA_KINDS + }, constants::{METADATA_CEP78, METADATA_CUSTOM_VALIDATED, METADATA_NFT721, METADATA_RAW}, error::CEP78Error, modalities::{ @@ -10,13 +15,64 @@ use super::{ } }; +simple_storage!( + Cep78MetadataRequirement, + MetadataRequirement, + NFT_METADATA_KINDS, + CEP78Error::MissingNFTMetadataKind +); +simple_storage!( + Cep78NFTMetadataKind, + NFTMetadataKind, + NFT_METADATA_KIND, + CEP78Error::MissingNFTMetadataKind +); +simple_storage!( + Cep78IdentifierMode, + NFTIdentifierMode, + IDENTIFIER_MODE, + CEP78Error::MissingIdentifierMode +); +simple_storage!( + Cep78MetadataMutability, + MetadataMutability, + METADATA_MUTABILITY, + CEP78Error::MissingMetadataMutability +); +simple_storage!( + Cep78JsonSchema, + String, + JSON_SCHEMA, + CEP78Error::MissingJsonSchema +); + +#[odra::module] +pub struct Cep78ValidatedMetadata; + +#[odra::module] +impl Cep78ValidatedMetadata { + pub fn set(&self, kind: &NFTMetadataKind, token_id: &String, value: String) { + let dictionary_name = get_metadata_key(kind); + self.env() + .set_dictionary_value(dictionary_name, token_id.as_bytes(), value); + } + + pub fn get(&self, kind: &NFTMetadataKind, token_id: &String) -> String { + let dictionary_name = get_metadata_key(kind); + let env = self.env(); + env.get_dictionary_value(dictionary_name, token_id.as_bytes()) + .unwrap_or_revert_with(&env, CEP78Error::InvalidTokenIdentifier) + } +} + #[odra::module] pub struct Metadata { - requirements: Var, - identifier_mode: Var, - mutability: Var, - json_schema: Var, - validated_metadata: Mapping<(String, String), String> + requirements: SubModule, + identifier_mode: SubModule, + mutability: SubModule, + json_schema: SubModule, + validated_metadata: SubModule, + nft_metadata_kind: SubModule } impl Metadata { @@ -36,7 +92,7 @@ impl Metadata { for required in additional_required_metadata.unwrap_or_default() { requirements.insert(required, Requirement::Required); } - requirements.insert(base_metadata_kind, Requirement::Required); + requirements.insert(base_metadata_kind.clone(), Requirement::Required); // Attempt to parse the provided schema if the `CustomValidated` metadata kind is required or // optional and fail installation if the schema cannot be parsed. @@ -47,7 +103,7 @@ impl Metadata { .unwrap_or_revert(&self.env()); } } - + self.nft_metadata_kind.set(base_metadata_kind); self.requirements.set(requirements); self.identifier_mode.set(identifier_mode); self.mutability.set(metadata_mutability); @@ -55,12 +111,11 @@ impl Metadata { } pub fn get_requirements(&self) -> MetadataRequirement { - self.requirements.get_or_default() + self.requirements.get() } pub fn get_identifier_mode(&self) -> NFTIdentifierMode { - self.identifier_mode - .get_or_revert_with(CEP78Error::InvalidIdentifierMode) + self.identifier_mode.get() } pub fn get_or_revert(&self, token_identifier: &TokenIdentifier) -> String { @@ -71,11 +126,7 @@ impl Metadata { match required { Requirement::Required => { let id = token_identifier.to_string(); - let kind = get_metadata_key(&metadata_kind); - let metadata = self - .validated_metadata - .get(&(kind, id)) - .unwrap_or_revert_with(&env, CEP78Error::InvalidTokenIdentifier); + let metadata = self.validated_metadata.get(&metadata_kind, &id); return metadata; } _ => continue @@ -86,16 +137,11 @@ impl Metadata { // test only pub fn get_metadata_by_kind(&self, token_identifier: String, kind: &NFTMetadataKind) -> String { - let kind = get_metadata_key(kind); - self.validated_metadata - .get(&(kind, token_identifier)) - .unwrap_or_default() + self.validated_metadata.get(kind, &token_identifier) } pub fn ensure_mutability(&self, error: CEP78Error) { - let current_mutability = self - .mutability - .get_or_revert_with(CEP78Error::InvalidMetadataMutability); + let current_mutability = self.mutability.get(); if current_mutability != MetadataMutability::Mutable { self.env().revert(error) } @@ -110,9 +156,8 @@ impl Metadata { let token_metadata_validation = self.validate(&metadata_kind, token_metadata); match token_metadata_validation { Ok(validated_token_metadata) => { - let kind = get_metadata_key(&metadata_kind); self.validated_metadata - .set(&(kind, token_id.to_owned()), validated_token_metadata); + .set(&metadata_kind, token_id, validated_token_metadata); } Err(err) => { self.env().revert(err); @@ -249,11 +294,11 @@ impl Metadata { ); CustomMetadataSchema { properties } } - NFTMetadataKind::CustomValidated => serde_json_wasm::from_str::( - &self.json_schema.get_or_default() - ) - .map_err(|_| CEP78Error::InvalidJsonSchema) - .unwrap_or_revert(&self.env()) + NFTMetadataKind::CustomValidated => { + serde_json_wasm::from_str::(&self.json_schema.get()) + .map_err(|_| CEP78Error::InvalidJsonSchema) + .unwrap_or_revert(&self.env()) + } } } } diff --git a/modules/src/cep78/mod.rs b/modules/src/cep78/mod.rs index 43414c4c..39ded96e 100644 --- a/modules/src/cep78/mod.rs +++ b/modules/src/cep78/mod.rs @@ -11,6 +11,5 @@ mod settings; #[cfg(test)] mod tests; pub mod token; -mod utils; +pub mod utils; mod whitelist; -mod storage; \ No newline at end of file diff --git a/modules/src/cep78/reverse_lookup.rs b/modules/src/cep78/reverse_lookup.rs index 1c461740..624e24f8 100644 --- a/modules/src/cep78/reverse_lookup.rs +++ b/modules/src/cep78/reverse_lookup.rs @@ -2,11 +2,14 @@ use odra::{ args::Maybe, casper_types::{AccessRights, URef}, prelude::*, - Address, Mapping, UnwrapOrRevert, Var + Address, Mapping, SubModule, UnwrapOrRevert }; +use crate::simple_storage; + use super::{ constants::PREFIX_PAGE_DICTIONARY, + constants::{RECEIPT_NAME, REPORTING_MODE}, error::CEP78Error, modalities::{OwnerReverseLookupMode, OwnershipMode, TokenIdentifier} }; @@ -14,14 +17,27 @@ use super::{ // to ease the math around addressing newly minted tokens. pub const PAGE_SIZE: u64 = 1000; +simple_storage!( + Cep78OwnerReverseLookupMode, + OwnerReverseLookupMode, + REPORTING_MODE, + CEP78Error::InvalidReportingMode +); +simple_storage!( + Cep78ReceiptName, + String, + RECEIPT_NAME, + CEP78Error::InvalidReceiptName +); + #[odra::module] pub struct ReverseLookup { - mode: Var, + mode: SubModule, hash_by_index: Mapping, index_by_hash: Mapping, page_table: Mapping>, pages: Mapping<(String, u64, Address), Vec>, - receipt_name: Var + receipt_name: SubModule } impl ReverseLookup { @@ -132,7 +148,7 @@ impl ReverseLookup { let (page_table_entry, _page_uref) = self.add_page_entry_and_page_record(tokens_count, &token_owner, true); - let receipt_name = self.receipt_name.get_or_default(); + let receipt_name = self.receipt_name.get(); let receipt_string = format!("{receipt_name}_m_{PAGE_SIZE}_p_{page_table_entry}"); // TODO: Implement the following // let receipt_address = Key::dictionary(page_uref, owned_tokens_item.as_bytes()); @@ -283,6 +299,6 @@ impl ReverseLookup { #[inline] fn get_mode(&self) -> OwnerReverseLookupMode { - self.mode.get_or_default() + self.mode.get() } } diff --git a/modules/src/cep78/settings.rs b/modules/src/cep78/settings.rs index 19cd0833..2bc1c39b 100644 --- a/modules/src/cep78/settings.rs +++ b/modules/src/cep78/settings.rs @@ -1,22 +1,48 @@ -use odra::prelude::*; -use odra::SubModule; -use odra::UnwrapOrRevert; - use crate::simple_storage; +use odra::{prelude::*, SubModule}; +use super::constants::*; use super::error::CEP78Error; use super::modalities::{BurnMode, EventsMode, MintingMode, NFTHolderMode, NFTKind, OwnershipMode}; -use super::storage::*; simple_storage!(Cep78AllowMinting, bool, ALLOW_MINTING); -simple_storage!(Cep78MintingMode, MintingMode, MINTING_MODE, CEP78Error::MissingMintingMode); -simple_storage!(Cep78OwnershipMode, OwnershipMode, OWNERSHIP_MODE , CEP78Error::MissingOwnershipMode); +simple_storage!( + Cep78MintingMode, + MintingMode, + MINTING_MODE, + CEP78Error::MissingMintingMode +); +simple_storage!( + Cep78OwnershipMode, + OwnershipMode, + OWNERSHIP_MODE, + CEP78Error::MissingOwnershipMode +); simple_storage!(Cep78NFTKind, NFTKind, NFT_KIND, CEP78Error::MissingNftKind); -simple_storage!(Cep78HolderMode, NFTHolderMode, HOLDER_MODE, CEP78Error::MissingHolderMode); -simple_storage!(Cep78BurnMode, BurnMode, BURN_MODE, CEP78Error::MissingBurnMode); -simple_storage!(Cep78EventsMode, EventsMode, EVENTS_MODE, CEP78Error::MissingEventsMode); -simple_storage!(Cep78OperatorBurnMode, bool, OPERATOR_BURN_MODE, CEP78Error::MissingOperatorBurnMode); - +simple_storage!( + Cep78HolderMode, + NFTHolderMode, + HOLDER_MODE, + CEP78Error::MissingHolderMode +); +simple_storage!( + Cep78BurnMode, + BurnMode, + BURN_MODE, + CEP78Error::MissingBurnMode +); +simple_storage!( + Cep78EventsMode, + EventsMode, + EVENTS_MODE, + CEP78Error::MissingEventsMode +); +simple_storage!( + Cep78OperatorBurnMode, + bool, + OPERATOR_BURN_MODE, + CEP78Error::MissingOperatorBurnMode +); #[odra::module] pub struct Settings { diff --git a/modules/src/cep78/storage/mod.rs b/modules/src/cep78/storage/mod.rs deleted file mode 100644 index 65798a04..00000000 --- a/modules/src/cep78/storage/mod.rs +++ /dev/null @@ -1,184 +0,0 @@ -pub const ACL_PACKAGE_MODE: &str = "acl_package_mode"; -pub const ACL_WHITELIST: &str = "acl_whitelist"; -pub const ALLOW_MINTING: &str = "allow_minting"; -pub const APPROVED: &str = "approved"; -pub const BURN_MODE: &str = "burn_mode"; -pub const BURNT_TOKENS: &str = "burnt_tokens"; -pub const COLLECTION_NAME: &str = "collection_name"; -pub const COLLECTION_SYMBOL: &str = "collection_symbol"; -pub const CONTRACT_WHITELIST: &str = "contract_whitelist"; -pub const EVENT_TYPE: &str = "event_type"; -pub const EVENTS: &str = "events"; -pub const EVENTS_MODE: &str = "events_mode"; -pub const HASH_BY_INDEX: &str = "hash_by_index"; -pub const HOLDER_MODE: &str = "holder_mode"; -pub const IDENTIFIER_MODE: &str = "identifier_mode"; -pub const INDEX_BY_HASH: &str = "index_by_hash"; -pub const INSTALLER: &str = "installer"; -pub const JSON_SCHEMA: &str = "json_schema"; -pub const METADATA_CEP78: &str = "metadata_cep78"; -pub const METADATA_CUSTOM_VALIDATED: &str = "metadata_custom_validated"; -pub const METADATA_MUTABILITY: &str = "metadata_mutability"; -pub const METADATA_NFT721: &str = "metadata_nft721"; -pub const METADATA_RAW: &str = "metadata_raw"; -pub const MIGRATION_FLAG: &str = "migration_flag"; -pub const MINTING_MODE: &str = "minting_mode"; -pub const NFT_KIND: &str = "nft_kind"; -pub const NFT_METADATA_KIND: &str = "nft_metadata_kind"; -pub const NFT_METADATA_KINDS: &str = "nft_metadata_kinds"; -pub const NUMBER_OF_MINTED_TOKENS: &str = "number_of_minted_tokens"; -pub const OPERATOR: &str = "operator"; -pub const OPERATORS: &str = "operators"; -pub const OPERATOR_BURN_MODE: &str = "operator_burn_mode"; -pub const OWNED_TOKENS: &str = "owned_tokens"; -pub const OWNER: &str = "owner"; -pub const BURNER: &str = "burner"; -pub const OWNERSHIP_MODE: &str = "ownership_mode"; -pub const PACKAGE_OPERATOR_MODE: &str = "package_operator_mode"; -pub const PAGE_LIMIT: &str = "page_limit"; -pub const PAGE_TABLE: &str = "page_table"; -pub const RECEIPT_NAME: &str = "receipt_name"; -pub const RECIPIENT: &str = "recipient"; -pub const REPORTING_MODE: &str = "reporting_mode"; -pub const RLO_MFLAG: &str = "rlo_mflag"; -pub const SENDER: &str = "sender"; -pub const SPENDER: &str = "spender"; -pub const TOKEN_COUNT: &str = "balances"; -pub const TOKEN_ID: &str = "token_id"; -pub const TOKEN_ISSUERS: &str = "token_issuers"; -pub const TOKEN_OWNERS: &str = "token_owners"; -pub const TOTAL_TOKEN_SUPPLY: &str = "total_token_supply"; -pub const TRANSFER_FILTER_CONTRACT: &str = "transfer_filter_contract"; -pub const TRANSFER_FILTER_CONTRACT_METHOD: &str = "can_transfer"; -pub const UNMATCHED_HASH_COUNT: &str = "unmatched_hash_count"; -pub const WHITELIST_MODE: &str = "whitelist_mode"; - -#[macro_export] -macro_rules! simple_storage { - ($name:ident, $value_ty:ty, $key:ident, $err:expr) => { - #[odra::module] - pub struct $name; - - impl $name { - pub fn set(&self, value: $value_ty) { - self.env().set_named_value($key, value); - } - - pub fn get(&self) -> $value_ty { - self.env() - .get_named_value($key) - .unwrap_or_revert_with(&self.env(), $err) - } - } - }; - ($name:ident, $value_ty:ty, $key:ident) => { - #[odra::module] - pub struct $name; - - impl $name { - pub fn set(&self, value: $value_ty) { - self.env().set_named_value($key, value); - } - - pub fn get(&self) -> Option<$value_ty> { - self.env().get_named_value($key) - } - } - }; -} - -#[macro_export] -macro_rules! compound_key_storage { - ($name:ident, $dict:expr, $k1_type:ty, $k2_type:ty, $value_type:ty) => { - #[odra::module] - pub struct $name; - - impl $name { - pub fn set(&self, key1: &$k1_type, key2: &$k2_type, value: $value_type) { - let env = self.env(); - let parts = [ - key1.to_bytes().unwrap_or_revert(&env), - key2.to_bytes().unwrap_or_revert(&env), - ]; - let key = crate::cep78::storage::compound_key(&env, &parts); - env.set_dictionary_value($dict, &key, value); - } - - pub fn get_or_default(&self, key1: &$k1_type, key2: &$k2_type) -> $value_type { - let env = self.env(); - let parts = [ - key1.to_bytes().unwrap_or_revert(&env), - key2.to_bytes().unwrap_or_revert(&env), - ]; - let key = crate::cep78::storage::compound_key(&env, &parts); - env.get_dictionary_value($dict, &key).unwrap_or_default() - } - } - }; - ($name:ident, $dict:expr, $k1_type:ty, $value_type:ty) => { - compound_key_storage!( - $name, - $dict, - $k1_type, - $k1_type, - $value_type - ); - } -} - -#[macro_export] -macro_rules! encoded_key_value_storage { - ($name:ident, $dict:expr, $key:ty, $value_type:ty) => { - #[odra::module] - pub struct $name; - - impl $name { - pub fn set(&self, key: &$key, value: $value_type) { - let env = self.env(); - let preimage = key.to_bytes().unwrap_or_revert(&env); - let key = BASE64_STANDARD.encode(preimage); - env.set_dictionary_value($dict, key.as_bytes(), value); - } - - pub fn get(&self, key: &$key) -> Option<$value_type> { - let env = self.env(); - let preimage = key.to_bytes().unwrap_or_revert(&env); - let key = BASE64_STANDARD.encode(preimage); - env.get_dictionary_value($dict, key.as_bytes()) - } - } - }; -} - -#[macro_export] -macro_rules! basic_key_value_storage { - ($name:ident, $dict:expr, $value_type:ty) => { - #[odra::module] - pub struct $name; - - impl $name { - pub fn set(&self, key: &str, value: $value_type) { - self.env().set_dictionary_value($dict, key.as_bytes(), value); - } - - pub fn get(&self, key: &str) -> Option<$value_type> { - self.env().get_dictionary_value($dict, key.as_bytes()) - } - } - }; -} - -pub fn compound_key(env: &odra::ContractEnv, parts: &[odra::prelude::Vec]) -> [u8; 64] { - use odra::casper_types::bytesrepr::ToBytes; - use odra::UnwrapOrRevert; - - let mut result = [0u8; 64]; - let mut preimage = odra::prelude::Vec::new(); - for part in parts { - preimage.append(&mut part.to_bytes().unwrap_or_revert(env)); - } - - let key_bytes = env.hash(&preimage); - odra::utils::hex_to_slice(&key_bytes, &mut result); - result -} \ No newline at end of file diff --git a/modules/src/cep78/tests/mod.rs b/modules/src/cep78/tests/mod.rs index c652ca8b..50c3188d 100644 --- a/modules/src/cep78/tests/mod.rs +++ b/modules/src/cep78/tests/mod.rs @@ -1,11 +1,10 @@ use alloc::collections::BTreeMap; use once_cell::sync::Lazy; -use self::utils::InitArgsBuilder; - use super::{ metadata::{CustomMetadataSchema, MetadataSchemaProperty}, - modalities::NFTMetadataKind + modalities::NFTMetadataKind, + utils::InitArgsBuilder }; mod acl; diff --git a/modules/src/cep78/tests/utils.rs b/modules/src/cep78/tests/utils.rs index 2e29662d..f9a8c319 100644 --- a/modules/src/cep78/tests/utils.rs +++ b/modules/src/cep78/tests/utils.rs @@ -1,185 +1,12 @@ -use crate::cep78::{ - modalities::{ - BurnMode, EventsMode, MetadataMutability, MintingMode, NFTHolderMode, NFTIdentifierMode, - NFTKind, NFTMetadataKind, OwnerReverseLookupMode, OwnershipMode, WhitelistMode - }, - token::Cep78InitArgs -}; use blake2::{digest::VariableOutput, Blake2bVar}; use odra::{ - args::Maybe, casper_types::{BLAKE2B_DIGEST_LENGTH, U512}, host::HostEnv, prelude::*, - Address, DeployReport + DeployReport }; use std::io::Write; -#[derive(Default)] -pub struct InitArgsBuilder { - collection_name: String, - collection_symbol: String, - total_token_supply: u64, - allow_minting: Maybe, - minting_mode: Maybe, - ownership_mode: OwnershipMode, - nft_kind: NFTKind, - holder_mode: Maybe, - whitelist_mode: Maybe, - acl_white_list: Maybe>, - json_schema: Maybe, - receipt_name: Maybe, - identifier_mode: NFTIdentifierMode, - burn_mode: Maybe, - operator_burn_mode: Maybe, - nft_metadata_kind: NFTMetadataKind, - metadata_mutability: MetadataMutability, - owner_reverse_lookup_mode: Maybe, - events_mode: Maybe, - transfer_filter_contract_contract_key: Maybe
, - additional_required_metadata: Maybe>, - optional_metadata: Maybe> -} - -impl InitArgsBuilder { - pub fn collection_name(mut self, collection_name: String) -> Self { - self.collection_name = collection_name; - self - } - - pub fn collection_symbol(mut self, collection_symbol: String) -> Self { - self.collection_symbol = collection_symbol; - self - } - - pub fn total_token_supply(mut self, total_token_supply: u64) -> Self { - self.total_token_supply = total_token_supply; - self - } - - pub fn allow_minting(mut self, allow_minting: bool) -> Self { - self.allow_minting = Maybe::Some(allow_minting); - self - } - - pub fn minting_mode(mut self, minting_mode: MintingMode) -> Self { - self.minting_mode = Maybe::Some(minting_mode); - self - } - - pub fn ownership_mode(mut self, ownership_mode: OwnershipMode) -> Self { - self.ownership_mode = ownership_mode; - self - } - - pub fn holder_mode(mut self, holder_mode: NFTHolderMode) -> Self { - self.holder_mode = Maybe::Some(holder_mode); - self - } - - pub fn whitelist_mode(mut self, whitelist_mode: WhitelistMode) -> Self { - self.whitelist_mode = Maybe::Some(whitelist_mode); - self - } - - pub fn acl_white_list(mut self, acl_white_list: Vec
) -> Self { - self.acl_white_list = Maybe::Some(acl_white_list); - self - } - - pub fn json_schema(mut self, json_schema: String) -> Self { - self.json_schema = Maybe::Some(json_schema); - self - } - - pub fn identifier_mode(mut self, identifier_mode: NFTIdentifierMode) -> Self { - self.identifier_mode = identifier_mode; - self - } - - pub fn burn_mode(mut self, burn_mode: BurnMode) -> Self { - self.burn_mode = Maybe::Some(burn_mode); - self - } - - pub fn operator_burn_mode(mut self, operator_burn_mode: bool) -> Self { - self.operator_burn_mode = Maybe::Some(operator_burn_mode); - self - } - - pub fn nft_metadata_kind(mut self, nft_metadata_kind: NFTMetadataKind) -> Self { - self.nft_metadata_kind = nft_metadata_kind; - self - } - - pub fn metadata_mutability(mut self, metadata_mutability: MetadataMutability) -> Self { - self.metadata_mutability = metadata_mutability; - self - } - - pub fn owner_reverse_lookup_mode( - mut self, - owner_reverse_lookup_mode: OwnerReverseLookupMode - ) -> Self { - self.owner_reverse_lookup_mode = Maybe::Some(owner_reverse_lookup_mode); - self - } - - pub fn events_mode(mut self, events_mode: EventsMode) -> Self { - self.events_mode = Maybe::Some(events_mode); - self - } - - pub fn transfer_filter_contract_contract_key( - mut self, - transfer_filter_contract_contract_key: Address - ) -> Self { - self.transfer_filter_contract_contract_key = - Maybe::Some(transfer_filter_contract_contract_key); - self - } - - pub fn additional_required_metadata( - mut self, - additional_required_metadata: Vec - ) -> Self { - self.additional_required_metadata = Maybe::Some(additional_required_metadata); - self - } - - pub fn optional_metadata(mut self, optional_metadata: Vec) -> Self { - self.optional_metadata = Maybe::Some(optional_metadata); - self - } - - pub fn build(self) -> Cep78InitArgs { - Cep78InitArgs { - collection_name: self.collection_name, - collection_symbol: self.collection_symbol, - total_token_supply: self.total_token_supply, - allow_minting: self.allow_minting, - minting_mode: self.minting_mode, - ownership_mode: self.ownership_mode, - nft_kind: self.nft_kind, - holder_mode: self.holder_mode, - whitelist_mode: self.whitelist_mode, - acl_white_list: self.acl_white_list, - json_schema: self.json_schema, - receipt_name: self.receipt_name, - nft_identifier_mode: self.identifier_mode, - burn_mode: self.burn_mode, - operator_burn_mode: self.operator_burn_mode, - nft_metadata_kind: self.nft_metadata_kind, - metadata_mutability: self.metadata_mutability, - owner_reverse_lookup_mode: self.owner_reverse_lookup_mode, - events_mode: self.events_mode, - transfer_filter_contract_contract: self.transfer_filter_contract_contract_key, - additional_required_metadata: self.additional_required_metadata, - optional_metadata: self.optional_metadata - } - } -} - pub const TEST_PRETTY_721_META_DATA: &str = r#"{ "name": "John Doe", "symbol": "abc", diff --git a/modules/src/cep78/token.rs b/modules/src/cep78/token.rs index 811fce51..3a01357b 100644 --- a/modules/src/cep78/token.rs +++ b/modules/src/cep78/token.rs @@ -1,5 +1,8 @@ #![allow(clippy::too_many_arguments)] +use crate::simple_storage; + use super::{ + constants::TRANSFER_FILTER_CONTRACT, data::CollectionData, error::CEP78Error, events::{ @@ -18,12 +21,18 @@ use super::{ }; use odra::{ args::Maybe, casper_types::bytesrepr::ToBytes, prelude::*, Address, OdraError, SubModule, - UnwrapOrRevert, Var + UnwrapOrRevert }; type MintReceipt = (String, Address, String); type TransferReceipt = (String, Address); +simple_storage!( + Cep78TransferFilterContract, + Address, + TRANSFER_FILTER_CONTRACT +); + /// CEP-78 is a standard for non-fungible tokens (NFTs) on the Casper network. /// It defines a set of interfaces that allow for the creation, management, and /// transfer of NFTs. The standard is designed to be flexible and modular, allowing @@ -48,7 +57,7 @@ pub struct Cep78 { settings: SubModule, whitelist: SubModule, reverse_lookup: SubModule, - transfer_filter_contract: Var
+ transfer_filter_contract: SubModule } #[odra::module] @@ -61,16 +70,16 @@ impl Cep78 { total_token_supply: u64, ownership_mode: OwnershipMode, nft_kind: NFTKind, - nft_identifier_mode: NFTIdentifierMode, + identifier_mode: NFTIdentifierMode, nft_metadata_kind: NFTMetadataKind, metadata_mutability: MetadataMutability, + receipt_name: String, allow_minting: Maybe, minting_mode: Maybe, holder_mode: Maybe, whitelist_mode: Maybe, acl_white_list: Maybe>, json_schema: Maybe, - receipt_name: Maybe, burn_mode: Maybe, operator_burn_mode: Maybe, owner_reverse_lookup_mode: Maybe, @@ -100,7 +109,7 @@ impl Cep78 { self.revert(CEP78Error::EmptyACLWhitelist) } - if nft_identifier_mode == NFTIdentifierMode::Hash + if identifier_mode == NFTIdentifierMode::Hash && metadata_mutability == MetadataMutability::Mutable { self.revert(CEP78Error::InvalidMetadataMutability) @@ -149,7 +158,7 @@ impl Cep78 { ); self.reverse_lookup - .init(owner_reverse_lookup_mode, receipt_name.unwrap_or_default()); + .init(owner_reverse_lookup_mode, receipt_name); self.whitelist.init(acl_white_list.clone(), whitelist_mode); @@ -158,7 +167,7 @@ impl Cep78 { additional_required_metadata, optional_metadata, metadata_mutability, - nft_identifier_mode, + identifier_mode, json_schema ); diff --git a/modules/src/cep78/utils.rs b/modules/src/cep78/utils.rs index 735917e9..a8d4e90d 100644 --- a/modules/src/cep78/utils.rs +++ b/modules/src/cep78/utils.rs @@ -1,4 +1,12 @@ #![allow(dead_code)] +#[cfg(not(target_arch = "wasm32"))] +use crate::cep78::{ + modalities::{ + BurnMode, EventsMode, MetadataMutability, MintingMode, NFTHolderMode, NFTIdentifierMode, + NFTKind, NFTMetadataKind, OwnerReverseLookupMode, OwnershipMode, WhitelistMode + }, + token::Cep78InitArgs +}; use odra::{args::Maybe, prelude::*, Address, Var}; #[odra::module] @@ -145,3 +153,180 @@ trait NftContract { fn approve(&mut self, spender: Address, token_id: Maybe, token_hash: Maybe); fn revoke(&mut self, token_id: Maybe, token_hash: Maybe); } + +#[cfg(not(target_arch = "wasm32"))] +#[derive(Default)] +pub struct InitArgsBuilder { + collection_name: String, + collection_symbol: String, + total_token_supply: u64, + allow_minting: Maybe, + minting_mode: Maybe, + ownership_mode: OwnershipMode, + nft_kind: NFTKind, + receipt_name: String, + holder_mode: Maybe, + whitelist_mode: Maybe, + acl_white_list: Maybe>, + json_schema: Maybe, + identifier_mode: NFTIdentifierMode, + burn_mode: Maybe, + operator_burn_mode: Maybe, + nft_metadata_kind: NFTMetadataKind, + metadata_mutability: MetadataMutability, + owner_reverse_lookup_mode: Maybe, + events_mode: Maybe, + transfer_filter_contract_contract_key: Maybe
, + additional_required_metadata: Maybe>, + optional_metadata: Maybe> +} + +#[cfg(not(target_arch = "wasm32"))] +impl InitArgsBuilder { + pub fn collection_name(mut self, collection_name: String) -> Self { + self.collection_name = collection_name; + self + } + + pub fn collection_symbol(mut self, collection_symbol: String) -> Self { + self.collection_symbol = collection_symbol; + self + } + + pub fn total_token_supply(mut self, total_token_supply: u64) -> Self { + self.total_token_supply = total_token_supply; + self + } + + pub fn allow_minting(mut self, allow_minting: bool) -> Self { + self.allow_minting = Maybe::Some(allow_minting); + self + } + + pub fn nft_kind(mut self, nft_kind: NFTKind) -> Self { + self.nft_kind = nft_kind; + self + } + + pub fn minting_mode(mut self, minting_mode: MintingMode) -> Self { + self.minting_mode = Maybe::Some(minting_mode); + self + } + + pub fn ownership_mode(mut self, ownership_mode: OwnershipMode) -> Self { + self.ownership_mode = ownership_mode; + self + } + + pub fn holder_mode(mut self, holder_mode: NFTHolderMode) -> Self { + self.holder_mode = Maybe::Some(holder_mode); + self + } + + pub fn whitelist_mode(mut self, whitelist_mode: WhitelistMode) -> Self { + self.whitelist_mode = Maybe::Some(whitelist_mode); + self + } + + pub fn acl_white_list(mut self, acl_white_list: Vec
) -> Self { + self.acl_white_list = Maybe::Some(acl_white_list); + self + } + + pub fn json_schema(mut self, json_schema: String) -> Self { + self.json_schema = Maybe::Some(json_schema); + self + } + + pub fn identifier_mode(mut self, identifier_mode: NFTIdentifierMode) -> Self { + self.identifier_mode = identifier_mode; + self + } + + pub fn receipt_name(mut self, receipt_name: String) -> Self { + self.receipt_name = receipt_name; + self + } + + pub fn burn_mode(mut self, burn_mode: BurnMode) -> Self { + self.burn_mode = Maybe::Some(burn_mode); + self + } + + pub fn operator_burn_mode(mut self, operator_burn_mode: bool) -> Self { + self.operator_burn_mode = Maybe::Some(operator_burn_mode); + self + } + + pub fn nft_metadata_kind(mut self, nft_metadata_kind: NFTMetadataKind) -> Self { + self.nft_metadata_kind = nft_metadata_kind; + self + } + + pub fn metadata_mutability(mut self, metadata_mutability: MetadataMutability) -> Self { + self.metadata_mutability = metadata_mutability; + self + } + + pub fn owner_reverse_lookup_mode( + mut self, + owner_reverse_lookup_mode: OwnerReverseLookupMode + ) -> Self { + self.owner_reverse_lookup_mode = Maybe::Some(owner_reverse_lookup_mode); + self + } + + pub fn events_mode(mut self, events_mode: EventsMode) -> Self { + self.events_mode = Maybe::Some(events_mode); + self + } + + pub fn transfer_filter_contract_contract_key( + mut self, + transfer_filter_contract_contract_key: Address + ) -> Self { + self.transfer_filter_contract_contract_key = + Maybe::Some(transfer_filter_contract_contract_key); + self + } + + pub fn additional_required_metadata( + mut self, + additional_required_metadata: Vec + ) -> Self { + self.additional_required_metadata = Maybe::Some(additional_required_metadata); + self + } + + pub fn optional_metadata(mut self, optional_metadata: Vec) -> Self { + self.optional_metadata = Maybe::Some(optional_metadata); + self + } + + pub fn build(self) -> Cep78InitArgs { + Cep78InitArgs { + collection_name: self.collection_name, + collection_symbol: self.collection_symbol, + total_token_supply: self.total_token_supply, + allow_minting: self.allow_minting, + minting_mode: self.minting_mode, + ownership_mode: self.ownership_mode, + nft_kind: self.nft_kind, + holder_mode: self.holder_mode, + whitelist_mode: self.whitelist_mode, + acl_white_list: self.acl_white_list, + json_schema: self.json_schema, + receipt_name: self.receipt_name, + identifier_mode: self.identifier_mode, + burn_mode: self.burn_mode, + operator_burn_mode: self.operator_burn_mode, + nft_metadata_kind: self.nft_metadata_kind, + metadata_mutability: self.metadata_mutability, + owner_reverse_lookup_mode: self.owner_reverse_lookup_mode, + events_mode: self.events_mode, + transfer_filter_contract_contract: self.transfer_filter_contract_contract_key, + additional_required_metadata: self.additional_required_metadata, + optional_metadata: self.optional_metadata + } + } +} diff --git a/modules/src/cep78/whitelist.rs b/modules/src/cep78/whitelist.rs index a39a63ab..0a154a96 100644 --- a/modules/src/cep78/whitelist.rs +++ b/modules/src/cep78/whitelist.rs @@ -1,17 +1,39 @@ -use odra::{args::Maybe, prelude::*, Address, Var}; +use odra::{args::Maybe, prelude::*, Address, SubModule}; -use super::{error::CEP78Error, modalities::WhitelistMode}; +use crate::{basic_key_value_storage, simple_storage}; + +use super::{ + constants::{ACL_PACKAGE_MODE, ACL_WHITELIST, WHITELIST_MODE}, + error::CEP78Error, + modalities::WhitelistMode +}; + +simple_storage!( + Cep78WhitelistMode, + WhitelistMode, + WHITELIST_MODE, + CEP78Error::InvalidACLPackageMode +); +simple_storage!( + Cep78PackageMode, + bool, + ACL_PACKAGE_MODE, + CEP78Error::InvalidACLPackageMode +); +basic_key_value_storage!(Cep78ACLWhitelist, ACL_WHITELIST, bool); #[odra::module] pub struct ACLWhitelist { - addresses: Var>, - mode: Var, - package_mode: Var + whitelist: SubModule, + mode: SubModule, + package_mode: SubModule } impl ACLWhitelist { pub fn init(&mut self, addresses: Vec
, mode: WhitelistMode) { - self.addresses.set(addresses); + for address in addresses.iter() { + self.whitelist.set(&address.to_string(), true); + } self.mode.set(mode); // Odra does not support version mode. self.package_mode.set(true); @@ -19,12 +41,12 @@ impl ACLWhitelist { #[inline] pub fn get_mode(&self) -> WhitelistMode { - self.mode.get_or_default() + self.mode.get() } #[inline] pub fn is_whitelisted(&self, address: &Address) -> bool { - self.addresses.get_or_default().contains(address) + self.whitelist.get(&address.to_string()).unwrap_or_default() } pub fn update(&mut self, new_addresses: Maybe>) { @@ -32,7 +54,9 @@ impl ACLWhitelist { if !new_addresses.is_empty() { match self.get_mode() { WhitelistMode::Unlocked => { - self.addresses.set(new_addresses); + for address in new_addresses.iter() { + self.whitelist.set(&address.to_string(), true); + } } WhitelistMode::Locked => self.env().revert(CEP78Error::InvalidWhitelistMode) } diff --git a/modules/src/lib.rs b/modules/src/lib.rs index 3fe54d85..de1036cd 100644 --- a/modules/src/lib.rs +++ b/modules/src/lib.rs @@ -6,9 +6,9 @@ extern crate alloc; pub mod access; -pub mod cep78; pub mod cep18; pub mod cep18_token; +pub mod cep78; pub mod erc1155; pub mod erc1155_receiver; pub mod erc1155_token; @@ -17,4 +17,5 @@ pub mod erc721; pub mod erc721_receiver; pub mod erc721_token; pub mod security; +mod storage; pub mod wrapped_native; diff --git a/modules/src/storage.rs b/modules/src/storage.rs new file mode 100644 index 00000000..951e6e6e --- /dev/null +++ b/modules/src/storage.rs @@ -0,0 +1,135 @@ +#[macro_export] +macro_rules! simple_storage { + ($name:ident, $value_ty:ty, $key:ident, $err:expr) => { + #[odra::module] + pub struct $name; + + impl $name { + pub fn set(&self, value: $value_ty) { + self.env().set_named_value($key, value); + } + + pub fn get(&self) -> $value_ty { + use odra::UnwrapOrRevert; + self.env() + .get_named_value($key) + .unwrap_or_revert_with(&self.env(), $err) + } + } + }; + ($name:ident, $value_ty:ty, $key:ident) => { + #[odra::module] + pub struct $name; + + impl $name { + pub fn set(&self, value: $value_ty) { + self.env().set_named_value($key, value); + } + + pub fn get(&self) -> Option<$value_ty> { + self.env().get_named_value($key) + } + } + }; +} + +#[macro_export] +macro_rules! compound_key_value_storage { + ($name:ident, $dict:expr, $k1_type:ty, $k2_type:ty, $value_type:ty) => { + #[odra::module] + pub struct $name; + + impl $name { + pub fn set(&self, key1: &$k1_type, key2: &$k2_type, value: $value_type) { + use odra::UnwrapOrRevert; + + let env = self.env(); + let parts = [ + key1.to_bytes().unwrap_or_revert(&env), + key2.to_bytes().unwrap_or_revert(&env) + ]; + let key = crate::storage::compound_key(&env, &parts); + env.set_dictionary_value($dict, &key, value); + } + + pub fn get_or_default(&self, key1: &$k1_type, key2: &$k2_type) -> $value_type { + use odra::UnwrapOrRevert; + + let env = self.env(); + let parts = [ + key1.to_bytes().unwrap_or_revert(&env), + key2.to_bytes().unwrap_or_revert(&env) + ]; + let key = crate::storage::compound_key(&env, &parts); + env.get_dictionary_value($dict, &key).unwrap_or_default() + } + } + }; + ($name:ident, $dict:expr, $k1_type:ty, $value_type:ty) => { + compound_key_value_storage!($name, $dict, $k1_type, $k1_type, $value_type); + }; +} + +#[macro_export] +macro_rules! encoded_key_value_storage { + ($name:ident, $dict:expr, $key:ty, $value_type:ty) => { + #[odra::module] + pub struct $name; + + impl $name { + pub fn set(&self, key: &$key, value: $value_type) { + let env = self.env(); + let encoded_key = Self::key(&env, key); + env.set_dictionary_value($dict, encoded_key.as_bytes(), value); + } + + pub fn get(&self, key: &$key) -> Option<$value_type> { + let env = self.env(); + let encoded_key = Self::key(&env, key); + env.get_dictionary_value($dict, encoded_key.as_bytes()) + } + + fn key(env: &odra::ContractEnv, key: &$key) -> String { + use base64::prelude::{Engine, BASE64_STANDARD}; + use odra::UnwrapOrRevert; + + let preimage = key.to_bytes().unwrap_or_revert(&env); + BASE64_STANDARD.encode(preimage) + } + } + }; +} + +#[macro_export] +macro_rules! basic_key_value_storage { + ($name:ident, $dict:expr, $value_type:ty) => { + #[odra::module] + pub struct $name; + + impl $name { + pub fn set(&self, key: &str, value: $value_type) { + self.env() + .set_dictionary_value($dict, key.as_bytes(), value); + } + + pub fn get(&self, key: &str) -> Option<$value_type> { + self.env().get_dictionary_value($dict, key.as_bytes()) + } + } + }; +} + +pub(crate) fn compound_key(env: &odra::ContractEnv, parts: &[odra::prelude::Vec]) -> [u8; 64] { + use odra::casper_types::bytesrepr::ToBytes; + use odra::UnwrapOrRevert; + + let mut result = [0u8; 64]; + let mut preimage = odra::prelude::Vec::new(); + for part in parts { + preimage.append(&mut part.to_bytes().unwrap_or_revert(env)); + } + + let key_bytes = env.hash(&preimage); + odra::utils::hex_to_slice(&key_bytes, &mut result); + result +} diff --git a/odra-casper/livenet-env/src/livenet_contract_env.rs b/odra-casper/livenet-env/src/livenet_contract_env.rs index fd48db58..dfa14c05 100644 --- a/odra-casper/livenet-env/src/livenet_contract_env.rs +++ b/odra-casper/livenet-env/src/livenet_contract_env.rs @@ -39,7 +39,7 @@ impl ContractContext for LivenetContractEnv { panic!("Cannot set named value in LivenetEnv without a deploy") } - fn get_dictionary_value(&self, dictionary_name: &str, key: &str) -> Option { + fn get_dictionary_value(&self, dictionary_name: &str, key: &[u8]) -> Option { self.casper_client.borrow().get_dictionary_value( self.callstack.borrow().current().address(), dictionary_name, @@ -47,7 +47,7 @@ impl ContractContext for LivenetContractEnv { ) } - fn set_dictionary_value(&self, _dictionary_name: &str, _key: &str, _value: CLValue) { + fn set_dictionary_value(&self, _dictionary_name: &str, _key: &[u8], _value: CLValue) { panic!("Cannot set dictionary value in LivenetEnv without a deploy") } diff --git a/odra-casper/rpc-client/src/casper_client.rs b/odra-casper/rpc-client/src/casper_client.rs index bcc5de43..213cea5d 100644 --- a/odra-casper/rpc-client/src/casper_client.rs +++ b/odra-casper/rpc-client/src/casper_client.rs @@ -126,9 +126,10 @@ impl CasperClient { &self, address: &Address, dictionary_name: &str, - key: &str + key: &[u8] ) -> Option { - self.query_dict_bytes(address, dictionary_name.to_string(), key.to_string()) + let key = String::from_utf8(key.to_vec()).unwrap(); + self.query_dict_bytes(address, dictionary_name.to_string(), key) .ok() } From 5aadae96b71584616f1d5b1f18acc0023c45af07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Zieli=C5=84ski?= Date: Mon, 6 May 2024 13:37:27 +0200 Subject: [PATCH 27/38] Allow token id as hash + mutable metadata. (#421) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Krzysztof Pobiarżyn --- .DS_Store | Bin 8196 -> 0 bytes .gitignore | 3 ++- modules/src/cep78/tests/metadata.rs | 7 ++----- modules/src/cep78/token.rs | 12 +++++++----- 4 files changed, 11 insertions(+), 11 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 8b5658987d56dc9efe5fa994d8ffceb376310072..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHMQBTuQ6h0RQ>lnfwG~q#$CceT%R3hICYVi;(Q8f@3XDqTy~j%f&F&-x$y z3%>eG{29Lcq~E=_BfVYm(U|OB(tFz5@9XLL&gpIMAR<=pJ9mkeh{(XkZKaGZr}6WA z8*5X^$W2%VJ~jQYZTq2(Cn0$KsBfL1^&@INSkcQ$Xv74Lm{R`*%~t-wSo zz~=`O8@IK@4y6@S2Rg9?fGuEHH0+B{e^`?bz}6BwlolgsOj2Pbl_g6ImaH6@q~m-o z!4IXCbYj9Yu9J9X$qI!f3lC-rII)_vy4MP51;!QNYxe?G>4-dv)%833g~#jSw`hV` z9O0-#eX>>b)KLaKcZv3J^?*X^k*jiO6P%wJ4<2>N0>h;!gZ}n+v^|JUl3yRb+b6yk z#jC47*wl1kX0~V)jk58s)d>f!*oym6-RiyJM|)xD+oOHUe(AY`gUa=-Fo-QLaC?H_ zIW8b?UwMHO4(ee)a3aBN$rDD&C>>Om&d%00)|bt-hY!w|&9jZQ)n)Vkz4i0+l5ykq zokzO|$9^{mKg$HLWD=umnS^}7F;_XI!D*`(_+bEb`CLkKv;}o~&?f}WN8hI7j80IM zswq`fP7F?${0s-$Dx7)vd4l{rr^nQwr?dlG1{f=i2;L4@d$dbUs?$F9n&K*X zY#wj=P78=8r%re!DmVkx@ew+7=>!^#RSK`g4E_TuHlmmgp)D9{6ub`UYf7;KFcxa| zD1+lFrEx_l(?rA#MA)K+iWb_a=&sBct3}#R==3(()U^B*+gFV zMHTPS>%2P^GhUiCi>u3L)6LGMLdT1gjwN8-q<$^2pEuj388bkA5?Re4g<>kQ4=k_q zERyS!Mxx|&n^*;=W#mg`*v8er|4*zwI&iJPBq+cN+pX<7A{~8}W`pKyZ4=vjY`k&Y vP+CDjCzj)|VmS_b`42 Date: Mon, 6 May 2024 20:54:03 +0200 Subject: [PATCH 28/38] fmt, add remove_dictionary to ContractEnv --- benchmark/src/storage.rs | 4 +- core/src/contract_context.rs | 6 ++ core/src/contract_env.rs | 7 +- modules/src/cep78/data.rs | 49 ++++++----- modules/src/cep78/metadata.rs | 19 ++-- modules/src/cep78/mod.rs | 6 +- modules/src/cep78/reverse_lookup.rs | 6 +- modules/src/cep78/settings.rs | 23 +++-- modules/src/cep78/token.rs | 14 +-- modules/src/cep78/whitelist.rs | 19 +++- modules/src/storage.rs | 86 +++++++++++-------- .../livenet-env/src/livenet_contract_env.rs | 4 + odra-casper/wasm-env/src/host_functions.rs | 6 ++ odra-casper/wasm-env/src/wasm_contract_env.rs | 4 + odra-vm/src/odra_vm_contract_env.rs | 4 + odra-vm/src/vm/odra_vm.rs | 12 ++- odra-vm/src/vm/odra_vm_state.rs | 5 ++ odra-vm/src/vm/storage.rs | 86 +++++++++++++++++-- 18 files changed, 253 insertions(+), 107 deletions(-) diff --git a/benchmark/src/storage.rs b/benchmark/src/storage.rs index c06285ee..41b52926 100644 --- a/benchmark/src/storage.rs +++ b/benchmark/src/storage.rs @@ -38,13 +38,13 @@ impl DictionaryStorage { /// Sets the value. pub fn set(&self, key: String, value: U256) { self.env() - .set_dictionary_value(DICT_KEY, self.key(key), value); + .set_dictionary_value(DICT_KEY, self.key(key).as_bytes(), value); } /// Gets the value. pub fn get_or_default(&self, key: String) -> U256 { self.env() - .get_dictionary_value(DICT_KEY, self.key(key)) + .get_dictionary_value(DICT_KEY, self.key(key).as_bytes()) .unwrap_or_default() } diff --git a/core/src/contract_context.rs b/core/src/contract_context.rs index fe459966..51f26432 100644 --- a/core/src/contract_context.rs +++ b/core/src/contract_context.rs @@ -60,6 +60,12 @@ pub trait ContractContext { /// * `value` - The value to set. fn set_dictionary_value(&self, dictionary_name: &str, key: &[u8], value: CLValue); + /// Removes the named key from the storage. + /// + /// # Arguments + /// * `dictionary_name` - The name of the dictionary. + fn remove_dictionary(&self, dictionary_name: &str); + /// Retrieves the address of the caller. fn caller(&self) -> Address; diff --git a/core/src/contract_env.rs b/core/src/contract_env.rs index b3a6732e..c5392c7a 100644 --- a/core/src/contract_env.rs +++ b/core/src/contract_env.rs @@ -129,7 +129,6 @@ impl ContractEnv { value: T ) { let dictionary_name = dictionary_name.as_ref(); - let key = key.as_ref(); let cl_value = CLValue::from_t(value) .map_err(|_| Formatting) .unwrap_or_revert(self); @@ -138,6 +137,12 @@ impl ContractEnv { .set_dictionary_value(dictionary_name, key, cl_value); } + /// Removes the dictionary from the contract storage. + pub fn remove_dictionary>(&self, dictionary_name: U) { + let dictionary_name = dictionary_name.as_ref(); + self.backend.borrow().remove_dictionary(dictionary_name); + } + /// Returns the address of the caller of the contract. pub fn caller(&self) -> Address { let backend = self.backend.borrow(); diff --git a/modules/src/cep78/data.rs b/modules/src/cep78/data.rs index e3b94cf0..d2b6fffa 100644 --- a/modules/src/cep78/data.rs +++ b/modules/src/cep78/data.rs @@ -1,32 +1,31 @@ -use crate::basic_key_value_storage; -use crate::compound_key_value_storage; -use crate::encoded_key_value_storage; -use crate::simple_storage; +use crate::{ + base64_encoded_key_value_storage, compound_key_value_storage, key_value_storage, + single_value_storage +}; use odra::{casper_types::bytesrepr::ToBytes, prelude::*, Address, SubModule, UnwrapOrRevert}; -use super::constants; use super::constants::*; use super::error::CEP78Error; -simple_storage!( +single_value_storage!( Cep78CollectionName, String, COLLECTION_NAME, CEP78Error::MissingCollectionName ); -simple_storage!( +single_value_storage!( Cep78CollectionSymbol, String, COLLECTION_SYMBOL, CEP78Error::MissingCollectionName ); -simple_storage!( +single_value_storage!( Cep78TotalSupply, u64, TOTAL_TOKEN_SUPPLY, CEP78Error::MissingTotalTokenSupply ); -simple_storage!(Cep78TokenCounter, u64, NUMBER_OF_MINTED_TOKENS); +single_value_storage!(Cep78TokenCounter, u64, NUMBER_OF_MINTED_TOKENS); impl Cep78TokenCounter { pub fn add(&mut self, value: u64) { match self.get() { @@ -35,18 +34,18 @@ impl Cep78TokenCounter { } } } -simple_storage!( +single_value_storage!( Cep78Installer, Address, INSTALLER, CEP78Error::MissingInstaller ); compound_key_value_storage!(Cep78Operators, OPERATORS, Address, bool); -basic_key_value_storage!(Cep78Owners, TOKEN_OWNERS, Address); -basic_key_value_storage!(Cep78Issuers, TOKEN_ISSUERS, Address); -basic_key_value_storage!(Cep78BurntTokens, BURNT_TOKENS, ()); -encoded_key_value_storage!(Cep78TokenCount, TOKEN_COUNT, Address, u64); -basic_key_value_storage!(Cep78Approved, APPROVED, Option
); +key_value_storage!(Cep78Owners, TOKEN_OWNERS, Address); +key_value_storage!(Cep78Issuers, TOKEN_ISSUERS, Address); +key_value_storage!(Cep78BurntTokens, BURNT_TOKENS, ()); +base64_encoded_key_value_storage!(Cep78TokenCount, TOKEN_COUNT, Address, u64); +key_value_storage!(Cep78Approved, APPROVED, Option
); #[odra::module] pub struct CollectionData { @@ -75,7 +74,7 @@ impl CollectionData { self.env().revert(CEP78Error::CannotInstallWithZeroSupply) } - if total_token_supply > constants::MAX_TOTAL_TOKEN_SUPPLY { + if total_token_supply > MAX_TOTAL_TOKEN_SUPPLY { self.env().revert(CEP78Error::ExceededMaxTotalSupply) } @@ -116,12 +115,12 @@ impl CollectionData { } #[inline] - pub fn set_owner(&mut self, token_id: &String, token_owner: Address) { + pub fn set_owner(&mut self, token_id: &str, token_owner: Address) { self.owners.set(token_id, token_owner); } #[inline] - pub fn set_issuer(&mut self, token_id: &String, issuer: Address) { + pub fn set_issuer(&mut self, token_id: &str, issuer: Address) { self.issuers.set(token_id, issuer); } @@ -148,39 +147,39 @@ impl CollectionData { } #[inline] - pub fn mark_burnt(&mut self, token_id: &String) { + pub fn mark_burnt(&mut self, token_id: &str) { self.burnt_tokens.set(token_id, ()); } #[inline] - pub fn is_burnt(&self, token_id: &String) -> bool { + pub fn is_burnt(&self, token_id: &str) -> bool { self.burnt_tokens.get(token_id).is_some() } #[inline] - pub fn issuer(&self, token_id: &String) -> Address { + pub fn issuer(&self, token_id: &str) -> Address { self.issuers .get(token_id) .unwrap_or_revert_with(&self.env(), CEP78Error::InvalidTokenIdentifier) } #[inline] - pub fn approve(&mut self, token_id: &String, operator: Address) { + pub fn approve(&mut self, token_id: &str, operator: Address) { self.approved.set(token_id, Some(operator)); } #[inline] - pub fn revoke(&mut self, token_id: &String) { + pub fn revoke(&mut self, token_id: &str) { self.approved.set(token_id, None); } #[inline] - pub fn approved(&self, token_id: &String) -> Option
{ + pub fn approved(&self, token_id: &str) -> Option
{ self.approved.get(token_id).flatten() } #[inline] - pub fn owner_of(&self, token_id: &String) -> Option
{ + pub fn owner_of(&self, token_id: &str) -> Option
{ self.owners.get(token_id) } diff --git a/modules/src/cep78/metadata.rs b/modules/src/cep78/metadata.rs index ba68bf27..59c9adf5 100644 --- a/modules/src/cep78/metadata.rs +++ b/modules/src/cep78/metadata.rs @@ -1,7 +1,7 @@ use odra::{args::Maybe, prelude::*, SubModule, UnwrapOrRevert}; use serde::{Deserialize, Serialize}; -use crate::simple_storage; +use crate::single_value_storage; use super::{ constants::{ @@ -15,31 +15,31 @@ use super::{ } }; -simple_storage!( +single_value_storage!( Cep78MetadataRequirement, MetadataRequirement, NFT_METADATA_KINDS, CEP78Error::MissingNFTMetadataKind ); -simple_storage!( +single_value_storage!( Cep78NFTMetadataKind, NFTMetadataKind, NFT_METADATA_KIND, CEP78Error::MissingNFTMetadataKind ); -simple_storage!( +single_value_storage!( Cep78IdentifierMode, NFTIdentifierMode, IDENTIFIER_MODE, CEP78Error::MissingIdentifierMode ); -simple_storage!( +single_value_storage!( Cep78MetadataMutability, MetadataMutability, METADATA_MUTABILITY, CEP78Error::MissingMetadataMutability ); -simple_storage!( +single_value_storage!( Cep78JsonSchema, String, JSON_SCHEMA, @@ -51,12 +51,13 @@ pub struct Cep78ValidatedMetadata; #[odra::module] impl Cep78ValidatedMetadata { + #[allow(clippy::ptr_arg)] pub fn set(&self, kind: &NFTMetadataKind, token_id: &String, value: String) { let dictionary_name = get_metadata_key(kind); self.env() .set_dictionary_value(dictionary_name, token_id.as_bytes(), value); } - + #[allow(clippy::ptr_arg)] pub fn get(&self, kind: &NFTMetadataKind, token_id: &String) -> String { let dictionary_name = get_metadata_key(kind); let env = self.env(); @@ -140,6 +141,10 @@ impl Metadata { self.validated_metadata.get(kind, &token_identifier) } + pub fn get_metadata_kind(&self) -> NFTMetadataKind { + self.nft_metadata_kind.get() + } + pub fn ensure_mutability(&self, error: CEP78Error) { let current_mutability = self.mutability.get(); if current_mutability != MetadataMutability::Mutable { diff --git a/modules/src/cep78/mod.rs b/modules/src/cep78/mod.rs index 39ded96e..f0ca8b97 100644 --- a/modules/src/cep78/mod.rs +++ b/modules/src/cep78/mod.rs @@ -4,12 +4,12 @@ mod constants; mod data; pub mod error; pub mod events; -mod metadata; +pub mod metadata; pub mod modalities; mod reverse_lookup; -mod settings; +pub mod settings; #[cfg(test)] mod tests; pub mod token; pub mod utils; -mod whitelist; +pub mod whitelist; diff --git a/modules/src/cep78/reverse_lookup.rs b/modules/src/cep78/reverse_lookup.rs index 624e24f8..d86930a8 100644 --- a/modules/src/cep78/reverse_lookup.rs +++ b/modules/src/cep78/reverse_lookup.rs @@ -5,7 +5,7 @@ use odra::{ Address, Mapping, SubModule, UnwrapOrRevert }; -use crate::simple_storage; +use crate::single_value_storage; use super::{ constants::PREFIX_PAGE_DICTIONARY, @@ -17,13 +17,13 @@ use super::{ // to ease the math around addressing newly minted tokens. pub const PAGE_SIZE: u64 = 1000; -simple_storage!( +single_value_storage!( Cep78OwnerReverseLookupMode, OwnerReverseLookupMode, REPORTING_MODE, CEP78Error::InvalidReportingMode ); -simple_storage!( +single_value_storage!( Cep78ReceiptName, String, RECEIPT_NAME, diff --git a/modules/src/cep78/settings.rs b/modules/src/cep78/settings.rs index 2bc1c39b..ded913c1 100644 --- a/modules/src/cep78/settings.rs +++ b/modules/src/cep78/settings.rs @@ -1,43 +1,43 @@ -use crate::simple_storage; +use crate::single_value_storage; use odra::{prelude::*, SubModule}; use super::constants::*; use super::error::CEP78Error; use super::modalities::{BurnMode, EventsMode, MintingMode, NFTHolderMode, NFTKind, OwnershipMode}; -simple_storage!(Cep78AllowMinting, bool, ALLOW_MINTING); -simple_storage!( +single_value_storage!(Cep78AllowMinting, bool, ALLOW_MINTING); +single_value_storage!( Cep78MintingMode, MintingMode, MINTING_MODE, CEP78Error::MissingMintingMode ); -simple_storage!( +single_value_storage!( Cep78OwnershipMode, OwnershipMode, OWNERSHIP_MODE, CEP78Error::MissingOwnershipMode ); -simple_storage!(Cep78NFTKind, NFTKind, NFT_KIND, CEP78Error::MissingNftKind); -simple_storage!( +single_value_storage!(Cep78NFTKind, NFTKind, NFT_KIND, CEP78Error::MissingNftKind); +single_value_storage!( Cep78HolderMode, NFTHolderMode, HOLDER_MODE, CEP78Error::MissingHolderMode ); -simple_storage!( +single_value_storage!( Cep78BurnMode, BurnMode, BURN_MODE, CEP78Error::MissingBurnMode ); -simple_storage!( +single_value_storage!( Cep78EventsMode, EventsMode, EVENTS_MODE, CEP78Error::MissingEventsMode ); -simple_storage!( +single_value_storage!( Cep78OperatorBurnMode, bool, OPERATOR_BURN_MODE, @@ -119,6 +119,11 @@ impl Settings { self.operator_burn_mode.get() } + #[inline] + pub fn nft_kind(&self) -> NFTKind { + self.nft_kind.get() + } + #[inline] pub fn set_operator_burn_mode(&mut self, value: bool) { self.operator_burn_mode.set(value) diff --git a/modules/src/cep78/token.rs b/modules/src/cep78/token.rs index fafea4e2..1a62fdac 100644 --- a/modules/src/cep78/token.rs +++ b/modules/src/cep78/token.rs @@ -1,5 +1,5 @@ #![allow(clippy::too_many_arguments)] -use crate::simple_storage; +use crate::single_value_storage; use super::{ constants::TRANSFER_FILTER_CONTRACT, @@ -27,7 +27,7 @@ use odra::{ type MintReceipt = (String, Address, String); type TransferReceipt = (String, Address); -simple_storage!( +single_value_storage!( Cep78TransferFilterContract, Address, TRANSFER_FILTER_CONTRACT @@ -664,7 +664,7 @@ impl Cep78 { } #[inline] - fn owner_of_by_id(&self, id: &String) -> Address { + fn owner_of_by_id(&self, id: &str) -> Address { match self.data.owner_of(id) { Some(token_owner) => token_owner, None => self @@ -674,12 +674,12 @@ impl Cep78 { } #[inline] - fn is_token_burned(&self, token_id: &String) -> bool { + fn is_token_burned(&self, token_id: &str) -> bool { self.data.is_burnt(token_id) } #[inline] - fn ensure_owner(&self, token_id: &String, address: &Address) { + fn ensure_owner(&self, token_id: &str, address: &Address) { let owner = self.owner_of_by_id(token_id); if address != &owner { self.revert(CEP78Error::InvalidAccount); @@ -687,7 +687,7 @@ impl Cep78 { } #[inline] - fn ensure_caller_is_owner(&self, token_id: &String) { + fn ensure_caller_is_owner(&self, token_id: &str) { let owner = self.owner_of_by_id(token_id); if self.caller() != owner { self.revert(CEP78Error::InvalidTokenOwner); @@ -695,7 +695,7 @@ impl Cep78 { } #[inline] - fn ensure_not_burned(&self, token_id: &String) { + fn ensure_not_burned(&self, token_id: &str) { if self.is_token_burned(token_id) { self.revert(CEP78Error::PreviouslyBurntToken); } diff --git a/modules/src/cep78/whitelist.rs b/modules/src/cep78/whitelist.rs index 0a154a96..e85b4c25 100644 --- a/modules/src/cep78/whitelist.rs +++ b/modules/src/cep78/whitelist.rs @@ -1,6 +1,6 @@ use odra::{args::Maybe, prelude::*, Address, SubModule}; -use crate::{basic_key_value_storage, simple_storage}; +use crate::{key_value_storage, single_value_storage}; use super::{ constants::{ACL_PACKAGE_MODE, ACL_WHITELIST, WHITELIST_MODE}, @@ -8,19 +8,24 @@ use super::{ modalities::WhitelistMode }; -simple_storage!( +single_value_storage!( Cep78WhitelistMode, WhitelistMode, WHITELIST_MODE, CEP78Error::InvalidACLPackageMode ); -simple_storage!( +single_value_storage!( Cep78PackageMode, bool, ACL_PACKAGE_MODE, CEP78Error::InvalidACLPackageMode ); -basic_key_value_storage!(Cep78ACLWhitelist, ACL_WHITELIST, bool); +key_value_storage!(Cep78ACLWhitelist, ACL_WHITELIST, bool); +impl Cep78ACLWhitelist { + pub fn clear(&self) { + self.env().remove_dictionary(ACL_WHITELIST); + } +} #[odra::module] pub struct ACLWhitelist { @@ -49,11 +54,17 @@ impl ACLWhitelist { self.whitelist.get(&address.to_string()).unwrap_or_default() } + #[inline] + pub fn get_package_mode(&self) -> bool { + self.package_mode.get() + } + pub fn update(&mut self, new_addresses: Maybe>) { let new_addresses = new_addresses.unwrap_or_default(); if !new_addresses.is_empty() { match self.get_mode() { WhitelistMode::Unlocked => { + self.whitelist.clear(); for address in new_addresses.iter() { self.whitelist.set(&address.to_string(), true); } diff --git a/modules/src/storage.rs b/modules/src/storage.rs index 951e6e6e..a15ab25e 100644 --- a/modules/src/storage.rs +++ b/modules/src/storage.rs @@ -1,6 +1,9 @@ +/// Creates an Odra module that stores a single value under a given named key. +/// The module has two methods: `set` and `get`. +/// If the value is not set and an error is passed as the fourth argument, `get` will revert with the provided error. #[macro_export] -macro_rules! simple_storage { - ($name:ident, $value_ty:ty, $key:ident, $err:expr) => { +macro_rules! single_value_storage { + ($name:ident, $value_ty:ty, $key:expr, $err:expr) => { #[odra::module] pub struct $name; @@ -17,7 +20,7 @@ macro_rules! simple_storage { } } }; - ($name:ident, $value_ty:ty, $key:ident) => { + ($name:ident, $value_ty:ty, $key:expr) => { #[odra::module] pub struct $name; @@ -33,45 +36,33 @@ macro_rules! simple_storage { }; } +/// Creates an Odra module that stores a values in a given dictionary. +/// The module has two methods: `set` and `get`. +/// The `key` argument of `set` and `get` is used as a dictionary key. #[macro_export] -macro_rules! compound_key_value_storage { - ($name:ident, $dict:expr, $k1_type:ty, $k2_type:ty, $value_type:ty) => { +macro_rules! key_value_storage { + ($name:ident, $dict:expr, $value_type:ty) => { #[odra::module] pub struct $name; impl $name { - pub fn set(&self, key1: &$k1_type, key2: &$k2_type, value: $value_type) { - use odra::UnwrapOrRevert; - - let env = self.env(); - let parts = [ - key1.to_bytes().unwrap_or_revert(&env), - key2.to_bytes().unwrap_or_revert(&env) - ]; - let key = crate::storage::compound_key(&env, &parts); - env.set_dictionary_value($dict, &key, value); + pub fn set(&self, key: &str, value: $value_type) { + self.env() + .set_dictionary_value($dict, key.as_bytes(), value); } - pub fn get_or_default(&self, key1: &$k1_type, key2: &$k2_type) -> $value_type { - use odra::UnwrapOrRevert; - - let env = self.env(); - let parts = [ - key1.to_bytes().unwrap_or_revert(&env), - key2.to_bytes().unwrap_or_revert(&env) - ]; - let key = crate::storage::compound_key(&env, &parts); - env.get_dictionary_value($dict, &key).unwrap_or_default() + pub fn get(&self, key: &str) -> Option<$value_type> { + self.env().get_dictionary_value($dict, key.as_bytes()) } } }; - ($name:ident, $dict:expr, $k1_type:ty, $value_type:ty) => { - compound_key_value_storage!($name, $dict, $k1_type, $k1_type, $value_type); - }; } +/// Creates an Odra module that stores a values in a given dictionary. +/// The module has two methods: `set` and `get`. +/// The `key` argument of `set` and `get` is base64-encoded and then used as a dictionary key. #[macro_export] -macro_rules! encoded_key_value_storage { +macro_rules! base64_encoded_key_value_storage { ($name:ident, $dict:expr, $key:ty, $value_type:ty) => { #[odra::module] pub struct $name; @@ -89,6 +80,7 @@ macro_rules! encoded_key_value_storage { env.get_dictionary_value($dict, encoded_key.as_bytes()) } + #[inline] fn key(env: &odra::ContractEnv, key: &$key) -> String { use base64::prelude::{Engine, BASE64_STANDARD}; use odra::UnwrapOrRevert; @@ -100,23 +92,45 @@ macro_rules! encoded_key_value_storage { }; } +/// Creates an Odra module that stores a values in a given dictionary. +/// The module has two methods: `set` and `get`. +/// The `key1` and `key2` arguments of `set` and `get` are converted to bytes, combined into a single bytes vector, +/// and finally hex-encoded and then used as a dictionary key. #[macro_export] -macro_rules! basic_key_value_storage { - ($name:ident, $dict:expr, $value_type:ty) => { +macro_rules! compound_key_value_storage { + ($name:ident, $dict:expr, $k1_type:ty, $k2_type:ty, $value_type:ty) => { #[odra::module] pub struct $name; impl $name { - pub fn set(&self, key: &str, value: $value_type) { - self.env() - .set_dictionary_value($dict, key.as_bytes(), value); + pub fn set(&self, key1: &$k1_type, key2: &$k2_type, value: $value_type) { + use odra::UnwrapOrRevert; + + let env = self.env(); + let parts = [ + key1.to_bytes().unwrap_or_revert(&env), + key2.to_bytes().unwrap_or_revert(&env) + ]; + let key = $crate::storage::compound_key(&env, &parts); + env.set_dictionary_value($dict, &key, value); } - pub fn get(&self, key: &str) -> Option<$value_type> { - self.env().get_dictionary_value($dict, key.as_bytes()) + pub fn get_or_default(&self, key1: &$k1_type, key2: &$k2_type) -> $value_type { + use odra::UnwrapOrRevert; + + let env = self.env(); + let parts = [ + key1.to_bytes().unwrap_or_revert(&env), + key2.to_bytes().unwrap_or_revert(&env) + ]; + let key = $crate::storage::compound_key(&env, &parts); + env.get_dictionary_value($dict, &key).unwrap_or_default() } } }; + ($name:ident, $dict:expr, $k1_type:ty, $value_type:ty) => { + compound_key_value_storage!($name, $dict, $k1_type, $k1_type, $value_type); + }; } pub(crate) fn compound_key(env: &odra::ContractEnv, parts: &[odra::prelude::Vec]) -> [u8; 64] { diff --git a/odra-casper/livenet-env/src/livenet_contract_env.rs b/odra-casper/livenet-env/src/livenet_contract_env.rs index dfa14c05..ca3753e7 100644 --- a/odra-casper/livenet-env/src/livenet_contract_env.rs +++ b/odra-casper/livenet-env/src/livenet_contract_env.rs @@ -51,6 +51,10 @@ impl ContractContext for LivenetContractEnv { panic!("Cannot set dictionary value in LivenetEnv without a deploy") } + fn remove_dictionary(&self, _dictionary_name: &str) { + panic!("Cannot remove dictionary value in LivenetEnv without a deploy") + } + fn caller(&self) -> Address { *self.callstack.borrow().first().address() } diff --git a/odra-casper/wasm-env/src/host_functions.rs b/odra-casper/wasm-env/src/host_functions.rs index 3f6627ff..4914d9dd 100644 --- a/odra-casper/wasm-env/src/host_functions.rs +++ b/odra-casper/wasm-env/src/host_functions.rs @@ -264,6 +264,12 @@ pub fn set_dictionary_value(dictionary_name: &str, key: &[u8], value: CLValue) { result.unwrap_or_revert() } +/// Removes the [`Key`] stored under `dictionary_name` in the current context's named keys. +#[inline] +pub fn remove_dictionary(dictionary_name: &str) { + runtime::remove_key(dictionary_name); +} + /// Gets a value under a key in a dictionary from the contract's storage. pub fn get_dictionary_value(dictionary_name: &str, key: &[u8]) -> Option { let dictionary_uref = get_dictionary(dictionary_name); diff --git a/odra-casper/wasm-env/src/wasm_contract_env.rs b/odra-casper/wasm-env/src/wasm_contract_env.rs index 6953089a..235b789b 100644 --- a/odra-casper/wasm-env/src/wasm_contract_env.rs +++ b/odra-casper/wasm-env/src/wasm_contract_env.rs @@ -38,6 +38,10 @@ impl ContractContext for WasmContractEnv { host_functions::set_dictionary_value(dictionary_name, key, value); } + fn remove_dictionary(&self, dictionary_name: &str) { + host_functions::remove_dictionary(dictionary_name); + } + fn caller(&self) -> Address { host_functions::caller() } diff --git a/odra-vm/src/odra_vm_contract_env.rs b/odra-vm/src/odra_vm_contract_env.rs index 62c1cb26..b5c4ff8d 100644 --- a/odra-vm/src/odra_vm_contract_env.rs +++ b/odra-vm/src/odra_vm_contract_env.rs @@ -42,6 +42,10 @@ impl ContractContext for OdraVmContractEnv { self.vm.borrow().set_dict_value(dictionary_name, key, value) } + fn remove_dictionary(&self, dictionary_name: &str) { + self.vm.borrow().remove_dictionary(dictionary_name); + } + fn caller(&self) -> Address { self.vm.borrow().caller() } diff --git a/odra-vm/src/vm/odra_vm.rs b/odra-vm/src/vm/odra_vm.rs index c6e41bba..abcab835 100644 --- a/odra-vm/src/vm/odra_vm.rs +++ b/odra-vm/src/vm/odra_vm.rs @@ -183,6 +183,14 @@ impl OdraVm { ); } + /// Removes the dictionary from the global state. + pub fn remove_dictionary(&self, dictionary_name: &str) { + self.state + .write() + .unwrap() + .remove_dictionary(dictionary_name.as_bytes()); + } + /// Gets the value of the dictionary item. /// /// Returns `None` if the dictionary or the key does not exist. @@ -563,7 +571,7 @@ mod tests { // when set a value let dict = "dict"; - let key = "key"; + let key = b"key"; let value = CLValue::from_t("value").unwrap(); instance.set_dict_value(dict, key, value.clone()); @@ -575,7 +583,7 @@ mod tests { // then the value under the key in unknown dict does not exist assert_eq!(instance.get_dict_value("other_dict", key), None); // then the value under unknown key does not exist - assert_eq!(instance.get_dict_value(dict, "other_key"), None); + assert_eq!(instance.get_dict_value(dict, b"other_key"), None); } #[test] diff --git a/odra-vm/src/vm/odra_vm_state.rs b/odra-vm/src/vm/odra_vm_state.rs index 617065f5..0caaf00b 100644 --- a/odra-vm/src/vm/odra_vm_state.rs +++ b/odra-vm/src/vm/odra_vm_state.rs @@ -62,6 +62,11 @@ impl OdraVmState { } } + pub fn remove_dictionary(&mut self, dict: &[u8]) { + let ctx = self.callstack.current().address(); + self.storage.remove_dict(ctx, dict); + } + pub fn get_dict_value(&self, dict: &[u8], key: &[u8]) -> Result, Error> { let ctx = &self.callstack.current().address(); self.storage.get_dict_value(ctx, dict, key) diff --git a/odra-vm/src/vm/storage.rs b/odra-vm/src/vm/storage.rs index 6e7597f3..57a9cf68 100644 --- a/odra-vm/src/vm/storage.rs +++ b/odra-vm/src/vm/storage.rs @@ -16,8 +16,10 @@ use super::balance::AccountBalance; #[derive(Default, Clone)] pub struct Storage { state: BTreeMap, + named_state: BTreeMap>, pub balances: BTreeMap, state_snapshot: Option>, + named_state_snapshot: Option>>, balances_snapshot: Option> } @@ -25,8 +27,10 @@ impl Storage { pub fn new(balances: BTreeMap) -> Self { Self { state: Default::default(), + named_state: Default::default(), balances, state_snapshot: Default::default(), + named_state_snapshot: Default::default(), balances_snapshot: Default::default() } } @@ -69,32 +73,44 @@ impl Storage { key: &[u8], value: Bytes ) -> Result<(), Error> { - let dict_key = [collection, key].concat(); - let hash = Storage::hashed_key(address, dict_key); - self.state.insert(hash, value); + let dict = Self::hashed_key(address, collection); + let hash = Storage::hashed_key(address, key); + let dict_values = self.named_state.entry(dict).or_default(); + dict_values.insert(hash, value); Ok(()) } + pub fn remove_dict(&mut self, address: &Address, collection: &[u8]) { + let dict = Self::hashed_key(address, collection); + self.named_state.remove(&dict); + } + pub fn get_dict_value( &self, address: &Address, collection: &[u8], key: &[u8] ) -> Result, Error> { - let dict_key = [collection, key].concat(); - let hash = Storage::hashed_key(address, dict_key); - let result = self.state.get(&hash).cloned(); - - Ok(result) + let dict = Self::hashed_key(address, collection); + let hash = Storage::hashed_key(address, key); + let dict_values = self.named_state.get(&dict); + if let Some(dict) = dict_values { + let result = dict.get(&hash).cloned(); + Ok(result) + } else { + Ok(None) + } } pub fn take_snapshot(&mut self) { self.state_snapshot = Some(self.state.clone()); + self.named_state_snapshot = Some(self.named_state.clone()); self.balances_snapshot = Some(self.balances.clone()); } pub fn drop_snapshot(&mut self) { self.state_snapshot = None; + self.named_state_snapshot = None; self.balances_snapshot = None; } @@ -103,6 +119,10 @@ impl Storage { self.state = snapshot; self.state_snapshot = None; }; + if let Some(snapshot) = self.named_state_snapshot.clone() { + self.named_state = snapshot; + self.named_state_snapshot = None; + }; if let Some(snapshot) = self.balances_snapshot.clone() { self.balances = snapshot; self.balances_snapshot = None; @@ -247,6 +267,46 @@ mod test { assert_eq!(result, None); } + #[test] + fn remove_dict_erases_all_dict_records() { + // given storage with some stored value + let mut storage = Storage::default(); + let address = utils::account_address_from_str("add"); + let key1 = b"key"; + let key2 = b"key2"; + let value1 = 88u8; + let value2 = 89u8; + let collection = b"dict"; + storage + .insert_dict_value(&address, collection, key1, serialize(&value1)) + .unwrap(); + storage + .insert_dict_value(&address, collection, key2, serialize(&value2)) + .unwrap(); + + assert_eq!( + storage.get_dict_value(&address, collection, key1).unwrap(), + Some(serialize(&value1)) + ); + assert_eq!( + storage.get_dict_value(&address, collection, key2).unwrap(), + Some(serialize(&value2)) + ); + + // when remove a dictionary + storage.remove_dict(&address, collection); + + // then all values from the dictionary are removed + assert_eq!( + storage.get_dict_value(&address, collection, key1).unwrap(), + None + ); + assert_eq!( + storage.get_dict_value(&address, collection, key2).unwrap(), + None + ); + } + #[test] fn restore_snapshot() { // given storage with some state and a snapshot of the previous state @@ -255,11 +315,17 @@ mod test { storage .set_value(&address, &key, serialize(&initial_value)) .unwrap(); + storage + .insert_dict_value(&address, b"dict", &key, serialize(&initial_value)) + .unwrap(); storage.take_snapshot(); let next_value = String::from("next_value"); storage .set_value(&address, &key, serialize(&next_value)) .unwrap(); + storage + .insert_dict_value(&address, b"dict", &key, serialize(&next_value)) + .unwrap(); // when restore the snapshot storage.restore_snapshot(); @@ -269,6 +335,10 @@ mod test { storage.get_value(&address, &key).unwrap(), Some(serialize(&initial_value)) ); + assert_eq!( + storage.get_dict_value(&address, b"dict", &key).unwrap(), + Some(serialize(&initial_value)) + ); // the snapshot is removed assert_eq!(storage.state_snapshot, None); } From 7aae5489ec70f7aac329a5f1021590fcae9afeb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Mon, 6 May 2024 21:44:12 +0200 Subject: [PATCH 29/38] bug fix --- core/src/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/error.rs b/core/src/error.rs index e810e948..cad0568d 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -148,7 +148,7 @@ impl ExecutionError { impl From for OdraError { fn from(error: ExecutionError) -> Self { - Self::ExecutionError(ExecutionError::User(error.code())) + Self::ExecutionError(error) } } From ff35352007e8b8ce9a1ab5b37dc0d3289bbf7ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Tue, 7 May 2024 09:07:44 +0200 Subject: [PATCH 30/38] fix tests --- modules/src/cep78/tests/burn.rs | 3 +-- modules/src/cep78/tests/metadata.rs | 11 +++++------ modules/src/cep78/tests/mint.rs | 3 +-- modules/src/cep78/tests/transfer.rs | 3 +-- modules/src/storage.rs | 5 +---- 5 files changed, 9 insertions(+), 16 deletions(-) diff --git a/modules/src/cep78/tests/burn.rs b/modules/src/cep78/tests/burn.rs index f6c642bf..d4b384b2 100644 --- a/modules/src/cep78/tests/burn.rs +++ b/modules/src/cep78/tests/burn.rs @@ -1,6 +1,5 @@ use odra::{ args::Maybe, - casper_types::bytesrepr::ToBytes, host::{Deployer, HostRef, NoArgs}, Address }; @@ -306,7 +305,7 @@ fn should_burn_token_in_hash_identifier_mode() { let token_owner = env.get_account(0); mint(&mut contract, true, token_owner); - let blake2b_hash = utils::create_blake2b_hash(TEST_PRETTY_721_META_DATA.to_bytes().unwrap()); + let blake2b_hash = utils::create_blake2b_hash(TEST_PRETTY_721_META_DATA); let token_hash = base16::encode_lower(&blake2b_hash); assert!(contract diff --git a/modules/src/cep78/tests/metadata.rs b/modules/src/cep78/tests/metadata.rs index 9d1ee540..b2e93853 100644 --- a/modules/src/cep78/tests/metadata.rs +++ b/modules/src/cep78/tests/metadata.rs @@ -1,6 +1,5 @@ use odra::{ args::Maybe, - casper_types::bytesrepr::ToBytes, host::{Deployer, HostRef, NoArgs} }; @@ -43,7 +42,7 @@ fn should_prevent_update_in_immutable_mode() { Maybe::None ); - let blake2b_hash = utils::create_blake2b_hash(TEST_PRETTY_721_META_DATA.to_bytes().unwrap()); + let blake2b_hash = utils::create_blake2b_hash(TEST_PRETTY_721_META_DATA); let token_hash = base16::encode_lower(&blake2b_hash); assert_eq!( @@ -161,7 +160,7 @@ fn should_allow_update_for_valid_metadata_based_on_kind( contract.mint(token_owner, original_metadata.to_string(), Maybe::None); - let blake2b_hash = utils::create_blake2b_hash(original_metadata.to_bytes().unwrap()); + let blake2b_hash = utils::create_blake2b_hash(original_metadata); let token_hash = base16::encode_lower(&blake2b_hash); let token_id = 0u64; @@ -204,7 +203,7 @@ fn should_allow_update_for_valid_metadata_based_on_kind( }; assert!(update_result.is_ok(), "failed to update metadata"); - let blake2b_hash = utils::create_blake2b_hash(updated_metadata.to_bytes().unwrap()); + let blake2b_hash = utils::create_blake2b_hash(updated_metadata); let token_hash = base16::encode_lower(&blake2b_hash); let actual_updated_metadata = match identifier_mode { @@ -305,7 +304,7 @@ fn should_get_metadata_using_token_metadata_hash() { minting_contract.mint(TEST_PRETTY_721_META_DATA.to_string(), false); - let blake2b_hash = utils::create_blake2b_hash(TEST_PRETTY_721_META_DATA.to_bytes().unwrap()); + let blake2b_hash = utils::create_blake2b_hash(TEST_PRETTY_721_META_DATA); let token_hash = base16::encode_lower(&blake2b_hash); let minted_metadata = contract.metadata(Maybe::None, Maybe::Some(token_hash)); @@ -334,7 +333,7 @@ fn should_revert_minting_token_metadata_hash_twice() { ); minting_contract.mint(TEST_PRETTY_721_META_DATA.to_string(), false); - let blake2b_hash = utils::create_blake2b_hash(TEST_PRETTY_721_META_DATA.to_bytes().unwrap()); + let blake2b_hash = utils::create_blake2b_hash(TEST_PRETTY_721_META_DATA); let token_hash = base16::encode_lower(&blake2b_hash); let minted_metadata = contract.metadata(Maybe::None, Maybe::Some(token_hash)); diff --git a/modules/src/cep78/tests/mint.rs b/modules/src/cep78/tests/mint.rs index 868c7bba..24b93456 100644 --- a/modules/src/cep78/tests/mint.rs +++ b/modules/src/cep78/tests/mint.rs @@ -1,6 +1,5 @@ use odra::{ args::Maybe, - casper_types::bytesrepr::ToBytes, host::{Deployer, HostEnv, HostRef, NoArgs} }; use serde::{Deserialize, Serialize}; @@ -554,7 +553,7 @@ fn should_approve_in_hash_identifier_mode() { TEST_PRETTY_721_META_DATA.to_string(), Maybe::None ); - let blake2b_hash = utils::create_blake2b_hash(TEST_PRETTY_721_META_DATA.to_bytes().unwrap()); + let blake2b_hash = utils::create_blake2b_hash(TEST_PRETTY_721_META_DATA); let token_hash = base16::encode_lower(&blake2b_hash); let spender = env.get_account(1); contract.approve(spender, Maybe::None, Maybe::Some(token_hash.clone())); diff --git a/modules/src/cep78/tests/transfer.rs b/modules/src/cep78/tests/transfer.rs index 9456356d..73b268bf 100644 --- a/modules/src/cep78/tests/transfer.rs +++ b/modules/src/cep78/tests/transfer.rs @@ -1,6 +1,5 @@ use odra::{ args::Maybe, - casper_types::bytesrepr::ToBytes, host::{Deployer, HostEnv, HostRef, NoArgs}, Address }; @@ -546,7 +545,7 @@ fn should_transfer_token_in_hash_identifier_mode() { Maybe::None ); - let blake2b_hash = utils::create_blake2b_hash(TEST_PRETTY_721_META_DATA.to_bytes().unwrap()); + let blake2b_hash = utils::create_blake2b_hash(TEST_PRETTY_721_META_DATA); let token_hash = base16::encode_lower(&blake2b_hash); contract.register_owner(Maybe::Some(new_owner)); assert!(contract diff --git a/modules/src/storage.rs b/modules/src/storage.rs index a15ab25e..6172a120 100644 --- a/modules/src/storage.rs +++ b/modules/src/storage.rs @@ -134,13 +134,10 @@ macro_rules! compound_key_value_storage { } pub(crate) fn compound_key(env: &odra::ContractEnv, parts: &[odra::prelude::Vec]) -> [u8; 64] { - use odra::casper_types::bytesrepr::ToBytes; - use odra::UnwrapOrRevert; - let mut result = [0u8; 64]; let mut preimage = odra::prelude::Vec::new(); for part in parts { - preimage.append(&mut part.to_bytes().unwrap_or_revert(env)); + preimage.extend_from_slice(part); } let key_bytes = env.hash(&preimage); From acd62a9e6f782796420cca478ef6f781127b01b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Tue, 7 May 2024 10:44:02 +0200 Subject: [PATCH 31/38] `get_named_key` does not revert if the key does not exists --- modules/src/cep78/tests/mint.rs | 19 +++++++++++-------- odra-casper/wasm-env/src/host_functions.rs | 6 ++++-- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/modules/src/cep78/tests/mint.rs b/modules/src/cep78/tests/mint.rs index 24b93456..ee581c09 100644 --- a/modules/src/cep78/tests/mint.rs +++ b/modules/src/cep78/tests/mint.rs @@ -788,7 +788,8 @@ fn should_approve_all_with_flat_gas_cost() { let mut contract = Cep78HostRef::deploy(&env, args); let token_owner = env.get_account(0); let operator = env.get_account(1); - let other_operator = env.get_account(2); + let operator1 = env.get_account(2); + let operator2 = env.get_account(3); contract.register_owner(Maybe::Some(token_owner)); contract.mint( @@ -796,20 +797,22 @@ fn should_approve_all_with_flat_gas_cost() { TEST_PRETTY_721_META_DATA.to_string(), Maybe::None ); - contract.set_approval_for_all(true, operator); - let is_operator = contract.is_approved_for_all(token_owner, operator); + + contract.set_approval_for_all(true, operator1); + let is_operator = contract.is_approved_for_all(token_owner, operator1); assert!(is_operator, "expected operator to be approved for all"); - contract.set_approval_for_all(true, other_operator); - let is_also_operator = contract.is_approved_for_all(token_owner, other_operator); + contract.set_approval_for_all(true, operator2); + let is_also_operator = contract.is_approved_for_all(token_owner, operator2); assert!( is_also_operator, "expected other operator to be approved for all" ); let costs = utils::get_gas_cost_of(&env, "set_approval_for_all"); - // Operator approval should have flat gas costs - // Therefore the second and first set_approve_for_all must have equivalent gas costs. - assert_eq!(costs.first(), costs.get(1)); + // Operator approval should have flat gas costs. + // First call creates necessary named keys. + // Therefore the second and third set_approve_for_all must have equivalent gas costs. + assert_eq!(costs.get(1), costs.get(2)); } diff --git a/odra-casper/wasm-env/src/host_functions.rs b/odra-casper/wasm-env/src/host_functions.rs index 4914d9dd..47ccfd2e 100644 --- a/odra-casper/wasm-env/src/host_functions.rs +++ b/odra-casper/wasm-env/src/host_functions.rs @@ -233,8 +233,10 @@ pub fn set_named_key(name: &str, value: CLValue) { /// Gets a value under a named key from the contract's storage. pub fn get_named_key(name: &str) -> Option { - let key = runtime::get_key(name).unwrap_or_revert(); - read(key) + match runtime::get_key(name) { + Some(key) => read(key), + None => None + } } /// Writes a value under a key in a dictionary to a contract's storage. From 958a81790660bd5fabd538ef08b10da3ada9bc6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Tue, 7 May 2024 10:44:11 +0200 Subject: [PATCH 32/38] register events --- modules/src/cep78/token.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/src/cep78/token.rs b/modules/src/cep78/token.rs index 1a62fdac..a3b82c85 100644 --- a/modules/src/cep78/token.rs +++ b/modules/src/cep78/token.rs @@ -50,7 +50,10 @@ single_value_storage!( /// - `nft_identifier_mode`: The identifier mode of the NFTs in the collection. See [NFTIdentifierMode] for more details. /// - `nft_metadata_kind`: The kind of metadata associated with the NFTs in the collection. See [NFTMetadataKind] for more details. /// - `metadata_mutability`: The mutability of the metadata associated with the NFTs in the collection. See [MetadataMutability] for more details. -#[odra::module] +#[odra::module( + version = "1.5.1", + events = [Approval, ApprovalForAll, ApprovalRevoked, Burn, MetadataUpdated, Mint, RevokedForAll, Transfer, VariablesSet] +)] pub struct Cep78 { data: SubModule, metadata: SubModule, From 67c04cf7cc79dbc67a7c3211546ebb8704202447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Tue, 7 May 2024 14:20:49 +0200 Subject: [PATCH 33/38] Update some args names to match the casper impl args --- modules/src/cep78/token.rs | 52 ++++++++++++++++++-------------------- modules/src/cep78/utils.rs | 2 +- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/modules/src/cep78/token.rs b/modules/src/cep78/token.rs index a3b82c85..91c6db6d 100644 --- a/modules/src/cep78/token.rs +++ b/modules/src/cep78/token.rs @@ -81,7 +81,7 @@ impl Cep78 { minting_mode: Maybe, holder_mode: Maybe, whitelist_mode: Maybe, - acl_white_list: Maybe>, + acl_whitelist: Maybe>, json_schema: Maybe, burn_mode: Maybe, operator_burn_mode: Maybe, @@ -94,7 +94,7 @@ impl Cep78 { let installer = self.caller(); let minting_mode = minting_mode.unwrap_or_default(); let owner_reverse_lookup_mode = owner_reverse_lookup_mode.unwrap_or_default(); - let acl_white_list = acl_white_list.unwrap_or_default(); + let acl_white_list = acl_whitelist.unwrap_or_default(); let whitelist_mode = whitelist_mode.unwrap_or_default(); let json_schema = json_schema.unwrap_or_default(); let is_whitelist_empty = acl_white_list.is_empty(); @@ -220,7 +220,7 @@ impl Cep78 { pub fn mint( &mut self, token_owner: Address, - token_metadata: String, + token_meta_data: String, token_hash: Maybe ) -> MintReceipt { if !self.settings.allow_minting() { @@ -261,7 +261,7 @@ impl Cep78 { let token_identifier: TokenIdentifier = match identifier_mode { NFTIdentifierMode::Ordinal => TokenIdentifier::Index(minted_tokens_count), NFTIdentifierMode::Hash => TokenIdentifier::Hash(if optional_token_hash.is_empty() { - let hash = self.__env.hash(token_metadata.clone()); + let hash = self.__env.hash(token_meta_data.clone()); base16::encode_lower(&hash) } else { optional_token_hash @@ -269,7 +269,7 @@ impl Cep78 { }; let token_id = token_identifier.to_string(); - self.metadata.update_or_revert(&token_metadata, &token_id); + self.metadata.update_or_revert(&token_meta_data, &token_id); let token_owner = if self.is_transferable_or_assigned() { token_owner @@ -288,7 +288,7 @@ impl Cep78 { self.data.increment_counter(&token_owner); self.data.increment_number_of_minted_tokens(); - self.emit_ces_event(Mint::new(token_owner, token_id.clone(), token_metadata)); + self.emit_ces_event(Mint::new(token_owner, token_id.clone(), token_meta_data)); self.reverse_lookup .on_mint(minted_tokens_count, token_owner, token_id) @@ -335,15 +335,15 @@ impl Cep78 { &mut self, token_id: Maybe, token_hash: Maybe, - source: Address, - target: Address + source_key: Address, + target_key: Address ) -> TransferReceipt { - self.ensure_minter_or_assigned(); + self.ensure_not_minter_or_assigned(); let token_identifier = self.checked_token_identifier(token_id, token_hash); let token_id = token_identifier.to_string(); self.ensure_not_burned(&token_id); - self.ensure_owner(&token_id, &source); + self.ensure_owner(&token_id, &source_key); let caller = self.caller(); let owner = self.owner_of_by_id(&token_id); @@ -356,14 +356,14 @@ impl Cep78 { }; let is_operator = if !is_owner && !is_approved { - self.data.operator(source, caller) + self.data.operator(source_key, caller) } else { false }; if let Some(filter_contract) = self.transfer_filter_contract.get() { let result = TransferFilterContractContractRef::new(self.env(), filter_contract) - .can_transfer(source, target, token_identifier.clone()); + .can_transfer(source_key, target_key, token_identifier.clone()); if TransferFilterContractResult::DenyTransfer == result { self.revert(CEP78Error::TransferFilterContractDenied); @@ -376,30 +376,30 @@ impl Cep78 { match self.data.owner_of(&token_id) { Some(token_actual_owner) => { - if token_actual_owner != source { + if token_actual_owner != source_key { self.revert(CEP78Error::InvalidTokenOwner) } - self.data.set_owner(&token_id, target); + self.data.set_owner(&token_id, target_key); } None => self.revert(CEP78Error::MissingOwnerTokenIdentifierKey) } - self.data.decrement_counter(&source); - self.data.increment_counter(&target); + self.data.decrement_counter(&source_key); + self.data.increment_counter(&target_key); self.data.revoke(&token_id); let spender = if caller == owner { None } else { Some(caller) }; - self.emit_ces_event(Transfer::new(owner, spender, target, token_id)); + self.emit_ces_event(Transfer::new(owner, spender, target_key, token_id)); self.reverse_lookup - .on_transfer(token_identifier, source, target) + .on_transfer(token_identifier, source_key, target_key) } /// Approves another token holder (an approved account) to transfer tokens. It /// reverts if token_id is invalid, if caller is not the owner nor operator, if token has already /// been burnt, or if caller tries to approve themselves as an approved account. pub fn approve(&mut self, spender: Address, token_id: Maybe, token_hash: Maybe) { - self.ensure_minter_or_assigned(); + self.ensure_not_minter_or_assigned(); let caller = self.caller(); let token_identifier = self.checked_token_identifier(token_id, token_hash); @@ -425,7 +425,7 @@ impl Cep78 { /// if token_id is invalid, if caller is not the owner, if token has already /// been burnt, if caller tries to approve itself. pub fn revoke(&mut self, token_id: Maybe, token_hash: Maybe) { - self.ensure_minter_or_assigned(); + self.ensure_not_minter_or_assigned(); let caller = self.caller(); let token_identifier = self.checked_token_identifier(token_id, token_hash); @@ -449,7 +449,7 @@ impl Cep78 { /// (an operator) to transfer tokens. It reverts if token_id is invalid, if caller is not the /// owner, if caller tries to approve itself as an operator. pub fn set_approval_for_all(&mut self, approve_all: bool, operator: Address) { - self.ensure_minter_or_assigned(); + self.ensure_not_minter_or_assigned(); self.ensure_not_caller(operator); let caller = self.caller(); @@ -501,7 +501,7 @@ impl Cep78 { &mut self, token_id: Maybe, token_hash: Maybe, - updated_token_metadata: String + token_meta_data: String ) { self.metadata .ensure_mutability(CEP78Error::ForbiddenMetadataUpdate); @@ -509,10 +509,9 @@ impl Cep78 { let token_identifier = self.checked_token_identifier(token_id, token_hash); let token_id = token_identifier.to_string(); self.ensure_caller_is_owner(&token_id); - self.metadata - .update_or_revert(&updated_token_metadata, &token_id); + self.metadata.update_or_revert(&token_meta_data, &token_id); - self.emit_ces_event(MetadataUpdated::new(token_id, updated_token_metadata)); + self.emit_ces_event(MetadataUpdated::new(token_id, token_meta_data)); } /// Returns number of owned tokens associated with the provided token holder @@ -535,7 +534,6 @@ impl Cep78 { /* Test only getters */ - pub fn is_whitelisted(&self, address: &Address) -> bool { self.whitelist.is_whitelisted(address) } @@ -627,7 +625,7 @@ impl Cep78 { } #[inline] - fn ensure_minter_or_assigned(&self) { + fn ensure_not_minter_or_assigned(&self) { if self.is_minter_or_assigned() { self.revert(CEP78Error::InvalidOwnershipMode) } diff --git a/modules/src/cep78/utils.rs b/modules/src/cep78/utils.rs index a8d4e90d..80336b61 100644 --- a/modules/src/cep78/utils.rs +++ b/modules/src/cep78/utils.rs @@ -314,7 +314,7 @@ impl InitArgsBuilder { nft_kind: self.nft_kind, holder_mode: self.holder_mode, whitelist_mode: self.whitelist_mode, - acl_white_list: self.acl_white_list, + acl_whitelist: self.acl_white_list, json_schema: self.json_schema, receipt_name: self.receipt_name, identifier_mode: self.identifier_mode, From a7776397d0abd41cdea0793a1d2cd754c7550a79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Tue, 7 May 2024 14:21:06 +0200 Subject: [PATCH 34/38] Add livenet example --- examples/Cargo.toml | 6 +++ examples/bin/cep78_on_livenet.rs | 78 ++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 examples/bin/cep78_on_livenet.rs diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 72a2d9ed..eba5aa84 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -48,6 +48,12 @@ path = "bin/cep18_on_livenet.rs" required-features = ["livenet"] test = false +[[bin]] +name = "cep78_on_livenet" +path = "bin/cep78_on_livenet.rs" +required-features = ["livenet"] +test = false + [[bin]] name = "livenet_tests" path = "bin/livenet_tests.rs" diff --git a/examples/bin/cep78_on_livenet.rs b/examples/bin/cep78_on_livenet.rs new file mode 100644 index 00000000..2897e3bd --- /dev/null +++ b/examples/bin/cep78_on_livenet.rs @@ -0,0 +1,78 @@ +//! Deploys a CEP-78 contract and transfers some tokens to another address. +use std::str::FromStr; + +use odra::args::Maybe; +use odra::casper_types::U256; +use odra::host::{Deployer, HostEnv, HostRef, HostRefLoader}; +use odra::Address; +use odra_modules::cep78::modalities::{ + EventsMode, MetadataMutability, NFTIdentifierMode, NFTKind, NFTMetadataKind, OwnershipMode +}; +use odra_modules::cep78::token::{Cep78HostRef, Cep78InitArgs}; +use odra_modules::cep78::utils::InitArgsBuilder; + +const CEP78_METADATA: &str = r#"{ + "name": "John Doe", + "token_uri": "https://www.barfoo.com", + "checksum": "940bffb3f2bba35f84313aa26da09ece3ad47045c6a1292c2bbd2df4ab1a55fb" +}"#; + +fn main() { + let env = odra_casper_livenet_env::env(); + + let owner = env.caller(); + let recipient = + Address::from_str("hash-7821386ecdda83ff100379a06558b69a675d5a170d1c5bf5fbe9fd35262d091f") + .unwrap(); + + // Deploy new contract. + // let mut token = deploy_contract(&env); + // println!("Token address: {}", token.address().to_string()); + + // Uncomment to load existing contract. + let mut token = _load_contract(&env); + + println!("Token name: {}", token.get_collection_name()); + + env.set_gas(3_000_000_000u64); + token.try_mint(owner, CEP78_METADATA.to_string(), Maybe::None); + println!("Owner's balance: {:?}", token.balance_of(owner)); + println!("Recipient's balance: {:?}", token.balance_of(recipient)); + let token_id = token.get_number_of_minted_tokens() - 1; + token.try_transfer(Maybe::Some(token_id), Maybe::None, owner, recipient); + + println!("Owner's balance: {:?}", token.balance_of(owner)); + println!("Recipient's balance: {:?}", token.balance_of(recipient)); +} + +/// Loads a Cep78 contract. +fn _load_contract(env: &HostEnv) -> Cep78HostRef { + // casper-contract + // let address = "hash-d4b8fa492d55ac7a515c0c6043d72ba43c49cd120e7ba7eec8c0a330dedab3fb"; + // odra-contract + let address = "hash-3d35238431c5c6fa1d7df70d73bfc2efd5a03fd5af99ab8c7828a56b2f330274"; + let address = Address::from_str(address).unwrap(); + Cep78HostRef::load(env, address) +} + +/// Deploys a Cep78 contract. +pub fn deploy_contract(env: &HostEnv) -> Cep78HostRef { + let name: String = String::from("PlascoinCollection with CES"); + let symbol = String::from("CEP78-PLS-CES"); + + let init_args = InitArgsBuilder::default() + .collection_name(name) + .collection_symbol(symbol) + .total_token_supply(1_000) + .ownership_mode(OwnershipMode::Transferable) + .nft_metadata_kind(NFTMetadataKind::CEP78) + .identifier_mode(NFTIdentifierMode::Ordinal) + .nft_kind(NFTKind::Digital) + .metadata_mutability(MetadataMutability::Mutable) + .receipt_name(String::from("PlascoinReceipt")) + .events_mode(EventsMode::CES) + .build(); + + env.set_gas(400_000_000_000u64); + Cep78HostRef::deploy(env, init_args) +} From f5611554f63a697ae28041d3fcf76b9dc8632c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Tue, 7 May 2024 14:38:50 +0200 Subject: [PATCH 35/38] Fix variable name in NftContract trait --- modules/src/cep78/utils.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/src/cep78/utils.rs b/modules/src/cep78/utils.rs index 80336b61..9d563736 100644 --- a/modules/src/cep78/utils.rs +++ b/modules/src/cep78/utils.rs @@ -138,7 +138,7 @@ trait NftContract { fn mint( &mut self, token_owner: Address, - token_metadata: String, + token_meta_data: String, token_hash: Maybe ) -> (String, Address, String); fn burn(&mut self, token_id: Maybe, token_hash: Maybe); @@ -147,8 +147,8 @@ trait NftContract { &mut self, token_id: Maybe, token_hash: Maybe, - source: Address, - target: Address + source_key: Address, + target_key: Address ) -> (String, Address); fn approve(&mut self, spender: Address, token_id: Maybe, token_hash: Maybe); fn revoke(&mut self, token_id: Maybe, token_hash: Maybe); From 9be222280161b4571775ebede9a861f127ce1e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Pobiar=C5=BCyn?= Date: Wed, 8 May 2024 10:02:31 +0200 Subject: [PATCH 36/38] Update CEP-78 livenet example --- examples/bin/cep78_on_livenet.rs | 41 ++++++++++++++++---------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/examples/bin/cep78_on_livenet.rs b/examples/bin/cep78_on_livenet.rs index 2897e3bd..987b87a6 100644 --- a/examples/bin/cep78_on_livenet.rs +++ b/examples/bin/cep78_on_livenet.rs @@ -1,4 +1,4 @@ -//! Deploys a CEP-78 contract and transfers some tokens to another address. +//! Deploys a CEP-78 contract, mints an nft token and transfers it to another address. use std::str::FromStr; use odra::args::Maybe; @@ -16,42 +16,42 @@ const CEP78_METADATA: &str = r#"{ "token_uri": "https://www.barfoo.com", "checksum": "940bffb3f2bba35f84313aa26da09ece3ad47045c6a1292c2bbd2df4ab1a55fb" }"#; +const CASPER_CONTRACT_ADDRESS: &str = + "hash-d4b8fa492d55ac7a515c0c6043d72ba43c49cd120e7ba7eec8c0a330dedab3fb"; +const ODRA_CONTRACT_ADDRESS: &str = + "hash-3d35238431c5c6fa1d7df70d73bfc2efd5a03fd5af99ab8c7828a56b2f330274"; +const RECIPIENT_ADDRESS: &str = + "hash-7821386ecdda83ff100379a06558b69a675d5a170d1c5bf5fbe9fd35262d091f"; fn main() { let env = odra_casper_livenet_env::env(); - let owner = env.caller(); - let recipient = - Address::from_str("hash-7821386ecdda83ff100379a06558b69a675d5a170d1c5bf5fbe9fd35262d091f") - .unwrap(); - // Deploy new contract. - // let mut token = deploy_contract(&env); - // println!("Token address: {}", token.address().to_string()); + let mut token = deploy_contract(&env); + println!("Token address: {}", token.address().to_string()); // Uncomment to load existing contract. - let mut token = _load_contract(&env); - - println!("Token name: {}", token.get_collection_name()); + // let mut token = load_contract(&env, CASPER_CONTRACT_ADDRESS); + // println!("Token name: {}", token.get_collection_name()); env.set_gas(3_000_000_000u64); - token.try_mint(owner, CEP78_METADATA.to_string(), Maybe::None); + let owner = env.caller(); + let recipient = + Address::from_str(RECIPIENT_ADDRESS).expect("Should be a valid recipient address"); + // casper contract may return a result or not, so deserialization may fail and it's better to use `try_transfer`/`try_mint`/`try_burn` methods + let _ = token.try_mint(owner, CEP78_METADATA.to_string(), Maybe::None); println!("Owner's balance: {:?}", token.balance_of(owner)); println!("Recipient's balance: {:?}", token.balance_of(recipient)); let token_id = token.get_number_of_minted_tokens() - 1; - token.try_transfer(Maybe::Some(token_id), Maybe::None, owner, recipient); + let _ = token.try_transfer(Maybe::Some(token_id), Maybe::None, owner, recipient); println!("Owner's balance: {:?}", token.balance_of(owner)); println!("Recipient's balance: {:?}", token.balance_of(recipient)); } /// Loads a Cep78 contract. -fn _load_contract(env: &HostEnv) -> Cep78HostRef { - // casper-contract - // let address = "hash-d4b8fa492d55ac7a515c0c6043d72ba43c49cd120e7ba7eec8c0a330dedab3fb"; - // odra-contract - let address = "hash-3d35238431c5c6fa1d7df70d73bfc2efd5a03fd5af99ab8c7828a56b2f330274"; - let address = Address::from_str(address).unwrap(); +pub fn load_contract(env: &HostEnv, address: &str) -> Cep78HostRef { + let address = Address::from_str(address).expect("Should be a valid contract address"); Cep78HostRef::load(env, address) } @@ -59,6 +59,7 @@ fn _load_contract(env: &HostEnv) -> Cep78HostRef { pub fn deploy_contract(env: &HostEnv) -> Cep78HostRef { let name: String = String::from("PlascoinCollection with CES"); let symbol = String::from("CEP78-PLS-CES"); + let receipt_name = String::from("PlascoinReceipt"); let init_args = InitArgsBuilder::default() .collection_name(name) @@ -69,7 +70,7 @@ pub fn deploy_contract(env: &HostEnv) -> Cep78HostRef { .identifier_mode(NFTIdentifierMode::Ordinal) .nft_kind(NFTKind::Digital) .metadata_mutability(MetadataMutability::Mutable) - .receipt_name(String::from("PlascoinReceipt")) + .receipt_name(receipt_name) .events_mode(EventsMode::CES) .build(); From f653b6ed33c201d4a1aa8929ae59d5ffb24056a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Zieli=C5=84ski?= Date: Thu, 9 May 2024 15:56:01 +0200 Subject: [PATCH 37/38] Compile cargo-odra with stable --- justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/justfile b/justfile index 7bd4abe9..27c29787 100644 --- a/justfile +++ b/justfile @@ -30,7 +30,7 @@ check-lint: clippy cd benchmark && cargo check --all-targets --features=benchmark install-cargo-odra: - cargo install cargo-odra --git {{CARGO_ODRA_GIT_REPO}} --branch {{CARGO_ODRA_BRANCH}} --locked + cargo +stable install cargo-odra --git {{CARGO_ODRA_GIT_REPO}} --branch {{CARGO_ODRA_BRANCH}} --locked prepare-test-env: install-cargo-odra rustup target add wasm32-unknown-unknown From 08e3f49f47d05a04ab64ce60ba114a2cf5fb88ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Zieli=C5=84ski?= Date: Fri, 10 May 2024 14:11:05 +0200 Subject: [PATCH 38/38] Remove 'new' from cep78 events. --- modules/src/cep78/events.rs | 84 ------------------------------------- 1 file changed, 84 deletions(-) diff --git a/modules/src/cep78/events.rs b/modules/src/cep78/events.rs index 59ac386a..090c9732 100644 --- a/modules/src/cep78/events.rs +++ b/modules/src/cep78/events.rs @@ -7,16 +7,6 @@ pub struct Mint { data: String } -impl Mint { - pub fn new(recipient: Address, token_id: String, data: String) -> Self { - Self { - recipient, - token_id, - data - } - } -} - #[odra::event] pub struct Burn { owner: Address, @@ -24,16 +14,6 @@ pub struct Burn { burner: Address } -impl Burn { - pub fn new(owner: Address, token_id: String, burner: Address) -> Self { - Self { - owner, - token_id, - burner - } - } -} - #[odra::event] pub struct Approval { owner: Address, @@ -41,52 +21,24 @@ pub struct Approval { token_id: String } -impl Approval { - pub fn new(owner: Address, spender: Address, token_id: String) -> Self { - Self { - owner, - spender, - token_id - } - } -} - #[odra::event] pub struct ApprovalRevoked { owner: Address, token_id: String } -impl ApprovalRevoked { - pub fn new(owner: Address, token_id: String) -> Self { - Self { owner, token_id } - } -} - #[odra::event] pub struct ApprovalForAll { owner: Address, operator: Address } -impl ApprovalForAll { - pub fn new(owner: Address, operator: Address) -> Self { - Self { owner, operator } - } -} - #[odra::event] pub struct RevokedForAll { owner: Address, operator: Address } -impl RevokedForAll { - pub fn new(owner: Address, operator: Address) -> Self { - Self { owner, operator } - } -} - #[odra::event] pub struct Transfer { owner: Address, @@ -95,50 +47,14 @@ pub struct Transfer { token_id: String } -impl Transfer { - pub fn new( - owner: Address, - spender: Option
, - recipient: Address, - token_id: String - ) -> Self { - Self { - owner, - spender, - recipient, - token_id - } - } -} - #[odra::event] pub struct MetadataUpdated { token_id: String, data: String } -impl MetadataUpdated { - pub fn new(token_id: String, data: String) -> Self { - Self { token_id, data } - } -} - #[odra::event] pub struct VariablesSet {} -impl VariablesSet { - #[allow(clippy::new_without_default)] - pub fn new() -> Self { - Self {} - } -} - #[odra::event] pub struct Migration {} - -impl Migration { - #[allow(clippy::new_without_default)] - pub fn new() -> Self { - Self {} - } -}