diff --git a/nekoton-contracts/src/access/mod.rs b/nekoton-contracts/src/access/mod.rs new file mode 100644 index 000000000..72d8c8a64 --- /dev/null +++ b/nekoton-contracts/src/access/mod.rs @@ -0,0 +1,19 @@ +use anyhow::Result; +use nekoton_abi::*; + +use super::RunLocalSimple; + +pub mod ownable_internal_contract; + +#[derive(Copy, Clone)] +pub struct OwnableInternalContract<'a>(pub ExecutionContext<'a>); + +impl OwnableInternalContract<'_> { + pub fn owner(&self) -> Result { + let result = self + .0 + .run_local_simple(ownable_internal_contract::owner(), &[])? + .unpack_first()?; + Ok(result) + } +} diff --git a/nekoton-contracts/src/access/ownable_internal_contract.rs b/nekoton-contracts/src/access/ownable_internal_contract.rs new file mode 100644 index 000000000..79179dcf0 --- /dev/null +++ b/nekoton-contracts/src/access/ownable_internal_contract.rs @@ -0,0 +1,23 @@ +use ton_abi::{Param, ParamType}; + +use crate::utils::declare_function; + +/// Returns collection owner address +/// +/// # Type +/// Simple getter method +/// +/// # Inputs +/// No inputs +/// +/// # Outputs +/// * `value0: address` - Address of NFT contract +/// +pub fn owner() -> &'static ton_abi::Function { + declare_function! { + header: [pubkey, time, expire], + name: "owner", + inputs: vec![], + outputs: vec![Param::new("value0", ParamType::Address)], + } +} diff --git a/nekoton-contracts/src/lib.rs b/nekoton-contracts/src/lib.rs index 1210886f2..4838bd13b 100644 --- a/nekoton-contracts/src/lib.rs +++ b/nekoton-contracts/src/lib.rs @@ -54,6 +54,7 @@ use anyhow::Result; use nekoton_abi::{ExecutionContext, ExecutionOutput}; +pub mod access; pub mod dens; pub mod old_tip3; pub mod tip1155; diff --git a/nekoton-contracts/src/tip4_2_2/collection_contract.rs b/nekoton-contracts/src/tip4_2_2/collection_contract.rs index b75046bad..098e9deed 100644 --- a/nekoton-contracts/src/tip4_2_2/collection_contract.rs +++ b/nekoton-contracts/src/tip4_2_2/collection_contract.rs @@ -24,3 +24,23 @@ pub fn get_nft_url() -> &'static ton_abi::Function { outputs: vec![Param::new("nftUrl", ParamType::String)] } } + +/// Return url to metadata for NFT collection. +/// +/// # Type +/// Responsible getter method +/// +/// # Inputs +/// * `answerId: uint32` - responsible answer id +/// +/// # Outputs +/// * `collectionUrl: string` - NFT collection metadata URL +pub fn get_collection_url() -> &'static ton_abi::Function { + declare_function! { + name: "getCollectionUrl", + inputs: vec![ + Param::new("answerId", ParamType::Uint(32)), + ], + outputs: vec![Param::new("collectionUrl", ParamType::String)] + } +} diff --git a/nekoton-contracts/src/tip4_2_2/mod.rs b/nekoton-contracts/src/tip4_2_2/mod.rs index 3c3240cd7..5c2f42930 100644 --- a/nekoton-contracts/src/tip4_2_2/mod.rs +++ b/nekoton-contracts/src/tip4_2_2/mod.rs @@ -10,7 +10,7 @@ pub mod metadata_contract; pub struct MetadataContract<'a>(pub ExecutionContext<'a>); impl MetadataContract<'_> { - pub fn get_url_parts(&self) -> Result { + pub fn get_url_parts(&self) -> Result { let inputs = [0u32.token_value().named("answerId")]; let result = self .0 @@ -35,4 +35,13 @@ impl CollectionContract<'_> { .unpack_first()?; Ok(result) } + + pub fn get_collection_url(&self) -> Result { + let inputs = [0u32.token_value().named("answerId")]; + let result = self + .0 + .run_local_responsible_simple(collection_contract::get_collection_url(), &inputs)? + .unpack_first()?; + Ok(result) + } } diff --git a/src/core/nft_wallet/mod.rs b/src/core/nft_wallet/mod.rs index 91d75fa81..efe2bdf13 100644 --- a/src/core/nft_wallet/mod.rs +++ b/src/core/nft_wallet/mod.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use anyhow::Result; use nekoton_abi::MessageBuilder; +use nekoton_contracts::access::OwnableInternalContract; use nekoton_contracts::tip4_1::nft_contract::{self, GetInfoOutputs, NftCallbackPayload}; use nekoton_contracts::tip4_3::index_contract::IndexGetInfoOutputs; use nekoton_contracts::*; @@ -25,7 +26,14 @@ pub struct NftCollection { collection_address: MsgAddressInt, state: ExistingContract, index_code: Cell, - json_info: Option, + interfaces: CollectionInterfaces, + json_info: Option, + owner: Option, +} + +pub enum JsonInfo { + Json(String), + Url(String), } impl NftCollection { @@ -48,17 +56,31 @@ impl NftCollection { let index_code = contract.resolve_collection_index_code(clock)?; - let json_info = interfaces + let ctx = state.as_context(clock); + let json_data = interfaces .tip4_2 - .then(|| tip4_2::MetadataContract(state.as_context(clock)).get_json()) - .transpose()?; + .then(|| tip4_2::MetadataContract(ctx).get_json()) + .transpose()? + .map(JsonInfo::Json); + + let json_url = interfaces + .tip4_2_2 + .then(|| tip4_2_2::CollectionContract(ctx).get_collection_url()) + .transpose()? + .map(JsonInfo::Url); + + let json_info = json_data.or(json_url); + + let owner = OwnableInternalContract(ctx).owner().ok(); Ok(NftCollection { transport, collection_address, state, index_code, + interfaces, json_info, + owner, }) } @@ -70,10 +92,14 @@ impl NftCollection { &self.index_code } - pub fn json_info(&self) -> &Option { + pub fn json_info(&self) -> &Option { &self.json_info } + pub fn owner(&self) -> &Option { + &self.owner + } + pub fn compute_collection_code_hash(&self, owner: &MsgAddressInt) -> Result { CollectionContractState(&self.state) .get_collection_code_hash(owner, self.index_code.clone()) @@ -90,6 +116,10 @@ impl NftCollection { .get_accounts_by_code_hash(&code_hash, limit, &continuation) .await } + + pub fn interfaces(&self) -> &CollectionInterfaces { + &self.interfaces + } } pub struct Nft { @@ -99,7 +129,7 @@ pub struct Nft { owner: MsgAddressInt, manager: MsgAddressInt, interfaces: NftInterfaces, - json_info: Option, + json_info: Option, contract_subscription: ContractSubscription, handler: Arc, } @@ -149,12 +179,28 @@ impl Nft { // NOTE: Assume that TIP4.1 support is mandatory let mut info = nft_state.get_info(clock.as_ref())?; - let json_info = if interfaces.tip4_2 { - Some(nft_state.get_json(clock.as_ref())?) - } else { - None + + let collection_contract = match transport.get_contract_state(&info.collection).await? { + RawContractState::Exists(state) => state, + RawContractState::NotExists { .. } => return Err(NftError::ContractNotExist.into()), }; + let collection_state = CollectionContractState(&collection_contract); + + let json_data = interfaces + .tip4_2 + .then(|| nft_state.get_json(clock.as_ref())) + .transpose()?; + + let json_url = interfaces + .tip4_2_2 + .then(|| nft_state.get_url_parts(clock.as_ref())) + .transpose()? + .map(|part| collection_state.get_nft_url(clock.as_ref(), part)) + .transpose()?; + + let json_info = json_data.or(json_url); + let contract_subscription = ContractSubscription::subscribe( clock.clone(), transport, @@ -209,7 +255,7 @@ impl Nft { &self.manager } - pub fn metadata(&self) -> &Option { + pub fn metadata(&self) -> &Option { &self.json_info } @@ -447,6 +493,8 @@ impl<'a> CollectionContractState<'a> { if tip6_interface.supports_interface(tip4_3::collection_contract::INTERFACE_ID)? { result.tip4_3 = true; + result.tip4_2_2 = + tip6_interface.supports_interface(tip4_2_2::collection_contract::INTERFACE_ID)?; result.tip4_2 = tip6_interface.supports_interface(tip4_2::metadata_contract::INTERFACE_ID)?; } @@ -482,11 +530,18 @@ impl<'a> CollectionContractState<'a> { let cell = nekoton_abi::set_code_salt(code_index, salt)?; Ok(cell.hash(0)) } + + pub fn get_nft_url(&self, clock: &dyn Clock, part: Cell) -> Result { + let ctx = self.0.as_context(clock); + let tip4_2_2_interface = tip4_2_2::CollectionContract(ctx); + tip4_2_2_interface.get_nft_url(part).map(JsonInfo::Url) + } } #[derive(Copy, Clone, Default)] pub struct CollectionInterfaces { pub tip4_3: bool, + pub tip4_2_2: bool, pub tip4_2: bool, } @@ -506,21 +561,26 @@ impl<'a> NftContractState<'a> { let ctx = self.0.as_context(clock); let tip6_interface = tip6::SidContract(ctx); - let mut result = NftInterfaces::default(); - result.tip4_1 = tip6_interface.supports_interface(tip4_1::nft_contract::INTERFACE_ID)?; - result.tip4_2 = - tip6_interface.supports_interface(tip4_2::metadata_contract::INTERFACE_ID)?; - result.tip4_2_2 = - tip6_interface.supports_interface(tip4_2_2::metadata_contract::INTERFACE_ID)?; - result.tip4_3 = tip6_interface.supports_interface(tip4_3::nft_contract::INTERFACE_ID)?; + let result = NftInterfaces { + tip4_1: tip6_interface.supports_interface(tip4_1::nft_contract::INTERFACE_ID)?, + tip4_2: tip6_interface.supports_interface(tip4_2::metadata_contract::INTERFACE_ID)?, + tip4_2_2: tip6_interface + .supports_interface(tip4_2_2::metadata_contract::INTERFACE_ID)?, + tip4_3: tip6_interface.supports_interface(tip4_3::nft_contract::INTERFACE_ID)?, + }; - Ok((result.tip4_1 || result.tip4_3).then(|| result)) + Ok((result.tip4_1 || result.tip4_3).then_some(result)) } - pub fn get_json(&self, clock: &dyn Clock) -> Result { + pub fn get_json(&self, clock: &dyn Clock) -> Result { let ctx = self.0.as_context(clock); let tip4_2_interface = tip4_2::MetadataContract(ctx); - tip4_2_interface.get_json() + tip4_2_interface.get_json().map(JsonInfo::Json) + } + + pub fn get_url_parts(&self, clock: &dyn Clock) -> Result { + let ctx = self.0.as_context(clock); + tip4_2_2::MetadataContract(ctx).get_url_parts() } pub fn get_info(&self, clock: &dyn Clock) -> Result {