From 7d473a7f945d602b720371ab0ca22eb44e158b9c Mon Sep 17 00:00:00 2001 From: veeso Date: Tue, 16 Apr 2024 11:52:40 +0200 Subject: [PATCH] feat: approval --- src/app.rs | 74 ++++++++++++++++++++++++++++++++++++--- src/app/inspect.rs | 6 ++++ src/app/storage/tokens.rs | 64 +++++++++++++++++++++++++++++++++ src/inspect.rs | 4 +++ 4 files changed, 144 insertions(+), 4 deletions(-) diff --git a/src/app.rs b/src/app.rs index e0bc536..a004e9a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -236,8 +236,12 @@ impl Dip721 for App { // If the approval goes through, returns a nat that represents the CAP History transaction ID that can be used at the transaction method. /// Interface: approval fn approve(operator: Principal, token_identifier: TokenIdentifier) -> Result { + if !Inspect::inspect_is_owner(caller(), &token_identifier) { + return Err(NftError::UnauthorizedOwner); + } + if Configuration::has_interface(SupportedInterface::Approval) { - todo!(); + TokensStorage::approve(operator, &token_identifier) } else { Err(NftError::Other("Not implemented".to_string())) } @@ -249,7 +253,20 @@ impl Dip721 for App { /// Interface: approval fn set_approval_for_all(operator: Principal, approved: bool) -> Result { if Configuration::has_interface(SupportedInterface::Approval) { - todo!(); + let tokens_by_owner = Self::owner_token_identifiers(caller())?; + let mut tx_id = None; + for token in tokens_by_owner { + if approved { + tx_id = Some(TokensStorage::approve(operator, &token)?); + } else { + tx_id = Some(TokensStorage::revoke_approval(operator, &token)?); + } + } + if let Some(tx_id) = tx_id { + Ok(tx_id) + } else { + Err(NftError::TokenNotFound) + } } else { Err(NftError::Other("Not implemented".to_string())) } @@ -259,7 +276,14 @@ impl Dip721 for App { /// Interface: approval fn is_approved_for_all(owner: Principal, operator: Principal) -> Result { if Configuration::has_interface(SupportedInterface::Approval) { - todo!(); + for token in Self::owner_token_identifiers(owner)? { + let token = TokensStorage::get_token(&token)?; + if token.operator != Some(operator) { + return Ok(false); + } + } + + Ok(true) } else { Err(NftError::Other("Not implemented".to_string())) } @@ -351,7 +375,7 @@ mod test { use std::time::Duration; use pretty_assertions::assert_eq; - use test::test_utils::{store_mock_token, store_mock_token_with}; + use test::test_utils::{bob, store_mock_token, store_mock_token_with}; use super::*; use crate::app::test_utils::mock_token; @@ -623,6 +647,48 @@ mod test { assert!(App::burn(5_u64.into()).is_err()); } + #[test] + fn test_should_approve() { + init_canister(); + store_mock_token(1); + assert!(App::approve(bob(), 1_u64.into()).is_ok()); + + let tokens_with_bob_as_op = TokensStorage::tokens_by_operator(bob()); + assert_eq!(tokens_with_bob_as_op, vec![Nat::from(1_u64)]); + } + + #[test] + fn test_should_approve_for_all() { + init_canister(); + store_mock_token(1); + store_mock_token(2); + assert!(App::set_approval_for_all(bob(), true).is_ok()); + + let tokens_with_bob_as_op = TokensStorage::tokens_by_operator(bob()); + assert_eq!( + tokens_with_bob_as_op, + vec![Nat::from(1_u64), Nat::from(2_u64)] + ); + + assert!(App::set_approval_for_all(bob(), false).is_ok()); + + let tokens_with_bob_as_op = TokensStorage::tokens_by_operator(bob()); + assert!(tokens_with_bob_as_op.is_empty()); + } + + #[test] + fn test_should_tell_if_approved_for_all() { + init_canister(); + store_mock_token(1); + store_mock_token(2); + assert!(App::set_approval_for_all(bob(), true).is_ok()); + assert!(App::is_approved_for_all(caller(), bob()).unwrap()); + assert!(!App::is_approved_for_all(caller(), Principal::management_canister()).unwrap()); + + store_mock_token(3); + assert!(!App::is_approved_for_all(caller(), bob()).unwrap()); + } + #[test] fn test_should_get_tx() { init_canister(); diff --git a/src/app/inspect.rs b/src/app/inspect.rs index ce90083..c9aaa74 100644 --- a/src/app/inspect.rs +++ b/src/app/inspect.rs @@ -15,6 +15,12 @@ impl Inspect { Configuration::is_custodian(caller) } + /// Returns whether caller is owner of the token + pub fn inspect_is_owner(caller: Principal, token_identifier: &Nat) -> bool { + let token = TokensStorage::get_token(token_identifier).unwrap(); + token.owner == Some(caller) + } + /// Returns whether caller is owner or operator of the token pub fn inspect_is_owner_or_operator( caller: Principal, diff --git a/src/app/storage/tokens.rs b/src/app/storage/tokens.rs index bafec90..594fbbd 100644 --- a/src/app/storage/tokens.rs +++ b/src/app/storage/tokens.rs @@ -116,6 +116,36 @@ impl TokensStorage { }) } + /// Approve operator for token + pub fn approve(operator: Principal, token_id: &TokenIdentifier) -> Result { + with_token_mut(token_id, |token| { + token.approved_at = Some(crate::utils::time()); + token.approved_by = Some(crate::utils::caller()); + token.operator = Some(operator); + + let tx_id = TxHistory::register_approve(token); + + Ok(tx_id) + }) + } + + /// Remove approval for operator + pub fn revoke_approval( + operator: Principal, + token_id: &TokenIdentifier, + ) -> Result { + with_token_mut(token_id, |token| { + if token.operator == Some(operator) { + token.approved_at = None; + token.approved_by = None; + token.operator = None; + } + let tx_id = TxHistory::register_approve(token); + + Ok(tx_id) + }) + } + /// Mint a new token pub fn mint( to: Principal, @@ -275,6 +305,40 @@ mod test { ); } + #[test] + fn test_should_approve_token() { + store_mock_token_with(1_u64, |token| { + token.owner = Some(alice()); + }); + assert!( + TokensStorage::approve(bob(), &1u64.into()).is_ok(), + "Should approve token" + ); + let token = TokensStorage::get_token(&1u64.into()).unwrap(); + assert_eq!(token.operator, Some(bob())); + assert!(token.approved_at.is_some()); + assert!(token.approved_by.is_some()); + + // disapprove, but with different operator + + assert!( + TokensStorage::revoke_approval(Principal::management_canister(), &1u64.into()).is_ok(), + "Should revoke approval" + ); + let token = TokensStorage::get_token(&1u64.into()).unwrap(); + assert_eq!(token.operator, Some(bob())); + + // revoke for bob + assert!( + TokensStorage::revoke_approval(bob(), &1u64.into()).is_ok(), + "Should revoke approval" + ); + let token = TokensStorage::get_token(&1u64.into()).unwrap(); + assert_eq!(token.operator, None); + assert!(token.approved_at.is_none()); + assert!(token.approved_by.is_none()); + } + #[test] fn test_should_transfer_token() { store_mock_token_with(1_u64, |token| { diff --git a/src/inspect.rs b/src/inspect.rs index b8a0acf..37ff94a 100644 --- a/src/inspect.rs +++ b/src/inspect.rs @@ -26,6 +26,10 @@ fn inspect_message_impl() { let token_identifier = api::call::arg_data::<(Nat,)>().0; Inspect::inspect_is_owner_or_operator(caller(), &token_identifier).is_ok() } + "approve" => { + let (_operator, token_identifier) = api::call::arg_data::<(Principal, Nat)>(); + Inspect::inspect_is_owner(caller(), &token_identifier) + } "transfer_from" => { let (_, _, token_identifier) = api::call::arg_data::<(Principal, Principal, Nat)>(); Inspect::inspect_is_owner_or_operator(caller(), &token_identifier).is_ok()