archid_token/
lib.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4use cosmwasm_std::{Empty, Addr, Reply, SubMsgResult};
5use cw2::{get_contract_version, set_contract_version};
6pub use cw721_archid::{ContractError, InstantiateMsg, MintMsg, MinterResponse, QueryMsg};
7use cw721_updatable::{ContractInfoResponse};
8
9#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
10#[serde(rename_all = "snake_case")]
11pub struct MigrateMsg {}
12
13#[allow(clippy::derive_partial_eq_without_eq)]
14#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
15pub struct Subdomain {
16    pub name: Option<String>,
17    pub resolver: Option<Addr>,
18    pub minted: Option<bool>,
19    pub created: Option<u64>,
20    pub expiry: Option<u64>,
21}
22
23#[allow(clippy::derive_partial_eq_without_eq)]
24#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
25pub struct Account {
26  pub username: Option<String>,
27  pub profile: Option<String>,
28  pub account_type: Option<String>,
29  pub verfication_hash: Option<String>,
30}
31
32#[allow(clippy::derive_partial_eq_without_eq)]
33#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
34pub struct Website {
35  pub url: Option<String>,
36  pub domain: Option<String>,
37  pub verfication_hash: Option<String>,
38}
39
40#[allow(clippy::derive_partial_eq_without_eq)]
41#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
42pub struct Metadata {
43  pub name: Option<String>,         // e.g. for interoperability with external marketplaces
44  pub description: Option<String>,  // e.g. ibid.
45  pub image: Option<String>,        // e.g. ibid.
46  pub created: Option<u64>,
47  pub expiry: Option<u64>,
48  pub domain: Option<String>,
49  pub subdomains: Option<Vec<Subdomain>>,
50  pub accounts: Option<Vec<Account>>,
51  pub websites: Option<Vec<Website>>,
52}
53
54pub type Extension = Option<Metadata>;
55
56pub type Cw721MetadataContract<'a> = cw721_archid::Cw721Contract<'a, Extension, Empty, Empty, Empty>;
57
58pub type ExecuteMsg = cw721_archid::ExecuteMsg<Extension, Empty>;
59pub type UpdateMetadataMsg = cw721_archid::msg::UpdateMetadataMsg<Extension>;
60
61const CONTRACT_NAME: &str = "crates.io:archid-token";
62const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
63
64pub mod entry {
65    use super::*;
66
67    #[cfg(not(feature = "library"))]
68    use cosmwasm_std::entry_point;
69    use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
70
71    #[cfg_attr(not(feature = "library"), entry_point)]
72    pub fn instantiate(
73        deps: DepsMut,
74        _env: Env,
75        _info: MessageInfo,
76        msg: InstantiateMsg,
77    ) -> StdResult<Response> {
78        set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
79
80        let info = ContractInfoResponse {
81            name: msg.name,
82            symbol: msg.symbol,
83        };
84        Cw721MetadataContract::default()
85            .contract_info
86            .save(deps.storage, &info)?;
87        let minter = deps.api.addr_validate(&msg.minter)?;
88        Cw721MetadataContract::default()
89            .minter
90            .save(deps.storage, &minter)?;
91        Ok(Response::default())
92    }
93
94    #[cfg_attr(not(feature = "library"), entry_point)]
95    pub fn execute(
96        deps: DepsMut,
97        env: Env,
98        info: MessageInfo,
99        msg: ExecuteMsg,
100    ) -> Result<Response, ContractError> {
101        Cw721MetadataContract::default().execute(deps, env, info, msg)
102    }
103
104    #[cfg_attr(not(feature = "library"), entry_point)]
105    pub fn reply(_deps: DepsMut, _env: Env, msg: Reply) -> Result<Response, ContractError> {
106        match msg.result {
107            SubMsgResult::Ok(_) => Ok(Response::default()),
108            SubMsgResult::Err(_) => Err(ContractError::Unauthorized {}),
109        }
110    }
111
112    #[cfg_attr(not(feature = "library"), entry_point)]
113    pub fn query(deps: Deps, env: Env, msg: QueryMsg<Empty>) -> StdResult<Binary> {
114        Cw721MetadataContract::default().query(deps, env, msg)
115    }
116
117    #[cfg_attr(not(feature = "library"), entry_point)]
118    pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
119        let original_version = get_contract_version(deps.storage)?;
120        let name = CONTRACT_NAME.to_string();
121        let version = CONTRACT_VERSION.to_string();
122        if original_version.contract != name {
123            return Err(ContractError::Unauthorized {});
124        }
125        if original_version.version >= version {
126            return Err(ContractError::Unauthorized {});
127        }
128        set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
129        Ok(Response::default())
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
138    use cw721_updatable::{Cw721Query, NftInfoResponse};
139
140    const CREATOR: &str = "creator";
141
142    #[test]
143    fn use_metadata_extension() {
144        let mut deps = mock_dependencies();
145        let contract = Cw721MetadataContract::default();
146
147        let info = mock_info(CREATOR, &[]);
148        let init_msg = InstantiateMsg {
149            name: "archid token".to_string(),
150            symbol: "AID".to_string(),
151            minter: CREATOR.to_string(),
152        };
153        contract
154            .instantiate(deps.as_mut(), mock_env(), info.clone(), init_msg)
155            .unwrap();
156
157        let resolver_addr = Addr::unchecked("archway1yvnw8xj5elngcq95e2n2p8f80zl7shfwyxk88858pl6cgzveeqtqy7xtf7".to_string()); 
158
159        let subdomain1 = Subdomain {
160            name: Some("game".to_string()),
161            resolver: Some(resolver_addr.clone()),
162            minted: Some(false),
163            created: Some(1000000),
164            expiry: Some(1234567),
165        };
166        let subdomain2 = Subdomain {
167            name: Some("dapp".to_string()),
168            resolver: Some(resolver_addr.clone()),
169            minted: Some(false),
170            created: Some(1000000),
171            expiry: Some(1234567),
172        };
173        let subdomain3 = Subdomain {
174            name: Some("market".to_string()),
175            resolver: Some(resolver_addr.clone()),
176            minted: Some(false),
177            created: Some(1000000),
178            expiry: Some(1234567),
179        };
180
181        let subdomains = vec![
182            subdomain1, 
183            subdomain2, 
184            subdomain3
185        ];
186
187        let accounts = vec![
188            Account {
189                username: Some("drew@chainofinsight.com".to_string()),
190                profile: None,
191                account_type: Some("email".to_string()),
192                verfication_hash: None, // XXX: Only "self attestations" for now
193            },
194            Account {
195                username: Some("@chainofinsight".to_string()),
196                profile: Some("twitter.com/chainofinsight".to_string()),
197                account_type: Some("twitter".to_string()),
198                verfication_hash: None,
199            }
200        ];
201    
202        let websites = vec![
203            Website {
204                url: Some("drewstaylor.com".to_string()),
205                domain: Some("drewstaylor.arch".to_string()),
206                verfication_hash: None,
207            },
208            Website {
209                url: Some("game.drewstaylor.com".to_string()),
210                domain: Some("game.drewstaylor.arch".to_string()),
211                verfication_hash: None,
212            },
213            Website {
214                url: Some("dapp.drewstaylor.com".to_string()),
215                domain: Some("dapp.drewstaylor.arch".to_string()),
216                verfication_hash: None,
217            },
218            Website {
219                url: Some("market.drewstaylor.com".to_string()),
220                domain: Some("market.drewstaylor.arch".to_string()),
221                verfication_hash: None,
222            }
223        ];
224    
225        let metadata_extension = Some(Metadata {
226            name: Some("drewstaylor.arch".into()),
227            description: Some("default token description".into()),
228            image: Some("ipfs://QmZdPdZzZum2jQ7jg1ekfeE3LSz1avAaa42G6mfimw9TEn".into()),
229            domain: Some("drewstaylor.arch".into()),
230            created: Some(1000000),
231            expiry: Some(1234567),
232            subdomains: Some(subdomains),
233            accounts: Some(accounts),
234            websites: Some(websites),
235        });
236
237        let token_id = "drewstaylor.arch";
238        let mint_msg = MintMsg {
239            token_id: token_id.to_string(),
240            owner: CREATOR.to_string(),
241            token_uri: None,
242            extension: metadata_extension,
243        };
244        let exec_msg = ExecuteMsg::Mint(mint_msg.clone());
245        contract
246            .execute(deps.as_mut(), mock_env(), info, exec_msg)
247            .unwrap();
248
249        let res = contract.nft_info(deps.as_ref(), token_id.into()).unwrap();
250
251        assert_eq!(res.token_uri, mint_msg.token_uri);
252        assert_eq!(res.extension, mint_msg.extension);
253    }
254
255    #[test]
256    fn updating_metadata() {
257        let mut deps = mock_dependencies();
258        let contract = Cw721MetadataContract::default();
259
260        let info = mock_info(CREATOR, &[]);
261        let init_msg = InstantiateMsg {
262            name: "archid token".to_string(),
263            symbol: "AID".to_string(),
264            minter: CREATOR.to_string(),
265        };
266        contract
267            .instantiate(deps.as_mut(), mock_env(), info.clone(), init_msg)
268            .unwrap();
269
270        let token_id1 = "updatable".to_string();
271        let token_id2 = "won't be updated".to_string();
272
273        let metadata_extension = Some(Metadata {
274            name: Some("original.arch".into()),
275            description: Some("default token description".into()),
276            image: Some("ipfs://QmZdPdZzZum2jQ7jg1ekfeE3LSz1avAaa42G6mfimw9TEn".into()),
277            domain: Some("original.arch".into()),
278            created: Some(1000000),
279            expiry: Some(1234567),
280            subdomains: None,
281            accounts: None,
282            websites: None,
283        });
284
285        let modified_metadata_extension = Some(Metadata {
286            name: Some("modified.arch".into()),
287            description: Some("default token description".into()),
288            image: Some("ipfs://QmZdPdZzZum2jQ7jg1ekfeE3LSz1avAaa42G6mfimw9TEn".into()),
289            domain: Some("modified.arch".into()),
290            created: Some(1000000),
291            expiry: Some(1234567),
292            subdomains: None,
293            accounts: None,
294            websites: None,
295        });
296
297        let mint_msg = ExecuteMsg::Mint(MintMsg {
298            token_id: token_id1.clone(),
299            owner: CREATOR.to_string(),
300            token_uri: None,
301            extension: metadata_extension.clone(),
302        });
303
304        let mint_msg2 = ExecuteMsg::Mint(MintMsg {
305            token_id: token_id2.clone(),
306            owner: "innocent hodlr".to_string(),
307            token_uri: None,
308            extension: metadata_extension.clone(),
309        });
310
311        let err_metadata_extension = Some(Metadata {
312            name: Some("evil doer".into()),
313            description: Some("has rugged your token".into()),
314            image: Some("rugged".into()),
315            domain: None,
316            created: None,
317            expiry: None,
318            subdomains: None,
319            accounts: None,
320            websites: None,
321        });
322
323        let update_msg = ExecuteMsg::UpdateMetadata(UpdateMetadataMsg {
324            token_id: token_id1.clone(),
325            extension: modified_metadata_extension.clone(),
326        });
327
328        let err_update_msg = ExecuteMsg::UpdateMetadata(UpdateMetadataMsg {
329            token_id: token_id1.clone(),
330            extension: err_metadata_extension.clone(),
331        });
332
333        // Mint
334        let admin = mock_info(CREATOR, &[]);
335        let _mint1 = contract
336            .execute(deps.as_mut(), mock_env(), admin.clone(), mint_msg)
337            .unwrap();
338
339        let _mint2 = contract
340            .execute(deps.as_mut(), mock_env(), admin.clone(), mint_msg2)
341            .unwrap();
342
343        // Original NFT infos are correct
344        let info1 = contract.nft_info(deps.as_ref(), token_id1.clone()).unwrap();
345        assert_eq!(
346            info1,
347            NftInfoResponse {
348                token_uri: None,
349                extension: metadata_extension.clone(),
350            }
351        );
352
353        let info2 = contract.nft_info(deps.as_ref(), token_id2.clone()).unwrap();
354        assert_eq!(
355            info2,
356            NftInfoResponse {
357                token_uri: None,
358                extension: metadata_extension.clone(),
359            }
360        );
361
362        // Random cannot update NFT
363        let random = mock_info("random", &[]);
364        
365        let err = contract
366            .execute(deps.as_mut(), mock_env(), random, err_update_msg)
367            .unwrap_err();
368        assert_eq!(err, ContractError::Unauthorized {});
369
370        // Only allowed minters can update NFT
371        let _update = contract
372            .execute(deps.as_mut(), mock_env(), admin.clone(), update_msg)
373            .unwrap();
374
375        let update_info = contract.nft_info(deps.as_ref(), token_id1.clone()).unwrap();
376
377        // Modified NFT info is correct
378        assert_eq!(
379            update_info,
380            NftInfoResponse {
381                token_uri: None,
382                extension: modified_metadata_extension,
383            }
384        );
385    }
386
387    #[test]
388    fn burning_admin_only() {
389        let mut deps = mock_dependencies();
390        let contract = Cw721MetadataContract::default();
391
392        let info = mock_info(CREATOR, &[]);
393        let init_msg = InstantiateMsg {
394            name: "archid token".to_string(),
395            symbol: "AID".to_string(),
396            minter: CREATOR.to_string(),
397        };
398        contract
399            .instantiate(deps.as_mut(), mock_env(), info.clone(), init_msg)
400            .unwrap();
401
402        let token_id = "petrify".to_string();
403        let token_uri = "https://www.merriam-webster.com/dictionary/petrify".to_string();
404
405        let mint_msg = ExecuteMsg::Mint(MintMsg {
406            token_id: token_id.clone(),
407            owner: "someone".to_string(),
408            token_uri: Some(token_uri),
409            extension: None,
410        });
411
412        let burn_msg = ExecuteMsg::Burn { token_id };
413
414        // Mint NFT
415        let admin = mock_info(CREATOR, &[]);
416        let _ = contract
417            .execute(deps.as_mut(), mock_env(), admin.clone(), mint_msg)
418            .unwrap();
419
420        // Owner not allowed to burn as admin
421        let owner = mock_info("someone", &[]);
422        let err = contract
423            .execute(deps.as_mut(), mock_env(), owner, burn_msg.clone())
424            .unwrap_err();
425
426        assert_eq!(err, ContractError::Unauthorized {});
427
428        // Admin can burn tokens owned by anyone
429        let _ = contract
430            .execute(deps.as_mut(), mock_env(), admin, burn_msg)
431            .unwrap();
432
433        // Ensure num tokens decreases
434        let count = contract.num_tokens(deps.as_ref()).unwrap();
435        assert_eq!(0, count.count);
436
437        // Requesting NFT metadata returns error
438        let _ = contract
439            .nft_info(deps.as_ref(), "petrify".to_string())
440            .unwrap_err();
441
442        // Listing token_ids should now be empty
443        let tokens = contract.all_tokens(deps.as_ref(), None, None).unwrap();
444        assert!(tokens.tokens.is_empty());
445    }
446}