1use rsa::{hash::Hash, padding::PaddingScheme, PublicKey};
2use serde_json;
3use sha2::{Digest, Sha256};
4
5use collectxyz::nft::{
6 base64_token_image, full_token_id, numeric_token_id, Config, Coordinates, ExecuteMsg,
7 InstantiateMsg, MigrateMsg, XyzExtension, XyzTokenInfo,
8};
9use cosmwasm_std::{
10 Attribute, BankMsg, Binary, Coin, DepsMut, Empty, Env, MessageInfo, Order, Response, StdError,
11 StdResult, Storage,
12};
13use cw721::{ContractInfoResponse, Cw721ReceiveMsg};
14use cw721_base::{msg::ExecuteMsg as Cw721ExecuteMsg, Cw721Contract};
15
16use crate::error::ContractError;
17use crate::state::{load_captcha_public_key, save_captcha_public_key, tokens, CONFIG, OWNER};
18
19const XYZ: &str = "xyz";
20
21pub fn instantiate(deps: DepsMut, info: MessageInfo, msg: InstantiateMsg) -> StdResult<Response> {
22 let cw721_contract = Cw721Contract::<Coordinates, Empty>::default();
23
24 let contract_info = ContractInfoResponse {
25 name: XYZ.to_string(),
26 symbol: XYZ.to_string(),
27 };
28 cw721_contract
29 .contract_info
30 .save(deps.storage, &contract_info)?;
31
32 CONFIG.save(deps.storage, &msg.config)?;
33 OWNER.save(deps.storage, &info.sender.to_string())?;
34
35 save_captcha_public_key(deps.storage, &msg.captcha_public_key)?;
36
37 Ok(Response::default())
38}
39
40pub fn execute_mint(
41 deps: DepsMut,
42 env: Env,
43 info: MessageInfo,
44 coordinates: Coordinates,
45 captcha_signature: String,
46) -> Result<Response, ContractError> {
47 let cw721_contract = Cw721Contract::<Coordinates, Empty>::default();
48
49 let owner = OWNER.load(deps.storage)?;
50 let config = CONFIG.load(deps.storage)?;
51 let num_tokens = cw721_contract.token_count(deps.storage)?;
52
53 if num_tokens >= config.token_supply {
54 return Err(ContractError::SupplyExhausted {});
55 }
56
57 if info.sender != owner {
58 if !config.public_minting_enabled {
59 return Err(ContractError::Unauthorized {});
60 }
61
62 check_sufficient_funds(info.funds, config.mint_fee)?;
64
65 check_wallet_limit(deps.storage, info.sender.clone(), config.wallet_limit)?;
67 }
68
69 check_coordinates(deps.storage, &coordinates)?;
71
72 check_captcha_signature(deps.storage, &coordinates, &captcha_signature)?;
74
75 let num_tokens = 1 + num_tokens;
77 let token_id = format!("xyz #{}", &num_tokens);
78 let token = XyzTokenInfo {
79 owner: info.sender.clone(),
80 approvals: vec![],
81 name: token_id.clone(),
82 description: String::from("Explore the metaverse, starting with xyz."),
83 image: Some(base64_token_image(&coordinates)),
84 extension: XyzExtension {
85 coordinates,
86 prev_coordinates: None,
87 arrival: env.block.time,
88 },
89 };
90 tokens().update(deps.storage, &token_id, |old| match old {
91 Some(_) => Err(ContractError::Claimed {}),
92 None => Ok(token),
93 })?;
94
95 cw721_contract.increment_tokens(deps.storage)?;
96
97 Ok(Response::new()
98 .add_attribute("action", "mint")
99 .add_attribute("minter", info.sender)
100 .add_attribute("token_id", numeric_token_id(token_id)?))
101}
102
103fn check_sufficient_funds(funds: Vec<Coin>, required: Coin) -> Result<(), ContractError> {
104 if required.amount.u128() == 0 {
105 return Ok(());
106 }
107 let sent_sufficient_funds = funds.iter().any(|coin| {
108 coin.denom == required.denom && coin.amount.u128() >= required.amount.u128()
111 });
112 if sent_sufficient_funds {
113 Ok(())
114 } else {
115 Err(ContractError::Std(StdError::generic_err(
116 "insufficient funds sent",
117 )))
118 }
119}
120
121fn check_wallet_limit(
122 storage: &dyn Storage,
123 owner: cosmwasm_std::Addr,
124 limit: u32,
125) -> Result<(), ContractError> {
126 let num_wallet_tokens = tokens()
127 .idx
128 .owner
129 .prefix(owner)
130 .range(storage, None, None, Order::Ascending)
131 .count();
132
133 if num_wallet_tokens >= limit as usize {
134 Err(ContractError::WalletLimit {})
135 } else {
136 Ok(())
137 }
138}
139
140fn check_captcha_signature(
141 storage: &dyn Storage,
142 coordinates: &Coordinates,
143 captcha_signature: &str,
144) -> Result<(), ContractError> {
145 let key = load_captcha_public_key(storage).unwrap();
146
147 let signature_bytes = base64::decode(captcha_signature).unwrap();
148
149 let coords_json_bytes = serde_json::to_vec(coordinates).unwrap();
150 let mut hasher = Sha256::new();
151 hasher.update(&coords_json_bytes);
152 let digest = hasher.finalize();
153
154 if key
155 .verify(
156 PaddingScheme::PKCS1v15Sign {
157 hash: Some(Hash::SHA2_256),
158 },
159 &digest,
160 &signature_bytes,
161 )
162 .is_ok()
163 {
164 Ok(())
165 } else {
166 Err(ContractError::Unauthorized {})
167 }
168}
169
170fn check_coordinates(storage: &dyn Storage, coords: &Coordinates) -> Result<(), ContractError> {
171 let config = CONFIG.load(storage)?;
172 config.check_bounds(*coords).map_err(ContractError::Std)?;
173 match tokens().idx.coordinates.item(storage, coords.to_bytes())? {
174 Some(_) => Err(ContractError::Claimed {}),
175 None => Ok(()),
176 }
177}
178
179pub fn execute_move(
180 deps: DepsMut,
181 env: Env,
182 info: MessageInfo,
183 token_id: String,
184 coordinates: Coordinates,
185) -> Result<Response, ContractError> {
186 let owner = OWNER.load(deps.storage)?;
187 let config = CONFIG.load(deps.storage)?;
188 let token = tokens().load(deps.storage, &token_id)?;
189
190 if token.owner != info.sender {
192 return Err(ContractError::Unauthorized {});
193 }
194
195 if !token.extension.has_arrived(env.block.time) {
197 return Err(ContractError::MoveInProgress {});
198 }
199
200 if owner != info.sender {
202 let move_fee = config.get_move_fee(token.extension.coordinates, coordinates);
203 check_sufficient_funds(info.funds, move_fee)?;
204 }
205
206 check_coordinates(deps.storage, &coordinates)?;
208
209 let mut new_token = token.clone();
211 new_token.image = Some(base64_token_image(&coordinates));
212 new_token.extension.coordinates = coordinates;
213 new_token.extension.prev_coordinates = Some(token.extension.coordinates);
214 let travel_time_nanos = config.get_move_nanos(token.extension.coordinates, coordinates);
215 new_token.extension.arrival = env.block.time.plus_nanos(travel_time_nanos);
216 tokens().replace(deps.storage, &token_id, Some(&new_token), Some(&token))?;
217
218 Ok(Response::default()
219 .add_attribute("action", "move")
220 .add_attribute("mover", info.sender)
221 .add_attribute("token_id", numeric_token_id(token_id)?))
222}
223
224pub fn execute_update_config(
225 deps: DepsMut,
226 info: MessageInfo,
227 config: Config,
228) -> Result<Response, ContractError> {
229 let owner = OWNER.load(deps.storage)?;
230 if info.sender != owner {
231 return Err(ContractError::Unauthorized {});
232 }
233 CONFIG.save(deps.storage, &config)?;
234 Ok(Response::new().add_attribute("action", "update_config"))
235}
236
237pub fn execute_update_captcha_public_key(
238 deps: DepsMut,
239 info: MessageInfo,
240 public_key: String,
241) -> Result<Response, ContractError> {
242 let owner = OWNER.load(deps.storage)?;
243
244 if info.sender != owner {
245 return Err(ContractError::Unauthorized {});
246 }
247
248 save_captcha_public_key(deps.storage, &public_key)?;
249
250 Ok(Response::new().add_attribute("action", "update_captcha_public_key"))
251}
252
253pub fn execute_withdraw(
254 deps: DepsMut,
255 _env: Env,
256 info: MessageInfo,
257 amount: Vec<Coin>,
258) -> Result<Response, ContractError> {
259 let owner = OWNER.load(deps.storage)?;
260 if info.sender != owner {
261 return Err(ContractError::Unauthorized {});
262 }
263
264 Ok(Response::new().add_message(BankMsg::Send {
265 amount,
266 to_address: owner,
267 }))
268}
269
270pub fn cw721_base_execute(
271 deps: DepsMut,
272 env: Env,
273 info: MessageInfo,
274 msg: ExecuteMsg,
275) -> Result<Response, ContractError> {
276 let cw721_contract = Cw721Contract::<XyzExtension, Empty>::default();
277 let cw721_msg: Cw721ExecuteMsg<XyzExtension> = msg.into();
278 let cw721_msg_full_token_id = match cw721_msg {
279 Cw721ExecuteMsg::Approve {
280 spender,
281 token_id,
282 expires,
283 } => Cw721ExecuteMsg::Approve {
284 spender,
285 expires,
286 token_id: full_token_id(token_id)?,
287 },
288 Cw721ExecuteMsg::Revoke { spender, token_id } => Cw721ExecuteMsg::Revoke {
289 spender,
290 token_id: full_token_id(token_id)?,
291 },
292 Cw721ExecuteMsg::TransferNft {
293 recipient,
294 token_id,
295 } => Cw721ExecuteMsg::TransferNft {
296 recipient,
297 token_id: full_token_id(token_id)?,
298 },
299 Cw721ExecuteMsg::SendNft {
300 contract,
301 token_id,
302 msg,
303 } => Cw721ExecuteMsg::SendNft {
304 contract,
305 msg,
306 token_id: full_token_id(token_id)?,
307 },
308 _ => cw721_msg,
309 };
310
311 let mut response = (match cw721_msg_full_token_id {
312 Cw721ExecuteMsg::SendNft {
313 contract,
314 token_id,
315 msg,
316 } => execute_send_nft(deps, env, info, contract, token_id, msg),
317 _ => cw721_contract
318 .execute(deps, env, info, cw721_msg_full_token_id)
319 .map_err(|err| err.into()),
320 })?;
321
322 response.attributes = response
323 .attributes
324 .iter()
325 .map(|attr| {
326 if attr.key == "token_id" {
327 Attribute::new(
328 "token_id",
329 numeric_token_id(attr.value.to_string()).unwrap(),
330 )
331 } else {
332 attr.clone()
333 }
334 })
335 .collect();
336 Ok(response)
337}
338
339pub fn execute_send_nft(
340 deps: DepsMut,
341 env: Env,
342 info: MessageInfo,
343 contract: String,
344 token_id: String,
345 msg: Binary,
346) -> Result<Response, ContractError> {
347 let cw721_contract = Cw721Contract::<XyzExtension, Empty>::default();
348 cw721_contract._transfer_nft(deps, &env, &info, &contract, &token_id)?;
350
351 let send = Cw721ReceiveMsg {
352 sender: info.sender.to_string(),
353 token_id: numeric_token_id(token_id.clone())?,
354 msg,
355 };
356
357 Ok(Response::new()
359 .add_message(send.into_cosmos_msg(contract.clone())?)
360 .add_attribute("action", "send_nft")
361 .add_attribute("sender", info.sender)
362 .add_attribute("recipient", contract)
363 .add_attribute("token_id", token_id))
364}
365
366pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
367 Ok(Response::default().add_attribute("action", "migrate"))
368}
369
370#[cfg(test)]
371mod test {
372 use super::*;
373
374 use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
375 use cosmwasm_std::{to_binary, Addr, Timestamp};
376 use cw721::{Cw721ReceiveMsg, Expiration};
377 use cw721_base::state::Approval;
378
379 const ADDR1: &str = "addr1";
380 const ADDR2: &str = "addr2";
381
382 fn token_examples() -> Vec<XyzTokenInfo> {
383 vec![
384 XyzTokenInfo {
385 owner: Addr::unchecked(ADDR1),
386 approvals: vec![],
387 name: "xyz #1".to_string(),
388 description: "".to_string(),
389 image: None,
390 extension: XyzExtension {
391 coordinates: Coordinates { x: 1, y: 1, z: 1 },
392 arrival: Timestamp::from_nanos(0),
393 prev_coordinates: None,
394 },
395 },
396 XyzTokenInfo {
397 owner: Addr::unchecked(ADDR2),
398 approvals: vec![],
399 name: "xyz #2".to_string(),
400 description: "".to_string(),
401 image: None,
402 extension: XyzExtension {
403 coordinates: Coordinates { x: 2, y: 2, z: 2 },
404 arrival: Timestamp::from_nanos(0),
405 prev_coordinates: None,
406 },
407 },
408 ]
409 }
410
411 fn setup_storage(deps: DepsMut) {
412 for token in token_examples().iter() {
413 tokens().save(deps.storage, &token.name, token).unwrap();
414 }
415 }
416
417 fn numeric_id_error() -> ContractError {
418 ContractError::Std(StdError::generic_err("expected numeric token identifier"))
419 }
420
421 #[test]
422 fn cw721_transfer() {
423 let mut deps = mock_dependencies(&[]);
424 setup_storage(deps.as_mut());
425
426 let err = cw721_base_execute(
428 deps.as_mut(),
429 mock_env(),
430 mock_info(ADDR1, &[]),
431 ExecuteMsg::TransferNft {
432 recipient: ADDR2.to_string(),
433 token_id: "xyz #1".to_string(),
434 },
435 )
436 .unwrap_err();
437 assert_eq!(err, numeric_id_error());
438
439 let res = cw721_base_execute(
441 deps.as_mut(),
442 mock_env(),
443 mock_info(ADDR1, &[]),
444 ExecuteMsg::TransferNft {
445 recipient: ADDR2.to_string(),
446 token_id: "1".to_string(),
447 },
448 )
449 .unwrap();
450
451 assert!(res
453 .attributes
454 .iter()
455 .any(|attr| attr.key == "token_id" && attr.value == "1"));
456
457 let token = tokens().load(&deps.storage, "xyz #1").unwrap();
459 assert_eq!(token.name, "xyz #1");
460 assert_eq!(token.owner.to_string(), ADDR2.to_string());
461 }
462
463 #[test]
464 fn cw721_approve_revoke() {
465 let mut deps = mock_dependencies(&[]);
466 setup_storage(deps.as_mut());
467
468 let err = cw721_base_execute(
470 deps.as_mut(),
471 mock_env(),
472 mock_info(ADDR1, &[]),
473 ExecuteMsg::Approve {
474 spender: ADDR2.to_string(),
475 token_id: "xyz #1".to_string(),
476 expires: None,
477 },
478 )
479 .unwrap_err();
480 assert_eq!(err, numeric_id_error());
481
482 let res = cw721_base_execute(
484 deps.as_mut(),
485 mock_env(),
486 mock_info(ADDR1, &[]),
487 ExecuteMsg::Approve {
488 spender: ADDR2.to_string(),
489 token_id: "1".to_string(),
490 expires: None,
491 },
492 )
493 .unwrap();
494
495 assert!(res
497 .attributes
498 .iter()
499 .any(|attr| attr.key == "token_id" && attr.value == "1"));
500
501 let token = tokens().load(&deps.storage, "xyz #1").unwrap();
503 assert_eq!(token.name, "xyz #1");
504 assert_eq!(
505 token.approvals,
506 vec![Approval {
507 spender: Addr::unchecked(ADDR2),
508 expires: Expiration::Never {}
509 }]
510 );
511
512 let err = cw721_base_execute(
514 deps.as_mut(),
515 mock_env(),
516 mock_info(ADDR1, &[]),
517 ExecuteMsg::Revoke {
518 spender: ADDR2.to_string(),
519 token_id: "xyz #1".to_string(),
520 },
521 )
522 .unwrap_err();
523 assert_eq!(err, numeric_id_error());
524
525 let res = cw721_base_execute(
527 deps.as_mut(),
528 mock_env(),
529 mock_info(ADDR1, &[]),
530 ExecuteMsg::Revoke {
531 spender: ADDR2.to_string(),
532 token_id: "1".to_string(),
533 },
534 )
535 .unwrap();
536
537 assert!(res
539 .attributes
540 .iter()
541 .any(|attr| attr.key == "token_id" && attr.value == "1"));
542
543 let token = tokens().load(&deps.storage, "xyz #1").unwrap();
545 assert_eq!(token.name, "xyz #1");
546 assert_eq!(token.approvals, vec![]);
547 }
548
549 #[test]
550 fn cw721_send_nft() {
551 let mut deps = mock_dependencies(&[]);
552 setup_storage(deps.as_mut());
553
554 let token_id = "1".to_string();
555 let target = "another_contract".to_string();
556 let msg = to_binary("my msg").unwrap();
557
558 let err = cw721_base_execute(
560 deps.as_mut(),
561 mock_env(),
562 mock_info(ADDR1, &[]),
563 ExecuteMsg::SendNft {
564 contract: target.clone(),
565 token_id: "xyz #1".to_string(),
566 msg: msg.clone(),
567 },
568 )
569 .unwrap_err();
570 assert_eq!(err, numeric_id_error());
571
572 let res = cw721_base_execute(
574 deps.as_mut(),
575 mock_env(),
576 mock_info(ADDR1, &[]),
577 ExecuteMsg::SendNft {
578 contract: target.clone(),
579 token_id: token_id.clone(),
580 msg: msg.clone(),
581 },
582 )
583 .unwrap();
584
585 let payload = Cw721ReceiveMsg {
586 sender: ADDR1.to_string(),
587 token_id: token_id.clone(),
588 msg,
589 };
590 let expected = payload.into_cosmos_msg(target).unwrap();
591 assert_eq!(
592 res,
593 Response::new()
594 .add_message(expected)
595 .add_attribute("action", "send_nft")
596 .add_attribute("sender", ADDR1)
597 .add_attribute("recipient", "another_contract")
598 .add_attribute("token_id", token_id)
599 );
600 }
601}