Skip to content

Commit

Permalink
feat: approval
Browse files Browse the repository at this point in the history
  • Loading branch information
veeso committed Apr 16, 2024
1 parent 7b5c55a commit 7d473a7
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 4 deletions.
74 changes: 70 additions & 4 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Nat, NftError> {
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()))
}
Expand All @@ -249,7 +253,20 @@ impl Dip721 for App {
/// Interface: approval
fn set_approval_for_all(operator: Principal, approved: bool) -> Result<Nat, NftError> {
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()))
}
Expand All @@ -259,7 +276,14 @@ impl Dip721 for App {
/// Interface: approval
fn is_approved_for_all(owner: Principal, operator: Principal) -> Result<bool, NftError> {
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()))
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
6 changes: 6 additions & 0 deletions src/app/inspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
64 changes: 64 additions & 0 deletions src/app/storage/tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,36 @@ impl TokensStorage {
})
}

/// Approve operator for token
pub fn approve(operator: Principal, token_id: &TokenIdentifier) -> Result<Nat, NftError> {
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<Nat, NftError> {
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,
Expand Down Expand Up @@ -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| {
Expand Down
4 changes: 4 additions & 0 deletions src/inspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 7d473a7

Please sign in to comment.