Skip to content

Commit

Permalink
transfer onchain metadata in case NFT is on home chain
Browse files Browse the repository at this point in the history
  • Loading branch information
taitruong committed Aug 13, 2024
1 parent cfe65dc commit dcdc99c
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 24 deletions.
2 changes: 1 addition & 1 deletion packages/ics721-types/src/token_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub struct Token {
pub id: TokenId,
/// Optional URI pointing to off-chain metadata about the token.
pub uri: Option<String>,
/// Optional base64 encoded metadata about the token.
/// Optional base64 encoded onchain metadata about the token.
pub data: Option<Binary>,
}

Expand Down
25 changes: 17 additions & 8 deletions packages/ics721/src/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ use crate::{
state::{
ClassIdInfo, CollectionData, UniversalAllNftInfoResponse, CLASS_ID_AND_NFT_CONTRACT_INFO,
CLASS_ID_TO_CLASS, CONTRACT_ADDR_LENGTH, CW721_ADMIN, CW721_CODE_ID,
INCOMING_CLASS_TOKEN_TO_CHANNEL, INCOMING_PROXY, OUTGOING_CLASS_TOKEN_TO_CHANNEL,
OUTGOING_PROXY, PO, TOKEN_METADATA,
IBC_RECEIVE_TOKEN_METADATA, INCOMING_CLASS_TOKEN_TO_CHANNEL, INCOMING_PROXY,
OUTGOING_CLASS_TOKEN_TO_CHANNEL, OUTGOING_PROXY, PO,
},
token_types::{VoucherCreation, VoucherRedemption},
ContractError,
Expand Down Expand Up @@ -179,7 +179,7 @@ where
// remove incoming channel entry and metadata
INCOMING_CLASS_TOKEN_TO_CHANNEL
.remove(deps.storage, (child_class_id.clone(), token_id.clone()));
TOKEN_METADATA.remove(deps.storage, (child_class_id.clone(), token_id.clone()));
IBC_RECEIVE_TOKEN_METADATA.remove(deps.storage, (child_class_id.clone(), token_id.clone()));

// check NFT on child collection owned by recipient
let maybe_nft_info: Option<UniversalAllNftInfoResponse> = deps
Expand Down Expand Up @@ -411,12 +411,17 @@ where
return Err(ContractError::NotEscrowedByIcs721(access.owner));
}

// cw721 doesn't support on-chain metadata yet
// here NFT is transferred to another chain, NFT itself may have been transferred to his chain before
// here NFT was transferred before, in this case it is stored in the storage, otherwise this is the home chain,
// and the NFT is transferred for the first time and onchain data comes from the cw721 contract
// in this case ICS721 may have metadata stored
let token_metadata = TOKEN_METADATA
let token_metadata = match IBC_RECEIVE_TOKEN_METADATA
.may_load(deps.storage, (class.id.clone(), token_id.clone()))?
.flatten();
.flatten()
{
Some(metadata) => Some(metadata),
// incase there is none in the storage, we use the one from the cw721 contract
None => info.extension.map(|ext| to_json_binary(&ext)).transpose()?,
};

