From 1411d27e3df59c0ff170b87a693c0e07b8ce4e48 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Wed, 21 Jun 2023 04:40:45 -0700 Subject: [PATCH] More reorganization and cleanup, add comments and basic README --- Cargo.lock | 2 +- Cargo.toml | 4 + contracts/cw721-base/README.md | 6 - contracts/cw721-sylvia-base/Cargo.toml | 15 +- contracts/cw721-sylvia-base/README.md | 8 ++ contracts/cw721-sylvia-base/src/base.rs | 3 +- contracts/cw721-sylvia-base/src/contract.rs | 88 ++++-------- contracts/cw721-sylvia-base/src/lib.rs | 6 +- contracts/cw721-sylvia-base/src/multitest.rs | 3 +- contracts/cw721-sylvia-base/src/responses.rs | 7 + contracts/cw721-sylvia-base/src/state.rs | 48 +++++++ packages/cw721/src/cw721_interface.rs | 141 ------------------ packages/cw721/src/interface.rs | 144 +++++++++++++++++++ packages/cw721/src/lib.rs | 3 +- 14 files changed, 257 insertions(+), 221 deletions(-) create mode 100644 contracts/cw721-sylvia-base/README.md create mode 100644 contracts/cw721-sylvia-base/src/responses.rs create mode 100644 contracts/cw721-sylvia-base/src/state.rs delete mode 100644 packages/cw721/src/cw721_interface.rs create mode 100644 packages/cw721/src/interface.rs diff --git a/Cargo.lock b/Cargo.lock index 7be8a5ba9..e532a1a27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -494,7 +494,7 @@ dependencies = [ [[package]] name = "cw721-sylvia-base" -version = "0.1.0" +version = "0.17.0" dependencies = [ "anyhow", "cosmwasm-schema", diff --git a/Cargo.toml b/Cargo.toml index ced441fda..a8fa154f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,10 @@ incremental = false codegen-units = 1 incremental = false +[profile.release.package.cw721-sylvia-base] +codegen-units = 1 +incremental = false + [profile.release] rpath = false lto = true diff --git a/contracts/cw721-base/README.md b/contracts/cw721-base/README.md index d3be359b9..c53112114 100644 --- a/contracts/cw721-base/README.md +++ b/contracts/cw721-base/README.md @@ -62,9 +62,3 @@ This allows you to use custom `ExecuteMsg` and `QueryMsg` with your additional calls, but then use the underlying implementation for the standard cw721 messages you want to support. The same with `QueryMsg`. You will most likely want to write a custom, domain-specific `instantiate`. - -**TODO: add example when written** - -For now, you can look at [`cw721-staking`](../cw721-staking/README.md) -for an example of how to "inherit" cw721 functionality and combine it with custom logic. -The process is similar for cw721. diff --git a/contracts/cw721-sylvia-base/Cargo.toml b/contracts/cw721-sylvia-base/Cargo.toml index b37a3b2fa..3baef412f 100644 --- a/contracts/cw721-sylvia-base/Cargo.toml +++ b/contracts/cw721-sylvia-base/Cargo.toml @@ -1,9 +1,14 @@ [package] -name = "cw721-sylvia-base" -version = "0.1.0" -edition = { workspace = true } -repository = { workspace = true } -license = { workspace = true } +name = "cw721-sylvia-base" +description = "Basic implementation of cw721 NFTs using the Sylvia framework." +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +rust-version = { workspace = true } + [features] library = [] diff --git a/contracts/cw721-sylvia-base/README.md b/contracts/cw721-sylvia-base/README.md new file mode 100644 index 000000000..528c8a07c --- /dev/null +++ b/contracts/cw721-sylvia-base/README.md @@ -0,0 +1,8 @@ +# cw721-sylvia-base + +This is a basic implementation of a cw721 NFT contract. It implements +the [CW721 spec](../../packages/cw721/README.md) and is designed to +be deployed as is, or imported into other contracts to easily build +cw721-compatible NFTs with custom logic. It uses the [Sylvia](https://github.com/cosmwasm/sylvia) smart contract framework for easier extension. + +Use this as a base to build your own custom NFT contract. diff --git a/contracts/cw721-sylvia-base/src/base.rs b/contracts/cw721-sylvia-base/src/base.rs index 8f63b9953..b55932b3e 100644 --- a/contracts/cw721-sylvia-base/src/base.rs +++ b/contracts/cw721-sylvia-base/src/base.rs @@ -11,7 +11,8 @@ use cw_utils::maybe_addr; use sylvia::contract; use sylvia::types::{ExecCtx, QueryCtx}; -use crate::contract::{Approval, Cw721Contract, TokenInfo, DEFAULT_LIMIT, MAX_LIMIT}; +use crate::contract::{Cw721Contract, DEFAULT_LIMIT, MAX_LIMIT}; +use crate::state::{Approval, TokenInfo}; use crate::ContractError; #[contract(module=crate::contract)] diff --git a/contracts/cw721-sylvia-base/src/contract.rs b/contracts/cw721-sylvia-base/src/contract.rs index 22d698792..8603225ed 100644 --- a/contracts/cw721-sylvia-base/src/contract.rs +++ b/contracts/cw721-sylvia-base/src/contract.rs @@ -1,82 +1,25 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, BlockInfo, Empty, Response, StdResult}; +use cosmwasm_std::{Addr, Empty, Response, StdResult}; use cw721::{cw721_interface, ContractInfoResponse, Expiration}; use cw_ownable::Ownership; -use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; +use cw_storage_plus::{IndexedMap, Item, Map, MultiIndex}; use sylvia::contract; use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx}; +use crate::responses::MinterResponse; +use crate::state::{token_owner_idx, TokenIndexes, TokenInfo}; use crate::ContractError; // Version info for migration pub const CONTRACT_NAME: &str = "crates.io:cw721-sylvia-base"; pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +/// Default limit for query pagination pub const DEFAULT_LIMIT: u32 = 10; +/// Maximum limit for query pagination pub const MAX_LIMIT: u32 = 100; -// TODO should this just be in cw721_interface along with the other responses? -#[cw_serde] -pub struct Approval { - /// Account that can transfer/send the token - pub spender: Addr, - /// When the Approval expires (maybe Expiration::never) - pub expires: Expiration, -} - -impl Approval { - pub fn is_expired(&self, block: &BlockInfo) -> bool { - self.expires.is_expired(block) - } -} - -/// TODO move to a responses file? -/// Shows who can mint these tokens -#[cw_serde] -pub struct MinterResponse { - pub minter: Option, -} - -// TODO what do we want to do with extensions? They seem to be unnessary now? -// TODO kill extensions -#[cw_serde] -pub struct TokenInfo { - /// The owner of the newly minted NFT - pub owner: Addr, - /// Approvals are stored here, as we clear them all upon transfer and cannot accumulate much - pub approvals: Vec, - - /// Universal resource identifier for this NFT - /// Should point to a JSON file that conforms to the ERC721 - /// Metadata JSON Schema - pub token_uri: Option, - - pub extension: Empty, -} - -pub fn token_owner_idx(_pk: &[u8], d: &TokenInfo) -> Addr { - d.owner.clone() -} - -/// Indexed map for NFT tokens by owner -pub struct TokenIndexes<'a> { - pub owner: MultiIndex<'a, Addr, TokenInfo, String>, -} -impl<'a> IndexList for TokenIndexes<'a> { - fn get_indexes(&'_ self) -> Box + '_)> + '_> { - let v: Vec<&dyn Index> = vec![&self.owner]; - Box::new(v.into_iter()) - } -} - -pub struct Cw721Contract<'a> { - pub contract_info: Item<'a, ContractInfoResponse>, - pub token_count: Item<'a, u64>, - /// Stored as (granter, operator) giving operator full control over granter's account - pub operators: Map<'a, (&'a Addr, &'a Addr), Expiration>, - pub tokens: IndexedMap<'a, &'a str, TokenInfo, TokenIndexes<'a>>, -} - +/// The instantiation message data for this contract, used to set initial state #[cw_serde] pub struct InstantiateMsgData { /// Name of the NFT contract @@ -90,6 +33,17 @@ pub struct InstantiateMsgData { pub minter: String, } +/// The struct representing this contract, holds contract state. +/// See Sylvia docmentation for more info about customizing this. +pub struct Cw721Contract<'a> { + pub contract_info: Item<'a, ContractInfoResponse>, + pub token_count: Item<'a, u64>, + /// Stored as (granter, operator) giving operator full control over granter's account + pub operators: Map<'a, (&'a Addr, &'a Addr), Expiration>, + pub tokens: IndexedMap<'a, &'a str, TokenInfo, TokenIndexes<'a>>, +} + +/// The actual contract implementation, base cw721 logic is implemented in base.rs #[cfg_attr(not(feature = "library"), sylvia::entry_points)] #[contract] #[error(ContractError)] @@ -183,3 +137,9 @@ impl Cw721Contract<'_> { cw_ownable::get_ownership(ctx.deps.storage) } } + +impl Default for Cw721Contract<'_> { + fn default() -> Self { + Self::new() + } +} diff --git a/contracts/cw721-sylvia-base/src/lib.rs b/contracts/cw721-sylvia-base/src/lib.rs index b84e73fd0..9d448947a 100644 --- a/contracts/cw721-sylvia-base/src/lib.rs +++ b/contracts/cw721-sylvia-base/src/lib.rs @@ -1,6 +1,10 @@ -mod base; +#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] + +pub mod base; pub mod contract; mod error; +pub mod responses; +pub mod state; #[cfg(test)] mod multitest; diff --git a/contracts/cw721-sylvia-base/src/multitest.rs b/contracts/cw721-sylvia-base/src/multitest.rs index 3c7ac6dd0..48e69c4e8 100644 --- a/contracts/cw721-sylvia-base/src/multitest.rs +++ b/contracts/cw721-sylvia-base/src/multitest.rs @@ -1,5 +1,5 @@ use crate::{ - contract::{multitest_utils::Cw721ContractProxy, InstantiateMsgData, MinterResponse}, + contract::{multitest_utils::Cw721ContractProxy, InstantiateMsgData}, ContractError, }; use cosmwasm_std::Empty; @@ -11,6 +11,7 @@ use sylvia::multitest::App; use crate::base::test_utils::Cw721Interface; use crate::contract::multitest_utils::CodeId; +use crate::responses::MinterResponse; const CREATOR: &str = "creator"; const RANDOM: &str = "random"; diff --git a/contracts/cw721-sylvia-base/src/responses.rs b/contracts/cw721-sylvia-base/src/responses.rs new file mode 100644 index 000000000..68eee6f59 --- /dev/null +++ b/contracts/cw721-sylvia-base/src/responses.rs @@ -0,0 +1,7 @@ +use cosmwasm_schema::cw_serde; + +/// Shows who can mint these tokens +#[cw_serde] +pub struct MinterResponse { + pub minter: Option, +} diff --git a/contracts/cw721-sylvia-base/src/state.rs b/contracts/cw721-sylvia-base/src/state.rs new file mode 100644 index 000000000..fabe91a90 --- /dev/null +++ b/contracts/cw721-sylvia-base/src/state.rs @@ -0,0 +1,48 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, BlockInfo, Empty}; +use cw_ownable::Expiration; +use cw_storage_plus::{Index, IndexList, MultiIndex}; + +#[cw_serde] +pub struct Approval { + /// Account that can transfer/send the token + pub spender: Addr, + /// When the Approval expires (maybe Expiration::never) + pub expires: Expiration, +} + +impl Approval { + pub fn is_expired(&self, block: &BlockInfo) -> bool { + self.expires.is_expired(block) + } +} + +#[cw_serde] +pub struct TokenInfo { + /// The owner of the newly minted NFT + pub owner: Addr, + /// Approvals are stored here, as we clear them all upon transfer and cannot accumulate much + pub approvals: Vec, + + /// Universal resource identifier for this NFT + /// Should point to a JSON file that conforms to the ERC721 + /// Metadata JSON Schema + pub token_uri: Option, + + pub extension: Empty, +} + +pub fn token_owner_idx(_pk: &[u8], d: &TokenInfo) -> Addr { + d.owner.clone() +} + +/// Indexed map for NFT tokens by owner +pub struct TokenIndexes<'a> { + pub owner: MultiIndex<'a, Addr, TokenInfo, String>, +} +impl<'a> IndexList for TokenIndexes<'a> { + fn get_indexes(&'_ self) -> Box + '_)> + '_> { + let v: Vec<&dyn Index> = vec![&self.owner]; + Box::new(v.into_iter()) + } +} diff --git a/packages/cw721/src/cw721_interface.rs b/packages/cw721/src/cw721_interface.rs deleted file mode 100644 index 7c8ab64c3..000000000 --- a/packages/cw721/src/cw721_interface.rs +++ /dev/null @@ -1,141 +0,0 @@ -use cosmwasm_std::{Binary, Empty, Response, StdResult}; -use cw_utils::Expiration; -use sylvia::cw_std::StdError; -use sylvia::interface; -use sylvia::types::{ExecCtx, QueryCtx}; - -use crate::{ - AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, NftInfoResponse, - NumTokensResponse, OperatorResponse, OperatorsResponse, OwnerOfResponse, TokensResponse, -}; - -#[interface] -pub trait Cw721Interface { - type Error: From; - - #[msg(exec)] - fn transfer_nft( - &self, - ctx: ExecCtx, - recipient: String, - token_id: String, - ) -> Result; - - #[msg(exec)] - fn send_nft( - &self, - ctx: ExecCtx, - contract: String, - token_id: String, - msg: Binary, - ) -> Result; - - #[msg(exec)] - fn approve( - &self, - ctx: ExecCtx, - spender: String, - token_id: String, - expires: Option, - ) -> Result; - - #[msg(exec)] - fn revoke( - &self, - ctx: ExecCtx, - spender: String, - token_id: String, - ) -> Result; - - #[msg(exec)] - fn approve_all( - &self, - ctx: ExecCtx, - operator: String, - expires: Option, - ) -> Result; - - #[msg(exec)] - fn revoke_all(&self, ctx: ExecCtx, operator: String) -> Result; - - #[msg(exec)] - fn burn(&self, ctx: ExecCtx, token_id: String) -> Result; - - #[msg(query)] - fn contract_info(&self, ctx: QueryCtx) -> StdResult; - - #[msg(query)] - fn num_tokens(&self, ctx: QueryCtx) -> StdResult; - - #[msg(query)] - fn nft_info(&self, ctx: QueryCtx, token_id: String) -> StdResult>; - - #[msg(query)] - fn owner_of( - &self, - ctx: QueryCtx, - token_id: String, - include_expired: bool, - ) -> StdResult; - - #[msg(query)] - fn operator( - &self, - ctx: QueryCtx, - owner: String, - operator: String, - include_expired: bool, - ) -> StdResult; - - #[msg(query)] - fn operators( - &self, - ctx: QueryCtx, - owner: String, - include_expired: bool, - start_after: Option, - limit: Option, - ) -> StdResult; - - #[msg(query)] - fn approval( - &self, - ctx: QueryCtx, - token_id: String, - spender: String, - include_expired: bool, - ) -> StdResult; - - #[msg(query)] - fn approvals( - &self, - ctx: QueryCtx, - token_id: String, - include_expired: bool, - ) -> StdResult; - - #[msg(query)] - fn tokens( - &self, - ctx: QueryCtx, - owner: String, - start_after: Option, - limit: Option, - ) -> StdResult; - - #[msg(query)] - fn all_tokens( - &self, - ctx: QueryCtx, - start_after: Option, - limit: Option, - ) -> StdResult; - - #[msg(query)] - fn all_nft_info( - &self, - ctx: QueryCtx, - token_id: String, - include_expired: bool, - ) -> StdResult>; -} diff --git a/packages/cw721/src/interface.rs b/packages/cw721/src/interface.rs new file mode 100644 index 000000000..6c30f08e3 --- /dev/null +++ b/packages/cw721/src/interface.rs @@ -0,0 +1,144 @@ +pub mod cw721_interface { + use cosmwasm_std::{Binary, Empty, Response, StdResult}; + use cw_utils::Expiration; + use sylvia::cw_std::StdError; + use sylvia::interface; + use sylvia::types::{ExecCtx, QueryCtx}; + + use crate::{ + AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, + NftInfoResponse, NumTokensResponse, OperatorResponse, OperatorsResponse, OwnerOfResponse, + TokensResponse, + }; + + #[interface] + pub trait Cw721Interface { + type Error: From; + + #[msg(exec)] + fn transfer_nft( + &self, + ctx: ExecCtx, + recipient: String, + token_id: String, + ) -> Result; + + #[msg(exec)] + fn send_nft( + &self, + ctx: ExecCtx, + contract: String, + token_id: String, + msg: Binary, + ) -> Result; + + #[msg(exec)] + fn approve( + &self, + ctx: ExecCtx, + spender: String, + token_id: String, + expires: Option, + ) -> Result; + + #[msg(exec)] + fn revoke( + &self, + ctx: ExecCtx, + spender: String, + token_id: String, + ) -> Result; + + #[msg(exec)] + fn approve_all( + &self, + ctx: ExecCtx, + operator: String, + expires: Option, + ) -> Result; + + #[msg(exec)] + fn revoke_all(&self, ctx: ExecCtx, operator: String) -> Result; + + #[msg(exec)] + fn burn(&self, ctx: ExecCtx, token_id: String) -> Result; + + #[msg(query)] + fn contract_info(&self, ctx: QueryCtx) -> StdResult; + + #[msg(query)] + fn num_tokens(&self, ctx: QueryCtx) -> StdResult; + + #[msg(query)] + fn nft_info(&self, ctx: QueryCtx, token_id: String) -> StdResult>; + + #[msg(query)] + fn owner_of( + &self, + ctx: QueryCtx, + token_id: String, + include_expired: bool, + ) -> StdResult; + + #[msg(query)] + fn operator( + &self, + ctx: QueryCtx, + owner: String, + operator: String, + include_expired: bool, + ) -> StdResult; + + #[msg(query)] + fn operators( + &self, + ctx: QueryCtx, + owner: String, + include_expired: bool, + start_after: Option, + limit: Option, + ) -> StdResult; + + #[msg(query)] + fn approval( + &self, + ctx: QueryCtx, + token_id: String, + spender: String, + include_expired: bool, + ) -> StdResult; + + #[msg(query)] + fn approvals( + &self, + ctx: QueryCtx, + token_id: String, + include_expired: bool, + ) -> StdResult; + + #[msg(query)] + fn tokens( + &self, + ctx: QueryCtx, + owner: String, + start_after: Option, + limit: Option, + ) -> StdResult; + + #[msg(query)] + fn all_tokens( + &self, + ctx: QueryCtx, + start_after: Option, + limit: Option, + ) -> StdResult; + + #[msg(query)] + fn all_nft_info( + &self, + ctx: QueryCtx, + token_id: String, + include_expired: bool, + ) -> StdResult>; + } +} diff --git a/packages/cw721/src/lib.rs b/packages/cw721/src/lib.rs index 6ff22bb6e..e1ef09210 100644 --- a/packages/cw721/src/lib.rs +++ b/packages/cw721/src/lib.rs @@ -1,4 +1,4 @@ -pub mod cw721_interface; +mod interface; mod msg; mod query; mod receiver; @@ -6,6 +6,7 @@ mod traits; pub use cw_utils::Expiration; +pub use crate::interface::cw721_interface; pub use crate::msg::Cw721ExecuteMsg; pub use crate::query::{ AllNftInfoResponse, Approval, ApprovalResponse, ApprovalsResponse, ContractInfoResponse,