From 2da4752c30a6735c0bf228dfad2594902871e7b5 Mon Sep 17 00:00:00 2001 From: mr-t Date: Thu, 1 Feb 2024 22:28:32 +0100 Subject: [PATCH 01/11] rename --- packages/ics721/src/error.rs | 2 +- packages/ics721/src/execute.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ics721/src/error.rs b/packages/ics721/src/error.rs index 23604e12..94ff26dd 100644 --- a/packages/ics721/src/error.rs +++ b/packages/ics721/src/error.rs @@ -59,7 +59,7 @@ pub enum ContractError { NoNftContractForClassId(String), #[error("Unknown nft contract: {child_collection}, Class Id: {class_id}, Token ID: {token_id} => NFT contract: {cw721_addr}")] - NoClassIdForNftContract { + NoNftContractMatch { child_collection: String, class_id: String, token_id: String, diff --git a/packages/ics721/src/execute.rs b/packages/ics721/src/execute.rs index 31a789a5..c5d6500c 100644 --- a/packages/ics721/src/execute.rs +++ b/packages/ics721/src/execute.rs @@ -143,7 +143,7 @@ where let child_collection = deps.api.addr_validate(&child_collection)?; let cw721_addr = CLASS_ID_TO_NFT_CONTRACT.load(deps.storage, child_class_id.clone())?; if cw721_addr != child_collection { - return Err(ContractError::NoClassIdForNftContract { + return Err(ContractError::NoNftContractMatch { child_collection: child_collection.to_string(), class_id: child_class_id.to_string(), token_id: token_id.into(), @@ -218,7 +218,7 @@ where let home_collection = deps.api.addr_validate(&home_collection)?; let cw721_addr = CLASS_ID_TO_NFT_CONTRACT.load(deps.storage, home_class_id.clone())?; if cw721_addr != home_collection { - return Err(ContractError::NoClassIdForNftContract { + return Err(ContractError::NoNftContractMatch { child_collection: home_collection.to_string(), class_id: home_class_id.to_string(), token_id, From 5059fc0e6831a6005fb7dcc0ee80295f46904984 Mon Sep 17 00:00:00 2001 From: mr-t Date: Mon, 22 Jan 2024 19:45:12 +0100 Subject: [PATCH 02/11] pass nft contract, instead of overriding info.sender --- packages/ics721/src/execute.rs | 43 +++++++++++-------------- packages/ics721/src/testing/contract.rs | 8 ++--- 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/packages/ics721/src/execute.rs b/packages/ics721/src/execute.rs index c5d6500c..9f94a49b 100644 --- a/packages/ics721/src/execute.rs +++ b/packages/ics721/src/execute.rs @@ -297,30 +297,23 @@ where } from_json::(msg.clone()) .ok() - .map(|msg| { - let mut info = info; - match deps.api.addr_validate(&msg.collection) { - Ok(collection_addr) => { - // set collection address as (initial) sender - info.sender = collection_addr; - self.receive_nft( - deps, - env, - info, - TokenId::new(token_id), - nft_owner, - msg.msg, - ) - } - Err(err) => Err(ContractError::Std(err)), - } + .map(|msg| match deps.api.addr_validate(&msg.collection) { + Ok(nft_contract) => self.receive_nft( + deps, + env, + &nft_contract, + TokenId::new(token_id), + nft_owner, + msg.msg, + ), + Err(err) => Err(ContractError::Std(err)), }) } None => from_json::(msg.clone()).ok().map(|_| { self.receive_nft( deps, env, - info, + &info.sender, TokenId::new(token_id), nft_owner, msg.clone(), @@ -336,7 +329,7 @@ where &self, deps: DepsMut, env: Env, - info: MessageInfo, + nft_contract: &Addr, token_id: TokenId, nft_owner: String, msg: Binary, @@ -344,15 +337,15 @@ where let nft_owner = deps.api.addr_validate(&nft_owner)?; let msg: IbcOutgoingMsg = from_json(msg)?; - let class = match NFT_CONTRACT_TO_CLASS_ID.may_load(deps.storage, info.sender.clone())? { + let class = match NFT_CONTRACT_TO_CLASS_ID.may_load(deps.storage, nft_contract.clone())? { Some(class_id) => CLASS_ID_TO_CLASS.load(deps.storage, class_id)?, // No class ID being present means that this is a local NFT // that has never been sent out of this contract. None => { - let class_data = self.get_class_data(&deps, &info.sender)?; + let class_data = self.get_class_data(&deps, nft_contract)?; let data = class_data.as_ref().map(to_json_binary).transpose()?; let class = Class { - id: ClassId::new(info.sender.to_string()), + id: ClassId::new(nft_contract.to_string()), // There is no collection-level uri nor data in the // cw721 specification so we set those values to // `None` for local, cw721 NFTs. @@ -360,8 +353,8 @@ where data, }; - NFT_CONTRACT_TO_CLASS_ID.save(deps.storage, info.sender.clone(), &class.id)?; - CLASS_ID_TO_NFT_CONTRACT.save(deps.storage, class.id.clone(), &info.sender)?; + NFT_CONTRACT_TO_CLASS_ID.save(deps.storage, nft_contract.clone(), &class.id)?; + CLASS_ID_TO_NFT_CONTRACT.save(deps.storage, class.id.clone(), nft_contract)?; // Merging and usage of this PR may change that: // @@ -372,7 +365,7 @@ where // make sure NFT is escrowed by ics721 let UniversalAllNftInfoResponse { access, info } = deps.querier.query_wasm_smart( - info.sender, + nft_contract, &cw721::Cw721QueryMsg::AllNftInfo { token_id: token_id.clone().into(), include_expired: None, diff --git a/packages/ics721/src/testing/contract.rs b/packages/ics721/src/testing/contract.rs index 4e0da119..83b1e1d3 100644 --- a/packages/ics721/src/testing/contract.rs +++ b/packages/ics721/src/testing/contract.rs @@ -221,7 +221,7 @@ fn test_receive_nft() { .receive_nft( deps.as_mut(), env, - info, + &info.sender, TokenId::new(token_id), sender.clone(), msg, @@ -302,7 +302,7 @@ fn test_receive_nft() { .receive_nft( deps.as_mut(), env, - info, + &info.sender, TokenId::new(token_id), sender.clone(), msg, @@ -383,7 +383,7 @@ fn test_receive_nft() { .receive_nft( deps.as_mut(), env, - info, + &info.sender, TokenId::new(token_id), sender.clone(), msg, @@ -454,7 +454,7 @@ fn test_receive_sets_uri() { .unwrap(); Ics721Contract {} - .receive_nft(deps.as_mut(), env, info, token_id, sender, msg) + .receive_nft(deps.as_mut(), env, &info.sender, token_id, sender, msg) .unwrap(); let class = CLASS_ID_TO_CLASS From d6d63835ec03da75e8a45a36cf4c0401137f1492 Mon Sep 17 00:00:00 2001 From: mr-t Date: Tue, 23 Jan 2024 10:33:00 +0100 Subject: [PATCH 03/11] remove NFT_CONTRACT_TO_CLASS_ID and CLASS_ID_TO_NFT_CONTRACT and merge into CLASS_ID_AND_NFT_CONTRACT_INFO --- packages/ics721/src/error.rs | 5 +- packages/ics721/src/execute.rs | 203 +++++++++++------ packages/ics721/src/ibc.rs | 28 ++- packages/ics721/src/ibc_packet_receive.rs | 6 +- packages/ics721/src/query.rs | 214 ++++++++++-------- packages/ics721/src/state.rs | 33 ++- packages/ics721/src/testing/ibc_tests.rs | 10 +- .../ics721/src/testing/integration_tests.rs | 5 +- 8 files changed, 313 insertions(+), 191 deletions(-) diff --git a/packages/ics721/src/error.rs b/packages/ics721/src/error.rs index 94ff26dd..955b8fe6 100644 --- a/packages/ics721/src/error.rs +++ b/packages/ics721/src/error.rs @@ -55,7 +55,7 @@ pub enum ContractError { #[error("Transfer Doesn't contain any action, no redemption or creation")] InvalidTransferNoAction, - #[error("Couldn't find nft contract for this class id: {0}")] + #[error("Couldn't find nft contract for class id: {0}")] NoNftContractForClassId(String), #[error("Unknown nft contract: {child_collection}, Class Id: {class_id}, Token ID: {token_id} => NFT contract: {cw721_addr}")] @@ -65,4 +65,7 @@ pub enum ContractError { token_id: String, cw721_addr: String, }, + + #[error("Couldn't find class id for nft contract: {0}")] + NoClassIdForNftContract(String), } diff --git a/packages/ics721/src/execute.rs b/packages/ics721/src/execute.rs index 9f94a49b..6245107a 100644 --- a/packages/ics721/src/execute.rs +++ b/packages/ics721/src/execute.rs @@ -2,8 +2,9 @@ use std::fmt::Debug; use cosmwasm_std::{ from_json, to_json_binary, Addr, Binary, ContractInfoResponse, Deps, DepsMut, Empty, Env, - IbcMsg, MessageInfo, Response, StdResult, SubMsg, WasmMsg, + IbcMsg, MessageInfo, Order, Response, StdResult, SubMsg, WasmMsg, }; +use cw_storage_plus::Map; use ics721_types::{ ibc_types::{IbcOutgoingMsg, IbcOutgoingProxyMsg, NonFungibleTokenPacketData}, token_types::{Class, ClassId, Token, TokenId}, @@ -18,11 +19,15 @@ use crate::{ INSTANTIATE_OUTGOING_PROXY_REPLY_ID, }, msg::{CallbackMsg, ExecuteMsg, InstantiateMsg, MigrateMsg}, + query::{ + load_class_id_for_nft_contract, load_nft_contract_for_class_id, + query_nft_contract_for_class_id, + }, state::{ - CollectionData, UniversalAllNftInfoResponse, ADMIN_USED_FOR_CW721, CLASS_ID_TO_CLASS, - CLASS_ID_TO_NFT_CONTRACT, CW721_CODE_ID, INCOMING_CLASS_TOKEN_TO_CHANNEL, INCOMING_PROXY, - NFT_CONTRACT_TO_CLASS_ID, OUTGOING_CLASS_TOKEN_TO_CHANNEL, OUTGOING_PROXY, PO, - TOKEN_METADATA, + ClassIdInfo, CollectionData, UniversalAllNftInfoResponse, ADMIN_USED_FOR_CW721, + CLASS_ID_AND_NFT_CONTRACT_INFO, CLASS_ID_TO_CLASS, CW721_CODE_ID, + INCOMING_CLASS_TOKEN_TO_CHANNEL, INCOMING_PROXY, OUTGOING_CLASS_TOKEN_TO_CHANNEL, + OUTGOING_PROXY, PO, TOKEN_METADATA, }, token_types::{VoucherCreation, VoucherRedemption}, ContractError, @@ -141,14 +146,22 @@ where let token_id = TokenId::new(token_id); let child_class_id = ClassId::new(child_class_id); let child_collection = deps.api.addr_validate(&child_collection)?; - let cw721_addr = CLASS_ID_TO_NFT_CONTRACT.load(deps.storage, child_class_id.clone())?; - if cw721_addr != child_collection { - return Err(ContractError::NoNftContractMatch { - child_collection: child_collection.to_string(), - class_id: child_class_id.to_string(), - token_id: token_id.into(), - cw721_addr: cw721_addr.to_string(), - }); + match query_nft_contract_for_class_id(deps.storage, child_class_id.to_string())? { + Some(cw721_addr) => { + if cw721_addr != child_collection { + return Err(ContractError::NoNftContractMatch { + child_collection: child_collection.to_string(), + class_id: child_class_id.to_string(), + token_id: token_id.into(), + cw721_addr: cw721_addr.to_string(), + }); + } + } + None => { + return Err(ContractError::NoNftContractForClassId( + child_class_id.to_string(), + )) + } } // remove incoming channel entry and metadata @@ -216,14 +229,22 @@ where // check given home class id and home collection is the same as stored in the contract let home_class_id = ClassId::new(home_class_id); let home_collection = deps.api.addr_validate(&home_collection)?; - let cw721_addr = CLASS_ID_TO_NFT_CONTRACT.load(deps.storage, home_class_id.clone())?; - if cw721_addr != home_collection { - return Err(ContractError::NoNftContractMatch { - child_collection: home_collection.to_string(), - class_id: home_class_id.to_string(), - token_id, - cw721_addr: cw721_addr.to_string(), - }); + match query_nft_contract_for_class_id(deps.storage, home_class_id.to_string())? { + Some(cw721_addr) => { + if cw721_addr != home_collection { + return Err(ContractError::NoNftContractMatch { + child_collection: home_collection.to_string(), + class_id: home_class_id.to_string(), + token_id, + cw721_addr: cw721_addr.to_string(), + }); + } + } + None => { + return Err(ContractError::NoNftContractForClassId( + home_class_id.to_string(), + )) + } } // remove outgoing channel entry @@ -337,7 +358,7 @@ where let nft_owner = deps.api.addr_validate(&nft_owner)?; let msg: IbcOutgoingMsg = from_json(msg)?; - let class = match NFT_CONTRACT_TO_CLASS_ID.may_load(deps.storage, nft_contract.clone())? { + let class = match load_class_id_for_nft_contract(deps.as_ref().storage, nft_contract)? { Some(class_id) => CLASS_ID_TO_CLASS.load(deps.storage, class_id)?, // No class ID being present means that this is a local NFT // that has never been sent out of this contract. @@ -353,8 +374,11 @@ where data, }; - NFT_CONTRACT_TO_CLASS_ID.save(deps.storage, nft_contract.clone(), &class.id)?; - CLASS_ID_TO_NFT_CONTRACT.save(deps.storage, class.id.clone(), nft_contract)?; + let class_id_info = ClassIdInfo { + class_id: class.id.clone(), + address: nft_contract.clone(), + }; + CLASS_ID_AND_NFT_CONTRACT_INFO.save(deps.storage, &class.id, &class_id_info)?; // Merging and usage of this PR may change that: // @@ -476,48 +500,52 @@ where create: VoucherCreation, ) -> Result, ContractError> { let VoucherCreation { class, tokens } = create; - let instantiate = if CLASS_ID_TO_NFT_CONTRACT.has(deps.storage, class.id.clone()) { - vec![] - } else { - let class_id = ClassId::new(class.id.clone()); - let cw721_code_id = CW721_CODE_ID.load(deps.storage)?; - // for creating a predictable nft contract using, using instantiate2, we need: checksum, creator, and salt: - // - using class id as salt for instantiating nft contract guarantees a) predictable address and b) uniqueness - // for this salt must be of length 32 bytes, so we use sha256 to hash class id - let mut hasher = Sha256::new(); - hasher.update(class_id.as_bytes()); - let salt = hasher.finalize().to_vec(); - - let cw721_addr = get_instantiate2_address( - deps.as_ref(), - env.contract.address.as_str(), - &salt, - cw721_code_id, - )?; - - // Save classId <-> contract mappings. - CLASS_ID_TO_NFT_CONTRACT.save(deps.storage, class_id.clone(), &cw721_addr)?; - NFT_CONTRACT_TO_CLASS_ID.save(deps.storage, cw721_addr, &class_id)?; - - let admin = ADMIN_USED_FOR_CW721 - .load(deps.storage)? - .map(|a| a.to_string()); - let message = SubMsg::::reply_on_success( - WasmMsg::Instantiate2 { - admin, - code_id: cw721_code_id, - msg: self.init_msg(deps.as_ref(), &env, &class)?, - funds: vec![], - // Attempting to fit the class ID in the label field - // can make this field too long which causes data - // errors in the SDK. - label: "ics-721 debt-voucher cw-721".to_string(), - salt: salt.into(), - }, - INSTANTIATE_CW721_REPLY_ID, - ); - vec![message] - }; + let instantiate = + if CLASS_ID_AND_NFT_CONTRACT_INFO.has(deps.storage, class.id.to_string().as_str()) { + vec![] + } else { + let class_id = ClassId::new(class.id.clone()); + let cw721_code_id = CW721_CODE_ID.load(deps.storage)?; + // for creating a predictable nft contract using, using instantiate2, we need: checksum, creator, and salt: + // - using class id as salt for instantiating nft contract guarantees a) predictable address and b) uniqueness + // for this salt must be of length 32 bytes, so we use sha256 to hash class id + let mut hasher = Sha256::new(); + hasher.update(class_id.as_bytes()); + let salt = hasher.finalize().to_vec(); + + let nft_contract = get_instantiate2_address( + deps.as_ref(), + env.contract.address.as_str(), + &salt, + cw721_code_id, + )?; + + // Save classId <-> contract mappings. + let class_id_info = ClassIdInfo { + class_id: class_id.clone(), + address: nft_contract.clone(), + }; + CLASS_ID_AND_NFT_CONTRACT_INFO.save(deps.storage, &class.id, &class_id_info)?; + + let admin = ADMIN_USED_FOR_CW721 + .load(deps.storage)? + .map(|a| a.to_string()); + let message = SubMsg::::reply_on_success( + WasmMsg::Instantiate2 { + admin, + code_id: cw721_code_id, + msg: self.init_msg(deps.as_ref(), &env, &class)?, + funds: vec![], + // Attempting to fit the class ID in the label field + // can make this field too long which causes data + // errors in the SDK. + label: "ics-721 debt-voucher cw-721".to_string(), + salt: salt.into(), + }, + INSTANTIATE_CW721_REPLY_ID, + ); + vec![message] + }; // Store mapping from classID to classURI. Notably, we don't check // if this has already been set. If a new NFT belonging to a class @@ -579,7 +607,7 @@ where redeem: VoucherRedemption, ) -> Result, ContractError> { let VoucherRedemption { class, token_ids } = redeem; - let nft_contract = CLASS_ID_TO_NFT_CONTRACT.load(deps.storage, class.id)?; + let nft_contract = load_nft_contract_for_class_id(deps.storage, class.id.to_string())?; let receiver = deps.api.addr_validate(&receiver)?; Ok(Response::default() .add_attribute("method", "callback_redeem_vouchers") @@ -608,7 +636,8 @@ where receiver: String, ) -> Result, ContractError> { let receiver = deps.api.addr_validate(&receiver)?; - let cw721_addr = CLASS_ID_TO_NFT_CONTRACT.load(deps.storage, class_id.clone())?; + let nft_contract = + load_nft_contract_for_class_id(deps.as_ref().storage, class_id.to_string())?; let mint = tokens .into_iter() @@ -626,7 +655,7 @@ where extension: Empty::default(), }; Ok(WasmMsg::Execute { - contract_addr: cw721_addr.to_string(), + contract_addr: nft_contract.to_string(), msg: to_json_binary(&msg)?, funds: vec![], }) @@ -702,7 +731,8 @@ where .save(deps.storage, &Some(deps.api.addr_validate(&cw721_admin)?))?; } } - Ok(Response::default() + + let response = Response::default() .add_attribute("method", "migrate") .add_attribute("pauser", pauser.map_or_else(|| "none".to_string(), |or| or)) .add_attribute( @@ -729,7 +759,38 @@ where } }, ), - )) + ); + + // TODO: once migrated, this complete block can be deleted + // - get legacy map and migrate it to new indexed map + let nft_contract_to_class_id: Map = Map::new("f"); + match cw_paginate_storage::paginate_map( + deps.as_ref(), + &nft_contract_to_class_id, + None, + None, + Order::Ascending, + ) { + Ok(nft_contract_to_class_id) => { + let response = response.add_attribute( + "migrated nft contracts", + nft_contract_to_class_id.len().to_string(), + ); + for (nft_contract, class_id) in nft_contract_to_class_id { + let class_id_info = ClassIdInfo { + class_id: class_id.clone(), + address: nft_contract.clone(), + }; + CLASS_ID_AND_NFT_CONTRACT_INFO.save( + deps.storage, + &class_id, + &class_id_info, + )?; + } + Ok(response) + } + Err(err) => Err(ContractError::Std(err)), + } } } } diff --git a/packages/ics721/src/ibc.rs b/packages/ics721/src/ibc.rs index 0b6b6b8b..5f7cdaf8 100644 --- a/packages/ics721/src/ibc.rs +++ b/packages/ics721/src/ibc.rs @@ -12,9 +12,10 @@ use crate::{ helpers::ack_callback_msg, ibc_helpers::{ack_fail, ack_success, try_get_ack_error, validate_order_and_version}, ibc_packet_receive::receive_ibc_packet, + query::{load_class_id_for_nft_contract, load_nft_contract_for_class_id}, state::{ - CLASS_ID_TO_NFT_CONTRACT, INCOMING_CLASS_TOKEN_TO_CHANNEL, INCOMING_PROXY, - NFT_CONTRACT_TO_CLASS_ID, OUTGOING_CLASS_TOKEN_TO_CHANNEL, OUTGOING_PROXY, TOKEN_METADATA, + INCOMING_CLASS_TOKEN_TO_CHANNEL, INCOMING_PROXY, OUTGOING_CLASS_TOKEN_TO_CHANNEL, + OUTGOING_PROXY, TOKEN_METADATA, }, ContractError, }; @@ -121,7 +122,8 @@ where } else { let msg: NonFungibleTokenPacketData = from_json(&ack.original_packet.data)?; - let nft_contract = CLASS_ID_TO_NFT_CONTRACT.load(deps.storage, msg.class_id.clone())?; + let nft_contract = + load_nft_contract_for_class_id(deps.storage, msg.class_id.to_string())?; // Burn all of the tokens being transfered out that were // previously transfered in on this channel. let burn_notices = msg.token_ids.iter().cloned().try_fold( @@ -188,7 +190,8 @@ where error: &str, ) -> Result { let message: NonFungibleTokenPacketData = from_json(&packet.data)?; - let nft_contract = CLASS_ID_TO_NFT_CONTRACT.load(deps.storage, message.class_id.clone())?; + let nft_contract = + load_nft_contract_for_class_id(deps.storage, message.class_id.to_string())?; let sender = deps.api.addr_validate(&message.sender)?; let messages = message @@ -241,15 +244,18 @@ where // `ACK_AND_DO_NOTHING`. let res = parse_reply_instantiate_data(reply)?; - let cw721_addr = deps.api.addr_validate(&res.contract_address)?; + let nft_contract = deps.api.addr_validate(&res.contract_address)?; // cw721 addr has already been stored, here just check whether it exists, otherwise a NotFound error is thrown - let class_id = NFT_CONTRACT_TO_CLASS_ID.load(deps.storage, cw721_addr.clone())?; - - Ok(Response::default() - .add_attribute("method", "instantiate_cw721_reply") - .add_attribute("class_id", class_id) - .add_attribute("cw721_addr", cw721_addr)) + match load_class_id_for_nft_contract(deps.storage, &nft_contract)? { + Some(class_id) => Ok(Response::default() + .add_attribute("method", "instantiate_cw721_reply") + .add_attribute("class_id", class_id) + .add_attribute("cw721_addr", nft_contract)), + None => Err(ContractError::NoClassIdForNftContract( + nft_contract.to_string(), + )), + } } INSTANTIATE_OUTGOING_PROXY_REPLY_ID => { let res = parse_reply_instantiate_data(reply)?; diff --git a/packages/ics721/src/ibc_packet_receive.rs b/packages/ics721/src/ibc_packet_receive.rs index 366274dd..5aff2200 100644 --- a/packages/ics721/src/ibc_packet_receive.rs +++ b/packages/ics721/src/ibc_packet_receive.rs @@ -13,7 +13,8 @@ use crate::{ ibc::ACK_AND_DO_NOTHING_REPLY_ID, ibc_helpers::{get_endpoint_prefix, try_pop_source_prefix}, msg::{CallbackMsg, ExecuteMsg}, - state::{CLASS_ID_TO_NFT_CONTRACT, CW721_CODE_ID, OUTGOING_CLASS_TOKEN_TO_CHANNEL, PO}, + query::load_nft_contract_for_class_id, + state::{CW721_CODE_ID, OUTGOING_CLASS_TOKEN_TO_CHANNEL, PO}, token_types::{VoucherCreation, VoucherRedemption}, ContractError, }; @@ -228,8 +229,7 @@ fn create_callback_msg( let nft_contract = if is_redemption { // If its a redemption, it means we already have the contract address in storage - CLASS_ID_TO_NFT_CONTRACT - .load(deps.storage, local_class_id.clone()) + load_nft_contract_for_class_id(deps.storage, local_class_id.to_string()) .map_err(|_| ContractError::NoNftContractForClassId(local_class_id.to_string())) } else { // If its a creation action, we can use the instantiate2 function to get the nft contract diff --git a/packages/ics721/src/query.rs b/packages/ics721/src/query.rs index e31a93ec..b4a55de6 100644 --- a/packages/ics721/src/query.rs +++ b/packages/ics721/src/query.rs @@ -1,13 +1,12 @@ -use cosmwasm_std::{to_json_binary, Addr, Binary, Deps, Env, Order, StdResult}; -use cw_storage_plus::Map; +use cosmwasm_std::{to_json_binary, Addr, Binary, Deps, Env, Order, StdError, StdResult, Storage}; +use cw_storage_plus::{Bound, Map}; use crate::{ msg::QueryMsg, state::{ - UniversalAllNftInfoResponse, ADMIN_USED_FOR_CW721, CLASS_ID_TO_CLASS, - CLASS_ID_TO_NFT_CONTRACT, CW721_CODE_ID, INCOMING_CLASS_TOKEN_TO_CHANNEL, INCOMING_PROXY, - NFT_CONTRACT_TO_CLASS_ID, OUTGOING_CLASS_TOKEN_TO_CHANNEL, OUTGOING_PROXY, PO, - TOKEN_METADATA, + UniversalAllNftInfoResponse, ADMIN_USED_FOR_CW721, CLASS_ID_AND_NFT_CONTRACT_INFO, + CLASS_ID_TO_CLASS, CW721_CODE_ID, INCOMING_CLASS_TOKEN_TO_CHANNEL, INCOMING_PROXY, + OUTGOING_CLASS_TOKEN_TO_CHANNEL, OUTGOING_PROXY, PO, TOKEN_METADATA, }, }; use ics721_types::token_types::{Class, ClassId, ClassToken, Token, TokenId}; @@ -16,28 +15,28 @@ pub trait Ics721Query { fn query(&self, deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::ClassId { contract } => { - to_json_binary(&self.query_class_id_for_nft_contract(deps, contract)?) + to_json_binary(&query_class_id_for_nft_contract(deps, contract)?) } QueryMsg::NftContract { class_id } => { - to_json_binary(&self.query_nft_contract_for_class_id(deps, class_id)?) + to_json_binary(&query_nft_contract_for_class_id(deps.storage, class_id)?) } QueryMsg::ClassMetadata { class_id } => { - to_json_binary(&self.query_class_metadata(deps, class_id)?) + to_json_binary(&query_class_metadata(deps, class_id)?) } QueryMsg::TokenMetadata { class_id, token_id } => { - to_json_binary(&self.query_token_metadata(deps, class_id, token_id)?) + to_json_binary(&query_token_metadata(deps, class_id, token_id)?) } QueryMsg::Owner { class_id, token_id } => { - to_json_binary(&self.query_owner(deps, class_id, token_id)?) + to_json_binary(&query_owner(deps, class_id, token_id)?) } QueryMsg::Pauser {} => to_json_binary(&PO.query_pauser(deps.storage)?), QueryMsg::Paused {} => to_json_binary(&PO.query_paused(deps.storage)?), QueryMsg::OutgoingProxy {} => to_json_binary(&OUTGOING_PROXY.load(deps.storage)?), QueryMsg::IncomingProxy {} => to_json_binary(&INCOMING_PROXY.load(deps.storage)?), - QueryMsg::Cw721CodeId {} => to_json_binary(&self.query_cw721_code_id(deps)?), + QueryMsg::Cw721CodeId {} => to_json_binary(&query_cw721_code_id(deps)?), QueryMsg::Cw721Admin {} => to_json_binary(&ADMIN_USED_FOR_CW721.load(deps.storage)?), QueryMsg::NftContracts { start_after, limit } => { - to_json_binary(&self.query_nft_contracts(deps, start_after, limit)?) + to_json_binary(&query_nft_contracts(deps, start_after, limit)?) } QueryMsg::OutgoingChannels { start_after, limit } => to_json_binary(&query_channels( deps, @@ -53,97 +52,120 @@ pub trait Ics721Query { )?), } } +} - fn query_class_id_for_nft_contract( - &self, - deps: Deps, - contract: String, - ) -> StdResult> { - let contract = deps.api.addr_validate(&contract)?; - NFT_CONTRACT_TO_CLASS_ID.may_load(deps.storage, contract) - } +pub fn query_class_id_for_nft_contract(deps: Deps, contract: String) -> StdResult> { + let contract = deps.api.addr_validate(&contract)?; + load_class_id_for_nft_contract(deps.storage, &contract) +} - fn query_nft_contract_for_class_id( - &self, - deps: Deps, - class_id: String, - ) -> StdResult> { - CLASS_ID_TO_NFT_CONTRACT.may_load(deps.storage, ClassId::new(class_id)) - } +pub fn load_class_id_for_nft_contract( + storage: &dyn Storage, + contract: &Addr, +) -> StdResult> { + CLASS_ID_AND_NFT_CONTRACT_INFO + .idx + .address + .item(storage, contract.clone()) + .map(|e| e.map(|(_, c)| c.class_id)) +} - fn query_class_metadata(&self, deps: Deps, class_id: String) -> StdResult> { - CLASS_ID_TO_CLASS.may_load(deps.storage, ClassId::new(class_id)) - } +pub fn query_nft_contract_for_class_id( + storage: &dyn Storage, + class_id: String, +) -> StdResult> { + // Convert the class_id string to ClassId type if necessary + let class_id_key = ClassId::new(class_id); - fn query_token_metadata( - &self, - deps: Deps, - class_id: String, - token_id: String, - ) -> StdResult> { - let token_id = TokenId::new(token_id); - let class_id = ClassId::new(class_id); + // Query the IndexedMap using the class_id index + CLASS_ID_AND_NFT_CONTRACT_INFO + .idx + .class_id + .item(storage, class_id_key) + .map(|e| e.map(|(_, v)| v.address)) +} - let Some(token_metadata) = - TOKEN_METADATA.may_load(deps.storage, (class_id.clone(), token_id.clone()))? - else { - // Token metadata is set unconditionaly on mint. If we have no - // metadata entry, we have no entry for this token at all. - return Ok(None); - }; - let Some(token_contract) = CLASS_ID_TO_NFT_CONTRACT.may_load(deps.storage, class_id)? - else { - debug_assert!(false, "token_metadata != None => token_contract != None"); - return Ok(None); - }; - let UniversalAllNftInfoResponse { info, .. } = deps.querier.query_wasm_smart( - token_contract, - &cw721::Cw721QueryMsg::AllNftInfo { - token_id: token_id.clone().into(), - include_expired: None, - }, - )?; - Ok(Some(Token { - id: token_id, - uri: info.token_uri, - data: token_metadata, - })) - } +pub fn load_nft_contract_for_class_id(storage: &dyn Storage, class_id: String) -> StdResult { + query_nft_contract_for_class_id(storage, class_id.clone())?.map_or_else( + || { + Err(StdError::NotFound { + kind: format!("NFT contract not found for class id {}", class_id), + }) + }, + Ok, + ) +} - fn query_owner( - &self, - deps: Deps, - class_id: String, - token_id: String, - ) -> StdResult { - let class_uri = CLASS_ID_TO_NFT_CONTRACT.load(deps.storage, ClassId::new(class_id))?; - let resp: cw721::OwnerOfResponse = deps.querier.query_wasm_smart( - class_uri, - &cw721::Cw721QueryMsg::OwnerOf { - token_id, - include_expired: None, - }, - )?; - Ok(resp) - } +pub fn query_class_metadata(deps: Deps, class_id: String) -> StdResult> { + CLASS_ID_TO_CLASS.may_load(deps.storage, ClassId::new(class_id)) +} - fn query_cw721_code_id(&self, deps: Deps) -> StdResult { - CW721_CODE_ID.load(deps.storage) - } +pub fn query_token_metadata( + deps: Deps, + class_id: String, + token_id: String, +) -> StdResult> { + let token_id = TokenId::new(token_id); + let class_id = ClassId::new(class_id); - fn query_nft_contracts( - &self, - deps: Deps, - start_after: Option, - limit: Option, - ) -> StdResult> { - cw_paginate_storage::paginate_map( - deps, - &CLASS_ID_TO_NFT_CONTRACT, - start_after, - limit, - Order::Ascending, - ) + let Some(token_metadata) = + TOKEN_METADATA.may_load(deps.storage, (class_id.clone(), token_id.clone()))? + else { + // Token metadata is set unconditionaly on mint. If we have no + // metadata entry, we have no entry for this token at all. + return Ok(None); + }; + let Some(nft_contract) = query_nft_contract_for_class_id(deps.storage, class_id.to_string())? + else { + debug_assert!(false, "token_metadata != None => token_contract != None"); + return Ok(None); + }; + let UniversalAllNftInfoResponse { info, .. } = deps.querier.query_wasm_smart( + nft_contract, + &cw721::Cw721QueryMsg::AllNftInfo { + token_id: token_id.clone().into(), + include_expired: None, + }, + )?; + Ok(Some(Token { + id: token_id, + uri: info.token_uri, + data: token_metadata, + })) +} + +pub fn query_owner( + deps: Deps, + class_id: String, + token_id: String, +) -> StdResult { + let nft_contract = load_nft_contract_for_class_id(deps.storage, class_id)?; + let resp: cw721::OwnerOfResponse = deps.querier.query_wasm_smart( + nft_contract, + &cw721::Cw721QueryMsg::OwnerOf { + token_id, + include_expired: None, + }, + )?; + Ok(resp) +} + +pub fn query_cw721_code_id(deps: Deps) -> StdResult { + CW721_CODE_ID.load(deps.storage) +} + +pub fn query_nft_contracts( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult> { + let start = start_after.map(|s| Bound::ExclusiveRaw(s.to_string().into())); + let all = CLASS_ID_AND_NFT_CONTRACT_INFO + .range(deps.storage, start, None, Order::Ascending) + .map(|item| item.map(|(k, v)| (k, v.address))); + match limit { + Some(limit) => all.take(limit as usize).collect(), + None => all.collect(), } } diff --git a/packages/ics721/src/state.rs b/packages/ics721/src/state.rs index 739f7568..86c3dfb4 100644 --- a/packages/ics721/src/state.rs +++ b/packages/ics721/src/state.rs @@ -1,7 +1,7 @@ -use cosmwasm_schema::schemars::JsonSchema; +use cosmwasm_schema::{cw_serde, schemars::JsonSchema}; use cosmwasm_std::{Addr, Binary, ContractInfoResponse, Empty}; use cw_pause_once::PauseOrchestrator; -use cw_storage_plus::{Item, Map}; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, UniqueIndex}; use serde::{Deserialize, Serialize}; use ics721_types::token_types::{Class, ClassId, TokenId}; @@ -19,9 +19,14 @@ pub const PO: PauseOrchestrator = PauseOrchestrator::new("c", "d"); /// Maps classID (from NonFungibleTokenPacketData) to the cw721 /// contract we have instantiated for that classID. -pub const CLASS_ID_TO_NFT_CONTRACT: Map = Map::new("e"); -/// Maps cw721 contracts to the classID they were instantiated for. -pub const NFT_CONTRACT_TO_CLASS_ID: Map = Map::new("f"); +pub const CLASS_ID_AND_NFT_CONTRACT_INFO: IndexedMap<&str, ClassIdInfo, ClassIdInfoIndexes> = + IndexedMap::new( + "e", + ClassIdInfoIndexes { + class_id: UniqueIndex::new(|d| d.class_id.clone(), "class_id_info__class_id"), + address: UniqueIndex::new(|d| d.address.clone(), "class_id_info__address"), + }, + ); /// Maps between classIDs and classs. We need to keep this state /// ourselves as cw721 contracts do not have class-level metadata. @@ -82,6 +87,24 @@ pub struct UniversalOwnerOfResponse { pub approvals: Vec, } +#[cw_serde] +pub struct ClassIdInfo { + pub class_id: ClassId, + pub address: Addr, +} + +pub struct ClassIdInfoIndexes<'a> { + pub class_id: UniqueIndex<'a, ClassId, ClassIdInfo>, + pub address: UniqueIndex<'a, Addr, ClassIdInfo>, +} + +impl<'a> IndexList for ClassIdInfoIndexes<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.class_id, &self.address]; + Box::new(v.into_iter()) + } +} + #[cfg(test)] mod tests { use cosmwasm_std::{from_json, to_json_binary, Coin, Empty}; diff --git a/packages/ics721/src/testing/ibc_tests.rs b/packages/ics721/src/testing/ibc_tests.rs index cd3b7d84..09bc90f1 100644 --- a/packages/ics721/src/testing/ibc_tests.rs +++ b/packages/ics721/src/testing/ibc_tests.rs @@ -16,7 +16,7 @@ use crate::{ ibc_helpers::{ack_fail, ack_success, try_get_ack_error}, msg::{CallbackMsg, ExecuteMsg, InstantiateMsg, QueryMsg}, query::Ics721Query, - state::{CollectionData, NFT_CONTRACT_TO_CLASS_ID, PO}, + state::{ClassIdInfo, CollectionData, CLASS_ID_AND_NFT_CONTRACT_INFO, PO}, utils::get_collection_data, ContractError, }; @@ -160,8 +160,12 @@ fn test_reply_cw721() { // save the class_id and cw721_addr, since reply assumes it has been stored before let cw721_addr = Addr::unchecked("cosmos2contract"); let class_id = ClassId::new("wasm.address1/channel-10/address2"); - NFT_CONTRACT_TO_CLASS_ID - .save(deps.as_mut().storage, cw721_addr, &class_id) + let class_id_info = ClassIdInfo { + class_id: class_id.clone(), + address: cw721_addr.clone(), + }; + CLASS_ID_AND_NFT_CONTRACT_INFO + .save(deps.as_mut().storage, &class_id, &class_id_info) .unwrap(); let res = Ics721Contract::default() diff --git a/packages/ics721/src/testing/integration_tests.rs b/packages/ics721/src/testing/integration_tests.rs index 67c5f4e8..7d97b902 100644 --- a/packages/ics721/src/testing/integration_tests.rs +++ b/packages/ics721/src/testing/integration_tests.rs @@ -2121,7 +2121,10 @@ fn test_admin_clean_and_unescrow_nft() { .unwrap_err() .downcast() .unwrap(); - assert_eq!(err, ContractError::Std(StdError::NotFound { kind: "type: cosmwasm_std::addresses::Addr; key: [00, 01, 65, 75, 6E, 6B, 6E, 6F, 77, 6E]".to_string() })); + assert_eq!( + err, + ContractError::NoNftContractForClassId("unknown".to_string()) + ); let clean_and_unescrow_msg = ExecuteMsg::AdminCleanAndUnescrowNft { recipient: recipient.to_string(), From 36e056766c2109c63cd787b5b5c8f72c201c00e4 Mon Sep 17 00:00:00 2001 From: mr-t Date: Tue, 23 Jan 2024 17:18:11 +0100 Subject: [PATCH 04/11] test migration --- packages/ics721/src/execute.rs | 11 +- packages/ics721/src/testing/contract.rs | 159 ++++++++++++++++++++---- 2 files changed, 146 insertions(+), 24 deletions(-) diff --git a/packages/ics721/src/execute.rs b/packages/ics721/src/execute.rs index 6245107a..70e06d95 100644 --- a/packages/ics721/src/execute.rs +++ b/packages/ics721/src/execute.rs @@ -771,12 +771,15 @@ where None, Order::Ascending, ) { - Ok(nft_contract_to_class_id) => { + Ok(nft_contract_and_class_id) => { + // legacy map needs to be cleared, before migrating to indexed map + let class_id_to_nft_contract: Map = Map::new("e"); + class_id_to_nft_contract.clear(deps.storage); let response = response.add_attribute( "migrated nft contracts", - nft_contract_to_class_id.len().to_string(), + nft_contract_and_class_id.len().to_string(), ); - for (nft_contract, class_id) in nft_contract_to_class_id { + for (nft_contract, class_id) in nft_contract_and_class_id { let class_id_info = ClassIdInfo { class_id: class_id.clone(), address: nft_contract.clone(), @@ -787,6 +790,8 @@ where &class_id_info, )?; } + // let's clear legacy map, so it wont get migrated again + nft_contract_to_class_id.clear(deps.storage); Ok(response) } Err(err) => Err(ContractError::Std(err)), diff --git a/packages/ics721/src/testing/contract.rs b/packages/ics721/src/testing/contract.rs index 83b1e1d3..29b5c11e 100644 --- a/packages/ics721/src/testing/contract.rs +++ b/packages/ics721/src/testing/contract.rs @@ -9,12 +9,16 @@ use cw721::{AllNftInfoResponse, NftInfoResponse, NumTokensResponse}; use cw721_base::QueryMsg; use cw_cii::ContractInstantiateInfo; use cw_ownable::Ownership; +use cw_storage_plus::Map; use crate::{ execute::Ics721Execute, ibc::{Ics721Ibc, INSTANTIATE_INCOMING_PROXY_REPLY_ID, INSTANTIATE_OUTGOING_PROXY_REPLY_ID}, - msg::InstantiateMsg, - query::Ics721Query, + msg::{InstantiateMsg, MigrateMsg}, + query::{ + query_class_id_for_nft_contract, query_nft_contract_for_class_id, query_nft_contracts, + Ics721Query, + }, state::{ CollectionData, ADMIN_USED_FOR_CW721, CLASS_ID_TO_CLASS, CW721_CODE_ID, INCOMING_PROXY, OUTGOING_CLASS_TOKEN_TO_CHANNEL, OUTGOING_PROXY, PO, @@ -26,7 +30,10 @@ use ics721_types::{ token_types::{ClassId, TokenId}, }; -const NFT_ADDR: &str = "nft"; +const NFT_CONTRACT_1: &str = "nft1"; +const NFT_CONTRACT_2: &str = "nft2"; +const CLASS_ID_1: &str = "some/class/id1"; +const CLASS_ID_2: &str = "some/class/id2"; const OWNER_ADDR: &str = "owner"; const ADMIN_ADDR: &str = "admin"; const PAUSER_ADDR: &str = "pauser"; @@ -206,7 +213,7 @@ fn test_receive_nft() { deps.querier = querier; let env = mock_env(); - let info = mock_info(NFT_ADDR, &[]); + let info = mock_info(NFT_CONTRACT_1, &[]); let token_id = "1"; let sender = "ekez".to_string(); let msg = to_json_binary(&IbcOutgoingMsg { @@ -236,7 +243,7 @@ fn test_receive_nft() { channel_id: channel_id.clone(), timeout: IbcTimeout::with_timestamp(Timestamp::from_seconds(42)), data: to_json_binary(&NonFungibleTokenPacketData { - class_id: ClassId::new(NFT_ADDR), + class_id: ClassId::new(NFT_CONTRACT_1), class_uri: None, class_data: Some( to_json_binary(&CollectionData { @@ -264,7 +271,7 @@ fn test_receive_nft() { .keys(deps.as_mut().storage, None, None, Order::Ascending) .collect::>>() .unwrap(); - assert_eq!(keys, [(NFT_ADDR.to_string(), token_id.to_string())]); + assert_eq!(keys, [(NFT_CONTRACT_1.to_string(), token_id.to_string())]); // check channel let key = ( @@ -287,7 +294,7 @@ fn test_receive_nft() { deps.querier = querier; let env = mock_env(); - let info = mock_info(NFT_ADDR, &[]); + let info = mock_info(NFT_CONTRACT_1, &[]); let token_id = "1"; let sender = "ekez".to_string(); let msg = to_json_binary(&IbcOutgoingMsg { @@ -317,7 +324,7 @@ fn test_receive_nft() { channel_id: channel_id.clone(), timeout: IbcTimeout::with_timestamp(Timestamp::from_seconds(42)), data: to_json_binary(&NonFungibleTokenPacketData { - class_id: ClassId::new(NFT_ADDR), + class_id: ClassId::new(NFT_CONTRACT_1), class_uri: None, class_data: Some( to_json_binary(&CollectionData { @@ -345,7 +352,7 @@ fn test_receive_nft() { .keys(deps.as_mut().storage, None, None, Order::Ascending) .collect::>>() .unwrap(); - assert_eq!(keys, [(NFT_ADDR.to_string(), token_id.to_string())]); + assert_eq!(keys, [(NFT_CONTRACT_1.to_string(), token_id.to_string())]); // check channel let key = ( @@ -368,7 +375,7 @@ fn test_receive_nft() { deps.querier = querier; let env = mock_env(); - let info = mock_info(NFT_ADDR, &[]); + let info = mock_info(NFT_CONTRACT_1, &[]); let token_id = "1"; let sender = "ekez".to_string(); let msg = to_json_binary(&IbcOutgoingMsg { @@ -398,7 +405,7 @@ fn test_receive_nft() { channel_id: channel_id.clone(), timeout: IbcTimeout::with_timestamp(Timestamp::from_seconds(42)), data: to_json_binary(&NonFungibleTokenPacketData { - class_id: ClassId::new(NFT_ADDR), + class_id: ClassId::new(NFT_CONTRACT_1), class_uri: None, class_data: None, token_data: None, @@ -417,7 +424,7 @@ fn test_receive_nft() { .keys(deps.as_mut().storage, None, None, Order::Ascending) .collect::>>() .unwrap(); - assert_eq!(keys, [(NFT_ADDR.to_string(), token_id.to_string())]); + assert_eq!(keys, [(NFT_CONTRACT_1.to_string(), token_id.to_string())]); // check channel let key = ( @@ -442,7 +449,7 @@ fn test_receive_sets_uri() { deps.querier = querier; let env = mock_env(); - let info = mock_info(NFT_ADDR, &[]); + let info = mock_info(NFT_CONTRACT_1, &[]); let token_id = TokenId::new("1"); let sender = "ekez".to_string(); let msg = to_json_binary(&IbcOutgoingMsg { @@ -458,7 +465,7 @@ fn test_receive_sets_uri() { .unwrap(); let class = CLASS_ID_TO_CLASS - .load(deps.as_ref().storage, ClassId::new(NFT_ADDR)) + .load(deps.as_ref().storage, ClassId::new(NFT_CONTRACT_1)) .unwrap(); assert_eq!(class.uri, None); let expected_contract_info: cosmwasm_std::ContractInfoResponse = from_json( @@ -487,6 +494,19 @@ fn test_receive_sets_uri() { ); } +fn instantiate_msg( + incoming_proxy: Option, + outgoing_proxy: Option, +) -> InstantiateMsg { + InstantiateMsg { + cw721_base_code_id: 0, + incoming_proxy, + outgoing_proxy, + pauser: Some(PAUSER_ADDR.to_string()), + cw721_admin: Some(ADMIN_ADDR.to_string()), + } +} + #[test] fn test_instantiate() { let mut deps = mock_dependencies(); @@ -508,13 +528,10 @@ fn test_instantiate() { }), label: "outgoing".to_string(), }; - let msg = InstantiateMsg { - cw721_base_code_id: 0, - incoming_proxy: Some(incoming_proxy_init_msg.clone()), - outgoing_proxy: Some(outgoing_proxy_init_msg.clone()), - pauser: Some(PAUSER_ADDR.to_string()), - cw721_admin: Some(ADMIN_ADDR.to_string()), - }; + let msg = instantiate_msg( + Some(incoming_proxy_init_msg.clone()), + Some(outgoing_proxy_init_msg.clone()), + ); let response = Ics721Contract {} .instantiate(deps.as_mut(), env.clone(), info, msg.clone()) .unwrap(); @@ -549,3 +566,103 @@ fn test_instantiate() { Some(Addr::unchecked(ADMIN_ADDR.to_string())) ); } + +#[test] +fn test_migrate() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info(OWNER_ADDR, &[]); + let msg = instantiate_msg(None, None); + Ics721Contract {} + .instantiate(deps.as_mut(), env.clone(), info, msg.clone()) + .unwrap(); + let msg = MigrateMsg::WithUpdate { + pauser: Some("some_other_pauser".to_string()), + outgoing_proxy: Some("outgoing".to_string()), + incoming_proxy: Some("incoming".to_string()), + cw721_base_code_id: Some(1), + cw721_admin: Some("some_other_admin".to_string()), + }; + + // before migrate, populate legacy + let class_id_to_nft_contract: Map = Map::new("e"); + class_id_to_nft_contract + .save( + deps.as_mut().storage, + ClassId::new(CLASS_ID_1), + &Addr::unchecked(NFT_CONTRACT_1), + ) + .unwrap(); + class_id_to_nft_contract + .save( + deps.as_mut().storage, + ClassId::new(CLASS_ID_2), + &Addr::unchecked(NFT_CONTRACT_2), + ) + .unwrap(); + let nft_contract_to_class_id: Map = Map::new("f"); + nft_contract_to_class_id + .save( + deps.as_mut().storage, + Addr::unchecked(NFT_CONTRACT_1), + &ClassId::new(CLASS_ID_1), + ) + .unwrap(); + nft_contract_to_class_id + .save( + deps.as_mut().storage, + Addr::unchecked(NFT_CONTRACT_2), + &ClassId::new(CLASS_ID_2), + ) + .unwrap(); + + // migrate + Ics721Contract {} + .migrate(deps.as_mut(), env.clone(), msg) + .unwrap(); + + assert_eq!( + PO.pauser.load(&deps.storage).unwrap(), + Some(Addr::unchecked("some_other_pauser")) + ); + assert_eq!( + OUTGOING_PROXY.load(&deps.storage).unwrap(), + Some(Addr::unchecked("outgoing")) + ); + assert_eq!( + INCOMING_PROXY.load(&deps.storage).unwrap(), + Some(Addr::unchecked("incoming")) + ); + assert_eq!(CW721_CODE_ID.load(&deps.storage).unwrap(), 1); + assert_eq!( + ADMIN_USED_FOR_CW721.load(&deps.storage).unwrap(), + Some(Addr::unchecked("some_other_admin")) + ); + let nft_contract_and_class_id_list = query_nft_contracts(deps.as_ref(), None, None).unwrap(); + assert_eq!(nft_contract_and_class_id_list.len(), 2); + assert_eq!( + nft_contract_to_class_id + .keys(&deps.storage, None, None, Order::Ascending) + .collect::>>() + .unwrap() + .len(), + 0 // after migration legacy data is cleared + ); + assert_eq!(nft_contract_and_class_id_list[0].0, CLASS_ID_1); + assert_eq!(nft_contract_and_class_id_list[0].1, NFT_CONTRACT_1); + assert_eq!(nft_contract_and_class_id_list[1].0, CLASS_ID_2); + assert_eq!(nft_contract_and_class_id_list[1].1, NFT_CONTRACT_2); + // test query and indexers for class id and addr are working + let nft_contract_1 = + query_nft_contract_for_class_id(&deps.storage, CLASS_ID_1.to_string()).unwrap(); + assert_eq!(nft_contract_1, Some(Addr::unchecked(NFT_CONTRACT_1))); + let nft_contract_2 = + query_nft_contract_for_class_id(&deps.storage, CLASS_ID_2.to_string()).unwrap(); + assert_eq!(nft_contract_2, Some(Addr::unchecked(NFT_CONTRACT_2))); + let class_id_1 = + query_class_id_for_nft_contract(deps.as_ref(), NFT_CONTRACT_1.to_string()).unwrap(); + assert_eq!(class_id_1, Some(ClassId::new(CLASS_ID_1))); + let class_id_2 = + query_class_id_for_nft_contract(deps.as_ref(), NFT_CONTRACT_2.to_string()).unwrap(); + assert_eq!(class_id_2, Some(ClassId::new(CLASS_ID_2))); +} From 86537cb185d74932bfc160c0e95d2a148d903c24 Mon Sep 17 00:00:00 2001 From: mr-t Date: Thu, 1 Feb 2024 23:19:27 +0100 Subject: [PATCH 05/11] migrate only in case CLASS_ID_AND_NFT_CONTRACT_INFO is not populated yet, also keep legacy store --- packages/ics721/src/execute.rs | 70 +++++++++++++------------ packages/ics721/src/state.rs | 3 +- packages/ics721/src/testing/contract.rs | 8 --- 3 files changed, 39 insertions(+), 42 deletions(-) diff --git a/packages/ics721/src/execute.rs b/packages/ics721/src/execute.rs index 70e06d95..86c9ac65 100644 --- a/packages/ics721/src/execute.rs +++ b/packages/ics721/src/execute.rs @@ -21,7 +21,7 @@ use crate::{ msg::{CallbackMsg, ExecuteMsg, InstantiateMsg, MigrateMsg}, query::{ load_class_id_for_nft_contract, load_nft_contract_for_class_id, - query_nft_contract_for_class_id, + query_nft_contract_for_class_id, query_nft_contracts, }, state::{ ClassIdInfo, CollectionData, UniversalAllNftInfoResponse, ADMIN_USED_FOR_CW721, @@ -761,40 +761,44 @@ where ), ); - // TODO: once migrated, this complete block can be deleted - // - get legacy map and migrate it to new indexed map - let nft_contract_to_class_id: Map = Map::new("f"); - match cw_paginate_storage::paginate_map( - deps.as_ref(), - &nft_contract_to_class_id, - None, - None, - Order::Ascending, - ) { - Ok(nft_contract_and_class_id) => { - // legacy map needs to be cleared, before migrating to indexed map - let class_id_to_nft_contract: Map = Map::new("e"); - class_id_to_nft_contract.clear(deps.storage); - let response = response.add_attribute( - "migrated nft contracts", - nft_contract_and_class_id.len().to_string(), - ); - for (nft_contract, class_id) in nft_contract_and_class_id { - let class_id_info = ClassIdInfo { - class_id: class_id.clone(), - address: nft_contract.clone(), - }; - CLASS_ID_AND_NFT_CONTRACT_INFO.save( - deps.storage, - &class_id, - &class_id_info, - )?; + // we migrate only in case CLASS_ID_AND_NFT_CONTRACT_INFO is not populated yet + // TODO once migrated: + // - this complete block can be deleted + // - legacy map 'e' and 'f' can be deleted + let is_empty = query_nft_contracts(deps.as_ref(), None, None) + .map(|nft_contracts| nft_contracts.is_empty())?; + if is_empty { + // - get legacy map and migrate it to new indexed map + let legacy_nft_contract_to_class_id: Map = Map::new("f"); + match cw_paginate_storage::paginate_map( + deps.as_ref(), + &legacy_nft_contract_to_class_id, + None, + None, + Order::Ascending, + ) { + Ok(nft_contract_and_class_id) => { + let response = response.add_attribute( + "migrated nft contracts", + nft_contract_and_class_id.len().to_string(), + ); + for (nft_contract, class_id) in nft_contract_and_class_id { + let class_id_info = ClassIdInfo { + class_id: class_id.clone(), + address: nft_contract.clone(), + }; + CLASS_ID_AND_NFT_CONTRACT_INFO.save( + deps.storage, + &class_id, + &class_id_info, + )?; + } + Ok(response) } - // let's clear legacy map, so it wont get migrated again - nft_contract_to_class_id.clear(deps.storage); - Ok(response) + Err(err) => Err(ContractError::Std(err)), } - Err(err) => Err(ContractError::Std(err)), + } else { + Ok(response) } } } diff --git a/packages/ics721/src/state.rs b/packages/ics721/src/state.rs index 86c3dfb4..a6a1dad0 100644 --- a/packages/ics721/src/state.rs +++ b/packages/ics721/src/state.rs @@ -19,9 +19,10 @@ pub const PO: PauseOrchestrator = PauseOrchestrator::new("c", "d"); /// Maps classID (from NonFungibleTokenPacketData) to the cw721 /// contract we have instantiated for that classID. +/// NOTE: legacy stores with keys `e` and `f` are no longer used. pub const CLASS_ID_AND_NFT_CONTRACT_INFO: IndexedMap<&str, ClassIdInfo, ClassIdInfoIndexes> = IndexedMap::new( - "e", + "m", ClassIdInfoIndexes { class_id: UniqueIndex::new(|d| d.class_id.clone(), "class_id_info__class_id"), address: UniqueIndex::new(|d| d.address.clone(), "class_id_info__address"), diff --git a/packages/ics721/src/testing/contract.rs b/packages/ics721/src/testing/contract.rs index 29b5c11e..409fe612 100644 --- a/packages/ics721/src/testing/contract.rs +++ b/packages/ics721/src/testing/contract.rs @@ -640,14 +640,6 @@ fn test_migrate() { ); let nft_contract_and_class_id_list = query_nft_contracts(deps.as_ref(), None, None).unwrap(); assert_eq!(nft_contract_and_class_id_list.len(), 2); - assert_eq!( - nft_contract_to_class_id - .keys(&deps.storage, None, None, Order::Ascending) - .collect::>>() - .unwrap() - .len(), - 0 // after migration legacy data is cleared - ); assert_eq!(nft_contract_and_class_id_list[0].0, CLASS_ID_1); assert_eq!(nft_contract_and_class_id_list[0].1, NFT_CONTRACT_1); assert_eq!(nft_contract_and_class_id_list[1].0, CLASS_ID_2); From f7b1b905cbaa3b41c49f450cc1cbe12934923a6a Mon Sep 17 00:00:00 2001 From: mr-t Date: Fri, 9 Feb 2024 17:40:36 +0100 Subject: [PATCH 06/11] fix optional minter --- Cargo.lock | 129 +++++++++--------- contracts/cw721-tester/src/lib.rs | 2 +- packages/ics721/src/execute.rs | 2 +- .../ics721/src/testing/integration_tests.rs | 13 +- 4 files changed, 73 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2d1f648f..2f471560 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,9 +27,9 @@ checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base64" -version = "0.21.5" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64ct" @@ -63,9 +63,9 @@ dependencies = [ [[package]] name = "bnum" -version = "0.8.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab9008b6bb9fc80b5277f2fe481c09e828743d9151203e804583eb4c9e15b31d" +checksum = "56953345e39537a3e18bdaeba4cb0c58a78c1f61f361dc0fa7c5c7340ae87c5f" [[package]] name = "byteorder" @@ -93,9 +93,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "cosmwasm-crypto" -version = "1.5.0" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8bb3c77c3b7ce472056968c745eb501c440fbc07be5004eba02782c35bfbbe3" +checksum = "9934c79e58d9676edfd592557dee765d2a6ef54c09d5aa2edb06156b00148966" dependencies = [ "digest 0.10.7", "ecdsa", @@ -107,18 +107,18 @@ dependencies = [ [[package]] name = "cosmwasm-derive" -version = "1.5.0" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea73e9162e6efde00018d55ed0061e93a108b5d6ec4548b4f8ce3c706249687" +checksum = "bc5e72e330bd3bdab11c52b5ecbdeb6a8697a004c57964caeb5d876f0b088b3c" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.5.0" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0df41ea55f2946b6b43579659eec048cc2f66e8c8e2e3652fc5e5e476f673856" +checksum = "ac3e3a2136e2a60e8b6582f5dffca5d1a683ed77bf38537d330bc1dfccd69010" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -129,9 +129,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.5.0" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43609e92ce1b9368aa951b334dd354a2d0dd4d484931a5f83ae10e12a26c8ba9" +checksum = "f5d803bea6bd9ed61bd1ee0b4a2eb09ee20dbb539cc6e0b8795614d20952ebb1" dependencies = [ "proc-macro2", "quote", @@ -140,9 +140,9 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.5.0" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04d6864742e3a7662d024b51a94ea81c9af21db6faea2f9a6d2232bb97c6e53e" +checksum = "ef8666e572a3a2519010dde88c04d16e9339ae751b56b2bb35081fe3f7d6be74" dependencies = [ "base64", "bech32", @@ -162,9 +162,9 @@ dependencies = [ [[package]] name = "cosmwasm-storage" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd2b4ae72a03e8f56c85df59d172d51d2d7dc9cec6e2bc811e3fb60c588032a4" +checksum = "66de2ab9db04757bcedef2b5984fbe536903ada4a8a9766717a4a71197ef34f6" dependencies = [ "cosmwasm-std", "serde", @@ -172,9 +172,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -234,14 +234,14 @@ dependencies = [ [[package]] name = "cw-ics721-incoming-proxy" version = "0.1.0" -source = "git+https://github.com/arkprotocol/cw-ics721-proxy.git?tag=v0.1.0#cc1c156b1c0d2941c62b4291444674baf1ec855b" +source = "git+https://github.com/arkprotocol/cw-ics721-proxy.git?tag=v0.1.0#58ac2ad8dcf70751975758d8b8925f5b009ddaa2" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-ics721-incoming-proxy-derive", "cw-paginate-storage", "cw-storage-plus 1.2.0", - "ics721-types 0.1.0 (git+https://github.com/arkprotocol/ark-cw-ics721?branch=incoming_proxy)", + "ics721-types 0.1.0 (git+https://github.com/public-awesome/cw-ics721?tag=v0.1.5)", "schemars", "serde", "thiserror", @@ -250,7 +250,7 @@ dependencies = [ [[package]] name = "cw-ics721-incoming-proxy-base" version = "0.1.0" -source = "git+https://github.com/arkprotocol/cw-ics721-proxy.git?tag=v0.1.0#cc1c156b1c0d2941c62b4291444674baf1ec855b" +source = "git+https://github.com/arkprotocol/cw-ics721-proxy.git?tag=v0.1.0#58ac2ad8dcf70751975758d8b8925f5b009ddaa2" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -261,7 +261,7 @@ dependencies = [ "cw-storage-plus 1.2.0", "cw2 1.1.2", "cw721 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", - "ics721-types 0.1.0 (git+https://github.com/arkprotocol/ark-cw-ics721?branch=incoming_proxy)", + "ics721-types 0.1.0 (git+https://github.com/public-awesome/cw-ics721?tag=v0.1.5)", "serde", "thiserror", ] @@ -269,11 +269,11 @@ dependencies = [ [[package]] name = "cw-ics721-incoming-proxy-derive" version = "0.1.0" -source = "git+https://github.com/arkprotocol/cw-ics721-proxy.git?tag=v0.1.0#cc1c156b1c0d2941c62b4291444674baf1ec855b" +source = "git+https://github.com/arkprotocol/cw-ics721-proxy.git?tag=v0.1.0#58ac2ad8dcf70751975758d8b8925f5b009ddaa2" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "ics721-types 0.1.0 (git+https://github.com/arkprotocol/ark-cw-ics721?branch=incoming_proxy)", + "ics721-types 0.1.0 (git+https://github.com/public-awesome/cw-ics721?tag=v0.1.5)", "proc-macro2", "quote", "syn 1.0.109", @@ -282,7 +282,7 @@ dependencies = [ [[package]] name = "cw-ics721-outgoing-proxy" version = "0.1.0" -source = "git+https://github.com/arkprotocol/cw-ics721-proxy.git?tag=v0.1.0#cc1c156b1c0d2941c62b4291444674baf1ec855b" +source = "git+https://github.com/arkprotocol/cw-ics721-proxy.git?tag=v0.1.0#58ac2ad8dcf70751975758d8b8925f5b009ddaa2" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -290,14 +290,14 @@ dependencies = [ "cw-storage-plus 1.2.0", "cw721 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", "cw721-base 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", - "ics721-types 0.1.0 (git+https://github.com/arkprotocol/ark-cw-ics721?branch=incoming_proxy)", + "ics721-types 0.1.0 (git+https://github.com/public-awesome/cw-ics721?tag=v0.1.5)", "thiserror", ] [[package]] name = "cw-ics721-outgoing-proxy-derive" version = "0.1.0" -source = "git+https://github.com/arkprotocol/cw-ics721-proxy.git?tag=v0.1.0#cc1c156b1c0d2941c62b4291444674baf1ec855b" +source = "git+https://github.com/arkprotocol/cw-ics721-proxy.git?tag=v0.1.0#58ac2ad8dcf70751975758d8b8925f5b009ddaa2" dependencies = [ "cw721 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", "proc-macro2", @@ -308,7 +308,7 @@ dependencies = [ [[package]] name = "cw-ics721-outgoing-proxy-rate-limit" version = "0.1.0" -source = "git+https://github.com/arkprotocol/cw-ics721-proxy.git?tag=v0.1.0#cc1c156b1c0d2941c62b4291444674baf1ec855b" +source = "git+https://github.com/arkprotocol/cw-ics721-proxy.git?tag=v0.1.0#58ac2ad8dcf70751975758d8b8925f5b009ddaa2" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -319,7 +319,7 @@ dependencies = [ "cw2 1.1.2", "cw721 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", "cw721-base 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", - "ics721-types 0.1.0 (git+https://github.com/arkprotocol/ark-cw-ics721?branch=incoming_proxy)", + "ics721-types 0.1.0 (git+https://github.com/public-awesome/cw-ics721?tag=v0.1.5)", "serde", "thiserror", ] @@ -336,7 +336,7 @@ dependencies = [ "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "derivative", - "itertools 0.12.0", + "itertools 0.12.1", "prost", "schemars", "serde", @@ -373,10 +373,9 @@ dependencies = [ [[package]] name = "cw-paginate-storage" version = "2.4.0" -source = "git+https://github.com/DA0-DA0/dao-contracts.git#3ead037e9366d922c862614974b16b4dc732b329" +source = "git+https://github.com/DA0-DA0/dao-contracts.git#43eccded799ea63ea19a81ed64054848e689ff73" dependencies = [ "cosmwasm-std", - "cosmwasm-storage", "cw-storage-plus 1.2.0", "serde", ] @@ -395,7 +394,7 @@ dependencies = [ [[package]] name = "cw-rate-limiter" version = "0.1.0" -source = "git+https://github.com/arkprotocol/cw-ics721-proxy.git?tag=v0.1.0#cc1c156b1c0d2941c62b4291444674baf1ec855b" +source = "git+https://github.com/arkprotocol/cw-ics721-proxy.git?tag=v0.1.0#58ac2ad8dcf70751975758d8b8925f5b009ddaa2" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -514,7 +513,7 @@ dependencies = [ [[package]] name = "cw721" version = "0.18.0" -source = "git+https://github.com/CosmWasm/cw-nfts?branch=main#cc27e27d54e237709ebdcd478568126e109a8845" +source = "git+https://github.com/CosmWasm/cw-nfts?branch=main#e63a7bbb620e6ea39224bb5580967477066cb7ae" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -562,7 +561,7 @@ dependencies = [ [[package]] name = "cw721-base" version = "0.18.0" -source = "git+https://github.com/CosmWasm/cw-nfts?branch=main#cc27e27d54e237709ebdcd478568126e109a8845" +source = "git+https://github.com/CosmWasm/cw-nfts?branch=main#e63a7bbb620e6ea39224bb5580967477066cb7ae" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -729,9 +728,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", @@ -841,7 +840,7 @@ dependencies = [ [[package]] name = "ics721-types" version = "0.1.0" -source = "git+https://github.com/arkprotocol/ark-cw-ics721?branch=incoming_proxy#1fadd3b4869b7cdcbc54128d34ad86b2fdfed0b2" +source = "git+https://github.com/public-awesome/cw-ics721?tag=v0.1.5#f1dfefc71c3ace567a5b79e98100ee17d9cfcc5d" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -872,9 +871,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] @@ -887,9 +886,9 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "k256" -version = "0.13.2" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f01b677d82ef7a676aa37e099defd83a28e15687112cafdd112d60236b6115b" +checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" dependencies = [ "cfg-if", "ecdsa", @@ -901,9 +900,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.151" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "once_cell" @@ -935,9 +934,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.74" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2de98502f212cfcea8d0bb305bd0f49d7ebdd75b64ba0a68f937d888f4e0d6db" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] @@ -962,7 +961,7 @@ dependencies = [ "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -1051,31 +1050,31 @@ checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" [[package]] name = "serde" -version = "1.0.194" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] [[package]] name = "serde-json-wasm" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16a62a1fad1e1828b24acac8f2b468971dade7b8c3c2e672bcadefefb1f8c137" +checksum = "9e9213a07d53faa0b8dd81e767a54a8188a242fdb9be99ab75ec576a774bfdd7" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.194" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -1091,9 +1090,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.110" +version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fbd975230bada99c8bb618e0c365c2eefa219158d5c6c29610fd09ff1833257" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" dependencies = [ "itoa", "ryu", @@ -1142,9 +1141,9 @@ dependencies = [ [[package]] name = "sg721" -version = "3.3.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65af43465c79bc5a2af7b41a526876cd757a6ba2a9051bbeb2f95133285f89dc" +checksum = "8f59f52a646afc7e20dd55a873df667c6c995deb7495c6cf9b0f3d8f340dd227" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1157,9 +1156,9 @@ dependencies = [ [[package]] name = "sg721-base" -version = "3.3.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcaffbe4a8d278a54c3ac4dcce30aa971cd55bce8184b43b6344a45cff9eea48" +checksum = "0e903e3e9bd2f8641d03a7ef3e9e40a7188f655d9e1cdfd220ba7c01e8d0b35b" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1245,9 +1244,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.46" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89456b690ff72fddcecf231caedbe615c59480c93358a93dfae7fc29e3ebbf0e" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -1271,7 +1270,7 @@ checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -1297,9 +1296,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" diff --git a/contracts/cw721-tester/src/lib.rs b/contracts/cw721-tester/src/lib.rs index dbbfc474..d7b74e5a 100644 --- a/contracts/cw721-tester/src/lib.rs +++ b/contracts/cw721-tester/src/lib.rs @@ -39,7 +39,7 @@ pub fn instantiate( msg::InstantiateMsg { name: msg.name, symbol: msg.symbol, - minter: msg.minter, + minter: Some(msg.minter), withdraw_address: None, }, )?; diff --git a/packages/ics721/src/execute.rs b/packages/ics721/src/execute.rs index 86c9ac65..7feb62b2 100644 --- a/packages/ics721/src/execute.rs +++ b/packages/ics721/src/execute.rs @@ -581,7 +581,7 @@ where let mut instantiate_msg = cw721_base::msg::InstantiateMsg { name: class.id.clone().into(), symbol: class.id.clone().into(), - minter: env.contract.address.to_string(), + minter: Some(env.contract.address.to_string()), withdraw_address: Some(creator), }; diff --git a/packages/ics721/src/testing/integration_tests.rs b/packages/ics721/src/testing/integration_tests.rs index 7d97b902..cf3dcf30 100644 --- a/packages/ics721/src/testing/integration_tests.rs +++ b/packages/ics721/src/testing/integration_tests.rs @@ -370,7 +370,7 @@ impl Test { &Cw721InstantiateMsg { name: "name".to_string(), symbol: "symbol".to_string(), - minter: source_cw721_owner.to_string(), + minter: Some(source_cw721_owner.to_string()), withdraw_address: None, }, &[], @@ -1800,11 +1800,12 @@ fn test_proxy_authorized() { &cw721_base::InstantiateMsg { name: "token".to_string(), symbol: "nonfungible".to_string(), - minter: test - .app - .api() - .addr_make(COLLECTION_OWNER_SOURCE_CHAIN) - .to_string(), + minter: Some( + test.app + .api() + .addr_make(COLLECTION_OWNER_SOURCE_CHAIN) + .to_string(), + ), withdraw_address: None, }, &[], From 7ecfb36ba1ab46682ebe5d1c1f9176424ab0d164 Mon Sep 17 00:00:00 2001 From: mr-t Date: Mon, 12 Feb 2024 12:01:49 +0100 Subject: [PATCH 07/11] typo --- packages/ics721/src/state.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ics721/src/state.rs b/packages/ics721/src/state.rs index a6a1dad0..4c4936fa 100644 --- a/packages/ics721/src/state.rs +++ b/packages/ics721/src/state.rs @@ -63,9 +63,9 @@ pub struct UniversalNftInfoResponse { } /// Collection data send by ICS721 on source chain. It is an optional class data for interchain transfer to target chain. -/// ICS721 on target chain is free to use this data or not. Lik in case of `sg721-base` it uses owner for defining creator in collection info. +/// ICS721 on target chain is free to use this data or not. Like in case of `sg721-base` it uses owner for defining creator in collection info. /// `ics721-base` uses name and symbol for instantiating new cw721 contract. -// NB: Please not cw_serde includes `deny_unknown_fields`: https://github.com/CosmWasm/cosmwasm/blob/v1.5.0/packages/schema-derive/src/cw_serde.rs +// NB: Please note cw_serde includes `deny_unknown_fields`: https://github.com/CosmWasm/cosmwasm/blob/v1.5.0/packages/schema-derive/src/cw_serde.rs // For incoming data, parsing needs to be more lenient/less strict, so we use `serde` directly. #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, PartialEq)] #[allow(clippy::derive_partial_eq_without_eq)] From 2d19b4d4cb4c8faea008bf62b74cbe5c8a7e7d7d Mon Sep 17 00:00:00 2001 From: mr-t Date: Wed, 14 Feb 2024 16:22:11 +0100 Subject: [PATCH 08/11] use Stargaze icon as placeholder --- contracts/sg-ics721/src/execute.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/sg-ics721/src/execute.rs b/contracts/sg-ics721/src/execute.rs index a937700d..8085bdbd 100644 --- a/contracts/sg-ics721/src/execute.rs +++ b/contracts/sg-ics721/src/execute.rs @@ -48,7 +48,9 @@ impl Ics721Execute for SgIcs721Contract { // therefore, we use ics721 creator as owner creator: ics721_contract_info.creator, description: "".to_string(), - image: "https://arkprotocol.io".to_string(), + // use Stargaze icon as placeholder + image: "ipfs://bafkreie5vwrm5zts4wiq6ebtopmztgl5qzyl4uszyllgwpaizyc5w2uycm" + .to_string(), external_link: None, explicit_content: None, start_trading_time: None, From e8e3c1ff4bcb578fd968fc84e5f10e277a431bd4 Mon Sep 17 00:00:00 2001 From: mr-t Date: Thu, 15 Feb 2024 21:33:30 +0100 Subject: [PATCH 09/11] move to constant --- contracts/sg-ics721/src/execute.rs | 5 ++-- contracts/sg-ics721/src/state.rs | 3 +++ .../src/testing/integration_tests.rs | 27 ++++++++++--------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/contracts/sg-ics721/src/execute.rs b/contracts/sg-ics721/src/execute.rs index 8085bdbd..2bcfba51 100644 --- a/contracts/sg-ics721/src/execute.rs +++ b/contracts/sg-ics721/src/execute.rs @@ -4,7 +4,7 @@ use ics721_types::token_types::Class; use sg721_base::msg::{CollectionInfoResponse, QueryMsg}; -use crate::state::{SgCollectionData, SgIcs721Contract}; +use crate::state::{SgCollectionData, SgIcs721Contract, STARGAZE_ICON_PLACEHOLDER}; impl Ics721Execute for SgIcs721Contract { type ClassData = SgCollectionData; @@ -49,8 +49,7 @@ impl Ics721Execute for SgIcs721Contract { creator: ics721_contract_info.creator, description: "".to_string(), // use Stargaze icon as placeholder - image: "ipfs://bafkreie5vwrm5zts4wiq6ebtopmztgl5qzyl4uszyllgwpaizyc5w2uycm" - .to_string(), + image: STARGAZE_ICON_PLACEHOLDER.to_string(), external_link: None, explicit_content: None, start_trading_time: None, diff --git a/contracts/sg-ics721/src/state.rs b/contracts/sg-ics721/src/state.rs index 3c8b32cd..e2f7c731 100644 --- a/contracts/sg-ics721/src/state.rs +++ b/contracts/sg-ics721/src/state.rs @@ -2,6 +2,9 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::ContractInfoResponse; use sg721_base::msg::CollectionInfoResponse; +pub const STARGAZE_ICON_PLACEHOLDER: &str = + "ipfs://bafkreie5vwrm5zts4wiq6ebtopmztgl5qzyl4uszyllgwpaizyc5w2uycm"; + /// Collection data provided by the (source) cw721 contract. This is pass as optional class data during interchain transfer to target chain. /// ICS721 on target chain is free to use this data or not. Lik in case of `sg721-base` it uses owner for defining creator in collection info. #[cw_serde] diff --git a/contracts/sg-ics721/src/testing/integration_tests.rs b/contracts/sg-ics721/src/testing/integration_tests.rs index 796a0619..f5af98a7 100644 --- a/contracts/sg-ics721/src/testing/integration_tests.rs +++ b/contracts/sg-ics721/src/testing/integration_tests.rs @@ -30,7 +30,10 @@ use sg721::InstantiateMsg as Sg721InstantiateMsg; use sg721_base::msg::{CollectionInfoResponse, QueryMsg as Sg721QueryMsg}; use sha2::{digest::Update, Digest, Sha256}; -use crate::{state::SgCollectionData, ContractError, SgIcs721Contract}; +use crate::{ + state::{SgCollectionData, STARGAZE_ICON_PLACEHOLDER}, + ContractError, SgIcs721Contract, +}; const ICS721_CREATOR: &str = "ics721-creator"; const CONTRACT_NAME: &str = "crates.io:sg-ics721"; @@ -371,7 +374,7 @@ impl Test { collection_info: sg721::CollectionInfo { creator: source_cw721_owner.to_string(), description: "".to_string(), - image: "https://arkprotocol.io".to_string(), + image: STARGAZE_ICON_PLACEHOLDER.to_string(), external_link: None, explicit_content: None, start_trading_time: None, @@ -728,7 +731,7 @@ fn test_do_instantiate_and_mint() { // creator of ics721 contract is also creator of collection, since no owner in ClassData provided creator: test.app.api().addr_make(ICS721_CREATOR).to_string(), description: "".to_string(), - image: "https://arkprotocol.io".to_string(), + image: STARGAZE_ICON_PLACEHOLDER.to_string(), external_link: None, explicit_content: None, start_trading_time: None, @@ -904,7 +907,7 @@ fn test_do_instantiate_and_mint() { // creator based on owner from collection in soure chain creator: test.app.api().addr_make(ICS721_CREATOR).to_string(), description: "".to_string(), - image: "https://arkprotocol.io".to_string(), + image: STARGAZE_ICON_PLACEHOLDER.to_string(), external_link: None, explicit_content: None, start_trading_time: None, @@ -1078,7 +1081,7 @@ fn test_do_instantiate_and_mint() { // creator of ics721 contract is creator of nft contract, since no owner in ClassData provided creator: test.app.api().addr_make(ICS721_CREATOR).to_string(), description: "".to_string(), - image: "https://arkprotocol.io".to_string(), + image: STARGAZE_ICON_PLACEHOLDER.to_string(), external_link: None, explicit_content: None, start_trading_time: None, @@ -1256,7 +1259,7 @@ fn test_do_instantiate_and_mint() { // creator of ics721 contract is creator of nft contract, since no owner in ClassData provided creator: test.app.api().addr_make(ICS721_CREATOR).to_string(), description: "".to_string(), - image: "https://arkprotocol.io".to_string(), + image: STARGAZE_ICON_PLACEHOLDER.to_string(), external_link: None, explicit_content: None, start_trading_time: None, @@ -1434,7 +1437,7 @@ fn test_do_instantiate_and_mint() { // creator of ics721 contract is creator of nft contract, since no owner in ClassData provided creator: test.app.api().addr_make(ICS721_CREATOR).to_string(), description: "".to_string(), - image: "https://arkprotocol.io".to_string(), + image: STARGAZE_ICON_PLACEHOLDER.to_string(), external_link: None, explicit_content: None, start_trading_time: None, @@ -1666,7 +1669,7 @@ fn test_do_instantiate_and_mint_2_different_collections() { // creator of ics721 contract is also creator of collection, since no owner in ClassData provided creator: test.app.api().addr_make(ICS721_CREATOR).to_string(), description: "".to_string(), - image: "https://arkprotocol.io".to_string(), + image: STARGAZE_ICON_PLACEHOLDER.to_string(), external_link: None, explicit_content: None, start_trading_time: None, @@ -1679,7 +1682,7 @@ fn test_do_instantiate_and_mint_2_different_collections() { // creator of ics721 contract is also creator of collection, since no owner in ClassData provided creator: test.app.api().addr_make(ICS721_CREATOR).to_string(), description: "".to_string(), - image: "https://arkprotocol.io".to_string(), + image: STARGAZE_ICON_PLACEHOLDER.to_string(), external_link: None, explicit_content: None, start_trading_time: None, @@ -1926,7 +1929,7 @@ fn test_do_instantiate_and_mint_no_instantiate() { CollectionInfoResponse { creator: test.app.api().addr_make(ICS721_CREATOR).to_string(), description: "".to_string(), - image: "https://arkprotocol.io".to_string(), + image: STARGAZE_ICON_PLACEHOLDER.to_string(), external_link: None, explicit_content: None, start_trading_time: None, @@ -2108,7 +2111,7 @@ fn test_proxy_authorized() { .addr_make(COLLECTION_OWNER_SOURCE_CHAIN) .to_string(), description: "".to_string(), - image: "https://arkprotocol.io".to_string(), + image: STARGAZE_ICON_PLACEHOLDER.to_string(), external_link: None, explicit_content: None, start_trading_time: None, @@ -2234,7 +2237,7 @@ fn test_receive_nft() { collection_info: Some(CollectionInfoResponse { creator: test.ics721.to_string(), description: "".to_string(), - image: "https://arkprotocol.io".to_string(), + image: STARGAZE_ICON_PLACEHOLDER.to_string(), external_link: None, explicit_content: None, start_trading_time: None, From bbc19e8fa00eedaade76ec7c2047f6fb70e7d7f8 Mon Sep 17 00:00:00 2001 From: mr-t Date: Thu, 15 Feb 2024 21:49:13 +0100 Subject: [PATCH 10/11] move to dedicated function --- packages/ics721/src/execute.rs | 185 ++++++++++++++++++--------------- 1 file changed, 101 insertions(+), 84 deletions(-) diff --git a/packages/ics721/src/execute.rs b/packages/ics721/src/execute.rs index 7feb62b2..cb84861c 100644 --- a/packages/ics721/src/execute.rs +++ b/packages/ics721/src/execute.rs @@ -500,52 +500,6 @@ where create: VoucherCreation, ) -> Result, ContractError> { let VoucherCreation { class, tokens } = create; - let instantiate = - if CLASS_ID_AND_NFT_CONTRACT_INFO.has(deps.storage, class.id.to_string().as_str()) { - vec![] - } else { - let class_id = ClassId::new(class.id.clone()); - let cw721_code_id = CW721_CODE_ID.load(deps.storage)?; - // for creating a predictable nft contract using, using instantiate2, we need: checksum, creator, and salt: - // - using class id as salt for instantiating nft contract guarantees a) predictable address and b) uniqueness - // for this salt must be of length 32 bytes, so we use sha256 to hash class id - let mut hasher = Sha256::new(); - hasher.update(class_id.as_bytes()); - let salt = hasher.finalize().to_vec(); - - let nft_contract = get_instantiate2_address( - deps.as_ref(), - env.contract.address.as_str(), - &salt, - cw721_code_id, - )?; - - // Save classId <-> contract mappings. - let class_id_info = ClassIdInfo { - class_id: class_id.clone(), - address: nft_contract.clone(), - }; - CLASS_ID_AND_NFT_CONTRACT_INFO.save(deps.storage, &class.id, &class_id_info)?; - - let admin = ADMIN_USED_FOR_CW721 - .load(deps.storage)? - .map(|a| a.to_string()); - let message = SubMsg::::reply_on_success( - WasmMsg::Instantiate2 { - admin, - code_id: cw721_code_id, - msg: self.init_msg(deps.as_ref(), &env, &class)?, - funds: vec![], - // Attempting to fit the class ID in the label field - // can make this field too long which causes data - // errors in the SDK. - label: "ics-721 debt-voucher cw-721".to_string(), - salt: salt.into(), - }, - INSTANTIATE_CW721_REPLY_ID, - ); - vec![message] - }; // Store mapping from classID to classURI. Notably, we don't check // if this has already been set. If a new NFT belonging to a class @@ -555,21 +509,76 @@ where CLASS_ID_TO_CLASS.save(deps.storage, class.id.clone(), &class)?; let mint = WasmMsg::Execute { - contract_addr: env.contract.address.into_string(), + contract_addr: env.contract.address.to_string(), msg: to_json_binary(&ExecuteMsg::Callback(CallbackMsg::Mint { - class_id: class.id, + class_id: class.id.clone(), receiver, tokens, }))?, funds: vec![], }; + let instantiate = self.create_instantiate_msg(deps, &env, class.clone())?; + Ok(Response::::default() .add_attribute("method", "callback_create_vouchers") .add_submessages(instantiate) .add_message(mint)) } + fn create_instantiate_msg( + &self, + deps: DepsMut, + env: &Env, + class: Class, + ) -> Result>, ContractError> { + if CLASS_ID_AND_NFT_CONTRACT_INFO.has(deps.storage, class.id.to_string().as_str()) { + Ok(vec![]) + } else { + let class_id = ClassId::new(class.id.clone()); + let cw721_code_id = CW721_CODE_ID.load(deps.storage)?; + // for creating a predictable nft contract using, using instantiate2, we need: checksum, creator, and salt: + // - using class id as salt for instantiating nft contract guarantees a) predictable address and b) uniqueness + // for this salt must be of length 32 bytes, so we use sha256 to hash class id + let mut hasher = Sha256::new(); + hasher.update(class_id.as_bytes()); + let salt = hasher.finalize().to_vec(); + + let nft_contract = get_instantiate2_address( + deps.as_ref(), + env.contract.address.as_str(), + &salt, + cw721_code_id, + )?; + + // Save classId <-> contract mappings. + let class_id_info = ClassIdInfo { + class_id: class_id.clone(), + address: nft_contract.clone(), + }; + CLASS_ID_AND_NFT_CONTRACT_INFO.save(deps.storage, &class.id, &class_id_info)?; + + let admin = ADMIN_USED_FOR_CW721 + .load(deps.storage)? + .map(|a| a.to_string()); + let message = SubMsg::::reply_on_success( + WasmMsg::Instantiate2 { + admin, + code_id: cw721_code_id, + msg: self.init_msg(deps.as_ref(), env, &class)?, + funds: vec![], + // Attempting to fit the class ID in the label field + // can make this field too long which causes data + // errors in the SDK. + label: "ics-721 debt-voucher cw-721".to_string(), + salt: salt.into(), + }, + INSTANTIATE_CW721_REPLY_ID, + ); + Ok(vec![message]) + } + } + /// Default implementation using `cw721_base::msg::InstantiateMsg` fn init_msg(&self, deps: Deps, env: &Env, class: &Class) -> StdResult { // use ics721 creator for withdraw address @@ -761,46 +770,54 @@ where ), ); - // we migrate only in case CLASS_ID_AND_NFT_CONTRACT_INFO is not populated yet - // TODO once migrated: - // - this complete block can be deleted - // - legacy map 'e' and 'f' can be deleted - let is_empty = query_nft_contracts(deps.as_ref(), None, None) - .map(|nft_contracts| nft_contracts.is_empty())?; - if is_empty { - // - get legacy map and migrate it to new indexed map - let legacy_nft_contract_to_class_id: Map = Map::new("f"); - match cw_paginate_storage::paginate_map( - deps.as_ref(), - &legacy_nft_contract_to_class_id, - None, - None, - Order::Ascending, - ) { - Ok(nft_contract_and_class_id) => { - let response = response.add_attribute( - "migrated nft contracts", - nft_contract_and_class_id.len().to_string(), - ); - for (nft_contract, class_id) in nft_contract_and_class_id { - let class_id_info = ClassIdInfo { - class_id: class_id.clone(), - address: nft_contract.clone(), - }; - CLASS_ID_AND_NFT_CONTRACT_INFO.save( - deps.storage, - &class_id, - &class_id_info, - )?; - } - Ok(response) - } - Err(err) => Err(ContractError::Std(err)), + self.migrate_legacy(deps, response) + } + } + } + + // TODO once migrated: + // - this complete block can be deleted + // - legacy map 'e' and 'f' can be deleted + fn migrate_legacy( + &self, + deps: DepsMut, + response: Response, + ) -> Result, ContractError> { + // we migrate only in case CLASS_ID_AND_NFT_CONTRACT_INFO is not populated yet + let is_empty = query_nft_contracts(deps.as_ref(), None, None) + .map(|nft_contracts| nft_contracts.is_empty())?; + if is_empty { + // - get legacy map and migrate it to new indexed map + let legacy_nft_contract_to_class_id: Map = Map::new("f"); + match cw_paginate_storage::paginate_map( + deps.as_ref(), + &legacy_nft_contract_to_class_id, + None, + None, + Order::Ascending, + ) { + Ok(nft_contract_and_class_id) => { + let response = response.add_attribute( + "migrated nft contracts", + nft_contract_and_class_id.len().to_string(), + ); + for (nft_contract, class_id) in nft_contract_and_class_id { + let class_id_info = ClassIdInfo { + class_id: class_id.clone(), + address: nft_contract.clone(), + }; + CLASS_ID_AND_NFT_CONTRACT_INFO.save( + deps.storage, + &class_id, + &class_id_info, + )?; } - } else { Ok(response) } + Err(err) => Err(ContractError::Std(err)), } + } else { + Ok(response) } } } From 9ef9bd76f25a37a5be88b48c2f949ec0203c29b5 Mon Sep 17 00:00:00 2001 From: mr-t Date: Fri, 16 Feb 2024 10:59:10 +0100 Subject: [PATCH 11/11] docs --- packages/ics721/src/state.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/ics721/src/state.rs b/packages/ics721/src/state.rs index 4c4936fa..8d297d0e 100644 --- a/packages/ics721/src/state.rs +++ b/packages/ics721/src/state.rs @@ -88,9 +88,12 @@ pub struct UniversalOwnerOfResponse { pub approvals: Vec, } +/// ClassIdInfo is used to store associated ClassId for given collection/cw721 address. #[cw_serde] pub struct ClassIdInfo { + /// Associated class_id for a given collection/CW721 address. pub class_id: ClassId, + /// Associated collection/CW721 address for a given class_id. pub address: Addr, }