diff --git a/Makefile b/Makefile index b9cc1c76..add9dc98 100644 --- a/Makefile +++ b/Makefile @@ -7,12 +7,16 @@ build-contract: cd client/balance_of_session && cargo build --release --target wasm32-unknown-unknown cd client/owner_of_session && cargo build --release --target wasm32-unknown-unknown cd client/get_approved_session && cargo build --release --target wasm32-unknown-unknown + cd client/minting_contract && cargo build --release --target wasm32-unknown-unknown + cd client/transfer_session && cargo build --release --target wasm32-unknown-unknown wasm-strip contract/target/wasm32-unknown-unknown/release/contract.wasm 2>/dev/null | true wasm-strip entrypoint_session/target/wasm32-unknown-unknown/release/entrypoint_call.wasm 2>/dev/null | true wasm-strip client/mint_session/target/wasm32-unknown-unknown/release/mint_call.wasm 2>/dev/null | true wasm-strip client/balance_of_session/target/wasm32-unknown-unknown/release/balance_of_call.wasm 2>/dev/null | true wasm-strip client/owner_of_session/target/wasm32-unknown-unknown/release/owner_of_call.wasm 2>/dev/null | true wasm-strip client/get_approved_session/target/wasm32-unknown-unknown/release/get_approved_call.wasm 2>/dev/null | true + wasm-strip client/minting_contract/target/wasm32-unknown-unknown/release/minting_contract.wasm 2>/dev/null | true + wasm-strip client/transfer_session/target/wasm32-unknown-unknown/release/transfer_call.wasm 2>/dev/null | true test: build-contract mkdir -p tests/wasm @@ -21,6 +25,8 @@ test: build-contract cp client/balance_of_session/target/wasm32-unknown-unknown/release/balance_of_call.wasm tests/wasm cp client/owner_of_session/target/wasm32-unknown-unknown/release/owner_of_call.wasm tests/wasm cp client/get_approved_session/target/wasm32-unknown-unknown/release/get_approved_call.wasm tests/wasm + cp client/minting_contract/target/wasm32-unknown-unknown/release/minting_contract.wasm tests/wasm + cp client/transfer_session/target/wasm32-unknown-unknown/release/transfer_call.wasm tests/wasm cd tests && cargo test clippy: diff --git a/README.md b/README.md index 465a47db..b0b55848 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ # Enhanced NFT standard ## Design goals -- User attempting to use an NFT contract should be able to install the contract - with any differentiating arguments easily. Must work out of the box. -- Reference implementation must be straightforward/clear and obvious. -- Externally observable association between `Accounts` and `NFT`s they "own". -- Should be well documented with sufficient/exhaustive tests. -- Must be entirely self-contained within a singular repo, this includes the contract, tests - and any utilities such as clients and/or links to documentation. -- Must support mainstream expectations about common NFT, additional features beyond the norms - as long as they don't interfere with the core functionality. -- Metadata and Payload should be conformant with the community standards and need not be - constrained to CLType. +- DApp developer attempting to create an NFT contract should be able to install the contract as is, + configured for the specific builtin behavior they want their NFT contract instance to have. Must work out of the box. +- Reference implementation must be straightforward, clear, and obvious. +- Externally observable association between `Accounts` and/or `Contracts` and `NFT`s they "own". +- Should be well documented with exhaustive tests that prove all possible combinations of defined behavior work as intended. +- Must be entirely self-contained within a singular repo, this includes the all code, all tests + all relevant Casperlabs provided SDKs, and all relevant documentation. +- Must support mainstream expectations about common NFT conventions. +- A given NFT contract instance must be able to choose when created if it is using a Metadata schema conformant with existing community standards or a specific custom schema which they provide. +- A NFT contract instance must validate provided metadata against the specified metadata schema for that contract. +- Standardized session code to interact with an NFT contract instance must be usable as is, so that a given DApp developer doesn't have to write any Wasm producing logic for normal usage of NFT contract instances produced by this contract. ## Features and usage diff --git a/client/balance_of_session/README.md b/client/balance_of_session/README.md new file mode 100644 index 00000000..7283ccb7 --- /dev/null +++ b/client/balance_of_session/README.md @@ -0,0 +1,20 @@ +# Session code for the Balance entry point + +Utility session code meant for interacting with the `balance_of` entry point on the main enhanced NFT contract. +The `balance_of` session code calls the relevant entry point and saves the amount of tokens owned by either an `Account` +or `Contract` and saves the value in the `NamedKeys` of `Account` executing the session code. + + +## Compiling session code + +The session code can be compiled to Wasm by running the `make build-contract` command provided in the Makefile at the top level. +The Wasm will be found in the `client/balance_of_session/target/wasm32-unknown-unknown/release` as `balance_of.wasm`. + +## Usage + +The `balance_of` session code takes in the following required runtime arguments. + +* `nft_contract_hash`: The hash of a given Enhanced NFT contract passed in as a `Key`. +* `token_owner`: The `Key` of either the `Account` or `Contract` whose balance is being queried. +* `key_name`: The name for the entry within the `NamedKeys` under which the token amount will be stored, passed in as a `String`. + diff --git a/client/balance_of_session/src/main.rs b/client/balance_of_session/src/main.rs index 509df60c..4370f809 100644 --- a/client/balance_of_session/src/main.rs +++ b/client/balance_of_session/src/main.rs @@ -6,8 +6,9 @@ compile_error!("target arch should be wasm32: compile with '--target wasm32-unkn extern crate alloc; use alloc::string::String; + use casper_contract::contract_api::{runtime, storage}; -use casper_types::{runtime_args, ContractHash, Key, RuntimeArgs, U256}; +use casper_types::{runtime_args, ContractHash, Key, RuntimeArgs}; const ENTRY_POINT_BALANCE_OF: &str = "balance_of"; const ARG_NFT_CONTRACT_HASH: &str = "nft_contract_hash"; @@ -16,11 +17,14 @@ const ARG_KEY_NAME: &str = "key_name"; #[no_mangle] pub extern "C" fn call() { - let nft_contract_hash: ContractHash = runtime::get_named_arg(ARG_NFT_CONTRACT_HASH); + let nft_contract_hash: ContractHash = runtime::get_named_arg::(ARG_NFT_CONTRACT_HASH) + .into_hash() + .map(|hash| ContractHash::new(hash)) + .unwrap(); let key_name: String = runtime::get_named_arg(ARG_KEY_NAME); let token_owner: Key = runtime::get_named_arg(ARG_TOKEN_OWNER); - let balance = runtime::call_contract::( + let balance = runtime::call_contract::( nft_contract_hash, ENTRY_POINT_BALANCE_OF, runtime_args! { diff --git a/client/get_approved_session/README.md b/client/get_approved_session/README.md new file mode 100644 index 00000000..e20f538f --- /dev/null +++ b/client/get_approved_session/README.md @@ -0,0 +1,18 @@ +# Session code for get_approved + +Utility session code for interacting with the `get_approved` entry point present on the enhanced NFT contract. It returns +a `Key` if a given NFT is approved to be spent by another `Account` or `Contract` apart from the owner of the +NFT itself. It returns `Some(Key)` if there is an approved spender, `None` if there is no spender. + +## Compiling session code + +The session code can be compiled to Wasm by running the `make build-contract` command provided in the Makefile at the top level. +The Wasm will be found in the `client/get_approved_session/target/wasm32-unknown-unknown/release` as `get_approved.wasm`. + +## Usage + +The `get_approved` session code takes in the following required runtime arguments. + +* `nft_contract_hash`: The hash of a given Enhanced NFT contract passed in as a `Key`. +* `token_id`: The `id` of the NFT, passed in as a `u64`. +* `key_name`: The name for the entry within the `NamedKeys` under which `Option` value is stored, passed in as a `String`. diff --git a/client/get_approved_session/src/main.rs b/client/get_approved_session/src/main.rs index fe8496ed..4617f943 100644 --- a/client/get_approved_session/src/main.rs +++ b/client/get_approved_session/src/main.rs @@ -6,26 +6,44 @@ compile_error!("target arch should be wasm32: compile with '--target wasm32-unkn extern crate alloc; use alloc::string::String; + use casper_contract::contract_api::{runtime, storage}; -use casper_types::{runtime_args, ContractHash, Key, RuntimeArgs, U256}; +use casper_types::{runtime_args, ContractHash, Key, RuntimeArgs}; const ENTRY_POINT_GET_APPROVED: &str = "get_approved"; const ARG_NFT_CONTRACT_HASH: &str = "nft_contract_hash"; const ARG_KEY_NAME: &str = "key_name"; const ARG_TOKEN_ID: &str = "token_id"; +const ARG_TOKEN_HASH: &str = "token_hash"; +const ARG_IS_HASH_IDENTIFIER_MODE: &str = "is_hash_identifier_mode"; #[no_mangle] pub extern "C" fn call() { - let nft_contract_hash: ContractHash = runtime::get_named_arg(ARG_NFT_CONTRACT_HASH); + let nft_contract_hash: ContractHash = runtime::get_named_arg::(ARG_NFT_CONTRACT_HASH) + .into_hash() + .map(|hash| ContractHash::new(hash)) + .unwrap(); let key_name: String = runtime::get_named_arg(ARG_KEY_NAME); - let token_id = runtime::get_named_arg::(ARG_TOKEN_ID); - let maybe_operator = runtime::call_contract::>( - nft_contract_hash, - ENTRY_POINT_GET_APPROVED, - runtime_args! { + + let maybe_operator = if runtime::get_named_arg::(ARG_IS_HASH_IDENTIFIER_MODE) { + let token_hash = runtime::get_named_arg::(ARG_TOKEN_HASH); + runtime::call_contract::>( + nft_contract_hash, + ENTRY_POINT_GET_APPROVED, + runtime_args! { + ARG_TOKEN_HASH => token_hash, + }, + ) + } else { + let token_id = runtime::get_named_arg::(ARG_TOKEN_ID); + runtime::call_contract::>( + nft_contract_hash, + ENTRY_POINT_GET_APPROVED, + runtime_args! { ARG_TOKEN_ID => token_id, }, - ); + ) + }; runtime::put_key(&key_name, storage::new_uref(maybe_operator).into()); } diff --git a/client/mint_session/README.md b/client/mint_session/README.md new file mode 100644 index 00000000..18eb59b2 --- /dev/null +++ b/client/mint_session/README.md @@ -0,0 +1,18 @@ +# Session code for minting + +Utility session code for interacting with the `mint` entry point present on the enhanced NFT contract. The session code retrieves +the read only reference and inserts the reference under the executing `Account`s `NamedKeys`. + +## Compiling session code + +The session code can be compiled to Wasm by running the `make build-contract` command provided in the Makefile at the top level. +The Wasm will be found in the `client/mint_session/target/wasm32-unknown-unknown/release` as `mint_call.wasm`. + +## Usage + +The `mint_call` session code takes in the following required runtime arguments. + +* `nft_contract_hash`: The hash of a given Enhanced NFT contract passed in as a `Key`. +* `token_owner`: The `Key` of the owner for the NFT to be minted. Note, this argument is ignored in the `Ownership::Minter` mode. +* `token_metadata`: The metadata describing the NFT to be minted, passed in as a `String`. +* `token_uri`: The URI for the off-chain resource represented by the NFT to be minted, passed in as a `String` \ No newline at end of file diff --git a/client/mint_session/src/main.rs b/client/mint_session/src/main.rs index 685a535e..b7948665 100644 --- a/client/mint_session/src/main.rs +++ b/client/mint_session/src/main.rs @@ -6,7 +6,7 @@ compile_error!("target arch should be wasm32: compile with '--target wasm32-unkn extern crate alloc; -use alloc::format; + use alloc::string::String; use casper_contract::contract_api::{runtime}; use casper_types::{runtime_args, ContractHash, Key, RuntimeArgs}; @@ -16,7 +16,7 @@ const ENTRY_POINT_MINT: &str = "mint"; const ARG_NFT_CONTRACT_HASH: &str = "nft_contract_hash"; const ARG_TOKEN_OWNER: &str = "token_owner"; const ARG_TOKEN_META_DATA: &str = "token_meta_data"; -const ARG_TOKEN_URI: &str = "token_uri"; + #[no_mangle] pub extern "C" fn call() { @@ -27,18 +27,15 @@ pub extern "C" fn call() { let token_owner = runtime::get_named_arg::(ARG_TOKEN_OWNER); let token_metadata: String = runtime::get_named_arg(ARG_TOKEN_META_DATA); - let token_uri: String = runtime::get_named_arg(ARG_TOKEN_URI); - let (owned_tokens_dictionary_key, collection_name) = runtime::call_contract::<(Key, String)>( + let (receipt_name, owned_tokens_dictionary_key, ) = runtime::call_contract::<(String, Key)>( nft_contract_hash, ENTRY_POINT_MINT, runtime_args! { ARG_TOKEN_OWNER => token_owner, ARG_TOKEN_META_DATA => token_metadata, - ARG_TOKEN_URI =>token_uri, }, ); - let nft_contract_named_key = format!("{}_{}", nft_contract_hash.to_formatted_string(), collection_name); - runtime::put_key(&nft_contract_named_key, owned_tokens_dictionary_key) + runtime::put_key(&receipt_name, owned_tokens_dictionary_key) } diff --git a/client/minting_contract/Cargo.toml b/client/minting_contract/Cargo.toml new file mode 100644 index 00000000..87927977 --- /dev/null +++ b/client/minting_contract/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "minting_contract" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +casper-contract = "1.4.3" +casper-types = "1.4.5" + +[[bin]] +name = "minting_contract" +path = "src/main.rs" +bench = false +doctest = false +test = false + +[profile.release] +codegen-units = 1 +lto = true diff --git a/client/minting_contract/src/main.rs b/client/minting_contract/src/main.rs new file mode 100644 index 00000000..8fcf1616 --- /dev/null +++ b/client/minting_contract/src/main.rs @@ -0,0 +1,163 @@ +#![no_std] +#![no_main] + +#[cfg(not(target_arch = "wasm32"))] +compile_error!("target arch should be wasm32: compile with '--target wasm32-unknown-unknown'"); + +extern crate alloc; + +use alloc::{vec,string::{String, ToString}}; + +use casper_contract::contract_api::{runtime, storage}; +use casper_types::{CLType, ContractHash, ContractVersion, EntryPoint, EntryPointAccess, EntryPoints, EntryPointType, Key, Parameter, runtime_args, RuntimeArgs}; +use casper_types::contracts::NamedKeys; + +const CONTRACT_NAME: &str = "minting_contract_hash"; +const CONTRACT_VERSION: &str = "minting_contract_version"; +const INSTALLER: &str = "installer"; +const HASH_KEY_NAME: &str = "minting_contract_package_hash"; +const ACCESS_KEY_NAME: &str = "minting_contract_access_uref"; + +const ENTRY_POINT_MINT: &str = "mint"; +const ENTRY_POINT_TRANSFER: &str = "transfer"; +const ENTRY_POINT_BURN: &str = "burn"; + +const ARG_NFT_CONTRACT_HASH: &str = "nft_contract_hash"; +const ARG_TOKEN_OWNER: &str = "token_owner"; +const ARG_TOKEN_META_DATA: &str = "token_meta_data"; +const ARG_TOKEN_URI: &str = "token_uri"; +const ARG_TARGET_KEY: &str = "target_key"; +const ARG_SOURCE_KEY: &str = "source_key"; +const ARG_TOKEN_ID: &str = "token_id"; + + +#[no_mangle] +pub extern "C" fn mint() { + let nft_contract_hash: ContractHash = runtime::get_named_arg::(ARG_NFT_CONTRACT_HASH) + .into_hash() + .map(|hash| ContractHash::new(hash)) + .unwrap(); + + let token_owner = runtime::get_named_arg::(ARG_TOKEN_OWNER); + let token_metadata: String = runtime::get_named_arg(ARG_TOKEN_META_DATA); + let token_uri: String = runtime::get_named_arg(ARG_TOKEN_URI); + + let (collection_name, owned_tokens_dictionary_key, ) = runtime::call_contract::<(String, Key)>( + nft_contract_hash, + ENTRY_POINT_MINT, + runtime_args! { + ARG_TOKEN_OWNER => token_owner, + ARG_TOKEN_META_DATA => token_metadata, + ARG_TOKEN_URI =>token_uri, + }, + ); + + runtime::put_key(&collection_name, owned_tokens_dictionary_key) +} + +#[no_mangle] +pub extern "C" fn transfer() { + let nft_contract_hash: ContractHash = runtime::get_named_arg::(ARG_NFT_CONTRACT_HASH) + .into_hash() + .map(|hash| ContractHash::new(hash)) + .unwrap(); + + let token_id = runtime::get_named_arg::(ARG_TOKEN_ID); + let from_token_owner = runtime::get_named_arg::(ARG_SOURCE_KEY); + let target_token_owner = runtime::get_named_arg::(ARG_TARGET_KEY); + + let (collection_name, owned_tokens_dictionary_key) = runtime::call_contract::<(String, Key)>( + nft_contract_hash, + ENTRY_POINT_TRANSFER, + runtime_args! { + ARG_TOKEN_ID => token_id, + ARG_SOURCE_KEY => from_token_owner, + ARG_TARGET_KEY => target_token_owner + } + ); + + runtime::put_key(&collection_name, owned_tokens_dictionary_key) +} + +#[no_mangle] +pub extern "C" fn burn() { + let nft_contract_hash: ContractHash = runtime::get_named_arg::(ARG_NFT_CONTRACT_HASH) + .into_hash() + .map(|hash| ContractHash::new(hash)) + .unwrap(); + + let token_id = runtime::get_named_arg::(ARG_TOKEN_ID); + + runtime::call_contract::<()>( + nft_contract_hash, + ENTRY_POINT_BURN, + runtime_args! { + ARG_TOKEN_ID => token_id + } + ) +} + + +fn install_minting_contract() -> (ContractHash, ContractVersion) { + let mint_entry_point = EntryPoint::new( + ENTRY_POINT_MINT, + vec![ + Parameter::new(ARG_TOKEN_META_DATA, CLType::Key), + Parameter::new(ARG_TOKEN_OWNER, CLType::Key), + Parameter::new(ARG_TOKEN_META_DATA, CLType::String), + Parameter::new(ARG_TOKEN_URI, CLType::String) + ], + CLType::Unit, + EntryPointAccess::Public, + EntryPointType::Session, + ); + + let transfer_entry_point = EntryPoint::new( + ENTRY_POINT_TRANSFER, + vec![ + Parameter::new(ARG_TOKEN_ID, CLType::U64), + Parameter::new(ARG_SOURCE_KEY, CLType::Key), + Parameter::new(ARG_TARGET_KEY, CLType::Key), + ], + CLType::Unit, + EntryPointAccess::Public, + EntryPointType::Contract, + ); + + let burn_entry_point = EntryPoint::new( + ENTRY_POINT_BURN, + vec![Parameter::new(ARG_TOKEN_ID, CLType::U64)], + CLType::Unit, + EntryPointAccess::Public, + EntryPointType::Contract, + ); + + + let mut entry_points = EntryPoints::new(); + entry_points.add_entry_point(mint_entry_point); + entry_points.add_entry_point(transfer_entry_point); + entry_points.add_entry_point(burn_entry_point); + + let named_keys = { + let mut named_keys = NamedKeys::new(); + named_keys.insert(INSTALLER.to_string(), runtime::get_caller().into()); + + named_keys + }; + + storage::new_contract( + entry_points, + Some(named_keys), + Some(HASH_KEY_NAME.to_string()), + Some(ACCESS_KEY_NAME.to_string()), + ) +} + +#[no_mangle] +pub extern "C" fn call() { + let (contract_hash, contract_version) = install_minting_contract(); + + runtime::put_key(CONTRACT_NAME, contract_hash.into()); + runtime::put_key(CONTRACT_VERSION, storage::new_uref(contract_version).into()); +} + diff --git a/client/owner_of_session/README.md b/client/owner_of_session/README.md new file mode 100644 index 00000000..c6ec4b81 --- /dev/null +++ b/client/owner_of_session/README.md @@ -0,0 +1,14 @@ +# Session code for `owner_of` + +Utility session code for calling the `owner_of` entrypoint on the enhanced NFT contract. It returns the `Key` of the owner +for a given NFT. + +## Compiling session code + +The session code can be compiled to Wasm by running the `make build-contract` command provided in the Makefile at the top level. +The Wasm will be found in the `client/owner_of_session/target/wasm32-unknown-unknown/release` as `owner_of_call.wasm`. + +## Usage + + + diff --git a/client/owner_of_session/src/main.rs b/client/owner_of_session/src/main.rs index 95ec9cf3..66730cde 100644 --- a/client/owner_of_session/src/main.rs +++ b/client/owner_of_session/src/main.rs @@ -6,26 +6,41 @@ compile_error!("target arch should be wasm32: compile with '--target wasm32-unkn extern crate alloc; use alloc::string::String; + use casper_contract::contract_api::{runtime, storage}; -use casper_types::{runtime_args, ContractHash, Key, RuntimeArgs, U256}; +use casper_types::{runtime_args, ContractHash, Key, RuntimeArgs}; const ENTRY_POINT_OWNER_OF: &str = "owner_of"; const ARG_NFT_CONTRACT_HASH: &str = "nft_contract_hash"; const ARG_KEY_NAME: &str = "key_name"; const ARG_TOKEN_ID: &str = "token_id"; +const ARG_TOKEN_HASH: &str = "token_hash"; +const ARG_IS_HASH_IDENTIFIER_MODE: &str = "is_hash_identifier_mode"; #[no_mangle] pub extern "C" fn call() { - let nft_contract_hash: ContractHash = runtime::get_named_arg(ARG_NFT_CONTRACT_HASH); + let nft_contract_hash: ContractHash = runtime::get_named_arg::(ARG_NFT_CONTRACT_HASH) + .into_hash() + .map(|hash| ContractHash::new(hash)) + .unwrap(); let key_name: String = runtime::get_named_arg(ARG_KEY_NAME); - let token_id: U256 = runtime::get_named_arg(ARG_TOKEN_ID); - let owner = runtime::call_contract::( - nft_contract_hash, - ENTRY_POINT_OWNER_OF, - runtime_args! { + let owner = if runtime::get_named_arg(ARG_IS_HASH_IDENTIFIER_MODE) { + let token_hash = runtime::get_named_arg::(ARG_TOKEN_HASH); + runtime::call_contract::( + nft_contract_hash, + ENTRY_POINT_OWNER_OF, + runtime_args! { + ARG_TOKEN_HASH => token_hash, + },) + } else { + let token_id = runtime::get_named_arg::(ARG_TOKEN_ID); + runtime::call_contract::( + nft_contract_hash, + ENTRY_POINT_OWNER_OF, + runtime_args! { ARG_TOKEN_ID => token_id, - }, - ); + },) + }; runtime::put_key(&key_name, storage::new_uref(owner).into()); } diff --git a/client/transfer_session/Cargo.toml b/client/transfer_session/Cargo.toml new file mode 100644 index 00000000..0933f36c --- /dev/null +++ b/client/transfer_session/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "transfer_session" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +casper-contract = "1.4.3" +casper-types = "1.4.5" + +[[bin]] +name = "transfer_call" +path = "src/main.rs" +bench = false +doctest = false +test = false + +[profile.release] +codegen-units = 1 +lto = true diff --git a/client/transfer_session/src/main.rs b/client/transfer_session/src/main.rs new file mode 100644 index 00000000..f56a8e46 --- /dev/null +++ b/client/transfer_session/src/main.rs @@ -0,0 +1,54 @@ +#![no_std] +#![no_main] + +extern crate alloc; + +use alloc::string::String; + +use casper_contract::contract_api::runtime; +use casper_types::{ContractHash, Key, runtime_args, RuntimeArgs}; + +const ENTRY_POINT_TRANSFER: &str = "transfer"; + +const ARG_NFT_CONTRACT_HASH: &str = "nft_contract_hash"; +const ARG_IS_HASH_IDENTIFIER_MODE: &str = "is_hash_identifier_mode"; +const ARG_TOKEN_ID: &str = "token_id"; +const ARG_TOKEN_HASH: &str = "token_hash"; +const ARG_TARGET_KEY: &str = "target_key"; +const ARG_SOURCE_KEY: &str = "source_key"; + +#[no_mangle] +pub extern "C" fn call() { + let nft_contract_hash: ContractHash = runtime::get_named_arg::(ARG_NFT_CONTRACT_HASH) + .into_hash() + .map(|hash| ContractHash::new(hash)) + .unwrap(); + + let source_key: Key = runtime::get_named_arg(ARG_SOURCE_KEY); + let target_key: Key = runtime::get_named_arg(ARG_TARGET_KEY); + + + let (receipt_name, owned_tokens_dictionary_key, ) = if !runtime::get_named_arg::(ARG_IS_HASH_IDENTIFIER_MODE) { + let token_id: u64 = runtime::get_named_arg(ARG_TOKEN_ID); + runtime::call_contract::<(String, Key)>( + nft_contract_hash, + ENTRY_POINT_TRANSFER, + runtime_args! { + ARG_TOKEN_ID => token_id, + ARG_TARGET_KEY => target_key, + ARG_SOURCE_KEY => source_key + }) + } else { + let token_hash: String = runtime::get_named_arg(ARG_TOKEN_HASH); + runtime::call_contract::<(String, Key)>( + nft_contract_hash, + ENTRY_POINT_TRANSFER, + runtime_args! { + ARG_TOKEN_HASH => token_hash, + ARG_TARGET_KEY => target_key, + ARG_SOURCE_KEY => source_key + }) + }; + + runtime::put_key(&receipt_name, owned_tokens_dictionary_key) +} \ No newline at end of file diff --git a/contract/Cargo.toml b/contract/Cargo.toml index 58877f1e..d4ce6b54 100644 --- a/contract/Cargo.toml +++ b/contract/Cargo.toml @@ -4,8 +4,11 @@ version = "0.1.0" edition = "2018" [dependencies] -casper-contract = "1.4.3" +casper-contract = {version = "1.4.3", features = ["test-support"]} casper-types = "1.4.5" +serde = { version = "1", features = ["derive", "alloc"], default-features = false } +base16 = { version = "0.2", default-features = false, features = ["alloc"] } +casper-serde-json-wasm = { git = "https://github.com/darthsiroftardis/casper-serde-json-wasm", branch = "casper-no-std"} [[bin]] name = "contract" diff --git a/contract/src/constants.rs b/contract/src/constants.rs index d3a06495..12747414 100644 --- a/contract/src/constants.rs +++ b/contract/src/constants.rs @@ -2,25 +2,31 @@ pub const ARG_COLLECTION_NAME: &str = "collection_name"; pub const ARG_COLLECTION_SYMBOL: &str = "collection_symbol"; pub const ARG_TOTAL_TOKEN_SUPPLY: &str = "total_token_supply"; pub const ARG_TOKEN_ID: &str = "token_id"; +pub const ARG_TOKEN_HASH: &str = "token_hash"; pub const ARG_TOKEN_OWNER: &str = "token_owner"; -pub const ARG_TO_ACCOUNT_HASH: &str = "to_account_hash"; -pub const ARG_FROM_ACCOUNT_HASH: &str = "from_account_hash"; +pub const ARG_TARGET_KEY: &str = "target_key"; +pub const ARG_SOURCE_KEY: &str = "source_key"; pub const ARG_ALLOW_MINTING: &str = "allow_minting"; pub const ARG_MINTING_MODE: &str = "minting_mode"; pub const ARG_TOKEN_META_DATA: &str = "token_meta_data"; pub const ARG_APPROVE_ALL: &str = "approve_all"; pub const ARG_OPERATOR: &str = "operator"; pub const ARG_OWNERSHIP_MODE: &str = "ownership_mode"; -pub const _ARG_HOLDER_MODE: &str = "holder_mode"; -pub const _ARG_WHITELIST_MODE: &str = "whitelist_mode"; +pub const ARG_HOLDER_MODE: &str = "holder_mode"; +pub const ARG_WHITELIST_MODE: &str = "whitelist_mode"; pub const ARG_NFT_KIND: &str = "nft_kind"; pub const ARG_JSON_SCHEMA: &str = "json_schema"; -pub const ARG_TOKEN_URI: &str = "token_uri"; -pub const _ARG_CONTRACT_WHITELIST: &str = "contract_whitelist"; + +pub const ARG_RECEIPT_NAME: &str = "receipt_name"; +pub const ARG_CONTRACT_WHITELIST: &str = "contract_whitelist"; +pub const ARG_NFT_METADATA_KIND: &str = "nft_metadata_kind"; +pub const ARG_IDENTIFIER_MODE: &str = "identifier_mode"; + pub const OPERATOR: &str = "operator"; pub const NUMBER_OF_MINTED_TOKENS: &str = "number_of_minted_tokens"; pub const INSTALLER: &str = "installer"; pub const JSON_SCHEMA: &str = "json_schema"; +pub const METADATA_SCHEMA: &str = "metadata_schema"; pub const CONTRACT_NAME: &str = "nft_contract"; pub const HASH_KEY_NAME: &str = "nft_contract_package"; pub const ACCESS_KEY_NAME: &str = "nft_contract_package_access"; @@ -29,18 +35,24 @@ pub const COLLECTION_NAME: &str = "collection_name"; pub const COLLECTION_SYMBOL: &str = "collection_symbol"; pub const TOTAL_TOKEN_SUPPLY: &str = "total_token_supply"; pub const OWNERSHIP_MODE: &str = "ownership_mode"; -pub const NFT_KIND: &str = "nft_asset_type"; +pub const NFT_KIND: &str = "nft_kind"; pub const ALLOW_MINTING: &str = "allow_minting"; pub const MINTING_MODE: &str = "minting_mode"; -pub const _HOLDER_MODE: &str = "holder_mode"; +pub const HOLDER_MODE: &str = "holder_mode"; +pub const WHITELIST_MODE: &str = "whitelist_mode"; pub const TOKEN_OWNERS: &str = "token_owners"; pub const TOKEN_ISSUERS: &str = "token_issuers"; -pub const TOKEN_META_DATA: &str = "token_meta_data"; -pub const TOKEN_URI: &str = "token_uri"; pub const OWNED_TOKENS: &str = "owned_tokens"; pub const BURNT_TOKENS: &str = "burnt_tokens"; pub const TOKEN_COUNTS: &str = "balances"; -pub const _CONTRACT_WHITELIST: &str = "contract_whitelist"; +pub const CONTRACT_WHITELIST: &str = "contract_whitelist"; +pub const RECEIPT_NAME: &str = "receipt_name"; +pub const NFT_METADATA_KIND: &str = "nft_metadata_kind"; +pub const IDENTIFIER_MODE: &str = "identifier_mode"; +pub const METADATA_CUSTOM_VALIDATED: &str = "metadata_custom_validated"; +pub const METADATA_CEP78: &str = "metadata_cep78"; +pub const METADATA_NFT721: &str = "metadata_nft721"; +pub const METADATA_RAW: &str = "metadata_raw"; pub const ENTRY_POINT_INIT: &str = "init"; pub const ENTRY_POINT_SET_VARIABLES: &str = "set_variables"; pub const ENTRY_POINT_MINT: &str = "mint"; diff --git a/contract/src/error.rs b/contract/src/error.rs index ea417472..f48310f7 100644 --- a/contract/src/error.rs +++ b/contract/src/error.rs @@ -85,6 +85,27 @@ pub enum NFTCoreError { 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, } impl From for ApiError { diff --git a/contract/src/main.rs b/contract/src/main.rs index 96837211..1e5249b7 100644 --- a/contract/src/main.rs +++ b/contract/src/main.rs @@ -9,19 +9,26 @@ mod error; mod utils; extern crate alloc; -use core::convert::TryInto; -use alloc::{boxed::Box, string::String, string::ToString, vec, vec::Vec}; +use alloc::{ + boxed::Box, + format, + string::{String, ToString}, + vec, + vec::Vec, +}; +use core::convert::TryInto; use casper_types::{ - account::AccountHash, contracts::NamedKeys, runtime_args, CLType, CLValue, ContractHash, - ContractVersion, EntryPoint, EntryPointAccess, EntryPointType, EntryPoints, Key, Parameter, - RuntimeArgs, U256, + contracts::NamedKeys, runtime_args, CLType, CLValue, ContractHash, ContractPackageHash, + ContractVersion, EntryPoint, EntryPointAccess, EntryPointType, EntryPoints, Key, KeyTag, + Parameter, RuntimeArgs, Tagged, }; use casper_contract::{ contract_api::{ runtime, + runtime::revert, storage::{self}, }, unwrap_or_revert::UnwrapOrRevert, @@ -67,7 +74,7 @@ pub extern "C" fn init() { ) .unwrap_or_revert(); - let total_token_supply: U256 = get_named_arg_with_user_errors( + let total_token_supply: u64 = get_named_arg_with_user_errors( ARG_TOTAL_TOKEN_SUPPLY, NFTCoreError::MissingTotalTokenSupply, NFTCoreError::InvalidTotalTokenSupply, @@ -108,6 +115,38 @@ pub extern "C" fn init() { .try_into() .unwrap_or_revert(); + let holder_mode: NFTHolderMode = get_named_arg_with_user_errors::( + ARG_HOLDER_MODE, + NFTCoreError::MissingHolderMode, + NFTCoreError::InvalidHolderMode, + ) + .unwrap_or_revert() + .try_into() + .unwrap_or_revert(); + + let whitelist_mode: WhitelistMode = get_named_arg_with_user_errors::( + ARG_WHITELIST_MODE, + NFTCoreError::MissingWhitelistMode, + NFTCoreError::InvalidWhitelistMode, + ) + .unwrap_or_revert() + .try_into() + .unwrap_or_revert(); + + let contract_whitelist = get_named_arg_with_user_errors::>( + ARG_CONTRACT_WHITELIST, + NFTCoreError::MissingContractWhiteList, + NFTCoreError::InvalidContractWhitelist, + ) + .unwrap_or_revert(); + + if WhitelistMode::Locked == whitelist_mode + && NFTHolderMode::Contracts == holder_mode + && contract_whitelist.is_empty() + { + runtime::revert(NFTCoreError::EmptyContractWhitelist) + } + let json_schema: String = get_named_arg_with_user_errors( ARG_JSON_SCHEMA, NFTCoreError::MissingJsonSchema, @@ -115,6 +154,31 @@ pub extern "C" fn init() { ) .unwrap_or_revert(); + let receipt_name: String = get_named_arg_with_user_errors( + ARG_RECEIPT_NAME, + NFTCoreError::MissingReceiptName, + NFTCoreError::InvalidReceiptName, + ) + .unwrap_or_revert(); + + let nft_metadata_kind: NFTMetadataKind = get_named_arg_with_user_errors::( + ARG_NFT_METADATA_KIND, + NFTCoreError::MissingNFTMetadataKind, + NFTCoreError::InvalidNFTMetadataKind, + ) + .unwrap_or_revert() + .try_into() + .unwrap_or_revert(); + + let identifier_mode: NFTIdentifierMode = get_named_arg_with_user_errors::( + ARG_IDENTIFIER_MODE, + NFTCoreError::MissingIdentifierMode, + NFTCoreError::InvalidIdentifierMode, + ) + .unwrap_or_revert() + .try_into() + .unwrap_or_revert(); + // Put all created URefs into the contract's context (necessary to retain access rights, // for future use). // @@ -132,29 +196,40 @@ pub extern "C" fn init() { OWNERSHIP_MODE, storage::new_uref(ownership_mode as u8).into(), ); - runtime::put_key(NFT_KIND, storage::new_uref(nft_kind as u8).into()); - runtime::put_key(JSON_SCHEMA, storage::new_uref(json_schema).into()); + runtime::put_key(MINTING_MODE, storage::new_uref(minting_mode as u8).into()); + runtime::put_key(HOLDER_MODE, storage::new_uref(holder_mode as u8).into()); + runtime::put_key( + WHITELIST_MODE, + storage::new_uref(whitelist_mode as u8).into(), + ); + runtime::put_key( + CONTRACT_WHITELIST, + storage::new_uref(contract_whitelist).into(), + ); + runtime::put_key(RECEIPT_NAME, storage::new_uref(receipt_name).into()); + runtime::put_key( + NFT_METADATA_KIND, + storage::new_uref(nft_metadata_kind as u8).into(), + ); + runtime::put_key( + IDENTIFIER_MODE, + storage::new_uref(identifier_mode as u8).into(), + ); // Initialize contract with variables which must be present but maybe set to // different values after initialization. runtime::put_key(ALLOW_MINTING, storage::new_uref(allow_minting).into()); - runtime::put_key(MINTING_MODE, storage::new_uref(minting_mode as u8).into()); // This is an internal variable that the installing account cannot change // but is incremented by the contract itself. - runtime::put_key( - NUMBER_OF_MINTED_TOKENS, - storage::new_uref(U256::zero()).into(), - ); + runtime::put_key(NUMBER_OF_MINTED_TOKENS, storage::new_uref(0u64).into()); // Create the data dictionaries to store essential values, topically. storage::new_dictionary(TOKEN_OWNERS) .unwrap_or_revert_with(NFTCoreError::FailedToCreateDictionary); storage::new_dictionary(TOKEN_ISSUERS) .unwrap_or_revert_with(NFTCoreError::FailedToCreateDictionary); - storage::new_dictionary(TOKEN_META_DATA) - .unwrap_or_revert_with(NFTCoreError::FailedToCreateDictionary); storage::new_dictionary(OWNED_TOKENS) .unwrap_or_revert_with(NFTCoreError::FailedToCreateDictionary); storage::new_dictionary(OPERATOR).unwrap_or_revert_with(NFTCoreError::FailedToCreateDictionary); @@ -162,7 +237,13 @@ pub extern "C" fn init() { .unwrap_or_revert_with(NFTCoreError::FailedToCreateDictionary); storage::new_dictionary(TOKEN_COUNTS) .unwrap_or_revert_with(NFTCoreError::FailedToCreateDictionary); - storage::new_dictionary(TOKEN_URI) + storage::new_dictionary(METADATA_CUSTOM_VALIDATED) + .unwrap_or_revert_with(NFTCoreError::FailedToCreateDictionary); + storage::new_dictionary(METADATA_CEP78) + .unwrap_or_revert_with(NFTCoreError::FailedToCreateDictionary); + storage::new_dictionary(METADATA_NFT721) + .unwrap_or_revert_with(NFTCoreError::FailedToCreateDictionary); + storage::new_dictionary(METADATA_RAW) .unwrap_or_revert_with(NFTCoreError::FailedToCreateDictionary); } @@ -192,6 +273,30 @@ pub extern "C" fn set_variables() { ); storage::write(allow_minting_uref, allow_minting); } + + if let Some(new_contract_whitelist) = get_optional_named_arg_with_user_errors::>( + ARG_CONTRACT_WHITELIST, + NFTCoreError::MissingContractWhiteList, + ) { + let whitelist_mode: WhitelistMode = get_stored_value_with_user_errors::( + WHITELIST_MODE, + NFTCoreError::MissingWhitelistMode, + NFTCoreError::InvalidWhitelistMode, + ) + .try_into() + .unwrap_or_revert(); + match whitelist_mode { + WhitelistMode::Unlocked => { + let whitelist_uref = get_uref( + CONTRACT_WHITELIST, + NFTCoreError::MissingContractWhiteList, + NFTCoreError::InvalidWhitelistMode, + ); + storage::write(whitelist_uref, new_contract_whitelist) + } + WhitelistMode::Locked => runtime::revert(NFTCoreError::InvalidWhitelistMode), + } + } } // Mints a new token. Minting will fail if allow_minting is set to false. @@ -207,17 +312,17 @@ pub extern "C" fn mint() { // If contract minting behavior is currently toggled off we revert. if !minting_status { - runtime::revert(NFTCoreError::MintingIsPaused); + revert(NFTCoreError::MintingIsPaused); } - let total_token_supply = get_stored_value_with_user_errors::( + let total_token_supply = get_stored_value_with_user_errors::( TOTAL_TOKEN_SUPPLY, NFTCoreError::MissingTotalTokenSupply, NFTCoreError::InvalidTotalTokenSupply, ); // The next_index is the number of minted tokens so far. - let mut next_index = get_stored_value_with_user_errors::( + let mut next_index = get_stored_value_with_user_errors::( NUMBER_OF_MINTED_TOKENS, NFTCoreError::MissingNumberOfMintedTokens, NFTCoreError::InvalidNumberOfMintedTokens, @@ -225,10 +330,9 @@ pub extern "C" fn mint() { // Revert if the token supply has been exhausted. if next_index >= total_token_supply { - runtime::revert(NFTCoreError::TokenSupplyDepleted); + revert(NFTCoreError::TokenSupplyDepleted); } - let caller = runtime::get_caller(); let minting_mode: MintingMode = get_stored_value_with_user_errors::( MINTING_MODE, NFTCoreError::MissingMintingMode, @@ -239,58 +343,99 @@ pub extern "C" fn mint() { // Revert if minting is private and caller is not installer. if let MintingMode::Installer = minting_mode { - let installer_account = runtime::get_key(INSTALLER) - .unwrap_or_revert_with(NFTCoreError::MissingInstallerKey) - .into_account() - .unwrap_or_revert_with(NFTCoreError::FailedToConvertToAccountHash); - - // Revert if private minting is required and caller is not installer. - if caller != installer_account { - runtime::revert(NFTCoreError::InvalidMinter) + let caller = get_verified_caller().unwrap_or_revert(); + match caller.tag() { + KeyTag::Hash => { + let calling_contract = caller + .into_hash() + .map(ContractHash::new) + .unwrap_or_revert_with(NFTCoreError::InvalidKey); + let contract_whitelist = get_stored_value_with_user_errors::>( + CONTRACT_WHITELIST, + NFTCoreError::MissingWhitelistMode, + NFTCoreError::InvalidWhitelistMode, + ); + // Revert if the calling contract is not in the whitelist. + if !contract_whitelist.contains(&calling_contract) { + revert(NFTCoreError::UnlistedContractHash) + } + } + KeyTag::Account => { + let installer_account = runtime::get_key(INSTALLER) + .unwrap_or_revert_with(NFTCoreError::MissingInstallerKey) + .into_account() + .unwrap_or_revert_with(NFTCoreError::FailedToConvertToAccountHash); + + // Revert if private minting is required and caller is not installer. + if runtime::get_caller() != installer_account { + runtime::revert(NFTCoreError::InvalidMinter) + } + } + _ => revert(NFTCoreError::InvalidKey), } } // The contract's ownership behavior (determined at installation) determines, // who owns the NFT we are about to mint.() - let ownership_mode = utils::get_ownership_mode().unwrap_or_revert(); - let token_owner_key = { - match ownership_mode { - OwnershipMode::Minter => Key::Account(caller), - OwnershipMode::Assigned | OwnershipMode::Transferable => { - runtime::get_named_arg::(ARG_TOKEN_OWNER) - } - } + let ownership_mode = get_ownership_mode().unwrap_or_revert(); + let caller = get_verified_caller().unwrap_or_revert(); + let token_owner_key: Key = if let OwnershipMode::Assigned = ownership_mode { + runtime::get_named_arg(ARG_TOKEN_OWNER) + } else { + caller }; - let token_uri: String = get_named_arg_with_user_errors( - ARG_TOKEN_URI, - NFTCoreError::MissingTokenURI, - NFTCoreError::InvalidTokenURI, + let metadata_kind: NFTMetadataKind = get_stored_value_with_user_errors::( + NFT_METADATA_KIND, + NFTCoreError::MissingNFTMetadataKind, + NFTCoreError::InvalidNFTMetadataKind, ) + .try_into() .unwrap_or_revert(); - // Get token metadata - let token_meta_data: String = get_named_arg_with_user_errors( + let token_metadata = get_named_arg_with_user_errors::( ARG_TOKEN_META_DATA, NFTCoreError::MissingTokenMetaData, NFTCoreError::InvalidTokenMetaData, ) .unwrap_or_revert(); + // Get token metadata if valid. + let metadata = validate_metadata(&metadata_kind, token_metadata).unwrap_or_revert(); + + let identifier_mode: NFTIdentifierMode = get_stored_value_with_user_errors::( + IDENTIFIER_MODE, + NFTCoreError::MissingIdentifierMode, + NFTCoreError::InvalidIdentifierMode, + ) + .try_into() + .unwrap_or_revert(); + // This is the token ID. - let dictionary_item_key = &next_index.to_string(); + let token_id: TokenIdentifier = match identifier_mode { + NFTIdentifierMode::Ordinal => TokenIdentifier::Index(next_index), + NFTIdentifierMode::Hash => { + TokenIdentifier::Hash(base16::encode_lower(&runtime::blake2b(&metadata))) + } + }; - upsert_dictionary_value_from_key(TOKEN_OWNERS, dictionary_item_key, token_owner_key); - upsert_dictionary_value_from_key(TOKEN_META_DATA, dictionary_item_key, token_meta_data); - upsert_dictionary_value_from_key(TOKEN_URI, dictionary_item_key, token_uri); - upsert_dictionary_value_from_key(TOKEN_ISSUERS, dictionary_item_key, Key::Account(caller)); + upsert_dictionary_value_from_key( + TOKEN_OWNERS, + &token_id.get_dictionary_item_key(), + token_owner_key, + ); + upsert_dictionary_value_from_key( + TOKEN_ISSUERS, + &token_id.get_dictionary_item_key(), + token_owner_key, + ); + upsert_dictionary_value_from_key( + &get_metadata_dictionary_name(&metadata_kind), + &token_id.get_dictionary_item_key(), + metadata, + ); - // We use the string representation of the account_hash as to not exceed the dictionary_item_key length limit, - // which is currently set to 64. (Using Key::Account().to_string() exceeds the limit of 64.) - let owned_tokens_item_key = token_owner_key - .into_account() - .unwrap_or_revert_with(NFTCoreError::InvalidKey) - .to_string(); + let owned_tokens_item_key = get_owned_tokens_dictionary_item_key(token_owner_key); let owned_tokens_actual_key = Key::dictionary( get_uref( @@ -302,39 +447,90 @@ pub extern "C" fn mint() { ); // Update owned tokens dictionary - let maybe_owned_tokens = - get_dictionary_value_from_key::>(OWNED_TOKENS, &owned_tokens_item_key); + let maybe_owned_tokens: Option> = { + match identifier_mode { + NFTIdentifierMode::Ordinal => { + get_dictionary_value_from_key::>(OWNED_TOKENS, &owned_tokens_item_key).map( + |token_indices| { + token_indices + .into_iter() + .map(TokenIdentifier::new_index) + .collect() + }, + ) + } + NFTIdentifierMode::Hash => { + get_dictionary_value_from_key::>(OWNED_TOKENS, &owned_tokens_item_key) + .map(|token_hashes| { + token_hashes + .into_iter() + .map(TokenIdentifier::new_hash) + .collect() + }) + } + } + }; // Update the value in the owned_tokens dictionary. match maybe_owned_tokens { Some(mut owned_tokens) => { // Check that we are not minting a duplicate token. - if owned_tokens.contains(&next_index) { + if owned_tokens.contains(&token_id) { runtime::revert(NFTCoreError::FatalTokenIdDuplication); } - owned_tokens.push(next_index); - upsert_dictionary_value_from_key(OWNED_TOKENS, &owned_tokens_item_key, owned_tokens); + owned_tokens.push(token_id); + match identifier_mode { + NFTIdentifierMode::Ordinal => { + let token_indices: Vec = owned_tokens + .into_iter() + .map(|identifier| identifier.get_index().unwrap_or_revert()) + .collect(); + upsert_dictionary_value_from_key( + OWNED_TOKENS, + &owned_tokens_item_key, + token_indices, + ) + } + NFTIdentifierMode::Hash => { + let token_hashes: Vec = owned_tokens + .into_iter() + .map(|identifier| identifier.get_hash().unwrap_or_revert()) + .collect(); + upsert_dictionary_value_from_key( + OWNED_TOKENS, + &owned_tokens_item_key, + token_hashes, + ) + } + } } None => { - upsert_dictionary_value_from_key( - OWNED_TOKENS, - &owned_tokens_item_key, - vec![next_index], - ); + match identifier_mode { + NFTIdentifierMode::Ordinal => upsert_dictionary_value_from_key( + OWNED_TOKENS, + &owned_tokens_item_key, + vec![token_id.get_index().unwrap_or_revert()], + ), + NFTIdentifierMode::Hash => upsert_dictionary_value_from_key( + OWNED_TOKENS, + &owned_tokens_item_key, + vec![token_id.get_hash().unwrap_or_revert()], + ), + }; } }; //Increment the count of owned tokens. let updated_token_count = - match get_dictionary_value_from_key::(TOKEN_COUNTS, &owned_tokens_item_key) { - Some(balance) => balance + U256::one(), - None => U256::one(), + match get_dictionary_value_from_key::(TOKEN_COUNTS, &owned_tokens_item_key) { + Some(balance) => balance + 1u64, + None => 1u64, }; upsert_dictionary_value_from_key(TOKEN_COUNTS, &owned_tokens_item_key, updated_token_count); // Increment number_of_minted_tokens by one - next_index += U256::one(); + next_index += 1u64; let number_of_minted_tokens_uref = get_uref( NUMBER_OF_MINTED_TOKENS, NFTCoreError::MissingTotalTokenSupply, @@ -342,13 +538,13 @@ pub extern "C" fn mint() { ); storage::write(number_of_minted_tokens_uref, next_index); - let collection_name: String = get_stored_value_with_user_errors( - COLLECTION_NAME, - NFTCoreError::MissingCollectionName, - NFTCoreError::InvalidCollectionName, + let receipt_name = get_stored_value_with_user_errors::( + RECEIPT_NAME, + NFTCoreError::MissingReceiptName, + NFTCoreError::InvalidReceiptName, ); - let receipt = CLValue::from_t((owned_tokens_actual_key, collection_name)) + let receipt = CLValue::from_t((receipt_name, owned_tokens_actual_key)) .unwrap_or_revert_with(NFTCoreError::FailedToConvertToCLValue); runtime::ret(receipt) } @@ -356,63 +552,92 @@ pub extern "C" fn mint() { // Marks token as burnt. This blocks and future call to transfer token. #[no_mangle] pub extern "C" fn burn() { - let token_id: U256 = get_named_arg_with_user_errors( - ARG_TOKEN_ID, - NFTCoreError::MissingTokenID, - NFTCoreError::InvalidTokenID, + let identifier_mode: NFTIdentifierMode = get_stored_value_with_user_errors::( + IDENTIFIER_MODE, + NFTCoreError::MissingIdentifierMode, + NFTCoreError::InvalidIdentifierMode, ) + .try_into() .unwrap_or_revert(); - let caller: AccountHash = runtime::get_caller(); + let token_identifier = match identifier_mode { + NFTIdentifierMode::Ordinal => get_named_arg_with_user_errors::( + ARG_TOKEN_ID, + NFTCoreError::MissingTokenID, + NFTCoreError::InvalidTokenID, + ) + .map(TokenIdentifier::new_index) + .unwrap_or_revert(), + NFTIdentifierMode::Hash => get_named_arg_with_user_errors::( + ARG_TOKEN_HASH, + NFTCoreError::MissingTokenID, + NFTCoreError::InvalidTokenID, + ) + .map(TokenIdentifier::new_hash) + .unwrap_or_revert(), + }; + + let expected_token_owner: Key = get_verified_caller().unwrap_or_revert(); // Revert if caller is not token_owner. This seems to be the only check we need to do. - let token_owner = - match get_dictionary_value_from_key::(TOKEN_OWNERS, &token_id.to_string()) { - Some(token_owner_key) => { - let token_owner_account_hash = token_owner_key - .into_account() - .unwrap_or_revert_with(NFTCoreError::InvalidKey); - if token_owner_account_hash != caller { - runtime::revert(NFTCoreError::InvalidTokenOwner) - } - token_owner_account_hash + let token_owner = match get_dictionary_value_from_key::( + TOKEN_OWNERS, + &token_identifier.get_dictionary_item_key(), + ) { + Some(token_owner_key) => { + if token_owner_key != expected_token_owner { + runtime::revert(NFTCoreError::InvalidTokenOwner) } - None => runtime::revert(NFTCoreError::InvalidTokenID), - }; + token_owner_key + } + None => runtime::revert(NFTCoreError::InvalidTokenID), + }; // 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. - if get_dictionary_value_from_key::<()>(BURNT_TOKENS, &token_id.to_string()).is_some() { + // owns a burnt token. That's all. Similarly, we should probably also not change the + // owned_tokens dictionary. + if get_dictionary_value_from_key::<()>( + BURNT_TOKENS, + &token_identifier.get_dictionary_item_key(), + ) + .is_some() + { runtime::revert(NFTCoreError::PreviouslyBurntToken); } // Mark the token as burnt by adding the token_id to the burnt tokens dictionary. - upsert_dictionary_value_from_key::<()>(BURNT_TOKENS, &token_id.to_string(), ()); + upsert_dictionary_value_from_key::<()>( + BURNT_TOKENS, + &token_identifier.get_dictionary_item_key(), + (), + ); + + let owned_tokens_item_key = get_owned_tokens_dictionary_item_key(token_owner); let updated_balance = - match get_dictionary_value_from_key::(TOKEN_COUNTS, &token_owner.to_string()) { + match get_dictionary_value_from_key::(TOKEN_COUNTS, &owned_tokens_item_key) { Some(balance) => { - if balance > U256::zero() { - balance - U256::one() + if balance > 0u64 { + balance - 1u64 } else { // This should never happen if contract is implemented correctly. - runtime::revert(NFTCoreError::FatalTokenIdDuplication); + revert(NFTCoreError::FatalTokenIdDuplication); } } None => { // This should never happen if contract is implemented correctly. - runtime::revert(NFTCoreError::FatalTokenIdDuplication); + revert(NFTCoreError::FatalTokenIdDuplication); } }; - upsert_dictionary_value_from_key(TOKEN_COUNTS, &caller.to_string(), updated_balance); + upsert_dictionary_value_from_key(TOKEN_COUNTS, &owned_tokens_item_key, updated_balance); } // approve marks a token as approved for transfer by an account #[no_mangle] pub extern "C" fn approve() { - // 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. let _ownership_mode = match get_ownership_mode().unwrap_or_revert() { OwnershipMode::Minter | OwnershipMode::Assigned => { runtime::revert(NFTCoreError::InvalidOwnershipMode) @@ -420,40 +645,50 @@ pub extern "C" fn approve() { OwnershipMode::Transferable => OwnershipMode::Transferable, }; - let caller = runtime::get_caller(); - let token_id = get_named_arg_with_user_errors::( - ARG_TOKEN_ID, - NFTCoreError::MissingTokenID, - NFTCoreError::InvalidTokenID, + let caller: Key = get_verified_caller().unwrap_or_revert(); + + let identifier_mode: NFTIdentifierMode = get_stored_value_with_user_errors::( + IDENTIFIER_MODE, + NFTCoreError::MissingIdentifierMode, + NFTCoreError::InvalidIdentifierMode, ) + .try_into() .unwrap_or_revert(); - let number_of_minted_tokens = get_stored_value_with_user_errors::( + let token_identifier = get_token_identifier_from_runtime_args(&identifier_mode); + let token_identifier_dictionary_key = token_identifier.get_dictionary_item_key(); + + let number_of_minted_tokens = get_stored_value_with_user_errors::( NUMBER_OF_MINTED_TOKENS, NFTCoreError::MissingNumberOfMintedTokens, NFTCoreError::InvalidNumberOfMintedTokens, ); - // Revert if token_id is out of bounds - if token_id >= number_of_minted_tokens { - runtime::revert(NFTCoreError::InvalidTokenID); + if let NFTIdentifierMode::Ordinal = identifier_mode { + // Revert if token_id is out of bounds + if let TokenIdentifier::Index(index) = &token_identifier { + if *index >= number_of_minted_tokens { + revert(NFTCoreError::InvalidTokenID); + } + } } - let token_owner_account_hash = - match get_dictionary_value_from_key::(TOKEN_OWNERS, &token_id.to_string()) { - Some(token_owner) => token_owner, - None => runtime::revert(NFTCoreError::InvalidAccountHash), - } - .into_account() - .unwrap_or_revert_with(NFTCoreError::InvalidKey); + let token_owner_key = match get_dictionary_value_from_key::( + TOKEN_OWNERS, + &token_identifier_dictionary_key, + ) { + Some(token_owner) => token_owner, + None => runtime::revert(NFTCoreError::InvalidAccountHash), + }; // Revert if caller is not the token_owner. Only the token owner can approve an operator - if token_owner_account_hash != caller { + if token_owner_key != caller { runtime::revert(NFTCoreError::InvalidAccountHash); } // We assume a burnt token cannot be approved - if get_dictionary_value_from_key::<()>(BURNT_TOKENS, &token_id.to_string()).is_some() { + if get_dictionary_value_from_key::<()>(BURNT_TOKENS, &token_identifier_dictionary_key).is_some() + { runtime::revert(NFTCoreError::PreviouslyBurntToken); } @@ -462,12 +697,10 @@ pub extern "C" fn approve() { NFTCoreError::MissingApprovedAccountHash, NFTCoreError::InvalidApprovedAccountHash, ) - .unwrap_or_revert() - .into_account() - .unwrap_or_revert_with(NFTCoreError::InvalidKey); + .unwrap_or_revert(); // If token_owner tries to approve themselves that's probably a mistake and we revert. - if token_owner_account_hash == operator { + if token_owner_key == operator { runtime::revert(NFTCoreError::InvalidAccount); } @@ -479,15 +712,16 @@ pub extern "C" fn approve() { storage::dictionary_put( approved_uref, - &token_id.to_string(), - Some(Key::Account(operator)), + &token_identifier_dictionary_key, + Some(operator), ); } // Approves the specified operator for transfer token_owner's tokens. #[no_mangle] pub extern "C" fn set_approval_for_all() { - // 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. let _ownership_mode = match utils::get_ownership_mode().unwrap_or_revert() { OwnershipMode::Minter | OwnershipMode::Assigned => { runtime::revert(NFTCoreError::InvalidOwnershipMode) @@ -510,30 +744,71 @@ pub extern "C" fn set_approval_for_all() { ) .unwrap_or_revert(); - let caller = runtime::get_caller().to_string(); + let caller: Key = get_verified_caller().unwrap_or_revert(); + + let caller_dictionary_item_key = get_owned_tokens_dictionary_item_key(caller); + let approved_uref = get_uref( OPERATOR, NFTCoreError::MissingStorageUref, NFTCoreError::InvalidStorageUref, ); - if let Some(owned_tokens) = get_dictionary_value_from_key::>(OWNED_TOKENS, &caller) { + let identifier_mode: NFTIdentifierMode = get_stored_value_with_user_errors::( + IDENTIFIER_MODE, + NFTCoreError::MissingIdentifierMode, + NFTCoreError::InvalidIdentifierMode, + ) + .try_into() + .unwrap_or_revert(); + + let maybe_owned_tokens: Option> = match identifier_mode { + NFTIdentifierMode::Ordinal => { + get_dictionary_value_from_key::>(OWNED_TOKENS, &caller_dictionary_item_key) + .map(|token_hashes| { + token_hashes + .into_iter() + .map(TokenIdentifier::new_index) + .collect() + }) + } + NFTIdentifierMode::Hash => { + get_dictionary_value_from_key::>(OWNED_TOKENS, &caller_dictionary_item_key) + .map(|token_hashes| { + token_hashes + .into_iter() + .map(TokenIdentifier::new_hash) + .collect() + }) + } + }; + + if let Some(owned_tokens) = maybe_owned_tokens { // Depending on approve_all we either approve all or disapprove all. - for t in owned_tokens { + for token_id in owned_tokens { if approve_all { - storage::dictionary_put(approved_uref, &t.to_string(), Some(operator)); + storage::dictionary_put( + approved_uref, + &token_id.get_dictionary_item_key(), + Some(operator), + ); } else { - storage::dictionary_put(approved_uref, &t.to_string(), Option::::None); + storage::dictionary_put( + approved_uref, + &token_id.get_dictionary_item_key(), + Option::::None, + ); } } }; } -// Transfers token from token_owner to specified account. Transfer will go through if caller is owner or an approved operator. -// Transfer will fail if OwnershipMode is Minter or Assigned. +// Transfers token from token_owner to specified account. Transfer will go through if caller is +// owner or an approved operator. Transfer will fail if OwnershipMode is Minter or Assigned. #[no_mangle] pub extern "C" fn transfer() { - // If we are in minter or assigned mode we are not allowed to transfer ownership of token, hence we revert. + // If we are in minter or assigned mode we are not allowed to transfer ownership of token, hence + // we revert. let _ownership_mode = match utils::get_ownership_mode().unwrap_or_revert() { OwnershipMode::Minter | OwnershipMode::Assigned => { runtime::revert(NFTCoreError::InvalidOwnershipMode) @@ -541,151 +816,152 @@ pub extern "C" fn transfer() { OwnershipMode::Transferable => OwnershipMode::Transferable, }; - // Get token_id argument - let token_id: U256 = get_named_arg_with_user_errors( - ARG_TOKEN_ID, - NFTCoreError::MissingTokenID, - NFTCoreError::InvalidTokenID, + let holder_mode = get_holder_mode().unwrap_or_revert(); + + let identifier_mode: NFTIdentifierMode = get_stored_value_with_user_errors::( + IDENTIFIER_MODE, + NFTCoreError::MissingIdentifierMode, + NFTCoreError::InvalidIdentifierMode, ) + .try_into() .unwrap_or_revert(); + let token_identifier = get_token_identifier_from_runtime_args(&identifier_mode); + // We assume we cannot transfer burnt tokens - if get_dictionary_value_from_key::<()>(BURNT_TOKENS, &token_id.to_string()).is_some() { + if get_dictionary_value_from_key::<()>( + BURNT_TOKENS, + &token_identifier.get_dictionary_item_key(), + ) + .is_some() + { runtime::revert(NFTCoreError::PreviouslyBurntToken); } - let token_owner_account_hash = - match get_dictionary_value_from_key::(TOKEN_OWNERS, &token_id.to_string()) { - Some(token_owner) => token_owner, - None => runtime::revert(NFTCoreError::InvalidTokenID), - } - .into_account() - .unwrap_or_revert_with(NFTCoreError::InvalidKey); + let token_owner_key = match get_dictionary_value_from_key::( + TOKEN_OWNERS, + &token_identifier.get_dictionary_item_key(), + ) { + Some(token_owner) => token_owner, + None => runtime::revert(NFTCoreError::InvalidTokenID), + }; - let from_token_owner_account_hash = get_named_arg_with_user_errors::( - ARG_FROM_ACCOUNT_HASH, + let from_token_owner_key = get_named_arg_with_user_errors::( + ARG_SOURCE_KEY, NFTCoreError::MissingAccountHash, NFTCoreError::InvalidAccountHash, ) - .unwrap_or_revert() - .into_account() - .unwrap_or_revert_with(NFTCoreError::InvalidKey); + .unwrap_or_revert(); - // Revert if from account is not the token_owner - if from_token_owner_account_hash != token_owner_account_hash { + if from_token_owner_key != token_owner_key { runtime::revert(NFTCoreError::InvalidAccount); } - let caller = runtime::get_caller(); + let caller = get_verified_caller().unwrap_or_revert(); // Check if caller is approved to execute transfer - let is_approved = - match get_dictionary_value_from_key::>(OPERATOR, &token_id.to_string()) { - Some(Some(approved_public_key)) => { - approved_public_key.into_account().unwrap_or_revert() == caller - } - Some(None) | None => false, - }; + let is_approved = match get_dictionary_value_from_key::>( + OPERATOR, + &token_identifier.get_dictionary_item_key(), + ) { + Some(Some(approved_key)) => approved_key == caller, + Some(None) | None => false, + }; // Revert if caller is not owner and not approved. - if caller != token_owner_account_hash && !is_approved { + if caller != token_owner_key && !is_approved && NFTHolderMode::Accounts == holder_mode { runtime::revert(NFTCoreError::InvalidAccount); } let target_owner_key: Key = get_named_arg_with_user_errors( - ARG_TO_ACCOUNT_HASH, + ARG_TARGET_KEY, NFTCoreError::MissingAccountHash, NFTCoreError::InvalidAccountHash, ) .unwrap_or_revert(); - let target_owner_item_key = target_owner_key - .into_account() - .unwrap_or_revert_with(NFTCoreError::InvalidKey) - .to_string(); + let target_owner_item_key = get_owned_tokens_dictionary_item_key(target_owner_key); + let from_owner_item_key = get_owned_tokens_dictionary_item_key(from_token_owner_key); // Updated token_owners dictionary. Revert if token_owner not found. - match get_dictionary_value_from_key::(TOKEN_OWNERS, &token_id.to_string()) { + match get_dictionary_value_from_key::( + TOKEN_OWNERS, + &token_identifier.get_dictionary_item_key(), + ) { Some(token_actual_owner) => { - let token_actual_owner_account_hash = token_actual_owner - .into_account() - .unwrap_or_revert_with(NFTCoreError::InvalidKey); - if token_actual_owner_account_hash != from_token_owner_account_hash { + if token_actual_owner != from_token_owner_key { runtime::revert(NFTCoreError::InvalidTokenOwner) } - - upsert_dictionary_value_from_key(TOKEN_OWNERS, &token_id.to_string(), target_owner_key); + upsert_dictionary_value_from_key( + TOKEN_OWNERS, + &token_identifier.get_dictionary_item_key(), + target_owner_key, + ); } None => runtime::revert(NFTCoreError::InvalidTokenID), } // Update to_account owned_tokens. Revert if owned_tokens list is not found - match get_dictionary_value_from_key::>( - OWNED_TOKENS, - &from_token_owner_account_hash.to_string(), - ) { + match get_token_identifiers_from_dictionary(&identifier_mode, &from_owner_item_key) { Some(mut owned_tokens) => { // Check that token_id is in owned tokens list. If so remove token_id from list // If not revert. - if let Some(id) = owned_tokens.iter().position(|id| *id == token_id) { + if let Some(id) = owned_tokens.iter().position(|id| *id == token_identifier) { owned_tokens.remove(id); } else { runtime::revert(NFTCoreError::InvalidTokenOwner) } - upsert_dictionary_value_from_key( - OWNED_TOKENS, - &from_token_owner_account_hash.to_string(), - owned_tokens, - ); + upsert_token_identifiers(&identifier_mode, &from_owner_item_key, owned_tokens) + .unwrap_or_revert(); } None => runtime::revert(NFTCoreError::InvalidTokenID), } // Update the from_account balance - let updated_from_account_balance = match get_dictionary_value_from_key::( - TOKEN_COUNTS, - &from_token_owner_account_hash.to_string(), - ) { - Some(balance) => { - if balance > U256::zero() { - balance - U256::one() - } else { + let updated_from_account_balance = + match get_dictionary_value_from_key::(TOKEN_COUNTS, &from_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); } - } - None => { - // This should never happen... - runtime::revert(NFTCoreError::FatalTokenIdDuplication); - } - }; + }; upsert_dictionary_value_from_key( TOKEN_COUNTS, - &from_token_owner_account_hash.to_string(), + &from_owner_item_key, updated_from_account_balance, ); // Update to_account owned_tokens - match get_dictionary_value_from_key::>(OWNED_TOKENS, &target_owner_item_key) { + match get_token_identifiers_from_dictionary(&identifier_mode, &target_owner_item_key) { Some(mut owned_tokens) => { - if owned_tokens.iter().any(|id| *id == token_id) { + if owned_tokens.iter().any(|id| *id == token_identifier) { runtime::revert(NFTCoreError::FatalTokenIdDuplication) } else { - owned_tokens.push(token_id); + owned_tokens.push(token_identifier.clone()); } - upsert_dictionary_value_from_key(OWNED_TOKENS, &target_owner_item_key, owned_tokens); + upsert_token_identifiers(&identifier_mode, &target_owner_item_key, owned_tokens) + .unwrap_or_revert(); } None => { - let owned_tokens = vec![token_id]; - upsert_dictionary_value_from_key(OWNED_TOKENS, &target_owner_item_key, owned_tokens); + let owned_tokens = vec![token_identifier.clone()]; + upsert_token_identifiers(&identifier_mode, &target_owner_item_key, owned_tokens) + .unwrap_or_revert(); } } // Update the to_account balance let updated_to_account_balance = - match get_dictionary_value_from_key::(TOKEN_COUNTS, &target_owner_item_key) { - Some(balance) => balance + U256::one(), - None => U256::one(), + match get_dictionary_value_from_key::(TOKEN_COUNTS, &target_owner_item_key) { + Some(balance) => balance + 1u64, + None => 1u64, }; upsert_dictionary_value_from_key( TOKEN_COUNTS, @@ -699,27 +975,47 @@ pub extern "C" fn transfer() { NFTCoreError::InvalidStorageUref, ); - storage::dictionary_put(approved_uref, &token_id.to_string(), Option::::None); + storage::dictionary_put( + approved_uref, + &token_identifier.get_dictionary_item_key(), + Option::::None, + ); + + let owned_tokens_actual_key = Key::dictionary( + get_uref( + OWNED_TOKENS, + NFTCoreError::MissingOwnedTokens, + NFTCoreError::InvalidOwnedTokens, + ), + target_owner_item_key.as_bytes(), + ); + + let receipt_name = get_stored_value_with_user_errors::( + RECEIPT_NAME, + NFTCoreError::MissingReceiptName, + NFTCoreError::InvalidReceiptName, + ); + + let receipt = CLValue::from_t((receipt_name, owned_tokens_actual_key)) + .unwrap_or_revert_with(NFTCoreError::FailedToConvertToCLValue); + runtime::ret(receipt) } -// Returns the length of the Vec in OWNED_TOKENS dictionary. If key is not found +// Returns the length of the Vec in OWNED_TOKENS dictionary. If key is not found // it returns 0. #[no_mangle] pub extern "C" fn balance_of() { - let account_key = get_named_arg_with_user_errors::( + let owner_key = get_named_arg_with_user_errors::( ARG_TOKEN_OWNER, NFTCoreError::MissingAccountHash, NFTCoreError::InvalidAccountHash, ) - .unwrap_or_revert() - .into_account() - .unwrap_or_revert_with(NFTCoreError::InvalidKey) - .to_string(); + .unwrap_or_revert(); - let balance = match get_dictionary_value_from_key(TOKEN_COUNTS, &account_key) { - Some(balance) => balance, - None => U256::zero(), - }; + let owner_key_item_string = get_owned_tokens_dictionary_item_key(owner_key); + + let balance = + get_dictionary_value_from_key::(TOKEN_COUNTS, &owner_key_item_string).unwrap_or(0u64); let balance_cl_value = CLValue::from_t(balance).unwrap_or_revert_with(NFTCoreError::FailedToConvertToCLValue); @@ -728,30 +1024,39 @@ pub extern "C" fn balance_of() { #[no_mangle] pub extern "C" fn owner_of() { - let token_id = get_named_arg_with_user_errors::( - ARG_TOKEN_ID, - NFTCoreError::MissingTokenID, - NFTCoreError::InvalidTokenID, + let identifier_mode: NFTIdentifierMode = get_stored_value_with_user_errors::( + IDENTIFIER_MODE, + NFTCoreError::MissingIdentifierMode, + NFTCoreError::InvalidIdentifierMode, ) + .try_into() .unwrap_or_revert(); - let number_of_minted_tokens = get_stored_value_with_user_errors::( + let token_identifier = get_token_identifier_from_runtime_args(&identifier_mode); + + let number_of_minted_tokens = get_stored_value_with_user_errors::( NUMBER_OF_MINTED_TOKENS, NFTCoreError::MissingNumberOfMintedTokens, NFTCoreError::InvalidNumberOfMintedTokens, ); - // Check if token_id is out of bounds. - if token_id >= number_of_minted_tokens { - runtime::revert(NFTCoreError::InvalidTokenID); + if let NFTIdentifierMode::Ordinal = identifier_mode { + // Revert if token_id is out of bounds + if token_identifier.get_index().unwrap_or_revert() >= number_of_minted_tokens { + runtime::revert(NFTCoreError::InvalidTokenID); + } } - let maybe_token_owner = - get_dictionary_value_from_key::(TOKEN_OWNERS, &token_id.to_string()); + let maybe_token_owner = get_dictionary_value_from_key::( + TOKEN_OWNERS, + &token_identifier.get_dictionary_item_key(), + ); let token_owner = match maybe_token_owner { Some(token_owner) => token_owner, - None => runtime::revert(NFTCoreError::InvalidTokenID), // If a token does not have an owner it could not have been minted. + None => runtime::revert(NFTCoreError::InvalidTokenID), /* If a token does not have an + * owner it could not have been + * minted. */ }; let token_owner_cl_value = @@ -762,31 +1067,45 @@ pub extern "C" fn owner_of() { #[no_mangle] pub extern "C" fn metadata() { - let token_id = get_named_arg_with_user_errors::( - ARG_TOKEN_ID, - NFTCoreError::MissingTokenID, - NFTCoreError::InvalidTokenID, - ) - .unwrap_or_revert(); - - let number_of_minted_tokens = get_stored_value_with_user_errors::( + let number_of_minted_tokens = get_stored_value_with_user_errors::( NUMBER_OF_MINTED_TOKENS, NFTCoreError::MissingNumberOfMintedTokens, NFTCoreError::InvalidNumberOfMintedTokens, ); - // Check if token_id is out of bounds. - if token_id >= number_of_minted_tokens { - runtime::revert(NFTCoreError::InvalidTokenID); + let identifier_mode: NFTIdentifierMode = get_stored_value_with_user_errors::( + IDENTIFIER_MODE, + NFTCoreError::MissingIdentifierMode, + NFTCoreError::InvalidIdentifierMode, + ) + .try_into() + .unwrap_or_revert(); + + let token_identifier = get_token_identifier_from_runtime_args(&identifier_mode); + + if let NFTIdentifierMode::Ordinal = identifier_mode { + // Revert if token_id is out of bounds + if token_identifier.get_index().unwrap_or_revert() >= number_of_minted_tokens { + runtime::revert(NFTCoreError::InvalidTokenID); + } } - let maybe_token_metadata = - get_dictionary_value_from_key::(TOKEN_META_DATA, &token_id.to_string()); + let metadata_kind: NFTMetadataKind = get_stored_value_with_user_errors::( + METADATA_SCHEMA, + NFTCoreError::MissingNFTMetadataKind, + NFTCoreError::InvalidNFTMetadataKind, + ) + .try_into() + .unwrap_or_revert(); + + let maybe_token_metadata = get_dictionary_value_from_key::( + &get_metadata_dictionary_name(&metadata_kind), + &token_identifier.get_dictionary_item_key(), + ); if let Some(metadata) = maybe_token_metadata { let metadata_cl_value = CLValue::from_t(metadata).unwrap_or_revert_with(NFTCoreError::FailedToConvertToCLValue); - runtime::ret(metadata_cl_value); } else { runtime::revert(NFTCoreError::InvalidTokenID) @@ -796,35 +1115,47 @@ pub extern "C" fn metadata() { // Returns approved account_hash from token_id, throws error if token id is not valid #[no_mangle] pub extern "C" fn get_approved() { - let token_id = get_named_arg_with_user_errors::( - ARG_TOKEN_ID, - NFTCoreError::MissingTokenID, - NFTCoreError::InvalidTokenID, + let identifier_mode: NFTIdentifierMode = get_stored_value_with_user_errors::( + IDENTIFIER_MODE, + NFTCoreError::MissingIdentifierMode, + NFTCoreError::InvalidIdentifierMode, ) + .try_into() .unwrap_or_revert(); - // Revert if already burnt - if get_dictionary_value_from_key::<()>(BURNT_TOKENS, &token_id.to_string()).is_some() { - runtime::revert(NFTCoreError::PreviouslyBurntToken); - } + let token_identifier = get_token_identifier_from_runtime_args(&identifier_mode); // Revert if token_id is out of bounds. - let number_of_minted_tokens = get_stored_value_with_user_errors::( + let number_of_minted_tokens = get_stored_value_with_user_errors::( NUMBER_OF_MINTED_TOKENS, NFTCoreError::MissingNumberOfMintedTokens, NFTCoreError::InvalidNumberOfMintedTokens, ); - // Check if token_id is out of bounds. - if token_id >= number_of_minted_tokens { - runtime::revert(NFTCoreError::InvalidTokenID); + if let NFTIdentifierMode::Ordinal = identifier_mode { + // Revert if token_id is out of bounds + if token_identifier.get_index().unwrap_or_revert() >= number_of_minted_tokens { + runtime::revert(NFTCoreError::InvalidTokenID); + } } - let maybe_approved = - match get_dictionary_value_from_key::>(OPERATOR, &token_id.to_string()) { - Some(maybe_approved) => maybe_approved, - None => None, - }; + // Revert if already burnt + if get_dictionary_value_from_key::<()>( + BURNT_TOKENS, + &token_identifier.get_dictionary_item_key(), + ) + .is_some() + { + runtime::revert(NFTCoreError::PreviouslyBurntToken); + } + + let maybe_approved = match get_dictionary_value_from_key::>( + OPERATOR, + &token_identifier.get_dictionary_item_key(), + ) { + Some(maybe_approved) => maybe_approved, + None => None, + }; let approved_cl_value = CLValue::from_t(maybe_approved) .unwrap_or_revert_with(NFTCoreError::FailedToConvertToCLValue); @@ -837,9 +1168,10 @@ fn install_nft_contract() -> (ContractHash, ContractVersion) { let mut entry_points = EntryPoints::new(); // This entrypoint initializes the contract and is required to be called during the session - // where the contract is installed; immediately after the contract has been installed but before - // exiting session. All parameters are required. - // This entrypoint is intended to be called exactly once and will error if called more than once. + // where the contract is installed; immediately after the contract has been installed but + // before exiting session. All parameters are required. + // This entrypoint is intended to be called exactly once and will error if called more than + // once. let init_contract = EntryPoint::new( ENTRY_POINT_INIT, vec![ @@ -850,16 +1182,24 @@ fn install_nft_contract() -> (ContractHash, ContractVersion) { Parameter::new(ARG_MINTING_MODE, CLType::U8), Parameter::new(ARG_OWNERSHIP_MODE, CLType::U8), Parameter::new(ARG_NFT_KIND, CLType::U8), + Parameter::new(ARG_HOLDER_MODE, CLType::U8), + Parameter::new(ARG_WHITELIST_MODE, CLType::U8), + Parameter::new( + ARG_CONTRACT_WHITELIST, + CLType::List(Box::new(CLType::ByteArray(32u32))), + ), Parameter::new(ARG_JSON_SCHEMA, CLType::String), + Parameter::new(ARG_RECEIPT_NAME, CLType::String), + Parameter::new(ARG_IDENTIFIER_MODE, CLType::U8), ], CLType::Unit, EntryPointAccess::Public, EntryPointType::Contract, ); - // This entrypoint 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. Each parameter of the entrypoint + // This entrypoint 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. Each parameter of the entrypoint // should only be passed if that variable is changed. // For instance if the allow_minting variable is being changed and nothing else // the managing account would send the new allow_minting value as the only argument. @@ -868,7 +1208,13 @@ fn install_nft_contract() -> (ContractHash, ContractVersion) { // By switching allow_minting to false we pause minting. let set_variables = EntryPoint::new( ENTRY_POINT_SET_VARIABLES, - vec![Parameter::new(ARG_ALLOW_MINTING, CLType::Bool)], + vec![ + Parameter::new(ARG_ALLOW_MINTING, CLType::Bool), + Parameter::new( + ARG_CONTRACT_WHITELIST, + CLType::List(Box::new(CLType::ByteArray(32u32))), + ), + ], CLType::Unit, EntryPointAccess::Public, EntryPointType::Contract, @@ -877,51 +1223,56 @@ fn install_nft_contract() -> (ContractHash, ContractVersion) { // This entrypoint mints a new token with provided metadata. // Meant to be called post installation. // Reverts with 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 U256 ID equal to the current number_of_minted_tokens. + // 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 the entrypoint checks if number_of_minted_tokens - // exceed the total_token_supply. If so, it reverts the minting with an error TokenSupplyDepleted. - // The mint entrypoint 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 InvalidAccount. - // The newly minted token is automatically assigned a U256 ID equal to the current number_of_minted_tokens. - // The account is listed as the token owner, as well as added to the accounts list of owned tokens. - // After minting is successful the number_of_minted_tokens is incremented by one. + // exceed the total_token_supply. If so, it reverts the minting with an error + // TokenSupplyDepleted. The mint entrypoint 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 InvalidAccount. The newly minted token is + // automatically assigned a U64 ID equal to the current number_of_minted_tokens. The + // account is listed as the token owner, as well as added to the accounts list of owned + // tokens. After minting is successful the number_of_minted_tokens is incremented by + // one. let mint = EntryPoint::new( ENTRY_POINT_MINT, vec![ Parameter::new(ARG_TOKEN_OWNER, CLType::Key), Parameter::new(ARG_TOKEN_META_DATA, CLType::String), - Parameter::new(ARG_TOKEN_URI, CLType::String), ], CLType::Unit, EntryPointAccess::Public, EntryPointType::Contract, ); - // This entrypoint 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 - // InvalidTokenOwner. If token id is invalid (e.g. out of bounds) it reverts with error InvalidTokenID. - // If token is listed as already burnt we revert with error PreviouslyBurntTOken. If not the token is then - // registered as burnt. + // This entrypoint 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 InvalidTokenOwner. If token id is invalid (e.g. out of bounds) it reverts + // with error InvalidTokenID. If token is listed as already burnt we revert with + // error PreviouslyBurntTOken. If not the token is then registered as burnt. let burn = EntryPoint::new( ENTRY_POINT_BURN, - vec![Parameter::new(ARG_TOKEN_ID, CLType::U256)], + vec![ + Parameter::new(ARG_TOKEN_ID, CLType::U64), + Parameter::new(ARG_TOKEN_HASH, CLType::String), + ], CLType::Unit, EntryPointAccess::Public, EntryPointType::Contract, ); // This entrypoint transfers ownership of token from one account to another. - // It looks up the owner of the supplied token_id arg. Revert if token is already burnt, token_id - // is unvalid, or if caller is not owner and not approved operator. + // It looks up the owner of the supplied token_id arg. Revert if token is already burnt, + // token_id is unvalid, or if caller is not owner and not approved operator. // If token id is invalid it reverts with error InvalidTokenID. let transfer = EntryPoint::new( ENTRY_POINT_TRANSFER, vec![ - Parameter::new(ARG_TOKEN_ID, CLType::U256), - Parameter::new(ARG_FROM_ACCOUNT_HASH, CLType::Key), - Parameter::new(ARG_TO_ACCOUNT_HASH, CLType::Key), + Parameter::new(ARG_TOKEN_ID, CLType::U64), + Parameter::new(ARG_TOKEN_HASH, CLType::String), + Parameter::new(ARG_SOURCE_KEY, CLType::Key), + Parameter::new(ARG_TARGET_KEY, CLType::Key), ], CLType::Unit, EntryPointAccess::Public, @@ -934,7 +1285,8 @@ fn install_nft_contract() -> (ContractHash, ContractVersion) { let approve = EntryPoint::new( ENTRY_POINT_APPROVE, vec![ - Parameter::new(ARG_TOKEN_ID, CLType::U256), + Parameter::new(ARG_TOKEN_ID, CLType::U64), + Parameter::new(ARG_TOKEN_HASH, CLType::String), Parameter::new(ARG_OPERATOR, CLType::Key), ], CLType::Unit, @@ -954,7 +1306,10 @@ fn install_nft_contract() -> (ContractHash, ContractVersion) { // is invalid. A burnt token still has an associated owner. let owner_of = EntryPoint::new( ENTRY_POINT_OWNER_OF, - vec![Parameter::new(ARG_TOKEN_ID, CLType::U256)], + vec![ + Parameter::new(ARG_TOKEN_ID, CLType::U64), + Parameter::new(ARG_TOKEN_HASH, CLType::String), + ], CLType::Key, EntryPointAccess::Public, EntryPointType::Contract, @@ -964,7 +1319,10 @@ fn install_nft_contract() -> (ContractHash, ContractVersion) { // Reverts if token has been burnt. let get_approved = EntryPoint::new( ENTRY_POINT_GET_APPROVED, - vec![Parameter::new(ARG_TOKEN_ID, CLType::U256)], + vec![ + Parameter::new(ARG_TOKEN_ID, CLType::U64), + Parameter::new(ARG_TOKEN_HASH, CLType::String), + ], CLType::Option(Box::new(CLType::Key)), EntryPointAccess::Public, EntryPointType::Contract, @@ -974,7 +1332,7 @@ fn install_nft_contract() -> (ContractHash, ContractVersion) { let balance_of = EntryPoint::new( ENTRY_POINT_BALANCE_OF, vec![Parameter::new(ARG_TOKEN_OWNER, CLType::Key)], - CLType::U256, + CLType::U64, EntryPointAccess::Public, EntryPointType::Contract, ); @@ -982,7 +1340,10 @@ fn install_nft_contract() -> (ContractHash, ContractVersion) { // This entrypoint returns the metadata associated with the provided token_id let metadata = EntryPoint::new( ENTRY_POINT_METADATA, - vec![Parameter::new(ARG_TOKEN_ID, CLType::U256)], + vec![ + Parameter::new(ARG_TOKEN_ID, CLType::U64), + Parameter::new(ARG_TOKEN_HASH, CLType::String), + ], CLType::String, EntryPointAccess::Public, EntryPointType::Contract, @@ -1042,7 +1403,7 @@ pub extern "C" fn call() { // This represents the total number of NFTs that will // be minted by a specific instance of a contract. // This value cannot be changed after installation. - let total_token_supply: U256 = get_named_arg_with_user_errors( + let total_token_supply: u64 = get_named_arg_with_user_errors( ARG_TOTAL_TOKEN_SUPPLY, NFTCoreError::MissingTotalTokenSupply, NFTCoreError::InvalidTotalTokenSupply, @@ -1086,6 +1447,44 @@ pub extern "C" fn call() { ) .unwrap_or_revert(); + // Represents whether Accounts or Contracts, or both can hold NFTs for + // a given contract instance. Refer to the enum `NFTHolderMode` + // in the `src/utils.rs` file for details. + // This value cannot be changed after installation + let holder_mode: u8 = + get_optional_named_arg_with_user_errors(ARG_HOLDER_MODE, NFTCoreError::InvalidHolderMode) + .unwrap_or_revert_with(NFTCoreError::InvalidHolderMode); + + // Represents whether a given contract whitelist can be modified + // for a given NFT contract instance. If not provided as an argument + // it will default to unlocked. + // This value cannot be changed after installation + let whitelist_lock: u8 = get_optional_named_arg_with_user_errors( + ARG_WHITELIST_MODE, + NFTCoreError::InvalidWhitelistMode, + ) + .unwrap_or(0u8); + + // A whitelist of contract hashes specifying which contracts 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 = get_optional_named_arg_with_user_errors( + ARG_CONTRACT_WHITELIST, + NFTCoreError::InvalidContractWhitelist, + ) + .unwrap_or_default(); + + // Represents the schema for the metadata for a given NFT contract instance. + // Refer to the `NFTMetadataKind` enum in src/utils for details. + // This value cannot be changed after installation. + let nft_metadata_kind: u8 = get_named_arg_with_user_errors( + ARG_NFT_METADATA_KIND, + NFTCoreError::MissingNFTMetadataKind, + NFTCoreError::InvalidNFTMetadataKind, + ) + .unwrap_or_revert(); + // The JSON schema representation of the NFT which will be minted. // This value cannot be changed after installation. let json_schema: String = get_named_arg_with_user_errors( @@ -1095,12 +1494,38 @@ pub extern "C" fn call() { ) .unwrap_or_revert(); + // Represents whether NFTs minted by a given contract will be identified + // by an ordinal u64 index or a base16 encoded SHA256 hash of an NFTs metadata. + // This value cannot be changed after installation. + let identifier_mode: u8 = get_named_arg_with_user_errors( + ARG_IDENTIFIER_MODE, + NFTCoreError::MissingIdentifierMode, + NFTCoreError::InvalidIdentifierMode, + ) + .unwrap_or_revert(); + let (contract_hash, contract_version) = install_nft_contract(); // Store contract_hash and contract_version under the keys CONTRACT_NAME and CONTRACT_VERSION runtime::put_key(CONTRACT_NAME, contract_hash.into()); runtime::put_key(CONTRACT_VERSION, storage::new_uref(contract_version).into()); + let package_hash: ContractPackageHash = runtime::get_key(HASH_KEY_NAME) + .unwrap_or_revert() + .into_hash() + .map(ContractPackageHash::new) + .unwrap(); + + // A sentinel string value which represents the entry for the addition + // of a read only reference to the NFTs owned by the calling `Account` or `Contract` + // This allows for users to look up a set of named keys and correctly identify + // the contract package from which the NFTs were obtained. + let receipt_name = format!( + "nft-{}-{}", + collection_name, + package_hash.to_formatted_string() + ); + // Call contract to initialize it runtime::call_contract::<()>( contract_hash, @@ -1113,7 +1538,13 @@ pub extern "C" fn call() { ARG_OWNERSHIP_MODE => ownership_mode, ARG_NFT_KIND => nft_kind, ARG_MINTING_MODE => minting_mode, + ARG_HOLDER_MODE => holder_mode, + ARG_WHITELIST_MODE => whitelist_lock, + ARG_CONTRACT_WHITELIST => contract_white_list, ARG_JSON_SCHEMA => json_schema, + ARG_RECEIPT_NAME => receipt_name, + ARG_NFT_METADATA_KIND => nft_metadata_kind, + ARG_IDENTIFIER_MODE => identifier_mode }, ); } diff --git a/contract/src/utils.rs b/contract/src/utils.rs index f4f7b82a..c9619a83 100644 --- a/contract/src/utils.rs +++ b/contract/src/utils.rs @@ -1,23 +1,34 @@ -use core::{convert::TryInto, mem::MaybeUninit}; +use alloc::{ + borrow::ToOwned, + collections::BTreeMap, + string::{String, ToString}, + vec, + vec::Vec, +}; -use alloc::{vec, vec::Vec}; use casper_contract::{ - contract_api::{self, runtime, storage}, + contract_api::{self, runtime, runtime::revert, storage}, ext_ffi, unwrap_or_revert::UnwrapOrRevert, }; use casper_types::{ account::AccountHash, api_error, - bytesrepr::{self, FromBytes, ToBytes}, - ApiError, CLTyped, ContractHash, Key, URef, + bytesrepr::{self, Error, FromBytes, ToBytes}, + ApiError, CLType, CLTyped, ContractHash, Key, URef, }; - use core::convert::TryFrom; -use crate::{constants::OWNERSHIP_MODE, error::NFTCoreError}; +use casper_types::system::CallStackElement; +use core::{convert::TryInto, mem::MaybeUninit}; + +use serde::{Deserialize, Serialize}; -const _CONTRACT_WHITELIST: &str = "contract_whitelist"; +use crate::{ + constants::OWNERSHIP_MODE, error::NFTCoreError, ARG_JSON_SCHEMA, ARG_TOKEN_HASH, ARG_TOKEN_ID, + HOLDER_MODE, METADATA_CEP78, METADATA_CUSTOM_VALIDATED, METADATA_NFT721, METADATA_RAW, + OWNED_TOKENS, +}; pub(crate) fn upsert_dictionary_value_from_key( dictionary_name: &str, @@ -30,7 +41,6 @@ pub(crate) fn upsert_dictionary_value_from_key NFTCoreError::InvalidStorageUref, ); - // TODO: Write a test for this upsert method. match storage::dictionary_get::(seed_uref, key) { Ok(None | Some(_)) => storage::dictionary_put(seed_uref, key, value), Err(error) => runtime::revert(error), @@ -38,6 +48,7 @@ pub(crate) fn upsert_dictionary_value_from_key } #[repr(u8)] +#[derive(PartialEq)] pub enum WhitelistMode { Unlocked = 0, Locked = 1, @@ -56,9 +67,11 @@ impl TryFrom for WhitelistMode { } #[repr(u8)] +#[derive(PartialEq, Clone, Copy)] pub enum NFTHolderMode { Accounts = 0, Contracts = 1, + Mixed = 2, } impl TryFrom for NFTHolderMode { @@ -68,6 +81,7 @@ impl TryFrom for NFTHolderMode { match value { 0 => Ok(NFTHolderMode::Accounts), 1 => Ok(NFTHolderMode::Contracts), + 2 => Ok(NFTHolderMode::Mixed), _ => Err(NFTCoreError::InvalidHolderMode), } } @@ -97,14 +111,14 @@ impl TryFrom for MintingMode { pub enum NFTKind { /// The NFT represents a real-world physical /// like a house. - Physical, + Physical = 0, /// The NFT represents a digital asset like a unique /// JPEG or digital art. - Digital, + Digital = 1, /// The NFT is the virtual representation /// of a physical notion, e.g a patent /// or copyright. - Virtual, + Virtual = 2, } impl TryFrom for NFTKind { @@ -120,6 +134,28 @@ impl TryFrom for NFTKind { } } +#[repr(u8)] +pub enum NFTMetadataKind { + CEP78 = 0, + NFT721 = 1, + Raw = 2, + CustomValidated = 3, +} + +impl TryFrom for NFTMetadataKind { + type Error = NFTCoreError; + + 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(NFTCoreError::InvalidNFTMetadataKind), + } + } +} + #[repr(u8)] pub enum OwnershipMode { /// The minter owns it and can never transfer it. @@ -128,8 +164,6 @@ pub enum OwnershipMode { Assigned = 1, /// The NFT can be transferred even to an recipient that does not exist. Transferable = 2, - // TODO - // Platform = 3, } impl TryFrom for OwnershipMode { @@ -145,6 +179,24 @@ impl TryFrom for OwnershipMode { } } +#[repr(u8)] +pub enum NFTIdentifierMode { + Ordinal = 0, + Hash = 1, +} + +impl TryFrom for NFTIdentifierMode { + type Error = NFTCoreError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(NFTIdentifierMode::Ordinal), + 1 => Ok(NFTIdentifierMode::Hash), + _ => Err(NFTCoreError::InvalidIdentifierMode), + } + } +} + pub(crate) fn get_ownership_mode() -> Result { get_stored_value_with_user_errors::( OWNERSHIP_MODE, @@ -154,6 +206,23 @@ pub(crate) fn get_ownership_mode() -> Result { .try_into() } +pub(crate) fn get_holder_mode() -> Result { + get_stored_value_with_user_errors::( + HOLDER_MODE, + NFTCoreError::MissingHolderMode, + NFTCoreError::InvalidHolderMode, + ) + .try_into() +} + +pub(crate) fn get_owned_tokens_dictionary_item_key(token_owner_key: Key) -> String { + match token_owner_key { + Key::Account(token_owner_account_hash) => token_owner_account_hash.to_string(), + Key::Hash(token_owner_hash_addr) => ContractHash::new(token_owner_hash_addr).to_string(), + _ => revert(NFTCoreError::InvalidKey), + } +} + pub(crate) fn get_dictionary_value_from_key( dictionary_name: &str, key: &str, @@ -349,19 +418,398 @@ pub(crate) fn to_ptr(t: T) -> (*const u8, usize, Vec) { (ptr, size, bytes) } -pub(crate) fn _get_calling_contract_hash() -> ContractHash { - let contract_hash = *runtime::get_call_stack() - .pop() +pub(crate) fn get_verified_caller() -> Result { + let holder_mode = get_holder_mode()?; + match *runtime::get_call_stack() + .iter() + .nth_back(1) + .to_owned() .unwrap_or_revert() - .contract_hash() - .unwrap_or_revert(); - let whitelist = get_stored_value_with_user_errors::>( - _CONTRACT_WHITELIST, - NFTCoreError::MissingContractWhiteList, - NFTCoreError::InvalidContractWhitelist, - ); - if !whitelist.contains(&contract_hash) { - runtime::revert(NFTCoreError::UnlistedContractHash) + { + CallStackElement::Session { + account_hash: calling_account_hash, + } => { + if let NFTHolderMode::Contracts = holder_mode { + return Err(NFTCoreError::InvalidHolderMode); + } + Ok(Key::Account(calling_account_hash)) + } + CallStackElement::StoredSession { contract_hash, .. } + | CallStackElement::StoredContract { contract_hash, .. } => { + if let NFTHolderMode::Accounts = holder_mode { + return Err(NFTCoreError::InvalidHolderMode); + } + Ok(contract_hash.into()) + } + } +} + +#[derive(PartialEq, Clone)] +pub(crate) enum TokenIdentifier { + Index(u64), + Hash(String), +} + +impl TokenIdentifier { + pub(crate) fn new_index(index: u64) -> Self { + TokenIdentifier::Index(index) + } + + pub(crate) fn new_hash(hash: String) -> Self { + TokenIdentifier::Hash(hash) + } + + pub(crate) fn get_index(&self) -> Option { + if let Self::Index(index) = self { + return Some(*index); + } + None + } + + pub(crate) fn get_hash(self) -> Option { + if let Self::Hash(hash) = self { + return Some(hash); + } + None + } + + pub(crate) fn get_dictionary_item_key(&self) -> String { + match self { + TokenIdentifier::Index(token_index) => token_index.to_string(), + TokenIdentifier::Hash(hash) => hash.clone(), + } + } +} + +pub(crate) fn get_token_identifier_from_runtime_args( + identifier_mode: &NFTIdentifierMode, +) -> TokenIdentifier { + match identifier_mode { + NFTIdentifierMode::Ordinal => get_named_arg_with_user_errors::( + ARG_TOKEN_ID, + NFTCoreError::MissingTokenID, + NFTCoreError::InvalidTokenID, + ) + .map(TokenIdentifier::new_index) + .unwrap_or_revert(), + NFTIdentifierMode::Hash => get_named_arg_with_user_errors::( + ARG_TOKEN_HASH, + NFTCoreError::MissingTokenID, + NFTCoreError::InvalidTokenID, + ) + .map(TokenIdentifier::new_hash) + .unwrap_or_revert(), + } +} + +pub(crate) fn get_token_identifiers_from_dictionary( + identifier_mode: &NFTIdentifierMode, + owners_item_key: &str, +) -> Option> { + match identifier_mode { + NFTIdentifierMode::Ordinal => { + get_dictionary_value_from_key::>(OWNED_TOKENS, owners_item_key).map( + |token_indices| { + token_indices + .into_iter() + .map(TokenIdentifier::new_index) + .collect() + }, + ) + } + NFTIdentifierMode::Hash => { + get_dictionary_value_from_key::>(OWNED_TOKENS, owners_item_key).map( + |token_hashes| { + token_hashes + .into_iter() + .map(TokenIdentifier::new_hash) + .collect() + }, + ) + } + } +} + +pub(crate) fn upsert_token_identifiers( + identifier_mode: &NFTIdentifierMode, + owners_item_key: &str, + token_identifiers: Vec, +) -> Result<(), NFTCoreError> { + match identifier_mode { + NFTIdentifierMode::Ordinal => { + let token_indices: Vec = token_identifiers + .into_iter() + .map(|token_identifier| { + token_identifier + .get_index() + .unwrap_or_revert_with(NFTCoreError::InvalidIdentifierMode) + }) + .collect(); + upsert_dictionary_value_from_key(OWNED_TOKENS, owners_item_key, token_indices); + Ok(()) + } + NFTIdentifierMode::Hash => { + let token_hashes: Vec = token_identifiers + .into_iter() + .map(|token_identifier| { + token_identifier + .get_hash() + .unwrap_or_revert_with(NFTCoreError::InvalidIdentifierMode) + }) + .collect(); + upsert_dictionary_value_from_key(OWNED_TOKENS, owners_item_key, token_hashes); + Ok(()) + } + } +} + +// Metadata mutability is different from schema mutability. +#[derive(Serialize, Deserialize, Clone)] +pub(crate) struct MetadataSchemaProperty { + name: String, + description: String, + required: bool, +} + +impl ToBytes for MetadataSchemaProperty { + fn to_bytes(&self) -> Result, Error> { + let mut result = bytesrepr::allocate_buffer(self)?; + result.extend(self.name.to_bytes()?); + result.extend(self.description.to_bytes()?); + result.extend(self.required.to_bytes()?); + Ok(result) + } + + fn serialized_length(&self) -> usize { + self.name.serialized_length() + + self.description.serialized_length() + + self.required.serialized_length() + } +} + +impl FromBytes for MetadataSchemaProperty { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), Error> { + let (name, remainder) = String::from_bytes(bytes)?; + let (description, remainder) = String::from_bytes(remainder)?; + let (required, remainder) = bool::from_bytes(remainder)?; + let metadata_schema_property = MetadataSchemaProperty { + name, + description, + required, + }; + Ok((metadata_schema_property, remainder)) } - contract_hash +} + +impl CLTyped for MetadataSchemaProperty { + fn cl_type() -> CLType { + CLType::Any + } +} + +#[derive(Serialize, Deserialize, Clone)] +pub(crate) struct CustomMetadataSchema { + properties: BTreeMap, +} + +pub(crate) fn get_metadata_schema(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 => { + let custom_schema_json = get_stored_value_with_user_errors::( + ARG_JSON_SCHEMA, + NFTCoreError::MissingJsonSchema, + NFTCoreError::InvalidJsonSchema, + ); + + casper_serde_json_wasm::from_str::(&custom_schema_json) + .map_err(|_| NFTCoreError::InvalidJsonSchema) + .unwrap_or_revert() + } + } +} + +impl ToBytes for CustomMetadataSchema { + fn to_bytes(&self) -> Result, Error> { + let mut result = bytesrepr::allocate_buffer(self)?; + result.extend(self.properties.to_bytes()?); + Ok(result) + } + + fn serialized_length(&self) -> usize { + self.properties.serialized_length() + } +} + +impl FromBytes for CustomMetadataSchema { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), Error> { + let (properties, remainder) = + BTreeMap::::from_bytes(bytes)?; + let metadata_schema = CustomMetadataSchema { properties }; + Ok((metadata_schema, remainder)) + } +} + +impl CLTyped for CustomMetadataSchema { + fn cl_type() -> CLType { + CLType::Any + } +} + +// 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 validate_metadata( + metadata_kind: &NFTMetadataKind, + token_metadata: String, +) -> Result { + let token_schema = get_metadata_schema(metadata_kind); + match metadata_kind { + NFTMetadataKind::CEP78 => { + let metadata = casper_serde_json_wasm::from_str::(&token_metadata) + .map_err(|_| NFTCoreError::FailedToParseCep99Metadata)?; + + if let Some(name_property) = token_schema.properties.get("name") { + if name_property.required && metadata.name.is_empty() { + revert(NFTCoreError::InvalidCEP99Metadata) + } + } + if let Some(token_uri_property) = token_schema.properties.get("token_uri") { + if token_uri_property.required && metadata.token_uri.is_empty() { + revert(NFTCoreError::InvalidCEP99Metadata) + } + } + if let Some(checksum_property) = token_schema.properties.get("checksum") { + if checksum_property.required && metadata.checksum.is_empty() { + revert(NFTCoreError::InvalidCEP99Metadata) + } + } + casper_serde_json_wasm::to_string_pretty(&metadata) + .map_err(|_| NFTCoreError::FailedToJsonifyCEP99Metadata) + } + NFTMetadataKind::NFT721 => { + let metadata = casper_serde_json_wasm::from_str::(&token_metadata) + .map_err(|_| NFTCoreError::FailedToParse721Metadata)?; + + if let Some(name_property) = token_schema.properties.get("name") { + if name_property.required && metadata.name.is_empty() { + revert(NFTCoreError::InvalidNFT721Metadata) + } + } + if let Some(token_uri_property) = token_schema.properties.get("token_uri") { + if token_uri_property.required && metadata.token_uri.is_empty() { + revert(NFTCoreError::InvalidNFT721Metadata) + } + } + if let Some(symbol_property) = token_schema.properties.get("symbol") { + if symbol_property.required && metadata.symbol.is_empty() { + revert(NFTCoreError::InvalidNFT721Metadata) + } + } + casper_serde_json_wasm::to_string_pretty(&metadata) + .map_err(|_| NFTCoreError::FailedToJsonifyNFT721Metadata) + } + NFTMetadataKind::Raw => Ok(token_metadata), + NFTMetadataKind::CustomValidated => { + let custom_metadata = + casper_serde_json_wasm::from_str::>(&token_metadata) + .map(|attributes| CustomMetadata { attributes }) + .map_err(|_| NFTCoreError::FailedToParseCustomMetadata)?; + + for (property_name, property_type) in token_schema.properties.iter() { + if property_type.required && custom_metadata.attributes.get(property_name).is_none() + { + revert(NFTCoreError::InvalidCustomMetadata) + } + } + casper_serde_json_wasm::to_string_pretty(&custom_metadata.attributes) + .map_err(|_| NFTCoreError::FailedToJsonifyCustomMetadata) + } + } +} + +pub(crate) fn get_metadata_dictionary_name(metadata_kind: &NFTMetadataKind) -> String { + let name = match metadata_kind { + NFTMetadataKind::CEP78 => METADATA_CEP78, + NFTMetadataKind::NFT721 => METADATA_NFT721, + NFTMetadataKind::Raw => METADATA_RAW, + NFTMetadataKind::CustomValidated => METADATA_CUSTOM_VALIDATED, + }; + name.to_string() } diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..3d2e76e9 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,4 @@ +wrap_comments = true +comment_width = 100 +imports_granularity = "Crate" +edition = "2018" diff --git a/tests/Cargo.toml b/tests/Cargo.toml index d9206e70..490483d8 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -7,3 +7,8 @@ edition = "2018" casper-engine-test-support = { version = "2.2.0", features = ["test-support"] } casper-execution-engine = "2.0.0" casper-types = "1.4.5" +serde = { version = "1.0", default-features = false } +serde_json = "1.0.81" +once_cell = "1" +blake2 = { version = "0.9.0", default-features = false } +base16 = { version = "0.2", default-features = false } \ No newline at end of file diff --git a/tests/src/burn.rs b/tests/src/burn.rs index 549423ca..c58a8e95 100644 --- a/tests/src/burn.rs +++ b/tests/src/burn.rs @@ -2,28 +2,33 @@ use casper_engine_test_support::{ ExecuteRequestBuilder, InMemoryWasmTestBuilder, DEFAULT_ACCOUNT_ADDR, DEFAULT_RUN_GENESIS_REQUEST, }; -use casper_types::{runtime_args, system::mint, ContractHash, Key, RuntimeArgs, U256}; +use casper_types::{runtime_args, system::mint, ContractHash, Key, RuntimeArgs}; use crate::utility::{ constants::{ ACCOUNT_USER_1, ARG_KEY_NAME, ARG_NFT_CONTRACT_HASH, ARG_TOKEN_ID, ARG_TOKEN_META_DATA, ARG_TOKEN_OWNER, ARG_TOKEN_URI, BALANCES, BURNT_TOKENS, CONTRACT_NAME, ENTRY_POINT_BURN, - MINT_SESSION_WASM, NFT_CONTRACT_WASM, OWNED_TOKENS, OWNED_TOKENS_DICTIONARY_KEY, - TEST_META_DATA, TEST_URI, + ENTRY_POINT_MINT, MINTING_CONTRACT_WASM, MINT_SESSION_WASM, NFT_CONTRACT_WASM, + OWNED_TOKENS, OWNED_TOKENS_DICTIONARY_KEY, TEST_PRETTY_721_META_DATA, TEST_URI, + TOKEN_COUNTS, + }, + installer_request_builder::{ + InstallerRequestBuilder, MintingMode, NFTHolderMode, OwnershipMode, WhitelistMode, + }, + support::{ + self, get_dictionary_value_from_key, get_minting_contract_hash, get_nft_contract_hash, }, - installer_request_builder::{InstallerRequestBuilder, OwnershipMode}, - support::{self, get_nft_contract_hash}, }; #[test] fn should_burn_minted_token() { - const TOKEN_ID: U256 = U256::zero(); + let token_id = 0u64; let mut builder = InMemoryWasmTestBuilder::default(); builder.run_genesis(&DEFAULT_RUN_GENESIS_REQUEST).commit(); let install_request_builder = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_total_token_supply(U256::from(100u64)) + .with_total_token_supply(100u64) .with_ownership_mode(OwnershipMode::Transferable) .build(); @@ -47,7 +52,7 @@ fn should_burn_minted_token() { ARG_NFT_CONTRACT_HASH => Key::Hash(nft_contract_hash.value()), ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), ARG_TOKEN_URI => TEST_URI.to_string(), }, ) @@ -55,23 +60,23 @@ fn should_burn_minted_token() { builder.exec(mint_session_call).expect_success().commit(); - let actual_owned_tokens = support::get_dictionary_value_from_key::>( + let actual_owned_tokens = support::get_dictionary_value_from_key::>( &builder, nft_contract_key, OWNED_TOKENS, &DEFAULT_ACCOUNT_ADDR.clone().to_string(), ); - let expected_owned_tokens = vec![U256::zero()]; + let expected_owned_tokens = vec![token_id]; assert_eq!(expected_owned_tokens, actual_owned_tokens); - let actual_balance_before_burn = support::get_dictionary_value_from_key::( + let actual_balance_before_burn = support::get_dictionary_value_from_key::( &builder, nft_contract_key, BALANCES, &DEFAULT_ACCOUNT_ADDR.clone().to_string(), ); - let expected_balance_before_burn = U256::one(); + let expected_balance_before_burn = 1u64; assert_eq!(actual_balance_before_burn, expected_balance_before_burn); let burn_request = ExecuteRequestBuilder::contract_call_by_name( @@ -79,7 +84,7 @@ fn should_burn_minted_token() { CONTRACT_NAME, ENTRY_POINT_BURN, runtime_args! { - ARG_TOKEN_ID => U256::zero(), + ARG_TOKEN_ID => token_id, }, ) .build(); @@ -90,18 +95,18 @@ fn should_burn_minted_token() { &builder, nft_contract_key, BURNT_TOKENS, - &TOKEN_ID.to_string(), + &token_id.to_string(), ); // This will error of token is not registered as - let actual_balance = support::get_dictionary_value_from_key::( + let actual_balance = support::get_dictionary_value_from_key::( &builder, nft_contract_key, BALANCES, &DEFAULT_ACCOUNT_ADDR.clone().to_string(), ); - let expected_balance = U256::zero(); + let expected_balance = 0u64; assert_eq!(actual_balance, expected_balance); } @@ -112,7 +117,7 @@ fn should_not_burn_previously_burnt_token() { let install_request_builder = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_total_token_supply(U256::from(100u64)) + .with_total_token_supply(100u64) .with_ownership_mode(OwnershipMode::Transferable) .build(); @@ -135,7 +140,7 @@ fn should_not_burn_previously_burnt_token() { ARG_NFT_CONTRACT_HASH => Key::Hash(nft_contract_hash.value()), ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), ARG_TOKEN_URI => TEST_URI.to_string(), }, ) @@ -143,14 +148,14 @@ fn should_not_burn_previously_burnt_token() { builder.exec(mint_session_call).expect_success().commit(); - let actual_owned_tokens = support::get_dictionary_value_from_key::>( + let actual_owned_tokens = support::get_dictionary_value_from_key::>( &builder, nft_contract_key, OWNED_TOKENS, &DEFAULT_ACCOUNT_ADDR.clone().to_string(), ); - let expected_owned_tokens = vec![U256::zero()]; + let expected_owned_tokens = vec![0u64]; assert_eq!(expected_owned_tokens, actual_owned_tokens); let burn_request = ExecuteRequestBuilder::contract_call_by_name( @@ -158,7 +163,7 @@ fn should_not_burn_previously_burnt_token() { CONTRACT_NAME, ENTRY_POINT_BURN, runtime_args! { - ARG_TOKEN_ID => U256::zero(), + ARG_TOKEN_ID => 0u64, }, ) .build(); @@ -170,7 +175,7 @@ fn should_not_burn_previously_burnt_token() { CONTRACT_NAME, ENTRY_POINT_BURN, runtime_args! { - ARG_TOKEN_ID => U256::zero(), + ARG_TOKEN_ID => 0u64, }, ) .build(); @@ -192,7 +197,7 @@ fn should_return_expected_error_when_burning_non_existing_token() { let install_request_builder = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_total_token_supply(U256::from(100u64)) + .with_total_token_supply(100u64) .with_ownership_mode(OwnershipMode::Transferable) .build(); @@ -206,7 +211,7 @@ fn should_return_expected_error_when_burning_non_existing_token() { CONTRACT_NAME, ENTRY_POINT_BURN, runtime_args! { - ARG_TOKEN_ID => U256::zero(), + ARG_TOKEN_ID => 0u64, }, ) .build(); @@ -228,7 +233,7 @@ fn should_return_expected_error_burning_of_others_users_token() { let install_request_builder = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_total_token_supply(U256::from(100u64)) + .with_total_token_supply(100u64) .with_ownership_mode(OwnershipMode::Transferable) .build(); @@ -270,7 +275,7 @@ fn should_return_expected_error_burning_of_others_users_token() { ARG_NFT_CONTRACT_HASH => Key::Hash(nft_contract_hash), ARG_KEY_NAME => Option::::None, ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), ARG_TOKEN_URI => TEST_URI.to_string(), }, ) @@ -278,22 +283,21 @@ fn should_return_expected_error_burning_of_others_users_token() { builder.exec(mint_session_call).expect_success().commit(); - let actual_owned_tokens = support::get_dictionary_value_from_key::>( + let actual_owned_tokens = support::get_dictionary_value_from_key::>( &builder, nft_contract_key, OWNED_TOKENS, &DEFAULT_ACCOUNT_ADDR.clone().to_string(), ); - let expected_owned_tokens = vec![U256::zero()]; - assert_eq!(expected_owned_tokens, actual_owned_tokens); + assert_eq!(vec![0u64], actual_owned_tokens); let incorrect_burn_request = ExecuteRequestBuilder::contract_call_by_hash( account_user_1.to_account_hash(), ContractHash::new(nft_contract_hash), ENTRY_POINT_BURN, runtime_args! { - ARG_TOKEN_ID => U256::zero(), + ARG_TOKEN_ID => 0u64, }, ) .build(); @@ -303,8 +307,6 @@ fn should_return_expected_error_burning_of_others_users_token() { let error = builder.get_error().expect("must have error"); support::assert_expected_error(error, 6u16, "should disallow burning of other users' token"); - - // TODO is this really diffferent than should_return_expected_error_when_burning_not_owned_token() ??? } #[test] @@ -314,7 +316,7 @@ fn should_return_expected_error_when_burning_not_owned_token() { let install_request_builder = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_total_token_supply(U256::from(100u64)) + .with_total_token_supply(100u64) .with_ownership_mode(OwnershipMode::Transferable) .build(); @@ -357,7 +359,7 @@ fn should_return_expected_error_when_burning_not_owned_token() { ARG_NFT_CONTRACT_HASH => Key::Hash(nft_contract_hash), ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), ARG_TOKEN_URI => TEST_URI.to_string(), }, ) @@ -365,22 +367,21 @@ fn should_return_expected_error_when_burning_not_owned_token() { builder.exec(mint_session_call).expect_success().commit(); - let actual_owned_tokens = support::get_dictionary_value_from_key::>( + let actual_owned_tokens = support::get_dictionary_value_from_key::>( &builder, nft_contract_key, OWNED_TOKENS, &DEFAULT_ACCOUNT_ADDR.clone().to_string(), ); - let expected_owned_tokens = vec![U256::zero()]; - assert_eq!(expected_owned_tokens, actual_owned_tokens); + assert_eq!(vec![0u64], actual_owned_tokens); let incorrect_burn_request = ExecuteRequestBuilder::contract_call_by_hash( account_user_1.to_account_hash(), ContractHash::new(nft_contract_hash), ENTRY_POINT_BURN, runtime_args! { - ARG_TOKEN_ID => U256::zero() + ARG_TOKEN_ID => 0u64 }, ) .build(); @@ -394,3 +395,92 @@ fn should_return_expected_error_when_burning_not_owned_token() { "should disallow burning on mismatch of owner key", ); } + +#[test] +fn should_allow_contract_to_burn_token() { + let mut builder = InMemoryWasmTestBuilder::default(); + builder.run_genesis(&DEFAULT_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, NFT_CONTRACT_WASM) + .with_total_token_supply(100u64) + .with_holder_mode(NFTHolderMode::Contracts) + .with_whitelist_mode(WhitelistMode::Locked) + .with_ownership_mode(OwnershipMode::Minter) + .with_minting_mode(Some(MintingMode::Installer as u8)) + .with_contract_whitelist(contract_whitelist) + .build(); + + builder.exec(install_request).expect_success().commit(); + + let nft_contract_key: Key = get_nft_contract_hash(&builder).into(); + + 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_TOKEN_URI => TEST_URI.to_string() + }; + + 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 current_token_balance = get_dictionary_value_from_key::( + &builder, + &nft_contract_key, + TOKEN_COUNTS, + &minting_contract_hash.to_string(), + ); + + assert_eq!(1u64, current_token_balance); + + let burn_via_contract_call = ExecuteRequestBuilder::contract_call_by_hash( + *DEFAULT_ACCOUNT_ADDR, + minting_contract_hash, + ENTRY_POINT_BURN, + runtime_args! { + ARG_NFT_CONTRACT_HASH => nft_contract_key, + ARG_TOKEN_ID => 0u64 + }, + ) + .build(); + + builder + .exec(burn_via_contract_call) + .expect_success() + .commit(); + + let updated_token_balance = get_dictionary_value_from_key::( + &builder, + &nft_contract_key, + TOKEN_COUNTS, + &minting_contract_hash.to_string(), + ); + + assert_eq!(updated_token_balance, 0u64) +} diff --git a/tests/src/installer.rs b/tests/src/installer.rs index 192b080e..90dd2de0 100644 --- a/tests/src/installer.rs +++ b/tests/src/installer.rs @@ -2,15 +2,16 @@ use casper_engine_test_support::{ ExecuteRequestBuilder, InMemoryWasmTestBuilder, DEFAULT_ACCOUNT_ADDR, DEFAULT_RUN_GENESIS_REQUEST, }; -use casper_types::{runtime_args, CLValue, RuntimeArgs, U256}; +use casper_types::{runtime_args, CLValue, ContractHash, RuntimeArgs}; use crate::utility::{ constants::{ - ARG_ALLOW_MINTING, ARG_COLLECTION_NAME, ARG_COLLECTION_SYMBOL, ARG_MINTING_MODE, - ARG_TOTAL_TOKEN_SUPPLY, CONTRACT_NAME, ENTRY_POINT_INIT, NFT_CONTRACT_WASM, - NFT_TEST_COLLECTION, NFT_TEST_SYMBOL, NUMBER_OF_MINTED_TOKENS, + ARG_ALLOW_MINTING, ARG_COLLECTION_NAME, ARG_COLLECTION_SYMBOL, ARG_CONTRACT_WHITELIST, + ARG_HOLDER_MODE, ARG_MINTING_MODE, ARG_TOTAL_TOKEN_SUPPLY, ARG_WHITELIST_MODE, + CONTRACT_NAME, ENTRY_POINT_INIT, NFT_CONTRACT_WASM, NFT_TEST_COLLECTION, NFT_TEST_SYMBOL, + NUMBER_OF_MINTED_TOKENS, }, - installer_request_builder::InstallerRequestBuilder, + installer_request_builder::{InstallerRequestBuilder, NFTHolderMode, WhitelistMode}, support, }; @@ -22,7 +23,7 @@ fn should_install_contract() { 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(U256::from(1u64)) + .with_total_token_supply(1u64) .build(); builder.exec(install_request).expect_success().commit(); @@ -57,15 +58,14 @@ fn should_install_contract() { "collection_symbol initialized at installation should exist" ); - let query_result: U256 = support::query_stored_value( + let query_result: u64 = support::query_stored_value( &mut builder, *nft_contract_key, vec![ARG_TOTAL_TOKEN_SUPPLY.to_string()], ); assert_eq!( - query_result, - U256::from(1u64), + query_result, 1u64, "total_token_supply initialized at installation should exist" ); @@ -88,15 +88,14 @@ fn should_install_contract() { "minting mode should default to installer" ); - let query_result: U256 = support::query_stored_value( + let query_result: u64 = support::query_stored_value( &mut builder, *nft_contract_key, vec![NUMBER_OF_MINTED_TOKENS.to_string()], ); assert_eq!( - query_result, - U256::zero(), + query_result, 0u64, "number_of_minted_tokens initialized at installation should exist" ); } @@ -108,7 +107,7 @@ fn should_only_allow_init_during_installation_session() { let install_request_builder = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_total_token_supply(U256::from(2u64)); + .with_total_token_supply(2u64); builder .exec(install_request_builder.build()) .expect_success() @@ -146,7 +145,7 @@ fn should_install_with_allow_minting_set_to_false() { 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(U256::from(1u64)) + .with_total_token_supply(1u64) .build(); builder.exec(install_request).expect_success().commit(); @@ -156,9 +155,7 @@ fn should_install_with_allow_minting_set_to_false() { fn should_reject_invalid_collection_name() { let install_request_builder = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_invalid_collection_name( - CLValue::from_t::(U256::zero()).expect("expected CLValue"), - ); + .with_invalid_collection_name(CLValue::from_t::(0u64).expect("expected CLValue")); support::assert_expected_invalid_installer_request( install_request_builder, @@ -172,7 +169,7 @@ fn should_reject_invalid_collection_symbol() { let install_request_builder = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) .with_invalid_collection_symbol( - CLValue::from_t::(U256::zero()).expect("expected CLValue"), + CLValue::from_t::(0u64).expect("expected CLValue"), ); support::assert_expected_invalid_installer_request( @@ -195,3 +192,78 @@ fn should_reject_non_numerical_total_token_supply_value() { "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(&DEFAULT_RUN_GENESIS_REQUEST).commit(); + + let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) + .with_holder_mode(NFTHolderMode::Contracts) + .with_whitelist_mode(WhitelistMode::Unlocked) + .with_contract_whitelist(vec![ContractHash::default()]); + + builder + .exec(install_request.build()) + .expect_success() + .commit(); + + let account = builder.get_expected_account(*DEFAULT_ACCOUNT_ADDR); + let nft_contract_key = account + .named_keys() + .get(CONTRACT_NAME) + .expect("must have key in named keys"); + + let actual_holder_mode: u8 = support::query_stored_value( + &mut 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( + &mut 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 actual_contract_whitelist: Vec = support::query_stored_value( + &mut builder, + *nft_contract_key, + vec![ARG_CONTRACT_WHITELIST.to_string()], + ); + + assert_eq!( + actual_contract_whitelist, + vec![ContractHash::default()], + "contract whitelist is incorrectly set" + ); +} + +#[test] +fn should_disallow_installation_of_contract_with_empty_locked_whitelist() { + let mut builder = InMemoryWasmTestBuilder::default(); + builder.run_genesis(&DEFAULT_RUN_GENESIS_REQUEST).commit(); + + let install_request_builder = + InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) + .with_holder_mode(NFTHolderMode::Contracts) + .with_whitelist_mode(WhitelistMode::Locked); + + support::assert_expected_invalid_installer_request( + install_request_builder, + 83, + "should fail execution since whitelist mode is locked and the provided whitelist is empty", + ); +} diff --git a/tests/src/mint.rs b/tests/src/mint.rs index a5020b5a..33507aed 100644 --- a/tests/src/mint.rs +++ b/tests/src/mint.rs @@ -1,33 +1,45 @@ +use serde::{Deserialize, Serialize}; + use casper_engine_test_support::{ ExecuteRequestBuilder, InMemoryWasmTestBuilder, WasmTestBuilder, DEFAULT_ACCOUNT_ADDR, DEFAULT_RUN_GENESIS_REQUEST, }; - use casper_execution_engine::storage::global_state::in_memory::InMemoryGlobalState; -use casper_types::{runtime_args, system::mint, Key, RuntimeArgs, U256}; +use casper_types::{runtime_args, system::mint, ContractHash, Key, RuntimeArgs}; -use crate::utility::constants::{ - ARG_APPROVE_ALL, ARG_KEY_NAME, ARG_OPERATOR, ARG_TOKEN_ID, ARG_TOKEN_URI, - BALANCE_OF_SESSION_WASM, ENTRY_POINT_APPROVE, ENTRY_POINT_SET_APPROVE_FOR_ALL, - MINT_SESSION_WASM, OWNED_TOKENS_DICTIONARY_KEY, TEST_URI, -}; -use crate::utility::installer_request_builder::{MintingMode, OwnershipMode}; -use crate::utility::support::{ - call_entry_point_with_ret, create_dummy_key_pair, get_nft_contract_hash, -}; use crate::utility::{ constants::{ - ACCOUNT_USER_1, ACCOUNT_USER_2, ARG_MINTING_MODE, ARG_NFT_CONTRACT_HASH, - ARG_TOKEN_META_DATA, ARG_TOKEN_OWNER, BALANCES, CONTRACT_NAME, ENTRY_POINT_MINT, - NFT_CONTRACT_WASM, NUMBER_OF_MINTED_TOKENS, OWNED_TOKENS, TEST_META_DATA, TOKEN_ISSUERS, - TOKEN_META_DATA, TOKEN_OWNERS, + ACCOUNT_USER_1, ACCOUNT_USER_2, ARG_APPROVE_ALL, ARG_CONTRACT_WHITELIST, + ARG_IS_HASH_IDENTIFIER_MODE, ARG_KEY_NAME, ARG_MINTING_MODE, ARG_NFT_CONTRACT_HASH, + ARG_OPERATOR, ARG_TOKEN_ID, ARG_TOKEN_META_DATA, ARG_TOKEN_OWNER, ARG_TOKEN_URI, BALANCES, + BALANCE_OF_SESSION_WASM, CONTRACT_NAME, ENTRY_POINT_APPROVE, ENTRY_POINT_MINT, + ENTRY_POINT_SET_APPROVE_FOR_ALL, ENTRY_POINT_SET_VARIABLES, MALFORMED_META_DATA, + METADATA_CEP78, METADATA_CUSTOM_VALIDATED, METADATA_NFT721, METADATA_RAW, + MINTING_CONTRACT_WASM, MINT_SESSION_WASM, NFT_CONTRACT_WASM, NUMBER_OF_MINTED_TOKENS, + OWNED_TOKENS, OWNED_TOKENS_DICTIONARY_KEY, RECEIPT_NAME, TEST_COMPACT_META_DATA, + TEST_PRETTY_721_META_DATA, TEST_PRETTY_CEP78_METADATA, TEST_URI, TOKEN_ISSUERS, + TOKEN_OWNERS, + }, + installer_request_builder::{ + InstallerRequestBuilder, MintingMode, NFTHolderMode, NFTIdentifierMode, NFTMetadataKind, + OwnershipMode, WhitelistMode, TEST_CUSTOM_METADATA, TEST_CUSTOM_METADATA_SCHEMA, + }, + support::{ + self, assert_expected_error, call_entry_point_with_ret, create_dummy_key_pair, + get_dictionary_value_from_key, get_minting_contract_hash, get_nft_contract_hash, + query_stored_value, }, - installer_request_builder::InstallerRequestBuilder, - support, }; +#[derive(Serialize, Deserialize, Debug)] +struct Metadata { + name: String, + symbol: String, + token_uri: String, +} + fn setup_nft_contract( - total_token_supply: Option, + total_token_supply: Option, allowing_minting: Option, ) -> WasmTestBuilder { let mut builder = InMemoryWasmTestBuilder::default(); @@ -52,14 +64,14 @@ fn setup_nft_contract( #[test] fn should_disallow_minting_when_allow_minting_is_set_to_false() { - let mut builder = setup_nft_contract(Some(U256::from(2u64)), Some(false)); + let mut builder = setup_nft_contract(Some(2u64), Some(false)); let mint_request = ExecuteRequestBuilder::contract_call_by_name( *DEFAULT_ACCOUNT_ADDR, CONTRACT_NAME, ENTRY_POINT_MINT, runtime_args! { - ARG_TOKEN_META_DATA=>TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA=>TEST_PRETTY_721_META_DATA.to_string(), }, ) .build(); @@ -67,7 +79,7 @@ fn should_disallow_minting_when_allow_minting_is_set_to_false() { // Error should be MintingIsPaused=59 let actual_error = builder.get_error().expect("must have error"); - support::assert_expected_error( + assert_expected_error( actual_error, 59u16, "should now allow minting when minting is disabled", @@ -79,14 +91,12 @@ fn entry_points_with_ret_should_return_correct_value() { let mut builder = InMemoryWasmTestBuilder::default(); builder.run_genesis(&DEFAULT_RUN_GENESIS_REQUEST).commit(); - let install_request_builder = - InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_total_token_supply(U256::from(2u64)) - .with_ownership_mode(OwnershipMode::Transferable); - builder - .exec(install_request_builder.build()) - .expect_success() - .commit(); + let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) + .with_total_token_supply(2u64) + .with_ownership_mode(OwnershipMode::Transferable) + .build(); + + builder.exec(install_request).expect_success().commit(); let nft_contract_key: Key = get_nft_contract_hash(&builder).into(); @@ -97,7 +107,7 @@ fn entry_points_with_ret_should_return_correct_value() { ARG_NFT_CONTRACT_HASH => nft_contract_key, ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), ARG_TOKEN_URI => TEST_URI.to_string(), }, ) @@ -108,10 +118,10 @@ fn entry_points_with_ret_should_return_correct_value() { let nft_contract_hash = get_nft_contract_hash(&builder); let account_hash = *DEFAULT_ACCOUNT_ADDR; - let actual_balance: U256 = call_entry_point_with_ret( + let actual_balance: u64 = call_entry_point_with_ret( &mut builder, account_hash, - nft_contract_hash, + nft_contract_key, runtime_args! { ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), }, @@ -119,7 +129,7 @@ fn entry_points_with_ret_should_return_correct_value() { "balance_of", ); - let expected_balance = U256::one(); + let expected_balance = 1u64; assert_eq!( actual_balance, expected_balance, "actual and expected balances should be equal" @@ -128,9 +138,10 @@ fn entry_points_with_ret_should_return_correct_value() { let actual_owner: Key = call_entry_point_with_ret( &mut builder, account_hash, - nft_contract_hash, + nft_contract_key, runtime_args! { - ARG_TOKEN_ID => U256::zero(), + ARG_IS_HASH_IDENTIFIER_MODE => false, + ARG_TOKEN_ID => 0u64, }, "owner_of_call.wasm", "owner_of", @@ -148,7 +159,7 @@ fn entry_points_with_ret_should_return_correct_value() { nft_contract_hash, ENTRY_POINT_APPROVE, runtime_args! { - ARG_TOKEN_ID => U256::zero(), + ARG_TOKEN_ID => 0u64, ARG_OPERATOR => Key::Account(operator_public_key.to_account_hash()) }, ) @@ -158,9 +169,10 @@ fn entry_points_with_ret_should_return_correct_value() { let actual_operator: Option = call_entry_point_with_ret( &mut builder, account_hash, - nft_contract_hash, + nft_contract_key, runtime_args! { - ARG_TOKEN_ID => U256::zero(), + ARG_IS_HASH_IDENTIFIER_MODE => false, + ARG_TOKEN_ID => 0u64, }, "get_approved_call.wasm", "get_approved", @@ -175,13 +187,21 @@ fn entry_points_with_ret_should_return_correct_value() { } #[test] -fn should_call_mint_via_session_code() { +fn should_mint() { let mut builder = InMemoryWasmTestBuilder::default(); builder.run_genesis(&DEFAULT_RUN_GENESIS_REQUEST).commit(); + let metadata = Metadata { + name: "Ed".to_string(), + symbol: "avc".to_string(), + token_uri: "http://www.google.com".to_string(), + }; + + let json_metadata = serde_json::to_string(&metadata).expect("must convert to JSON string"); + let install_request_builder = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_total_token_supply(U256::from(2u64)); + .with_total_token_supply(2u64); builder .exec(install_request_builder.build()) .expect_success() @@ -196,7 +216,7 @@ fn should_call_mint_via_session_code() { ARG_NFT_CONTRACT_HASH => nft_contract_key, ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => json_metadata, ARG_TOKEN_URI => TEST_URI.to_string() }, ) @@ -213,7 +233,7 @@ fn mint_should_return_dictionary_key_to_callers_owned_tokens() { let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) .with_collection_name(NFT_COLLECTION_NAME.to_string()) - .with_total_token_supply(U256::from(100u8)) + .with_total_token_supply(100u64) .with_allowing_minting(Some(true)) .build(); @@ -226,7 +246,7 @@ fn mint_should_return_dictionary_key_to_callers_owned_tokens() { runtime_args! { ARG_NFT_CONTRACT_HASH => nft_contract_hash, ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), ARG_TOKEN_URI => TEST_URI.to_string() }, ) @@ -236,21 +256,23 @@ fn mint_should_return_dictionary_key_to_callers_owned_tokens() { let account = builder.get_expected_account(*DEFAULT_ACCOUNT_ADDR); - let owned_key_name = format!( - "{}_{}", - get_nft_contract_hash(&builder).to_formatted_string(), - NFT_COLLECTION_NAME + let receipt: String = query_stored_value( + &mut builder, + nft_contract_hash, + vec![RECEIPT_NAME.to_string()], ); let (_, owned_tokens_key) = account .named_keys() - .get_key_value(&owned_key_name) + .get_key_value(&receipt) .expect("should have owned_tokens_key"); match builder.query(None, *owned_tokens_key, &[]).unwrap() { casper_types::StoredValue::CLValue(val) => { - let expected = val.into_t::>().expect("should be Vec"); - println!("{:?}", expected); + let expected = val + .into_t::>() + .expect("should be Vec as Identifier defaults to indices"); + assert_eq!(vec![0u64], expected); } _ => panic!("wrong stored value type"), } @@ -261,7 +283,7 @@ fn mint_should_return_dictionary_key_to_callers_owned_tokens() { runtime_args! { ARG_NFT_CONTRACT_HASH => nft_contract_hash, ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), ARG_TOKEN_URI => TEST_URI.to_string() }, ) @@ -271,10 +293,8 @@ fn mint_should_return_dictionary_key_to_callers_owned_tokens() { match builder.query(None, *owned_tokens_key, &[]).unwrap() { casper_types::StoredValue::CLValue(val) => { - let expected = val - .into_t::>() - .expect("should still be Vec"); - println!("{:?}", expected); + let expected = val.into_t::>().expect("should still be Vec"); + assert_eq!(vec![0u64, 1u64], expected); } _ => panic!("also the wrong stored value type"), } @@ -287,7 +307,7 @@ fn mint_should_increment_number_of_minted_tokens_by_one_and_add_public_key_to_to let install_request_builder = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_total_token_supply(U256::from(2u64)); + .with_total_token_supply(2u64); builder .exec(install_request_builder.build()) .expect_success() @@ -302,7 +322,7 @@ fn mint_should_increment_number_of_minted_tokens_by_one_and_add_public_key_to_to ARG_NFT_CONTRACT_HASH => nft_contract_key, ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), ARG_TOKEN_URI => TEST_URI.to_string() }, ) @@ -318,47 +338,45 @@ fn mint_should_increment_number_of_minted_tokens_by_one_and_add_public_key_to_to .expect("must have key in named keys"); //mint should have incremented number_of_minted_tokens by one - let query_result: U256 = support::query_stored_value( + let query_result: u64 = support::query_stored_value( &mut builder, *nft_contract_key, vec![NUMBER_OF_MINTED_TOKENS.to_string()], ); assert_eq!( - query_result, - U256::one(), + query_result, 1u64, "number_of_minted_tokens initialized at installation should have incremented by one" ); let actual_token_meta_data = support::get_dictionary_value_from_key::( &builder, nft_contract_key, - TOKEN_META_DATA, - &U256::zero().to_string(), + METADATA_NFT721, + &0u64.to_string(), ); - assert_eq!(actual_token_meta_data, TEST_META_DATA); + assert_eq!(actual_token_meta_data, TEST_PRETTY_721_META_DATA); let minter_account_hash = support::get_dictionary_value_from_key::( &builder, nft_contract_key, TOKEN_OWNERS, - &U256::zero().to_string(), + &0u64.to_string(), ) .into_account() .unwrap(); assert_eq!(DEFAULT_ACCOUNT_ADDR.clone(), minter_account_hash); - let actual_token_ids = support::get_dictionary_value_from_key::>( + let actual_token_ids = support::get_dictionary_value_from_key::>( &builder, nft_contract_key, OWNED_TOKENS, &DEFAULT_ACCOUNT_ADDR.clone().to_string(), ); - let expected_token_ids = vec![U256::zero()]; - assert_eq!(expected_token_ids, actual_token_ids); + assert_eq!(vec![0u64], actual_token_ids); // If total_token_supply is initialized to 1 the following test should fail. // If we set total_token_supply > 1 it should pass @@ -370,8 +388,8 @@ fn mint_should_increment_number_of_minted_tokens_by_one_and_add_public_key_to_to ARG_NFT_CONTRACT_HASH => *nft_contract_key, ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), - ARG_TOKEN_URI => TEST_URI.to_string() + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), + ARG_TOKEN_URI => TEST_URI.to_string() }, ) .build(); @@ -385,7 +403,7 @@ fn should_set_meta_data() { let install_request_builder = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_total_token_supply(U256::from(2u32)); + .with_total_token_supply(2u64); builder .exec(install_request_builder.build()) .expect_success() @@ -400,7 +418,7 @@ fn should_set_meta_data() { ARG_NFT_CONTRACT_HASH => nft_contract_key, ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), ARG_TOKEN_URI => TEST_URI.to_string() }, ) @@ -417,11 +435,11 @@ fn should_set_meta_data() { let actual_token_meta_data = support::get_dictionary_value_from_key::( &builder, nft_contract_key, - TOKEN_META_DATA, - &U256::zero().to_string(), + METADATA_NFT721, + &0u64.to_string(), ); - assert_eq!(actual_token_meta_data, TEST_META_DATA); + assert_eq!(actual_token_meta_data, TEST_PRETTY_721_META_DATA); } #[test] @@ -431,7 +449,7 @@ fn should_set_issuer() { let install_request_builder = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_total_token_supply(U256::from(2u32)); + .with_total_token_supply(2u64); builder .exec(install_request_builder.build()) .expect_success() @@ -446,7 +464,7 @@ fn should_set_issuer() { ARG_NFT_CONTRACT_HASH => nft_contract_key, ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), ARG_TOKEN_URI => TEST_URI.to_string() }, ) @@ -464,7 +482,7 @@ fn should_set_issuer() { &builder, nft_contract_key, TOKEN_ISSUERS, - &U256::zero().to_string(), + &0u64.to_string(), ) .into_account() .unwrap(); @@ -479,7 +497,7 @@ fn should_track_token_balance_by_owner() { let install_request_builder = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_total_token_supply(U256::from(2u32)); + .with_total_token_supply(2u64); builder .exec(install_request_builder.build()) .expect_success() @@ -494,7 +512,7 @@ fn should_track_token_balance_by_owner() { ARG_NFT_CONTRACT_HASH => nft_contract_key, ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), ARG_TOKEN_URI => TEST_URI.to_string() }, ) @@ -510,16 +528,14 @@ fn should_track_token_balance_by_owner() { let token_owner = DEFAULT_ACCOUNT_ADDR.clone().to_string(); - let actual_minter_balance = support::get_dictionary_value_from_key::( + let actual_minter_balance = support::get_dictionary_value_from_key::( &builder, nft_contract_key, BALANCES, &token_owner, ); - let expected_minter_balance = U256::one(); + let expected_minter_balance = 1u64; assert_eq!(actual_minter_balance, expected_minter_balance); - - // TODO: come up with better name than balance. Or not... } #[test] @@ -528,7 +544,7 @@ fn should_allow_public_minting_with_flag_set_to_true() { builder.run_genesis(&DEFAULT_RUN_GENESIS_REQUEST).commit(); let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_total_token_supply(U256::from(100u64)) + .with_total_token_supply(100u64) .with_minting_mode(Some(MintingMode::Public as u8)) .build(); builder.exec(install_request).expect_success().commit(); @@ -578,7 +594,7 @@ fn should_allow_public_minting_with_flag_set_to_true() { ARG_NFT_CONTRACT_HASH => nft_contract_key, ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), ARG_TOKEN_OWNER => Key::Account(account_1_public_key.to_account_hash()), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), ARG_TOKEN_URI => TEST_URI.to_string() }, ) @@ -590,7 +606,7 @@ fn should_allow_public_minting_with_flag_set_to_true() { &builder, &nft_contract_key, TOKEN_OWNERS, - &U256::zero().to_string(), + &0u64.to_string(), ) .into_account() .unwrap(); @@ -604,7 +620,7 @@ fn should_disallow_public_minting_with_flag_set_to_false() { builder.run_genesis(&DEFAULT_RUN_GENESIS_REQUEST).commit(); let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_total_token_supply(U256::from(100u64)) + .with_total_token_supply(100u64) .with_minting_mode(Some(MintingMode::Installer as u8)) .build(); builder.exec(install_request).expect_success().commit(); @@ -651,7 +667,7 @@ fn should_disallow_public_minting_with_flag_set_to_false() { runtime_args! { ARG_NFT_CONTRACT_HASH => *nft_contract_key, ARG_TOKEN_OWNER => Key::Account(account_1_public_key.to_account_hash()), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), ARG_TOKEN_URI => TEST_URI.to_string() }, ) @@ -666,7 +682,7 @@ fn should_allow_minting_for_different_public_key_with_minting_mode_set_to_public builder.run_genesis(&DEFAULT_RUN_GENESIS_REQUEST).commit(); let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_total_token_supply(U256::from(100u64)) + .with_total_token_supply(100u64) .with_minting_mode(Some(MintingMode::Public as u8)) .build(); builder.exec(install_request).expect_success().commit(); @@ -727,7 +743,7 @@ fn should_allow_minting_for_different_public_key_with_minting_mode_set_to_public ARG_NFT_CONTRACT_HASH => nft_contract_key, ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), ARG_TOKEN_OWNER => Key::Account(account_1_public_key.to_account_hash()), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), ARG_TOKEN_URI => TEST_URI.to_string() }, ) @@ -742,7 +758,7 @@ fn should_set_approval_for_all() { builder.run_genesis(&DEFAULT_RUN_GENESIS_REQUEST).commit(); let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) - .with_total_token_supply(U256::from(100u64)) + .with_total_token_supply(100u64) .with_ownership_mode(OwnershipMode::Transferable) .build(); builder.exec(install_request).expect_success().commit(); @@ -756,7 +772,7 @@ fn should_set_approval_for_all() { ARG_NFT_CONTRACT_HASH => nft_contract_key, ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), ARG_TOKEN_URI => TEST_URI.to_string() }, ) @@ -770,7 +786,7 @@ fn should_set_approval_for_all() { ARG_NFT_CONTRACT_HASH => nft_contract_key, ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), ARG_TOKEN_URI => TEST_URI.to_string() }, ) @@ -797,9 +813,10 @@ fn should_set_approval_for_all() { let actual_operator: Option = call_entry_point_with_ret( &mut builder, *DEFAULT_ACCOUNT_ADDR, - nft_contract_hash, + nft_contract_key, runtime_args! { - ARG_TOKEN_ID => U256::zero(), + ARG_IS_HASH_IDENTIFIER_MODE => false, + ARG_TOKEN_ID => 0u64, }, "get_approved_call.wasm", "get_approved", @@ -815,9 +832,10 @@ fn should_set_approval_for_all() { let actual_operator: Option = call_entry_point_with_ret( &mut builder, *DEFAULT_ACCOUNT_ADDR, - nft_contract_hash, + nft_contract_key, runtime_args! { - ARG_TOKEN_ID => U256::one(), + ARG_IS_HASH_IDENTIFIER_MODE => false, + ARG_TOKEN_ID => 0u64, }, "get_approved_call.wasm", "get_approved", @@ -830,3 +848,518 @@ fn should_set_approval_for_all() { "actual and expected operator should be equal" ); } + +#[test] +fn should_allow_whitelisted_contract_to_mint() { + let mut builder = InMemoryWasmTestBuilder::default(); + builder.run_genesis(&DEFAULT_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, NFT_CONTRACT_WASM) + .with_total_token_supply(100u64) + .with_holder_mode(NFTHolderMode::Contracts) + .with_whitelist_mode(WhitelistMode::Locked) + .with_ownership_mode(OwnershipMode::Minter) + .with_minting_mode(Some(MintingMode::Installer as u8)) + .with_contract_whitelist(contract_whitelist.clone()) + .build(); + + builder.exec(install_request).expect_success().commit(); + + let nft_contract_key: Key = get_nft_contract_hash(&builder).into(); + + let actual_contract_whitelist: Vec = query_stored_value( + &mut builder, + nft_contract_key, + vec![ARG_CONTRACT_WHITELIST.to_string()], + ); + + assert_eq!(actual_contract_whitelist, contract_whitelist); + + 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_TOKEN_URI => TEST_URI.to_string() + }; + + 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.to_string(); + + let actual_token_owner: Key = + get_dictionary_value_from_key(&builder, &nft_contract_key, TOKEN_OWNERS, &token_id); + + let minting_contract_key: Key = minting_contract_hash.into(); + + assert_eq!(actual_token_owner, minting_contract_key) +} + +#[test] +fn should_disallow_unlisted_contract_from_minting() { + let mut builder = InMemoryWasmTestBuilder::default(); + builder.run_genesis(&DEFAULT_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![ + ContractHash::from([1u8; 32]), + ContractHash::from([2u8; 32]), + ContractHash::from([3u8; 32]), + ]; + + 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_minting_mode(Some(MintingMode::Installer as u8)) + .with_contract_whitelist(contract_whitelist) + .build(); + + builder.exec(install_request).expect_success().commit(); + + let nft_contract_key: Key = get_nft_contract_hash(&builder).into(); + + 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_TOKEN_URI => TEST_URI.to_string() + }; + + 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 contract hash should not be permitted to mint", + ); +} + +#[test] +fn should_be_able_to_update_whitelist_for_minting() { + let mut builder = InMemoryWasmTestBuilder::default(); + builder.run_genesis(&DEFAULT_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 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_minting_mode(Some(MintingMode::Installer as u8)) + .with_contract_whitelist(vec![]) + .build(); + + builder.exec(install_request).expect_success().commit(); + + let nft_contract_hash = get_nft_contract_hash(&builder); + let nft_contract_key = nft_contract_hash.into(); + + let current_contract_whitelist: Vec = query_stored_value( + &mut builder, + nft_contract_key, + vec![ARG_CONTRACT_WHITELIST.to_string()], + ); + + assert!(current_contract_whitelist.is_empty()); + 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_TOKEN_URI => TEST_URI.to_string() + }; + + 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 => Some(vec![minting_contract_hash]) + }, + ) + .build(); + + builder + .exec(update_whitelist_request) + .expect_success() + .commit(); + + let updated_contract_whitelist: Vec = query_stored_value( + &mut builder, + nft_contract_key, + vec![ARG_CONTRACT_WHITELIST.to_string()], + ); + + assert_eq!(vec![minting_contract_hash], updated_contract_whitelist); + + 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_not_mint_with_invalid_nft721_metadata() { + let mut builder = InMemoryWasmTestBuilder::default(); + builder.run_genesis(&DEFAULT_RUN_GENESIS_REQUEST).commit(); + + let install_request_builder = + InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) + .with_total_token_supply(2u64); + builder + .exec(install_request_builder.build()) + .expect_success() + .commit(); + + let nft_contract_key: Key = get_nft_contract_hash(&builder).into(); + + let mint_session_call = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + MINT_SESSION_WASM, + runtime_args! { + ARG_NFT_CONTRACT_HASH => nft_contract_key, + ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), + ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), + ARG_TOKEN_META_DATA => MALFORMED_META_DATA, + ARG_TOKEN_URI => TEST_URI.to_string() + }, + ) + .build(); + + builder.exec(mint_session_call).expect_failure(); + + let error = builder.get_error().expect("mint request must have failed"); + assert_expected_error( + error, + 89, + "FailedToParse721Metadata error (89) must have been raised due to mangled metadata", + ) +} + +#[test] +fn should_mint_with_compactified_metadata() { + let mut builder = InMemoryWasmTestBuilder::default(); + builder.run_genesis(&DEFAULT_RUN_GENESIS_REQUEST).commit(); + + let install_request_builder = + InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) + .with_total_token_supply(2u64) + .build(); + + builder + .exec(install_request_builder) + .expect_success() + .commit(); + + let nft_contract_key: Key = get_nft_contract_hash(&builder).into(); + + let mint_session_call = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + MINT_SESSION_WASM, + runtime_args! { + ARG_NFT_CONTRACT_HASH => nft_contract_key, + ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), + ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), + ARG_TOKEN_META_DATA => TEST_COMPACT_META_DATA, + ARG_TOKEN_URI => TEST_URI.to_string() + }, + ) + .build(); + + builder.exec(mint_session_call).expect_success().commit(); + + let actual_metadata = get_dictionary_value_from_key::( + &builder, + &nft_contract_key, + METADATA_NFT721, + &0u64.to_string(), + ); + + assert_eq!(TEST_PRETTY_721_META_DATA, actual_metadata) +} + +#[test] +fn should_mint_with_valid_cep99_metadata() { + let mut builder = InMemoryWasmTestBuilder::default(); + builder.run_genesis(&DEFAULT_RUN_GENESIS_REQUEST).commit(); + + let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) + .with_total_token_supply(2u64) + .with_nft_metadata_kind(NFTMetadataKind::CEP99) + .build(); + + builder.exec(install_request).expect_success().commit(); + + let nft_contract_key: Key = get_nft_contract_hash(&builder).into(); + + let mint_session_call = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + MINT_SESSION_WASM, + runtime_args! { + ARG_NFT_CONTRACT_HASH => nft_contract_key, + ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), + ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), + ARG_TOKEN_META_DATA => TEST_PRETTY_CEP78_METADATA, + ARG_TOKEN_URI => TEST_URI.to_string() + }, + ) + .build(); + + builder.exec(mint_session_call).expect_success().commit(); + + let actual_metadata = get_dictionary_value_from_key::( + &builder, + &nft_contract_key, + METADATA_CEP78, + &0u64.to_string(), + ); + + assert_eq!(TEST_PRETTY_CEP78_METADATA, actual_metadata) +} + +#[test] +fn should_mint_with_custom_metadata_validation() { + let mut builder = InMemoryWasmTestBuilder::default(); + builder.run_genesis(&DEFAULT_RUN_GENESIS_REQUEST).commit(); + + let custom_json_schema = + serde_json::to_string(&*TEST_CUSTOM_METADATA_SCHEMA).expect("must convert to json schema"); + + let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) + .with_total_token_supply(2u64) + .with_nft_metadata_kind(NFTMetadataKind::CustomValidated) + .with_json_schema(custom_json_schema) + .build(); + + builder.exec(install_request).expect_success().commit(); + + let nft_contract_key: Key = get_nft_contract_hash(&builder).into(); + + let custom_metadata = + serde_json::to_string(&*TEST_CUSTOM_METADATA).expect("must convert to json metadata"); + + let mint_session_call = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + MINT_SESSION_WASM, + runtime_args! { + ARG_NFT_CONTRACT_HASH => nft_contract_key, + ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), + ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), + ARG_TOKEN_META_DATA => custom_metadata , + ARG_TOKEN_URI => TEST_URI.to_string() + }, + ) + .build(); + + builder.exec(mint_session_call).expect_success().commit(); + + let actual_metadata = get_dictionary_value_from_key::( + &builder, + &nft_contract_key, + METADATA_CUSTOM_VALIDATED, + &0u64.to_string(), + ); + + 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 mut builder = InMemoryWasmTestBuilder::default(); + builder.run_genesis(&DEFAULT_RUN_GENESIS_REQUEST).commit(); + + let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) + .with_total_token_supply(2u64) + .with_nft_metadata_kind(NFTMetadataKind::Raw) + .build(); + + builder.exec(install_request).expect_success().commit(); + + let nft_contract_key: Key = get_nft_contract_hash(&builder).into(); + + let mint_session_call = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + MINT_SESSION_WASM, + runtime_args! { + ARG_NFT_CONTRACT_HASH => nft_contract_key, + ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), + ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), + ARG_TOKEN_META_DATA => "raw_string".to_string() , + }, + ) + .build(); + + builder.exec(mint_session_call).expect_success().commit(); + + let actual_metadata = get_dictionary_value_from_key::( + &builder, + &nft_contract_key, + METADATA_RAW, + &0u64.to_string(), + ); + + assert_eq!("raw_string".to_string(), actual_metadata) +} + +#[test] +fn should_mint_with_hash_identifier_mode() { + let mut builder = InMemoryWasmTestBuilder::default(); + builder.run_genesis(&DEFAULT_RUN_GENESIS_REQUEST).commit(); + + let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) + .with_identifier_mode(NFTIdentifierMode::Hash) + .with_total_token_supply(10u64) + .build(); + + builder.exec(install_request).expect_success().commit(); + + let nft_contract_key: Key = get_nft_contract_hash(&builder).into(); + + let mint_session_call = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + MINT_SESSION_WASM, + 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 , + ARG_TOKEN_URI => TEST_URI.to_string() + }, + ) + .build(); + + builder.exec(mint_session_call).expect_success().commit(); + + let token_id_hash: String = + base16::encode_lower(&support::create_blake2b_hash(&TEST_PRETTY_721_META_DATA)); + + let actual_token_ids = get_dictionary_value_from_key::>( + &builder, + &nft_contract_key, + OWNED_TOKENS, + &DEFAULT_ACCOUNT_ADDR.clone().to_string(), + ); + + assert_eq!(vec![token_id_hash], actual_token_ids); +} + +#[test] +fn should_fail_to_mint_when_immediate_caller_is_account_in_contract_mode() { + let mut builder = InMemoryWasmTestBuilder::default(); + builder.run_genesis(&DEFAULT_RUN_GENESIS_REQUEST).commit(); + + let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) + .with_total_token_supply(2u64) + .with_holder_mode(NFTHolderMode::Contracts) + .with_whitelist_mode(WhitelistMode::Unlocked) + .build(); + + builder.exec(install_request).expect_success().commit(); + + let nft_contract_key: Key = get_nft_contract_hash(&builder).into(); + + let mint_session_call = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + MINT_SESSION_WASM, + runtime_args! { + ARG_NFT_CONTRACT_HASH => nft_contract_key, + ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), + ARG_TOKEN_META_DATA => TEST_COMPACT_META_DATA, + }, + ) + .build(); + + builder.exec(mint_session_call).expect_failure(); + + let error = builder.get_error().expect("must have error"); + + assert_expected_error(error, 76, "InvalidHolderMode(76) must have been raised"); +} diff --git a/tests/src/set_variables.rs b/tests/src/set_variables.rs index 2d85459f..d5dfcb9a 100644 --- a/tests/src/set_variables.rs +++ b/tests/src/set_variables.rs @@ -2,7 +2,7 @@ use casper_engine_test_support::{ ExecuteRequestBuilder, InMemoryWasmTestBuilder, DEFAULT_ACCOUNT_ADDR, DEFAULT_RUN_GENESIS_REQUEST, }; -use casper_types::{runtime_args, ContractHash, Key, RuntimeArgs, U256}; +use casper_types::{runtime_args, ContractHash, Key, RuntimeArgs}; use crate::utility::{ constants::{ @@ -23,7 +23,7 @@ fn only_installer_should_be_able_to_toggle_allow_minting() { 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(U256::one()) + .with_total_token_supply(1u64) .with_allowing_minting(Some(false)) .build(); diff --git a/tests/src/transfer.rs b/tests/src/transfer.rs index e5d8f1ab..59908bb3 100644 --- a/tests/src/transfer.rs +++ b/tests/src/transfer.rs @@ -1,20 +1,31 @@ use casper_engine_test_support::{ ExecuteRequestBuilder, InMemoryWasmTestBuilder, DEFAULT_ACCOUNT_ADDR, - DEFAULT_ACCOUNT_PUBLIC_KEY, DEFAULT_RUN_GENESIS_REQUEST, + DEFAULT_ACCOUNT_PUBLIC_KEY, DEFAULT_RUN_GENESIS_REQUEST, MINIMUM_ACCOUNT_CREATION_BALANCE, +}; +use casper_types::{ + account::AccountHash, runtime_args, system::mint, ContractHash, Key, PublicKey, RuntimeArgs, + SecretKey, U512, }; -use casper_types::{runtime_args, system::mint, ContractHash, Key, RuntimeArgs, U256}; use crate::utility::{ constants::{ - ACCOUNT_USER_1, ACCOUNT_USER_2, ACCOUNT_USER_3, ARG_FROM_ACCOUNT_HASH, ARG_KEY_NAME, - ARG_NFT_CONTRACT_HASH, ARG_OPERATOR, ARG_TOKEN_ID, ARG_TOKEN_META_DATA, ARG_TOKEN_OWNER, - ARG_TOKEN_URI, ARG_TO_ACCOUNT_HASH, BALANCES, CONTRACT_NAME, ENTRY_POINT_APPROVE, - ENTRY_POINT_TRANSFER, MINT_SESSION_WASM, NFT_CONTRACT_WASM, NFT_TEST_COLLECTION, - NFT_TEST_SYMBOL, OPERATOR, OWNED_TOKENS, OWNED_TOKENS_DICTIONARY_KEY, TEST_META_DATA, - TEST_URI, TOKEN_OWNERS, + ACCOUNT_USER_1, ACCOUNT_USER_2, ACCOUNT_USER_3, ARG_CONTRACT_WHITELIST, + ARG_IS_HASH_IDENTIFIER_MODE, ARG_KEY_NAME, ARG_NFT_CONTRACT_HASH, ARG_OPERATOR, + ARG_SOURCE_KEY, ARG_TARGET_KEY, ARG_TOKEN_HASH, ARG_TOKEN_ID, ARG_TOKEN_META_DATA, + ARG_TOKEN_OWNER, ARG_TOKEN_URI, BALANCES, CONTRACT_NAME, ENTRY_POINT_APPROVE, + ENTRY_POINT_MINT, ENTRY_POINT_TRANSFER, MINTING_CONTRACT_WASM, MINT_SESSION_WASM, + NFT_CONTRACT_WASM, NFT_TEST_COLLECTION, NFT_TEST_SYMBOL, OPERATOR, OWNED_TOKENS, + OWNED_TOKENS_DICTIONARY_KEY, TEST_PRETTY_721_META_DATA, TEST_URI, TOKEN_OWNERS, + TRANSFER_SESSION_WASM, + }, + installer_request_builder::{ + InstallerRequestBuilder, MintingMode, NFTHolderMode, NFTIdentifierMode, OwnershipMode, + WhitelistMode, + }, + support::{ + self, assert_expected_error, get_dictionary_value_from_key, get_minting_contract_hash, + get_nft_contract_hash, query_stored_value, }, - installer_request_builder::{InstallerRequestBuilder, OwnershipMode}, - support::{self, get_dictionary_value_from_key, get_nft_contract_hash}, }; #[test] @@ -25,7 +36,7 @@ fn should_dissallow_transfer_with_minter_or_assigned_ownership_mode() { 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(U256::from(1u64)) + .with_total_token_supply(1u64) .with_ownership_mode(OwnershipMode::Minter) .build(); @@ -51,7 +62,7 @@ fn should_dissallow_transfer_with_minter_or_assigned_ownership_mode() { ARG_NFT_CONTRACT_HASH => nft_contract_key, ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), ARG_TOKEN_URI => TEST_URI.to_string() }, ) @@ -65,13 +76,13 @@ fn should_dissallow_transfer_with_minter_or_assigned_ownership_mode() { .get(CONTRACT_NAME) .expect("must have key in named keys"); - let actual_owner_balance: U256 = support::get_dictionary_value_from_key( + let actual_owner_balance: u64 = support::get_dictionary_value_from_key( &builder, nft_contract_key, BALANCES, &token_owner.to_string(), ); - let expected_owner_balance = U256::one(); + let expected_owner_balance = 1u64; assert_eq!(actual_owner_balance, expected_owner_balance); let (_, token_receiver) = support::create_dummy_key_pair(ACCOUNT_USER_1); @@ -80,9 +91,10 @@ fn should_dissallow_transfer_with_minter_or_assigned_ownership_mode() { nft_contract_hash, ENTRY_POINT_TRANSFER, runtime_args! { - ARG_TOKEN_ID => U256::zero(),// We need mint to return the token_id!! - ARG_FROM_ACCOUNT_HASH => Key::Account(token_owner), - ARG_TO_ACCOUNT_HASH => Key::Account( token_receiver.to_account_hash()), + ARG_SOURCE_KEY => Key::Account(token_owner), + ARG_TARGET_KEY => Key::Account( token_receiver.to_account_hash()), + ARG_IS_HASH_IDENTIFIER_MODE => false, + ARG_TOKEN_ID => 0u64, }, ) .build(); @@ -104,21 +116,12 @@ fn should_transfer_token_from_sender_to_receiver() { 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(U256::from(1u64)) + .with_total_token_supply(1u64) .with_ownership_mode(OwnershipMode::Transferable) .build(); builder.exec(install_request).expect_success().commit(); - let account = builder.get_expected_account(*DEFAULT_ACCOUNT_ADDR); - let nft_contract_hash = account - .named_keys() - .get(CONTRACT_NAME) - .cloned() - .and_then(Key::into_hash) - .map(ContractHash::new) - .expect("failed to find nft contract"); - let token_owner = *DEFAULT_ACCOUNT_ADDR; let nft_contract_key: Key = get_nft_contract_hash(&builder).into(); @@ -130,7 +133,7 @@ fn should_transfer_token_from_sender_to_receiver() { ARG_NFT_CONTRACT_HASH => nft_contract_key, ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), ARG_TOKEN_URI => TEST_URI.to_string() }, ) @@ -139,29 +142,30 @@ fn should_transfer_token_from_sender_to_receiver() { builder.exec(mint_session_call).expect_success().commit(); let installing_account = builder.get_expected_account(*DEFAULT_ACCOUNT_ADDR); - let nft_contract_key = installing_account + let nft_contract_key = *installing_account .named_keys() .get(CONTRACT_NAME) .expect("must have key in named keys"); - let actual_owner_balance: U256 = support::get_dictionary_value_from_key( + let actual_owner_balance: u64 = support::get_dictionary_value_from_key( &builder, - nft_contract_key, + &nft_contract_key, BALANCES, &token_owner.to_string(), ); - let expected_owner_balance = U256::one(); + let expected_owner_balance = 1u64; assert_eq!(actual_owner_balance, expected_owner_balance); let (_, token_receiver) = support::create_dummy_key_pair(ACCOUNT_USER_1); - let transfer_request = ExecuteRequestBuilder::contract_call_by_hash( + let transfer_request = ExecuteRequestBuilder::standard( *DEFAULT_ACCOUNT_ADDR, - nft_contract_hash, - ENTRY_POINT_TRANSFER, + TRANSFER_SESSION_WASM, runtime_args! { - ARG_TOKEN_ID => U256::zero(),// We need mint to return the token_id!! - ARG_FROM_ACCOUNT_HASH => Key::Account(token_owner), - ARG_TO_ACCOUNT_HASH => Key::Account( token_receiver.to_account_hash()), + ARG_NFT_CONTRACT_HASH => nft_contract_key, + ARG_TOKEN_ID => 0u64, + ARG_IS_HASH_IDENTIFIER_MODE => false, + ARG_SOURCE_KEY => Key::Account(*DEFAULT_ACCOUNT_ADDR), + ARG_TARGET_KEY => Key::Account(token_receiver.to_account_hash()), }, ) .build(); @@ -169,41 +173,40 @@ fn should_transfer_token_from_sender_to_receiver() { let actual_token_owner = support::get_dictionary_value_from_key::( &builder, - nft_contract_key, + &nft_contract_key, TOKEN_OWNERS, - &U256::zero().to_string(), + &0u64.to_string(), ) .into_account() .unwrap(); assert_eq!(actual_token_owner, token_receiver.to_account_hash()); // Change token_receiver to token_owner for red test - let actual_owned_tokens: Vec = support::get_dictionary_value_from_key( + let actual_owned_tokens: Vec = support::get_dictionary_value_from_key( &builder, - nft_contract_key, + &nft_contract_key, OWNED_TOKENS, &token_receiver.to_account_hash().to_string(), ); - let expected_owned_tokens = vec![U256::zero()]; //Change zero() to one() for red test - assert_eq!(actual_owned_tokens, expected_owned_tokens); + assert_eq!(actual_owned_tokens, vec![0u64]); - let actual_sender_balance: U256 = support::get_dictionary_value_from_key( + let actual_sender_balance: u64 = support::get_dictionary_value_from_key( &builder, - nft_contract_key, + &nft_contract_key, BALANCES, &token_owner.to_string(), ); - let expected_sender_balance = U256::zero(); + let expected_sender_balance = 0u64; assert_eq!(actual_sender_balance, expected_sender_balance); - let actual_receiver_balance: U256 = support::get_dictionary_value_from_key( + let actual_receiver_balance: u64 = support::get_dictionary_value_from_key( &builder, - nft_contract_key, + &nft_contract_key, BALANCES, &token_receiver.to_account_hash().to_string(), ); - let expected_receiver_balance = U256::one(); + let expected_receiver_balance = 1u64; assert_eq!(actual_receiver_balance, expected_receiver_balance); } @@ -215,7 +218,7 @@ fn approve_token_for_transfer_should_add_entry_to_approved_dictionary() { 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(U256::one()) + .with_total_token_supply(1u64) .with_ownership_mode(OwnershipMode::Transferable) .build(); @@ -239,7 +242,7 @@ fn approve_token_for_transfer_should_add_entry_to_approved_dictionary() { ARG_NFT_CONTRACT_HASH => nft_contract_key, ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), ARG_TOKEN_URI => TEST_URI.to_string() }, ) @@ -253,7 +256,7 @@ fn approve_token_for_transfer_should_add_entry_to_approved_dictionary() { nft_contract_hash, ENTRY_POINT_APPROVE, runtime_args! { - ARG_TOKEN_ID => U256::zero(), + ARG_TOKEN_ID => 0u64, ARG_OPERATOR => Key::Account(approve_public_key.to_account_hash()) }, ) @@ -270,7 +273,7 @@ fn approve_token_for_transfer_should_add_entry_to_approved_dictionary() { &builder, nft_contract_key, OPERATOR, - &U256::zero().to_string(), + &0u64.to_string(), ); assert_eq!( @@ -287,7 +290,7 @@ fn should_dissallow_approving_when_ownership_mode_is_minter_or_assigned() { 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(U256::one()) + .with_total_token_supply(1u64) .with_ownership_mode(OwnershipMode::Assigned) .build(); @@ -311,7 +314,7 @@ fn should_dissallow_approving_when_ownership_mode_is_minter_or_assigned() { ARG_NFT_CONTRACT_HASH => nft_contract_key, ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), ARG_TOKEN_URI => TEST_URI.to_string() }, @@ -326,7 +329,7 @@ fn should_dissallow_approving_when_ownership_mode_is_minter_or_assigned() { nft_contract_hash, ENTRY_POINT_APPROVE, runtime_args! { - ARG_TOKEN_ID => U256::zero(), + ARG_TOKEN_ID => 0u64, ARG_OPERATOR => Key::Account(approve_public_key.to_account_hash()) }, ) @@ -349,7 +352,7 @@ fn should_be_able_to_transfer_token_using_approved_operator() { 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(U256::one()) + .with_total_token_supply(1u64) .with_ownership_mode(OwnershipMode::Transferable) .build(); @@ -374,7 +377,7 @@ fn should_be_able_to_transfer_token_using_approved_operator() { ARG_NFT_CONTRACT_HASH => nft_contract_key, ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), ARG_TOKEN_URI => TEST_URI.to_string() }, ) @@ -401,7 +404,7 @@ fn should_be_able_to_transfer_token_using_approved_operator() { nft_contract_hash, ENTRY_POINT_APPROVE, runtime_args! { - ARG_TOKEN_ID => U256::zero(), + ARG_TOKEN_ID => 0u64, ARG_OPERATOR => Key::Account (operator.to_account_hash()) }, ) @@ -409,16 +412,12 @@ fn should_be_able_to_transfer_token_using_approved_operator() { builder.exec(approve_request).expect_success().commit(); let installing_account = builder.get_expected_account(*DEFAULT_ACCOUNT_ADDR); - let nft_contract_key = installing_account + let nft_contract_key = *installing_account .named_keys() .get(CONTRACT_NAME) .expect("must have key in named keys"); - let actual_operator: Option = get_dictionary_value_from_key( - &builder, - nft_contract_key, - OPERATOR, - &U256::zero().to_string(), - ); + let actual_operator: Option = + get_dictionary_value_from_key(&builder, &nft_contract_key, OPERATOR, &0u64.to_string()); let expected_operator = Some(Key::Account(operator.to_account_hash())); assert_eq!( @@ -441,26 +440,23 @@ fn should_be_able_to_transfer_token_using_approved_operator() { .exec(transfer_to_to_account) .expect_success() .commit(); - - let transfer_request = ExecuteRequestBuilder::contract_call_by_hash( - operator.to_account_hash(), - nft_contract_hash, - ENTRY_POINT_TRANSFER, + // + let transfer_request = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + TRANSFER_SESSION_WASM, runtime_args! { - ARG_TOKEN_ID => U256::zero(), - ARG_FROM_ACCOUNT_HASH => Key::Account(token_owner), - ARG_TO_ACCOUNT_HASH => Key::Account( to_account_public_key.to_account_hash()), + ARG_NFT_CONTRACT_HASH => nft_contract_key, + ARG_SOURCE_KEY => Key::Account(token_owner), + ARG_TARGET_KEY => Key::Account(to_account_public_key.to_account_hash()), + ARG_IS_HASH_IDENTIFIER_MODE => false, + ARG_TOKEN_ID => 0u64, }, ) .build(); builder.exec(transfer_request).expect_success().commit(); - let actual_approved_account_hash: Option = get_dictionary_value_from_key( - &builder, - nft_contract_key, - OPERATOR, - &U256::zero().to_string(), - ); + let actual_approved_account_hash: Option = + get_dictionary_value_from_key(&builder, &nft_contract_key, OPERATOR, &0u64.to_string()); assert_eq!( actual_approved_account_hash, None, @@ -476,7 +472,7 @@ fn should_dissallow_same_operator_to_tranfer_token_twice() { 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(U256::one()) + .with_total_token_supply(1u64) .with_ownership_mode(OwnershipMode::Transferable) .build(); @@ -501,7 +497,7 @@ fn should_dissallow_same_operator_to_tranfer_token_twice() { ARG_NFT_CONTRACT_HASH => nft_contract_key, ARG_KEY_NAME => Some(OWNED_TOKENS_DICTIONARY_KEY.to_string()), ARG_TOKEN_OWNER => Key::Account(*DEFAULT_ACCOUNT_ADDR), - ARG_TOKEN_META_DATA => TEST_META_DATA.to_string(), + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), ARG_TOKEN_URI => TEST_URI.to_string() }, ) @@ -528,7 +524,7 @@ fn should_dissallow_same_operator_to_tranfer_token_twice() { nft_contract_hash, ENTRY_POINT_APPROVE, runtime_args! { - ARG_TOKEN_ID => U256::zero(), + ARG_TOKEN_ID => 0u64, ARG_OPERATOR => Key::Account (operator.to_account_hash()) }, ) @@ -536,16 +532,12 @@ fn should_dissallow_same_operator_to_tranfer_token_twice() { builder.exec(approve_request).expect_success().commit(); let installing_account = builder.get_expected_account(*DEFAULT_ACCOUNT_ADDR); - let nft_contract_key = installing_account + let nft_contract_key = *installing_account .named_keys() .get(CONTRACT_NAME) .expect("must have key in named keys"); - let actual_operator: Option = get_dictionary_value_from_key( - &builder, - nft_contract_key, - OPERATOR, - &U256::zero().to_string(), - ); + let actual_operator: Option = + get_dictionary_value_from_key(&builder, &nft_contract_key, OPERATOR, &0u64.to_string()); let expected_operator = Some(Key::Account(operator.to_account_hash())); @@ -570,30 +562,259 @@ fn should_dissallow_same_operator_to_tranfer_token_twice() { .expect_success() .commit(); - let transfer_request = ExecuteRequestBuilder::contract_call_by_hash( - operator.to_account_hash(), - nft_contract_hash, - ENTRY_POINT_TRANSFER, + let transfer_request = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + TRANSFER_SESSION_WASM, runtime_args! { - ARG_TOKEN_ID => U256::zero(), - ARG_FROM_ACCOUNT_HASH => Key::Account(token_owner), - ARG_TO_ACCOUNT_HASH => Key::Account( to_account_public_key.to_account_hash()), + ARG_NFT_CONTRACT_HASH => nft_contract_key, + ARG_TOKEN_ID => 0u64, + ARG_IS_HASH_IDENTIFIER_MODE => false, + ARG_SOURCE_KEY => Key::Account(token_owner), + ARG_TARGET_KEY => Key::Account(to_account_public_key.to_account_hash()), }, ) .build(); builder.exec(transfer_request).expect_success().commit(); let (_, to_other_account_public_key) = support::create_dummy_key_pair(ACCOUNT_USER_3); + let transfer_request = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + TRANSFER_SESSION_WASM, + runtime_args! { + ARG_NFT_CONTRACT_HASH => nft_contract_key, + ARG_TOKEN_ID => 0u64, + ARG_IS_HASH_IDENTIFIER_MODE => false, + ARG_SOURCE_KEY => Key::Account(token_owner), + ARG_TARGET_KEY => Key::Account(to_other_account_public_key.to_account_hash()), + }, + ) + .build(); + builder.exec(transfer_request).expect_failure(); +} + +#[test] +fn should_transfer_between_contract_to_account() { + let mut builder = InMemoryWasmTestBuilder::default(); + builder.run_genesis(&DEFAULT_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_key: Key = minting_contract_hash.into(); + + let contract_whitelist = vec![minting_contract_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::Transferable) + .with_minting_mode(Some(MintingMode::Installer as u8)) + .with_contract_whitelist(contract_whitelist.clone()) + .build(); + + builder.exec(install_request).expect_success().commit(); + + let nft_contract_key: Key = get_nft_contract_hash(&builder).into(); + + let actual_contract_whitelist: Vec = query_stored_value( + &mut builder, + nft_contract_key, + vec![ARG_CONTRACT_WHITELIST.to_string()], + ); + + assert_eq!(actual_contract_whitelist, contract_whitelist); + + let mint_runtime_args = runtime_args! { + ARG_NFT_CONTRACT_HASH => nft_contract_key, + ARG_TOKEN_OWNER => minting_contract_key, + ARG_TOKEN_META_DATA => TEST_PRETTY_721_META_DATA.to_string(), + ARG_TOKEN_URI => TEST_URI.to_string() + }; + + let minting_request = ExecuteRequestBuilder::contract_call_by_hash( + *DEFAULT_ACCOUNT_ADDR, + minting_contract_hash, + ENTRY_POINT_MINT, + mint_runtime_args, + ) + .build(); + + builder.exec(minting_request).expect_success().commit(); + + let token_id = 0u64.to_string(); + + let actual_token_owner: Key = + get_dictionary_value_from_key(&builder, &nft_contract_key, TOKEN_OWNERS, &token_id); + + assert_eq!(minting_contract_key, actual_token_owner); + + let transfer_runtime_arguments = runtime_args! { + ARG_NFT_CONTRACT_HASH => nft_contract_key, + ARG_TOKEN_ID => 0u64, + ARG_TARGET_KEY => Key::Account(*DEFAULT_ACCOUNT_ADDR), + ARG_SOURCE_KEY => minting_contract_key + }; + let transfer_request = ExecuteRequestBuilder::contract_call_by_hash( - operator.to_account_hash(), - nft_contract_hash, + *DEFAULT_ACCOUNT_ADDR, + minting_contract_hash, ENTRY_POINT_TRANSFER, + transfer_runtime_arguments, + ) + .build(); + + builder.exec(transfer_request).expect_success().commit(); + + let updated_token_owner: Key = + get_dictionary_value_from_key(&builder, &nft_contract_key, TOKEN_OWNERS, &token_id); + + assert_eq!(Key::Account(*DEFAULT_ACCOUNT_ADDR), updated_token_owner); +} + +#[test] +fn should_prevent_transfer_when_caller_is_not_owner() { + const ARG_AMOUNT: &str = "amount"; + const ARG_TARGET: &str = "target"; + const ARG_ID: &str = "id"; + const ID_NONE: Option = None; + + let mut builder = InMemoryWasmTestBuilder::default(); + builder.run_genesis(&DEFAULT_RUN_GENESIS_REQUEST).commit(); + + // Create an account that is not the owner of the NFT to transfer the token itself. + let other_account_secret_key = SecretKey::ed25519_from_bytes([9u8; 32]).unwrap(); + let other_account_public_key = PublicKey::from(&other_account_secret_key); + + let other_account_fund_request = ExecuteRequestBuilder::transfer( + *DEFAULT_ACCOUNT_ADDR, runtime_args! { - ARG_TOKEN_ID => U256::zero(), - ARG_FROM_ACCOUNT_HASH => Key::Account(token_owner), - ARG_TO_ACCOUNT_HASH => Key::Account( to_other_account_public_key.to_account_hash()), + ARG_TARGET => other_account_public_key.to_account_hash(), + ARG_AMOUNT => U512::from(MINIMUM_ACCOUNT_CREATION_BALANCE), + ARG_ID => ID_NONE }, ) .build(); - builder.exec(transfer_request).expect_failure(); + + builder + .exec(other_account_fund_request) + .expect_success() + .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(100u64) + .with_ownership_mode(OwnershipMode::Transferable) + .with_holder_mode(NFTHolderMode::Accounts) + .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 mint_session_call = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + MINT_SESSION_WASM, + 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_TOKEN_URI => TEST_URI.to_string() + }, + ) + .build(); + + builder.exec(mint_session_call).expect_success().commit(); + + let actual_token_owner: Key = + get_dictionary_value_from_key(&builder, &nft_contract_key, TOKEN_OWNERS, &0u64.to_string()); + + assert_eq!(Key::Account(*DEFAULT_ACCOUNT_ADDR), actual_token_owner); + + let unauthorized_transfer = ExecuteRequestBuilder::standard( + other_account_public_key.to_account_hash(), + TRANSFER_SESSION_WASM, + runtime_args! { + ARG_NFT_CONTRACT_HASH => nft_contract_key, + ARG_TOKEN_ID => 0u64, + ARG_IS_HASH_IDENTIFIER_MODE => false, + ARG_SOURCE_KEY => Key::Account(*DEFAULT_ACCOUNT_ADDR), + ARG_TARGET_KEY => Key::Account(other_account_public_key.to_account_hash()) + }, + ) + .build(); + + builder.exec(unauthorized_transfer).expect_failure(); + + let error = builder + .get_error() + .expect("previous execution must have failed"); + + assert_expected_error( + error, + 1u16, + "transfer from another account must raise InvalidAccount", + ); +} + +#[test] +fn should_transfer_token_in_hash_identifier_mode() { + let mut builder = InMemoryWasmTestBuilder::default(); + builder.run_genesis(&DEFAULT_RUN_GENESIS_REQUEST).commit(); + + let install_request = InstallerRequestBuilder::new(*DEFAULT_ACCOUNT_ADDR, NFT_CONTRACT_WASM) + .with_identifier_mode(NFTIdentifierMode::Hash) + .with_ownership_mode(OwnershipMode::Transferable) + .with_total_token_supply(10u64) + .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 mint_session_call = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + MINT_SESSION_WASM, + 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 , + ARG_TOKEN_URI => TEST_URI.to_string() + }, + ) + .build(); + + builder.exec(mint_session_call).expect_success().commit(); + + let token_hash: String = + base16::encode_lower(&support::create_blake2b_hash(&TEST_PRETTY_721_META_DATA)); + + let transfer_request = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + TRANSFER_SESSION_WASM, + runtime_args! { + ARG_NFT_CONTRACT_HASH => nft_contract_key, + ARG_IS_HASH_IDENTIFIER_MODE => true, + ARG_TOKEN_HASH => token_hash, + ARG_SOURCE_KEY => Key::Account(*DEFAULT_ACCOUNT_ADDR), + ARG_TARGET_KEY => Key::Account(AccountHash::new([3u8;32])), + }, + ) + .build(); + + builder.exec(transfer_request).expect_success().commit(); } diff --git a/tests/src/utility/constants.rs b/tests/src/utility/constants.rs index 8f1bc4ba..8dad8091 100644 --- a/tests/src/utility/constants.rs +++ b/tests/src/utility/constants.rs @@ -1,7 +1,10 @@ pub(crate) const NFT_CONTRACT_WASM: &str = "contract.wasm"; pub(crate) const MINT_SESSION_WASM: &str = "mint_call.wasm"; pub(crate) const BALANCE_OF_SESSION_WASM: &str = "balance_of_call.wasm"; +pub(crate) const MINTING_CONTRACT_WASM: &str = "minting_contract.wasm"; +pub(crate) const TRANSFER_SESSION_WASM: &str = "transfer_call.wasm"; pub(crate) const CONTRACT_NAME: &str = "nft_contract"; +pub(crate) const MINTING_CONTRACT_NAME: &str = "minting_contract_hash"; pub(crate) const NFT_TEST_COLLECTION: &str = "nft_test"; pub(crate) const NFT_TEST_SYMBOL: &str = "TEST"; pub(crate) const ENTRY_POINT_INIT: &str = "init"; @@ -16,30 +19,58 @@ pub(crate) const ARG_COLLECTION_SYMBOL: &str = "collection_symbol"; pub(crate) const ARG_TOTAL_TOKEN_SUPPLY: &str = "total_token_supply"; pub(crate) const ARG_ALLOW_MINTING: &str = "allow_minting"; pub(crate) const ARG_MINTING_MODE: &str = "minting_mode"; +pub(crate) const ARG_HOLDER_MODE: &str = "holder_mode"; +pub(crate) const ARG_WHITELIST_MODE: &str = "whitelist_mode"; +pub(crate) const ARG_CONTRACT_WHITELIST: &str = "contract_whitelist"; pub(crate) const NUMBER_OF_MINTED_TOKENS: &str = "number_of_minted_tokens"; pub(crate) const ARG_TOKEN_META_DATA: &str = "token_meta_data"; -pub(crate) const TOKEN_META_DATA: &str = "token_meta_data"; +pub(crate) const METADATA_CUSTOM_VALIDATED: &str = "metadata_custom_validated"; +pub(crate) const METADATA_CEP78: &str = "metadata_cep78"; +pub(crate) const METADATA_NFT721: &str = "metadata_nft721"; +pub(crate) const METADATA_RAW: &str = "metadata_raw"; pub(crate) const ARG_TOKEN_OWNER: &str = "token_owner"; pub(crate) const ARG_NFT_CONTRACT_HASH: &str = "nft_contract_hash"; pub(crate) const ARG_JSON_SCHEMA: &str = "json_schema"; pub(crate) const ARG_TOKEN_URI: &str = "token_uri"; pub(crate) const ARG_APPROVE_ALL: &str = "approve_all"; +pub(crate) const ARG_NFT_METADATA_KIND: &str = "nft_metadata_kind"; +pub(crate) const ARG_IDENTIFIER_MODE: &str = "identifier_mode"; pub(crate) const TOKEN_ISSUERS: &str = "token_issuers"; pub(crate) const ARG_OWNERSHIP_MODE: &str = "ownership_mode"; pub(crate) const ARG_NFT_KIND: &str = "nft_kind"; pub(crate) const TOKEN_OWNERS: &str = "token_owners"; pub(crate) const OWNED_TOKENS: &str = "owned_tokens"; pub(crate) const BURNT_TOKENS: &str = "burnt_tokens"; +pub(crate) const TOKEN_COUNTS: &str = "balances"; pub(crate) const OPERATOR: &str = "operator"; pub(crate) const BALANCES: &str = "balances"; +pub(crate) const RECEIPT_NAME: &str = "receipt_name"; pub(crate) const ARG_OPERATOR: &str = "operator"; pub(crate) const OWNED_TOKENS_DICTIONARY_KEY: &str = "owned_tokens_dictionary_key"; pub(crate) const ARG_KEY_NAME: &str = "key_name"; -pub(crate) const ARG_TO_ACCOUNT_HASH: &str = "to_account_hash"; -pub(crate) const ARG_FROM_ACCOUNT_HASH: &str = "from_account_hash"; +pub(crate) const ARG_TARGET_KEY: &str = "target_key"; +pub(crate) const ARG_SOURCE_KEY: &str = "source_key"; pub(crate) const ARG_TOKEN_ID: &str = "token_id"; +pub(crate) const ARG_TOKEN_HASH: &str = "token_hash"; +pub(crate) const ARG_IS_HASH_IDENTIFIER_MODE: &str = "is_hash_identifier_mode"; pub(crate) const ACCOUNT_USER_1: [u8; 32] = [1u8; 32]; pub(crate) const ACCOUNT_USER_2: [u8; 32] = [2u8; 32]; pub(crate) const ACCOUNT_USER_3: [u8; 32] = [2u8; 32]; -pub(crate) const TEST_META_DATA: &str = "test meta"; +pub(crate) const TEST_PRETTY_721_META_DATA: &str = r#"{ + "name": "John Doe", + "symbol": "abc", + "token_uri": "https://www.google.com" +}"#; +pub(crate) const TEST_PRETTY_CEP78_METADATA: &str = r#"{ + "name": "John Doe", + "token_uri": "https://www.google.com", + "checksum": "940bffb3f2bba35f84313aa26da09ece3ad47045c6a1292c2bbd2df4ab1a55fb" +}"#; +pub(crate) const TEST_COMPACT_META_DATA: &str = + r#"{"name": "John Doe","symbol": "abc","token_uri": "https://www.google.com"}"#; pub(crate) const TEST_URI: &str = "www.google.com"; +pub(crate) const MALFORMED_META_DATA: &str = r#"{ + "name": "John Doe", + "symbol": abc, + "token_uri": "https://www.google.com" +}"#; diff --git a/tests/src/utility/installer_request_builder.rs b/tests/src/utility/installer_request_builder.rs index c4fc248e..858ee3f0 100644 --- a/tests/src/utility/installer_request_builder.rs +++ b/tests/src/utility/installer_request_builder.rs @@ -1,12 +1,63 @@ +use std::collections::BTreeMap; + +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; + use casper_engine_test_support::ExecuteRequestBuilder; use casper_execution_engine::core::engine_state::ExecuteRequest; -use casper_types::{account::AccountHash, CLValue, RuntimeArgs, U256}; +use casper_types::{account::AccountHash, CLValue, ContractHash, RuntimeArgs}; + +use crate::utility::constants::{ + ARG_CONTRACT_WHITELIST, ARG_HOLDER_MODE, ARG_IDENTIFIER_MODE, ARG_WHITELIST_MODE, +}; use super::constants::{ ARG_ALLOW_MINTING, ARG_COLLECTION_NAME, ARG_COLLECTION_SYMBOL, ARG_JSON_SCHEMA, - ARG_MINTING_MODE, ARG_NFT_KIND, ARG_OWNERSHIP_MODE, ARG_TOTAL_TOKEN_SUPPLY, + ARG_MINTING_MODE, ARG_NFT_KIND, ARG_NFT_METADATA_KIND, ARG_OWNERSHIP_MODE, + ARG_TOTAL_TOKEN_SUPPLY, }; +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 } +}); + +pub(crate) 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 +}); + +#[repr(u8)] +pub enum WhitelistMode { + Unlocked = 0, + Locked = 1, +} + +#[repr(u8)] +pub enum NFTHolderMode { + Accounts = 0, + Contracts = 1, + Mixed = 2, +} + #[repr(u8)] pub enum MintingMode { /// The ability to mint NFTs is restricted to the installing account only. @@ -32,6 +83,39 @@ pub enum NFTKind { Virtual = 2, // The NFT can be transferred even to an recipient that does not exist } +#[derive(Serialize, Deserialize, Clone)] +pub(crate) struct MetadataSchemaProperty { + name: String, + description: String, + required: bool, +} + +#[derive(Serialize, Deserialize, Clone)] +pub(crate) struct CustomMetadataSchema { + properties: BTreeMap, +} + +#[derive(Serialize, Deserialize)] +struct Metadata { + name: String, + symbol: String, + token_uri: String, +} + +#[repr(u8)] +pub enum NFTMetadataKind { + CEP99 = 0, + NFT721 = 1, + Raw = 2, + CustomValidated = 3, +} + +#[repr(u8)] +pub enum NFTIdentifierMode { + Ordinal = 0, + Hash = 1, +} + #[derive(Debug)] pub(crate) struct InstallerRequestBuilder { account_hash: AccountHash, @@ -43,7 +127,12 @@ pub(crate) struct InstallerRequestBuilder { minting_mode: CLValue, ownership_mode: CLValue, nft_kind: CLValue, + holder_mode: CLValue, + whitelist_mode: CLValue, + contract_whitelist: CLValue, json_schema: CLValue, + nft_metadata_kind: CLValue, + identifier_mode: CLValue, } impl InstallerRequestBuilder { @@ -59,14 +148,18 @@ impl InstallerRequestBuilder { session_file: String::default(), collection_name: CLValue::from_t("name".to_string()).expect("name is legit CLValue"), collection_symbol: CLValue::from_t("SYM").expect("collection_symbol is legit CLValue"), - total_token_supply: CLValue::from_t(U256::one()) - .expect("total_token_supply is legit CLValue"), + total_token_supply: CLValue::from_t(1u64).expect("total_token_supply is legit CLValue"), allow_minting: CLValue::from_t(Some(true)).unwrap(), minting_mode: CLValue::from_t(Some(MintingMode::Installer as u8)).unwrap(), ownership_mode: CLValue::from_t(OwnershipMode::Minter as u8).unwrap(), nft_kind: CLValue::from_t(NFTKind::Physical as u8).unwrap(), - json_schema: CLValue::from_t("my_json_schema".to_string()) - .expect("my_json_schema is legit CLValue"), + holder_mode: CLValue::from_t(Some(NFTHolderMode::Mixed as u8)).unwrap(), + whitelist_mode: CLValue::from_t(Some(WhitelistMode::Locked as u8)).unwrap(), + contract_whitelist: CLValue::from_t(Some(Vec::::new())).unwrap(), + json_schema: CLValue::from_t("test".to_string()) + .expect("test_metadata was created from a concrete value"), + nft_metadata_kind: CLValue::from_t(NFTMetadataKind::NFT721 as u8).unwrap(), + identifier_mode: CLValue::from_t(NFTIdentifierMode::Ordinal as u8).unwrap(), } } @@ -102,7 +195,7 @@ impl InstallerRequestBuilder { self } - pub(crate) fn with_total_token_supply(mut self, total_token_supply: U256) -> Self { + pub(crate) fn with_total_token_supply(mut self, total_token_supply: u64) -> Self { self.total_token_supply = CLValue::from_t(total_token_supply).expect("total_token_supply is legit CLValue"); self @@ -130,11 +223,36 @@ impl InstallerRequestBuilder { self } - pub(crate) fn _with_json_schema(mut self, json_schema: &str) -> Self { + pub(crate) fn with_holder_mode(mut self, holder_mode: NFTHolderMode) -> Self { + self.holder_mode = CLValue::from_t(Some(holder_mode as u8)).unwrap(); + self + } + + pub(crate) fn with_whitelist_mode(mut self, whitelist_mode: WhitelistMode) -> Self { + self.whitelist_mode = CLValue::from_t(Some(whitelist_mode as u8)).unwrap(); + self + } + + pub(crate) fn with_contract_whitelist(mut self, contract_whitelist: Vec) -> Self { + self.contract_whitelist = CLValue::from_t(Some(contract_whitelist)).unwrap(); + self + } + + pub(crate) fn with_nft_metadata_kind(mut self, nft_metadata_kind: NFTMetadataKind) -> Self { + self.nft_metadata_kind = CLValue::from_t(nft_metadata_kind as u8).unwrap(); + self + } + + pub(crate) fn with_json_schema(mut self, json_schema: String) -> Self { self.json_schema = CLValue::from_t(json_schema).expect("json_schema is legit CLValue"); self } + pub(crate) fn with_identifier_mode(mut self, identifier_mode: NFTIdentifierMode) -> Self { + self.identifier_mode = CLValue::from_t(identifier_mode as u8).unwrap(); + self + } + pub(crate) fn build(self) -> ExecuteRequest { let mut runtime_args = RuntimeArgs::new(); runtime_args.insert_cl_value(ARG_COLLECTION_NAME, self.collection_name); @@ -144,7 +262,12 @@ impl InstallerRequestBuilder { runtime_args.insert_cl_value(ARG_MINTING_MODE, self.minting_mode.clone()); runtime_args.insert_cl_value(ARG_OWNERSHIP_MODE, self.ownership_mode); runtime_args.insert_cl_value(ARG_NFT_KIND, self.nft_kind); + runtime_args.insert_cl_value(ARG_HOLDER_MODE, self.holder_mode); + runtime_args.insert_cl_value(ARG_WHITELIST_MODE, self.whitelist_mode); + runtime_args.insert_cl_value(ARG_CONTRACT_WHITELIST, self.contract_whitelist); runtime_args.insert_cl_value(ARG_JSON_SCHEMA, self.json_schema); + runtime_args.insert_cl_value(ARG_NFT_METADATA_KIND, self.nft_metadata_kind); + runtime_args.insert_cl_value(ARG_IDENTIFIER_MODE, self.identifier_mode); ExecuteRequestBuilder::standard(self.account_hash, &self.session_file, runtime_args).build() } } diff --git a/tests/src/utility/support.rs b/tests/src/utility/support.rs index 7da47180..3c00b822 100644 --- a/tests/src/utility/support.rs +++ b/tests/src/utility/support.rs @@ -1,4 +1,8 @@ -use crate::utility::constants::{ARG_KEY_NAME, ARG_NFT_CONTRACT_HASH}; +use crate::utility::constants::{ARG_KEY_NAME, ARG_NFT_CONTRACT_HASH, MINTING_CONTRACT_NAME}; +use blake2::{ + digest::{Update, VariableOutput}, + VarBlake2b, +}; use super::{constants::CONTRACT_NAME, installer_request_builder::InstallerRequestBuilder}; use casper_engine_test_support::{ @@ -11,7 +15,7 @@ use casper_execution_engine::{ }; use casper_types::{ account::AccountHash, bytesrepr::FromBytes, ApiError, CLTyped, ContractHash, Key, PublicKey, - RuntimeArgs, SecretKey, URef, + RuntimeArgs, SecretKey, URef, BLAKE2B_DIGEST_LENGTH, }; pub(crate) fn get_nft_contract_hash( @@ -28,6 +32,20 @@ pub(crate) fn get_nft_contract_hash( ContractHash::new(nft_hash_addr) } +pub(crate) fn get_minting_contract_hash( + builder: &WasmTestBuilder, +) -> ContractHash { + let minting_contract_hash = builder + .get_expected_account(*DEFAULT_ACCOUNT_ADDR) + .named_keys() + .get(MINTING_CONTRACT_NAME) + .expect("must have minting contract hash entry in named keys") + .into_hash() + .expect("must get hash_addr"); + + ContractHash::new(minting_contract_hash) +} + pub(crate) fn get_dictionary_value_from_key( builder: &WasmTestBuilder, nft_contract_key: &Key, @@ -120,13 +138,13 @@ pub(crate) fn query_stored_value( pub(crate) fn call_entry_point_with_ret( builder: &mut InMemoryWasmTestBuilder, account_hash: AccountHash, - nft_contract_hash: ContractHash, + nft_contract_key: Key, mut runtime_args: RuntimeArgs, wasm_file_name: &str, key_name: &str, ) -> T { runtime_args - .insert(ARG_NFT_CONTRACT_HASH, nft_contract_hash) + .insert(ARG_NFT_CONTRACT_HASH, nft_contract_key) .unwrap(); runtime_args @@ -141,3 +159,15 @@ pub(crate) fn call_entry_point_with_ret( println!("Querying: {}", key_name); query_stored_value::(builder, account_hash.into(), [key_name.to_string()].into()) } + +pub(crate) fn create_blake2b_hash>(data: T) -> [u8; BLAKE2B_DIGEST_LENGTH] { + let mut result = [0; BLAKE2B_DIGEST_LENGTH]; + // NOTE: Assumed safe as `BLAKE2B_DIGEST_LENGTH` is a valid value for a hasher + let mut hasher = VarBlake2b::new(BLAKE2B_DIGEST_LENGTH).expect("should create hasher"); + + hasher.update(data); + hasher.finalize_variable(|slice| { + result.copy_from_slice(slice); + }); + result +}