diff --git a/contracts/community-sbt/CHANGELOG.md b/contracts/community-sbt/CHANGELOG.md index f89b3c6..2b776ae 100644 --- a/contracts/community-sbt/CHANGELOG.md +++ b/contracts/community-sbt/CHANGELOG.md @@ -23,6 +23,8 @@ Change log entries are to be added to the Unreleased section. Example entry: ### Breaking Changes +- The registry method `sbt_revoke_by_owner` now returns `boolean` indicating if all the tokens were revoked. `true` if all the tokens were revoked succesfully or `false` if not and the method needs to be called agian. + ### Bug Fixes ## v4.3.0 (2023-09-07) diff --git a/contracts/registry/src/lib.rs b/contracts/registry/src/lib.rs index 871be08..c58649d 100644 --- a/contracts/registry/src/lib.rs +++ b/contracts/registry/src/lib.rs @@ -2266,8 +2266,8 @@ mod tests { ctr.sbt_mint(vec![(alice(), vec![m1_1.clone(), m1_2.clone()])]); - // revoke (burn) tokens minted for alice from issuer2 - ctr.sbt_revoke_by_owner(alice(), true); + let res = ctr.sbt_revoke_by_owner(alice(), true); + assert!(res); let log_burn = mk_log_str( "burn", @@ -2302,7 +2302,8 @@ mod tests { ctx.predecessor_account_id = issuer1(); testing_env!(ctx.clone()); assert_eq!(test_utils::get_logs().len(), 0); - ctr.sbt_revoke_by_owner(alice(), false); + let res = ctr.sbt_revoke_by_owner(alice(), false); + assert!(res); let log_revoke = mk_log_str( "revoke", @@ -2320,7 +2321,7 @@ mod tests { assert!(res_with_expired.is_empty()); let res_without_expired = ctr.sbt_tokens_by_owner(alice(), None, None, None, Some(true)); assert!(res_without_expired.len() == 1); - assert_eq!(res[0].1.len(), 2); + assert_eq!(res_without_expired[0].1.len(), 2); assert_eq!(ctr.sbt_supply(issuer1()), 2); assert_eq!(ctr.sbt_supply(issuer2()), 0); assert_eq!(ctr.sbt_supply_by_class(issuer1(), 1), 1); @@ -2381,6 +2382,49 @@ mod tests { assert_eq!(ctr.sbt_supply(issuer2()), 9); } + #[test] + fn sbt_revoke_by_owner_limit() { + let (mut ctx, mut ctr) = setup(&issuer1(), 200 * MINT_DEPOSIT); + + // mint tokens to alice from issuer1 + let batch_metadata = mk_batch_metadata(100); + ctr.sbt_mint(vec![(alice(), batch_metadata[..50].to_vec())]); + + // mint tokens to alice from issuer2 + ctx.predecessor_account_id = issuer2(); + ctx.prepaid_gas = max_gas(); + testing_env!(ctx.clone()); + ctr.sbt_mint(vec![(alice(), batch_metadata[..50].to_vec())]); + + ctx.prepaid_gas = max_gas(); + testing_env!(ctx.clone()); + let res = ctr.sbt_tokens_by_owner(alice(), None, None, None, None); + assert_eq!(res[0].1.len(), 50); + assert_eq!(res[1].1.len(), 50); + + assert_eq!(ctr.sbt_supply(issuer1()), 50); + assert_eq!(ctr.sbt_supply(issuer2()), 50); + + ctx.prepaid_gas = max_gas(); + testing_env!(ctx.clone()); + // revoke (burn) tokens minted for alice from issuer2 + let res = ctr.sbt_revoke_by_owner(alice(), true); + assert!(!res); + + ctx.prepaid_gas = max_gas(); + testing_env!(ctx); + // revoke (burn) tokens minted for alice from issuer2 + let res = ctr.sbt_revoke_by_owner(alice(), true); + assert!(res); + + // make sure the balances are updated correctly + let res = ctr.sbt_tokens_by_owner(alice(), None, None, None, None); + assert_eq!(res[0].1.len(), 50); + + assert_eq!(ctr.sbt_supply(issuer1()), 50); + assert_eq!(ctr.sbt_supply(issuer2()), 0); + } + #[test] fn is_human() { let (mut ctx, mut ctr) = setup(&fractal_mainnet(), 150 * MINT_DEPOSIT); diff --git a/contracts/registry/src/registry.rs b/contracts/registry/src/registry.rs index dde4406..6509d68 100644 --- a/contracts/registry/src/registry.rs +++ b/contracts/registry/src/registry.rs @@ -5,6 +5,7 @@ use near_sdk::{json_types::Base64VecU8, near_bindgen, AccountId}; use crate::*; const MAX_LIMIT: u32 = 1000; +const MAX_REVOKE_PER_CALL: u32 = 25; #[near_bindgen] impl SBTRegistry for Contract { @@ -350,20 +351,28 @@ impl SBTRegistry for Contract { } /// Revokes owners SBTs issued by the caller either by burning or updating their expire - /// time. The function will try to revoke at most `MAX_LIMIT` tokens (to fit into the tx + /// time. The function will try to revoke at most `MAX_REVOKE_PER_CALL` tokens (to fit into the tx /// gas limit), so when an owner has many tokens from the issuer, the issuer may need to - /// call this function multiple times, until all tokens are revoked. Issuer should query - /// `sbt_supply_by_owner` to check if the function should be called again. + /// call this function multiple times, until all tokens are revoked. + /// Retuns true if all the tokens were revoked, false otherwise. + /// If false is returned issuer must call the method until true is returned /// Must be called by an SBT contract. /// Must emit `Revoke` event. /// Must also emit `Burn` event if the SBT tokens are burned (removed). - fn sbt_revoke_by_owner(&mut self, owner: AccountId, burn: bool) { + fn sbt_revoke_by_owner(&mut self, owner: AccountId, burn: bool) -> bool { let issuer = env::predecessor_account_id(); let issuer_id = self.assert_issuer(&issuer); - let mut tokens_by_owner = - self.sbt_tokens_by_owner(owner.clone(), Some(issuer.clone()), None, None, Some(true)); + let mut tokens_by_owner = self.sbt_tokens_by_owner( + owner.clone(), + Some(issuer.clone()), + None, + Some(MAX_REVOKE_PER_CALL), + Some(true), + ); + let remaining_supply = self.sbt_supply_by_owner(owner.clone(), issuer.clone(), None); + let tokens_len = tokens_by_owner[0].1.len(); if tokens_by_owner.is_empty() { - return; + return true; }; let (_, tokens) = tokens_by_owner.pop().unwrap(); @@ -441,6 +450,7 @@ impl SBTRegistry for Contract { tokens: token_ids, } .emit_revoke(); + tokens_len >= remaining_supply as usize } /// Allows issuer to update token metadata reference and reference_hash. diff --git a/contracts/sbt/src/lib.rs b/contracts/sbt/src/lib.rs index 2009995..58e11e0 100644 --- a/contracts/sbt/src/lib.rs +++ b/contracts/sbt/src/lib.rs @@ -158,7 +158,7 @@ pub trait SBTRegistry { /// Must be called by an SBT contract. /// Must emit `Revoke` event. /// Must also emit `Burn` event if the SBT tokens are burned (removed). - fn sbt_revoke_by_owner(&mut self, owner: AccountId, burn: bool); + fn sbt_revoke_by_owner(&mut self, owner: AccountId, burn: bool) -> bool; /// Allows issuer to update token metadata reference and reference_hash. /// * `updates` is a list of triples: (token ID, reference, reference hash).