let ibc_message = NonFungibleTokenPacketData {
class_id: class.id.clone(),
Expand Down Expand Up @@ -713,7 +718,11 @@ where
// Note, once cw721 doesn't support on-chain metadata yet - but this is where we will set
// that value on the debt-voucher token once it is supported.
// Also note that this is set for every token, regardless of if data is None.
TOKEN_METADATA.save(deps.storage, (class_id.clone(), id.clone()), &data)?;
IBC_RECEIVE_TOKEN_METADATA.save(
deps.storage,
(class_id.clone(), id.clone()),
&data,
)?;

let msg = cw721_metadata_onchain::msg::ExecuteMsg::Mint {
token_id: id.into(),
Expand Down
7 changes: 4 additions & 3 deletions packages/ics721/src/ibc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ use crate::{
ibc_packet_receive::receive_ibc_packet,
query::{load_class_id_for_nft_contract, load_nft_contract_for_class_id},
state::{
INCOMING_CLASS_TOKEN_TO_CHANNEL, INCOMING_PROXY, OUTGOING_CLASS_TOKEN_TO_CHANNEL,
OUTGOING_PROXY, TOKEN_METADATA,
IBC_RECEIVE_TOKEN_METADATA, INCOMING_CLASS_TOKEN_TO_CHANNEL, INCOMING_PROXY,
OUTGOING_CLASS_TOKEN_TO_CHANNEL, OUTGOING_PROXY,
},
ContractError,
};
Expand Down Expand Up @@ -138,7 +138,8 @@ where
if returning_to_source {
// This token's journey is complete, for now.
INCOMING_CLASS_TOKEN_TO_CHANNEL.remove(deps.storage, key);
TOKEN_METADATA.remove(deps.storage, (msg.class_id.clone(), token.clone()));
IBC_RECEIVE_TOKEN_METADATA
.remove(deps.storage, (msg.class_id.clone(), token.clone()));

messages.push(WasmMsg::Execute {
contract_addr: nft_contract.to_string(),
Expand Down
13 changes: 6 additions & 7 deletions packages/ics721/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ use crate::{
helpers::get_instantiate2_address,
msg::QueryMsg,
state::{
UniversalAllNftInfoResponse, CW721_ADMIN, CLASS_ID_AND_NFT_CONTRACT_INFO,
CLASS_ID_TO_CLASS, CONTRACT_ADDR_LENGTH, CW721_CODE_ID, INCOMING_CLASS_TOKEN_TO_CHANNEL,
INCOMING_PROXY, OUTGOING_CLASS_TOKEN_TO_CHANNEL, OUTGOING_PROXY, PO, TOKEN_METADATA,
UniversalAllNftInfoResponse, CLASS_ID_AND_NFT_CONTRACT_INFO, CLASS_ID_TO_CLASS,
CONTRACT_ADDR_LENGTH, CW721_ADMIN, CW721_CODE_ID, IBC_RECEIVE_TOKEN_METADATA,
INCOMING_CLASS_TOKEN_TO_CHANNEL, INCOMING_PROXY, OUTGOING_CLASS_TOKEN_TO_CHANNEL,
OUTGOING_PROXY, PO,
},
ContractError,
};
Expand Down Expand Up @@ -46,9 +47,7 @@ pub trait Ics721Query {
QueryMsg::OutgoingProxy {} => Ok(to_json_binary(&OUTGOING_PROXY.load(deps.storage)?)?),
QueryMsg::IncomingProxy {} => Ok(to_json_binary(&INCOMING_PROXY.load(deps.storage)?)?),
QueryMsg::Cw721CodeId {} => Ok(to_json_binary(&query_cw721_code_id(deps)?)?),
QueryMsg::Cw721Admin {} => {
Ok(to_json_binary(&CW721_ADMIN.load(deps.storage)?)?)
}
QueryMsg::Cw721Admin {} => Ok(to_json_binary(&CW721_ADMIN.load(deps.storage)?)?),
QueryMsg::ContractAddrLength {} => Ok(to_json_binary(
&CONTRACT_ADDR_LENGTH.may_load(deps.storage)?,
)?),
Expand Down Expand Up @@ -138,7 +137,7 @@ pub fn query_token_metadata(
let class_id = ClassId::new(class_id);

let Some(token_metadata) =
TOKEN_METADATA.may_load(deps.storage, (class_id.clone(), token_id.clone()))?
IBC_RECEIVE_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.
Expand Down
5 changes: 4 additions & 1 deletion packages/ics721/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,15 @@ pub const OUTGOING_CLASS_TOKEN_TO_CHANNEL: Map<(ClassId, TokenId), String> = Map
/// Same as above, but for NFTs arriving at this contract.
pub const INCOMING_CLASS_TOKEN_TO_CHANNEL: Map<(ClassId, TokenId), String> = Map::new("i");

/// IMPORTANT: collections can either come from (a) smart contracts or (b) nft module.
/// This map is the truth of source. Only for smart contracts and in case of `receive_nft`
/// onchain data is retrieved directly from cw721 contract and stored in this map during ibc receive.
/// Maps (class ID, token ID) -> token metadata. Used to store
/// on-chain metadata for tokens that have arrived from other
/// chains. When a token arrives, it's metadata (regardless of if it
/// is `None`) is stored in this map. When the token is returned to
/// it's source chain, the metadata is removed from the map.
pub const TOKEN_METADATA: Map<(ClassId, TokenId), Option<Binary>> = Map::new("j");
pub const IBC_RECEIVE_TOKEN_METADATA: Map<(ClassId, TokenId), Option<Binary>> = Map::new("j");

/// The admin address for instantiating new cw721 contracts. In case of None, contract is immutable.
pub const CW721_ADMIN: Item<Option<Addr>> = Item::new("l");
Expand Down
2 changes: 1 addition & 1 deletion packages/ics721/src/testing/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use ics721_types::{
token_types::{Class, ClassId, Token, TokenId},
};

use super::contract::Ics721Contract;
use super::unit_tests::Ics721Contract;

const ICS721_CREATOR: &str = "ics721-creator";
const ICS721_ADMIN: &str = "ics721-admin";
Expand Down
2 changes: 1 addition & 1 deletion packages/ics721/src/testing/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
mod contract;
mod ibc_tests;
pub mod integration_tests;
mod unit_tests;
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use cw721_metadata_onchain::msg::QueryMsg;
use cw_cii::ContractInstantiateInfo;
use cw_ownable::Ownership;
use cw_storage_plus::Map;
use serde::{Deserialize, Serialize};

use crate::{
execute::Ics721Execute,
Expand All @@ -27,7 +28,8 @@ use crate::{
},
state::{
CollectionData, CLASS_ID_TO_CLASS, CONTRACT_ADDR_LENGTH, CW721_ADMIN, CW721_CODE_ID,
INCOMING_PROXY, OUTGOING_CLASS_TOKEN_TO_CHANNEL, OUTGOING_PROXY, PO,
IBC_RECEIVE_TOKEN_METADATA, INCOMING_PROXY, OUTGOING_CLASS_TOKEN_TO_CHANNEL,
OUTGOING_PROXY, PO,
},
utils::get_collection_data,
};
Expand Down Expand Up @@ -117,6 +119,9 @@ fn mock_querier(query: &WasmQuery) -> QuerierResult {
info: NftInfoResponse {
token_uri: Some("https://moonphase.is/image.svg".to_string()),
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()
}),
},
Expand Down Expand Up @@ -326,7 +331,132 @@ fn test_receive_nft() {
class_uri: None,
class_data: Some(to_json_binary(&expected_class_data).unwrap()),
token_ids: vec![TokenId::new(token_id)],
token_data: None,
token_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()
})
.unwrap()]
.to_vec()
),
token_uris: Some(vec!["https://moonphase.is/image.svg".to_string()]),
sender,
receiver: "callum".to_string(),
memo: None,
}
);
}
_ => panic!("unexpected message type"),
}

