diff --git a/Cargo.lock b/Cargo.lock index 36f954a..91a01e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1266,12 +1266,25 @@ dependencies = [ "cw721-base 0.18.0 (git+https://github.com/public-awesome/cw-nfts?branch=release/v0.18.0)", "ics721", "ics721-types 0.1.0", + "sg-metadata", "sg-std", "sg721", "sg721-base", + "sg721-metadata-onchain", "sha2 0.10.8", ] +[[package]] +name = "sg-metadata" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79c457d59b63bc9bf33c08eab50dd857af54d89e2f7098ea2a7eef6cd082e95" +dependencies = [ + "cosmwasm-schema", + "schemars", + "serde", +] + [[package]] name = "sg-std" version = "3.2.0" @@ -1323,6 +1336,25 @@ dependencies = [ "url", ] +[[package]] +name = "sg721-metadata-onchain" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b5bb48d339e3d75cec1ad4dcd2e80ecd2175b349aafb63974517d383162d686" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-ownable 0.5.1", + "cw2 1.1.2", + "cw721-base 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", + "schemars", + "serde", + "sg-metadata", + "sg-std", + "sg721", + "sg721-base", +] + [[package]] name = "sha2" version = "0.9.9" diff --git a/Cargo.toml b/Cargo.toml index 15be996..dafe07d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,10 +34,12 @@ sha2 = "^0.10" serde = "^1.0" thiserror = "^1.0" # Stargaze libs +sg-metadata = "^3.14" sg-std = "^3.2" sg-multi-test = "^3.1" sg721 = "^3.3" -sg721-base = "^3.3" +sg721-base = "^3.14" +sg721-metadata-onchain = "^3.14" # packages and contracts cw-cii = { path = "./packages/cw-cii" } cw-pause-once = { path = "./packages/cw-pause-once" } diff --git a/contracts/sg-ics721/Cargo.toml b/contracts/sg-ics721/Cargo.toml index 409bf53..3681154 100644 --- a/contracts/sg-ics721/Cargo.toml +++ b/contracts/sg-ics721/Cargo.toml @@ -19,9 +19,11 @@ cw2 = { workspace = true } cw721 = { workspace = true } ics721 = { workspace = true } ics721-types = { workspace = true } +sg-metadata = { workspace = true} sg-std = { workspace = true} sg721 = { workspace = true } sg721-base = { workspace = true, features = ["library"] } +sg721-metadata-onchain = { workspace = true, features = ["library"] } [dev-dependencies] anyhow = { workspace = true } diff --git a/contracts/sg-ics721/src/execute.rs b/contracts/sg-ics721/src/execute.rs index ff4f90c..61ebceb 100644 --- a/contracts/sg-ics721/src/execute.rs +++ b/contracts/sg-ics721/src/execute.rs @@ -1,12 +1,14 @@ use cosmwasm_std::{ from_json, to_json_binary, Addr, Binary, ContractInfoResponse, Deps, DepsMut, Env, StdResult, }; -use cw721::{CollectionExtension, RoyaltyInfo}; +use cw721::{CollectionExtension, NftExtension, RoyaltyInfo}; use ics721::{execute::Ics721Execute, state::CollectionData, utils::get_collection_data}; use ics721_types::token_types::Class; use sg721::RoyaltyInfoResponse; -use sg721_base::msg::{CollectionInfoResponse, QueryMsg}; +use sg721_base::msg::CollectionInfoResponse; +use sg721_metadata_onchain::QueryMsg; +use sg_metadata::{Metadata, Trait}; use crate::state::{SgIcs721Contract, STARGAZE_ICON_PLACEHOLDER}; @@ -113,4 +115,49 @@ impl Ics721Execute for SgIcs721Contract { to_json_binary(&instantiate_msg) } + + fn mint_msg( + &self, + token_id: String, + token_uri: Option, + owner: String, + data: Option, + ) -> StdResult { + // parse token data and check whether it is of type NftExtension + let extension = data + .and_then(|binary| { + from_json::(binary).ok().map(|ext| Metadata { + animation_url: ext.animation_url, + attributes: ext.attributes.map(|traits| { + traits + .into_iter() + .map(|t| Trait { + trait_type: t.trait_type, + value: t.value, + display_type: t.display_type, + }) + .collect() + }), + background_color: ext.background_color, + description: ext.description, + external_url: ext.external_url, + image: ext.image, + image_data: ext.image_data, + youtube_url: ext.youtube_url, + name: ext.name, + }) + }) + .unwrap_or(Metadata { + // no onchain metadata (only offchain), in this case empty metadata is created + ..Default::default() + }); + + let msg = sg721_metadata_onchain::ExecuteMsg::Mint { + token_id, + token_uri, // holds off-chain metadata + owner, + extension, // holds on-chain metadata + }; + to_json_binary(&msg) + } } diff --git a/contracts/sg-ics721/src/testing/integration_tests.rs b/contracts/sg-ics721/src/testing/integration_tests.rs index 73e1880..d53c501 100644 --- a/contracts/sg-ics721/src/testing/integration_tests.rs +++ b/contracts/sg-ics721/src/testing/integration_tests.rs @@ -8,7 +8,7 @@ use cosmwasm_std::{ VerificationError, WasmMsg, }; use cw2::set_contract_version; -use cw721::{CollectionExtension, RoyaltyInfo}; +use cw721::{CollectionExtension, DefaultOptionalNftExtension, NftExtension, RoyaltyInfo}; use cw721_base_018::msg::QueryMsg as Cw721QueryMsg; use cw_cii::{Admin, ContractInstantiateInfo}; use cw_multi_test::{ @@ -29,7 +29,8 @@ use ics721_types::{ token_types::{Class, ClassId, Token, TokenId}, }; use sg721::{InstantiateMsg as Sg721InstantiateMsg, RoyaltyInfoResponse}; -use sg721_base::msg::{CollectionInfoResponse, QueryMsg as Sg721QueryMsg}; +use sg721_base::msg::CollectionInfoResponse; +use sg721_metadata_onchain::QueryMsg as Sg721QueryMsg; use sha2::{digest::Update, Digest, Sha256}; use crate::{state::STARGAZE_ICON_PLACEHOLDER, ContractError, SgIcs721Contract}; @@ -361,7 +362,7 @@ impl Test { ) .unwrap(); - // minter of sg721-base must be a contract! + // minter of sg721-metadata-onchain must be a contract! let source_cw721_owner = ics721.clone(); let source_cw721 = app .instantiate_contract( @@ -533,27 +534,34 @@ impl Test { } } -fn sg721_base_contract() -> Box> { - // sg721_base's execute and instantiate function deals Response +fn sg721_metadata_onchain_contract() -> Box> { + // sg721_metadata_onchain's execute and instantiate function deals Response // but App multi test deals Response - // so we need to wrap sg721_base's execute and instantiate function + // so we need to wrap sg721_metadata_onchain's execute and instantiate function fn exececute_fn( deps: DepsMut, env: Env, info: MessageInfo, - msg: sg721::ExecuteMsg, Empty>, - ) -> Result { - sg721_base::entry::execute(deps, env, info, msg).map(|_| Response::default()) + msg: sg721_metadata_onchain::ExecuteMsg, + ) -> Result { + sg721_metadata_onchain::Sg721MetadataContract::default() + .execute(deps, env, info, msg) + .map(|_| Response::default()) } fn instantiate_fn( deps: DepsMut, env: Env, info: MessageInfo, msg: sg721::InstantiateMsg, - ) -> Result { - sg721_base::entry::instantiate(deps, env, info, msg).map(|_| Response::default()) + ) -> Result { + sg721_metadata_onchain::Sg721MetadataContract::default() + .instantiate(deps, env, info, msg) + .map(|_| Response::default()) } - let contract = ContractWrapper::new(exececute_fn, instantiate_fn, sg721_base::entry::query); + fn query_fn(deps: Deps, env: Env, msg: sg721_metadata_onchain::QueryMsg) -> StdResult { + sg721_metadata_onchain::Sg721MetadataContract::default().query(deps, env, msg) + } + let contract = ContractWrapper::new(exececute_fn, instantiate_fn, query_fn); Box::new(contract) } @@ -589,7 +597,7 @@ fn outgoing_proxy_contract() -> Box> { #[test] fn test_instantiate() { - let mut test = Test::new(true, true, None, None, sg721_base_contract()); + let mut test = Test::new(true, true, None, None, sg721_metadata_onchain_contract()); // check stores are properly initialized let cw721_id = test.query_cw721_id(); @@ -608,7 +616,7 @@ fn test_instantiate() { #[test] fn test_do_instantiate_and_mint_weird_data() { - let mut test = Test::new(false, false, None, None, sg721_base_contract()); + let mut test = Test::new(false, false, None, None, sg721_metadata_onchain_contract()); let collection_contract_source_chain = ClassId::new(test.app.api().addr_make(COLLECTION_CONTRACT_SOURCE_CHAIN)); let class_id = format!( @@ -675,7 +683,7 @@ fn test_do_instantiate_and_mint_weird_data() { fn test_do_instantiate_and_mint() { // test case: instantiate cw721 with no ClassData (without owner, name, and symbol) { - let mut test = Test::new(false, false, None, None, sg721_base_contract()); + let mut test = Test::new(false, false, None, None, sg721_metadata_onchain_contract()); let collection_contract_source_chain = ClassId::new(test.app.api().addr_make(COLLECTION_CONTRACT_SOURCE_CHAIN)); let class_id = format!( @@ -698,7 +706,16 @@ fn test_do_instantiate_and_mint() { Token { id: TokenId::new("1"), uri: Some("https://moonphase.is/image.svg".to_string()), - data: None, + data: Some(to_json_binary(&NftExtension { + image: Some("https://ark.pass/image.png".to_string()), + external_url: Some( + "https://interchain.arkprotocol.io".to_string(), + ), + description: Some("description".to_string()), + ..Default::default() + })) + .transpose() + .unwrap(), }, Token { id: TokenId::new("2"), @@ -765,8 +782,8 @@ fn test_do_instantiate_and_mint() { } ); - // Check that token_uri was set properly. - let token_info: cw721_018::NftInfoResponse> = test + // Check that token_uri and extension was set properly. + let token_info: cw721::msg::NftInfoResponse = test .app .wrap() .query_wasm_smart( @@ -780,7 +797,16 @@ fn test_do_instantiate_and_mint() { token_info.token_uri, Some("https://moonphase.is/image.svg".to_string()) ); - let token_info: cw721_018::NftInfoResponse> = test + assert_eq!( + token_info.extension, + Some(NftExtension { + image: Some("https://ark.pass/image.png".to_string()), + external_url: Some("https://interchain.arkprotocol.io".to_string()), + description: Some("description".to_string()), + ..Default::default() + }) + ); + let token_info: cw721::msg::NftInfoResponse = test .app .wrap() .query_wasm_smart( @@ -791,6 +817,12 @@ fn test_do_instantiate_and_mint() { ) .unwrap(); assert_eq!(token_info.token_uri, Some("https://foo.bar".to_string())); + assert_eq!( + token_info.extension, + Some(NftExtension { + ..Default::default() + }) + ); // After transfer to target, test owner can do any action, like transfer, on collection test.app @@ -840,7 +872,7 @@ fn test_do_instantiate_and_mint() { false, None, Some(COLLECTION_OWNER_SOURCE_CHAIN.to_string()), // admin is used for royalty payment address! - sg721_base_contract(), + sg721_metadata_onchain_contract(), ); let collection_contract_source_chain = ClassId::new(test.app.api().addr_make(COLLECTION_CONTRACT_SOURCE_CHAIN)); @@ -1035,7 +1067,7 @@ fn test_do_instantiate_and_mint() { // test case: instantiate cw721 with CustomClassData (includes name, but without owner and symbol) // results in nft contract using class id for name and symbol { - let mut test = Test::new(false, false, None, None, sg721_base_contract()); + let mut test = Test::new(false, false, None, None, sg721_metadata_onchain_contract()); let collection_contract_source_chain = ClassId::new(test.app.api().addr_make(COLLECTION_CONTRACT_SOURCE_CHAIN)); let class_id = format!( @@ -1210,7 +1242,7 @@ fn test_do_instantiate_and_mint() { // test case: instantiate cw721 with PartialCustomCollectionData (includes name and symbol) // results in nft contract using name and symbol { - let mut test = Test::new(false, false, None, None, sg721_base_contract()); + let mut test = Test::new(false, false, None, None, sg721_metadata_onchain_contract()); let collection_contract_source_chain = ClassId::new(test.app.api().addr_make(COLLECTION_CONTRACT_SOURCE_CHAIN)); let class_id = format!( @@ -1388,7 +1420,7 @@ fn test_do_instantiate_and_mint() { // test case: instantiate cw721 with PartialCustomCollectionData (includes name and symbol) // results in nft contract using name and symbol { - let mut test = Test::new(false, false, None, None, sg721_base_contract()); + let mut test = Test::new(false, false, None, None, sg721_metadata_onchain_contract()); let collection_contract_source_chain = ClassId::new(test.app.api().addr_make(COLLECTION_CONTRACT_SOURCE_CHAIN)); let class_id = format!( @@ -1569,7 +1601,7 @@ fn test_do_instantiate_and_mint() { fn test_do_instantiate_and_mint_2_different_collections() { // test case: instantiate two cw721 contracts with different class id and make sure instantiate2 creates 2 different, predictable contracts { - let mut test = Test::new(false, false, None, None, sg721_base_contract()); + let mut test = Test::new(false, false, None, None, sg721_metadata_onchain_contract()); let collection_contract_source_chain_1 = ClassId::new(test.app.api().addr_make(COLLECTION_CONTRACT_SOURCE_CHAIN)); let class_id_1 = format!( @@ -1879,7 +1911,7 @@ fn test_do_instantiate_and_mint_no_instantiate() { false, None, Some(COLLECTION_OWNER_SOURCE_CHAIN.to_string()), // admin is used for royalty payment address! - sg721_base_contract(), + sg721_metadata_onchain_contract(), ); let collection_contract_source_chain = ClassId::new(test.app.api().addr_make(COLLECTION_CONTRACT_SOURCE_CHAIN)); @@ -2027,7 +2059,7 @@ fn test_do_instantiate_and_mint_no_instantiate() { #[test] fn test_do_instantiate_and_mint_permissions() { - let mut test = Test::new(false, false, None, None, sg721_base_contract()); + let mut test = Test::new(false, false, None, None, sg721_metadata_onchain_contract()); let collection_contract_source_chain = ClassId::new(test.app.api().addr_make(COLLECTION_CONTRACT_SOURCE_CHAIN)); let class_id = format!( @@ -2096,7 +2128,7 @@ fn test_do_instantiate_and_mint_permissions() { /// Tests that we can not send IbcOutgoingProxyMsg if no proxy is configured. #[test] fn test_no_proxy_unknown_msg() { - let mut test = Test::new(false, false, None, None, sg721_base_contract()); + let mut test = Test::new(false, false, None, None, sg721_metadata_onchain_contract()); let msg = IbcOutgoingProxyMsg { collection: "foo".to_string(), msg: to_json_binary(&IbcOutgoingMsg { @@ -2134,7 +2166,7 @@ fn test_no_proxy_unknown_msg() { /// Tests that we can non-proxy addresses can send if proxy is configured. #[test] fn test_no_proxy_unauthorized() { - let mut test = Test::new(true, false, None, None, sg721_base_contract()); + let mut test = Test::new(true, false, None, None, sg721_metadata_onchain_contract()); let msg = IbcOutgoingProxyMsg { collection: "foo".to_string(), msg: to_json_binary(&IbcOutgoingMsg { @@ -2168,7 +2200,7 @@ fn test_no_proxy_unauthorized() { #[test] fn test_proxy_authorized() { - let mut test = Test::new(true, false, None, None, sg721_base_contract()); + let mut test = Test::new(true, false, None, None, sg721_metadata_onchain_contract()); let proxy_address: Option = test .app .wrap() @@ -2178,7 +2210,7 @@ fn test_proxy_authorized() { let proxy_address = proxy_address.expect("expected a proxy"); // create collection and mint NFT for sending to proxy - let source_cw721_id = test.app.store_code(sg721_base_contract()); + let source_cw721_id = test.app.store_code(sg721_metadata_onchain_contract()); let source_cw721 = test .app .instantiate_contract( @@ -2263,7 +2295,7 @@ fn test_proxy_authorized() { #[test] fn test_receive_nft() { - let mut test = Test::new(false, false, None, None, sg721_base_contract()); + let mut test = Test::new(false, false, None, None, sg721_metadata_onchain_contract()); // simplify: mint and escrowed/owned by ics721, as a precondition for receive nft let token_id = test.execute_cw721_mint(test.ics721.clone()).unwrap(); // ics721 receives NFT from sender/collection contract, @@ -2344,7 +2376,7 @@ fn test_receive_nft() { /// In case proxy for ICS721 is defined, ICS721 only accepts receival from proxy - not from nft contract! #[test] fn test_no_receive_with_proxy() { - let mut test = Test::new(true, false, None, None, sg721_base_contract()); + let mut test = Test::new(true, false, None, None, sg721_metadata_onchain_contract()); // unauthorized to receive nft from nft contract let err: ContractError = test .app @@ -2381,7 +2413,7 @@ fn test_pause() { false, None, Some(ICS721_ADMIN_AND_PAUSER.to_string()), - sg721_base_contract(), + sg721_metadata_onchain_contract(), ); // Should start unpaused. let (paused, pauser) = test.query_pause_info(); @@ -2468,7 +2500,7 @@ fn test_migration() { false, None, Some(ICS721_ADMIN_AND_PAUSER.to_string()), - sg721_base_contract(), + sg721_metadata_onchain_contract(), ); // assert instantiation worked let (_, pauser) = test.query_pause_info(); diff --git a/packages/ics721/src/execute.rs b/packages/ics721/src/execute.rs index 3b5dadf..c704186 100644 --- a/packages/ics721/src/execute.rs +++ b/packages/ics721/src/execute.rs @@ -727,33 +727,9 @@ where &data, )?; - // parse token data and check whether it is of type NftExtension - let extension: Option = match data { - Some(data) => from_json::(data) - .ok() - .map(|ext| NftExtensionMsg { - animation_url: ext.animation_url, - attributes: ext.attributes, - background_color: ext.background_color, - description: ext.description, - external_url: ext.external_url, - image: ext.image, - image_data: ext.image_data, - youtube_url: ext.youtube_url, - name: ext.name, - }), - None => None, - }; - - let msg = cw721_metadata_onchain::msg::ExecuteMsg::Mint { - token_id: id.into(), - token_uri: uri, - owner: receiver.to_string(), - extension, - }; Ok(WasmMsg::Execute { contract_addr: nft_contract.to_string(), - msg: to_json_binary(&msg)?, + msg: self.mint_msg(id.into(), uri, receiver.to_string(), data)?, funds: vec![], }) }) @@ -764,6 +740,39 @@ where .add_messages(mint)) } + fn mint_msg( + &self, + token_id: String, + token_uri: Option, + owner: String, + data: Option, + ) -> StdResult { + // parse token data and check whether it is of type NftExtension + let extension = data.and_then(|binary| { + from_json::(binary) + .ok() + .map(|ext| NftExtensionMsg { + animation_url: ext.animation_url, + attributes: ext.attributes, + background_color: ext.background_color, + description: ext.description, + external_url: ext.external_url, + image: ext.image, + image_data: ext.image_data, + youtube_url: ext.youtube_url, + name: ext.name, + }) + }); + + let msg = cw721_metadata_onchain::msg::ExecuteMsg::Mint { + token_id, + token_uri, // holds off-chain metadata + owner, + extension, // holds on-chain metadata + }; + to_json_binary(&msg) + } + fn callback_redeem_outgoing_channel_entries( &self, deps: DepsMut, diff --git a/packages/ics721/src/testing/integration_tests.rs b/packages/ics721/src/testing/integration_tests.rs index 5a78433..59ea9e8 100644 --- a/packages/ics721/src/testing/integration_tests.rs +++ b/packages/ics721/src/testing/integration_tests.rs @@ -11,7 +11,7 @@ use cw2::set_contract_version; use cw721::{ msg::{CollectionExtensionMsg, CollectionInfoAndExtensionResponse, RoyaltyInfoResponse}, CollectionExtension, DefaultOptionalCollectionExtension, DefaultOptionalNftExtension, - RoyaltyInfo, + NftExtension, RoyaltyInfo, }; use cw721_metadata_onchain::msg::{ InstantiateMsg as Cw721InstantiateMsg, QueryMsg as Cw721QueryMsg, @@ -734,7 +734,16 @@ fn test_do_instantiate_and_mint() { Token { id: TokenId::new("1"), uri: Some("https://moonphase.is/image.svg".to_string()), - data: None, + data: Some(to_json_binary(&NftExtension { + image: Some("https://ark.pass/image.png".to_string()), + external_url: Some( + "https://interchain.arkprotocol.io".to_string(), + ), + description: Some("description".to_string()), + ..Default::default() + })) + .transpose() + .unwrap(), }, Token { id: TokenId::new("2"), @@ -782,7 +791,7 @@ fn test_do_instantiate_and_mint() { } ); - // Check that token_uri was set properly. + // Check that token_uri and extension was set properly. let token_info: cw721::msg::NftInfoResponse = test .app .wrap() @@ -797,6 +806,15 @@ fn test_do_instantiate_and_mint() { token_info.token_uri, Some("https://moonphase.is/image.svg".to_string()) ); + assert_eq!( + token_info.extension, + Some(NftExtension { + image: Some("https://ark.pass/image.png".to_string()), + external_url: Some("https://interchain.arkprotocol.io".to_string()), + description: Some("description".to_string()), + ..Default::default() + }) + ); let token_info: cw721::msg::NftInfoResponse = test .app .wrap() @@ -808,6 +826,7 @@ fn test_do_instantiate_and_mint() { ) .unwrap(); assert_eq!(token_info.token_uri, Some("https://foo.bar".to_string())); + assert_eq!(token_info.extension, None); // After transfer to target, test owner can do any action, like transfer, on collection test.app