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>, pub description: Option<String>, pub image: Option<String>, 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, },
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 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 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 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 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 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 let admin = mock_info(CREATOR, &[]);
416 let _ = contract
417 .execute(deps.as_mut(), mock_env(), admin.clone(), mint_msg)
418 .unwrap();
419
420 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 let _ = contract
430 .execute(deps.as_mut(), mock_env(), admin, burn_msg)
431 .unwrap();
432
433 let count = contract.num_tokens(deps.as_ref()).unwrap();
435 assert_eq!(0, count.count);
436
437 let _ = contract
439 .nft_info(deps.as_ref(), "petrify".to_string())
440 .unwrap_err();
441
442 let tokens = contract.all_tokens(deps.as_ref(), None, None).unwrap();
444 assert!(tokens.tokens.is_empty());
445 }
446}