// check outgoing classID and tokenID
let keys = OUTGOING_CLASS_TOKEN_TO_CHANNEL
.keys(deps.as_mut().storage, None, None, Order::Ascending)
.collect::<StdResult<Vec<(String, String)>>>()
.unwrap();
assert_eq!(keys, [(NFT_CONTRACT_1.to_string(), token_id.to_string())]);

// check channel
let key = (
ClassId::new(keys[0].clone().0),
TokenId::new(keys[0].clone().1),
);
assert_eq!(
OUTGOING_CLASS_TOKEN_TO_CHANNEL
.load(deps.as_mut().storage, key)
.unwrap(),
channel_id
)
}
// test case: receive nft with metadata from IBC_RECEIVE_TOKEN_METADATA storage
{
let mut querier = MockQuerier::default();
querier.update_wasm(mock_querier);

let mut deps = mock_dependencies();
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
struct UnknownMetadata {
pub unknown: String,
}
let token_id = "1";
IBC_RECEIVE_TOKEN_METADATA
.save(
deps.as_mut().storage,
(ClassId::new(NFT_CONTRACT_1), TokenId::new(token_id)),
&Some(to_json_binary(&UnknownMetadata {
unknown: "unknown".to_string(),
}))
.transpose()
.unwrap(),
)
.unwrap();
deps.querier = querier;
let env = mock_env();

let info = mock_info(NFT_CONTRACT_1, &[]);
let sender = "ekez".to_string();
let msg = to_json_binary(&IbcOutgoingMsg {
receiver: "callum".to_string(),
channel_id: "channel-1".to_string(),
timeout: IbcTimeout::with_timestamp(Timestamp::from_seconds(42)),
memo: None,
})
.unwrap();

let res: cosmwasm_std::Response<_> = Ics721Contract::default()
.receive_nft(
deps.as_mut(),
env,
&info.sender,
TokenId::new(token_id),
sender.clone(),
msg,
)
.unwrap();
assert_eq!(res.messages.len(), 1);

let channel_id = "channel-1".to_string();
let sub_msg = res.messages[0].clone();
match sub_msg.msg {
CosmosMsg::Ibc(IbcMsg::SendPacket { data, .. }) => {
let packet_data: NonFungibleTokenPacketData = from_json(data).unwrap();
let class_data: CollectionData =
from_json(packet_data.class_data.clone().unwrap()).unwrap();
let expected_class_data = CollectionData {
owner: Some(OWNER_ADDR.to_string()),
contract_info: Some(expected_contract_info.clone()),
name: "name".to_string(),
symbol: "symbol".to_string(),
extension: Some(CollectionExtension {
description: "description".to_string(),
explicit_content: Some(false),
external_link: Some("https://interchain.arkprotocol.io".to_string()),
image: "https://ark.pass/image.png".to_string(),
royalty_info: Some(RoyaltyInfo {
payment_address: Addr::unchecked("payment_address".to_string()),
share: Decimal::one(),
}),
start_trading_time: Some(Timestamp::from_seconds(42)),
}),
num_tokens: Some(1),
};
assert_eq!(class_data, expected_class_data);
assert_eq!(
packet_data,
NonFungibleTokenPacketData {
class_id: ClassId::new(NFT_CONTRACT_1),
class_uri: None,
class_data: Some(to_json_binary(&expected_class_data).unwrap()),
token_ids: vec![TokenId::new(token_id)],
token_data: Some(
[to_json_binary(&UnknownMetadata {
unknown: "unknown".to_string(),
})
.unwrap()]
.to_vec()
),
token_uris: Some(vec!["https://moonphase.is/image.svg".to_string()]),
sender,
receiver: "callum".to_string(),
Expand Down

0 comments on commit dcdc99c

Please sign in to comment.