diff --git a/contracts/manage_hub/src/batch.rs b/contracts/manage_hub/src/batch.rs new file mode 100644 index 00000000..b92fe697 --- /dev/null +++ b/contracts/manage_hub/src/batch.rs @@ -0,0 +1,72 @@ +// Allow deprecated events API until migration to #[contractevent] macro +#![allow(deprecated)] + +use crate::errors::Error; +use crate::membership_token::MembershipTokenContract; +use crate::types::{BatchMintParams, BatchTransferParams, BatchUpdateParams}; +use crate::validation::BatchValidator; +use soroban_sdk::{symbol_short, Env, Vec}; + +pub struct BatchModule; + +impl BatchModule { + /// Mints multiple tokens in a single transaction. + /// Requires admin authorization for each mint if issue_token requires it. + /// + /// # Arguments + /// * `env` - The contract environment + /// * `params_vec` - Vector of minting parameters for each token + pub fn batch_mint(env: Env, params_vec: Vec) -> Result<(), Error> { + BatchValidator::validate_batch_size(params_vec.len())?; + + MembershipTokenContract::batch_issue_tokens(env.clone(), params_vec.clone())?; + + // Emit batch event for tracking and monitoring + env.events().publish( + (symbol_short!("bat_mint"),), + (params_vec.len(), env.ledger().timestamp()), + ); + + Ok(()) + } + + /// Transfers multiple tokens to different recipients in a single transaction. + /// Requires authorization from each current token owner. + /// + /// # Arguments + /// * `env` - The contract environment + /// * `params_vec` - Vector of transfer parameters for each token + pub fn batch_transfer(env: Env, params_vec: Vec) -> Result<(), Error> { + BatchValidator::validate_batch_size(params_vec.len())?; + + MembershipTokenContract::batch_transfer_tokens(env.clone(), params_vec.clone())?; + + // Emit batch event for tracking and monitoring + env.events().publish( + (symbol_short!("bat_xfr"),), + (params_vec.len(), env.ledger().timestamp()), + ); + + Ok(()) + } + + /// Updates metadata for multiple tokens in a single transaction. + /// Requires authorization from each token owner (or admin). + /// + /// # Arguments + /// * `env` - The contract environment + /// * `params_vec` - Vector of update parameters for each token + pub fn batch_update(env: Env, params_vec: Vec) -> Result<(), Error> { + BatchValidator::validate_batch_size(params_vec.len())?; + + MembershipTokenContract::batch_set_token_metadata(env.clone(), params_vec.clone())?; + + // Emit batch event for tracking and monitoring + env.events().publish( + (symbol_short!("bat_upd"),), + (params_vec.len(), env.ledger().timestamp()), + ); + + Ok(()) + } +} diff --git a/contracts/manage_hub/src/lib.rs b/contracts/manage_hub/src/lib.rs index 76fd8bc9..66de6fb9 100644 --- a/contracts/manage_hub/src/lib.rs +++ b/contracts/manage_hub/src/lib.rs @@ -63,6 +63,7 @@ use soroban_sdk::{contract, contractimpl, vec, Address, BytesN, Env, Map, String mod allowance; mod attendance_log; +mod batch; mod errors; mod fractionalization; mod guards; @@ -76,8 +77,10 @@ mod subscription; mod types; mod upgrade; mod upgrade_errors; +mod validation; use attendance_log::{AttendanceLog, AttendanceLogModule}; +use batch::BatchModule; use common_types::{ AttendanceFrequency, DateRange, DayPattern, MetadataUpdate, MetadataValue, PeakHourData, TimePeriod, TokenMetadata, UserAttendanceStats, @@ -88,11 +91,12 @@ use membership_token::{MembershipToken, MembershipTokenContract}; use staking::StakingModule; use subscription::SubscriptionContract; use types::{ - AttendanceAction, AttendanceSummary, BatchUpgradeResult, BillingCycle, CreatePromotionParams, - CreateTierParams, DividendDistribution, EmergencyPauseState, FractionHolder, MembershipStatus, - PauseConfig, PauseHistoryEntry, PauseStats, StakeInfo, StakingConfig, StakingTier, - Subscription, SubscriptionTier, TierAnalytics, TierFeature, TierPromotion, TokenAllowance, - UpdateTierParams, UpgradeConfig, UpgradeRecord, UserSubscriptionInfo, + AttendanceAction, AttendanceSummary, BatchMintParams, BatchTransferParams, BatchUpdateParams, + BatchUpgradeResult, BillingCycle, CreatePromotionParams, CreateTierParams, + DividendDistribution, EmergencyPauseState, FractionHolder, MembershipStatus, PauseConfig, + PauseHistoryEntry, PauseStats, StakeInfo, StakingConfig, StakingTier, Subscription, + SubscriptionTier, TierAnalytics, TierFeature, TierPromotion, TokenAllowance, UpdateTierParams, + UpgradeConfig, UpgradeRecord, UserSubscriptionInfo, }; use upgrade::UpgradeModule; @@ -105,6 +109,21 @@ impl Contract { vec![&env, String::from_str(&env, "Hello"), to] } + /// Mints multiple tokens in a single transaction. + pub fn batch_mint(env: Env, params: Vec) -> Result<(), Error> { + BatchModule::batch_mint(env, params) + } + + /// Transfers multiple tokens in a single transaction. + pub fn batch_transfer(env: Env, params: Vec) -> Result<(), Error> { + BatchModule::batch_transfer(env, params) + } + + /// Updates metadata for multiple tokens in a single transaction. + pub fn batch_update(env: Env, params: Vec) -> Result<(), Error> { + BatchModule::batch_update(env, params) + } + pub fn issue_token( env: Env, id: BytesN<32>, diff --git a/contracts/manage_hub/src/membership_token.rs b/contracts/manage_hub/src/membership_token.rs index 383491ac..b46d4b4d 100644 --- a/contracts/manage_hub/src/membership_token.rs +++ b/contracts/manage_hub/src/membership_token.rs @@ -78,6 +78,16 @@ impl MembershipTokenContract { .ok_or(Error::AdminNotSet)?; admin.require_auth(); + Self::internal_issue_token(&env, &admin, id, user, expiry_date) + } + + fn internal_issue_token( + env: &Env, + admin: &Address, + id: BytesN<32>, + user: Address, + expiry_date: u64, + ) -> Result<(), Error> { // Check if token already exists if env.storage().persistent().has(&DataKey::Token(id.clone())) { return Err(Error::TokenAlreadyIssued); @@ -121,12 +131,36 @@ impl MembershipTokenContract { Ok(()) } + pub fn batch_issue_tokens( + env: Env, + params: Vec, + ) -> Result<(), Error> { + PauseGuard::require_not_paused(&env)?; + + let admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .ok_or(Error::AdminNotSet)?; + admin.require_auth(); + + for p in params.iter() { + Self::internal_issue_token(&env, &admin, p.id, p.user, p.expiry_date)?; + } + + Ok(()) + } + pub fn transfer_token(env: Env, id: BytesN<32>, new_user: Address) -> Result<(), Error> { // Block transfers when the contract is globally paused or this token is paused. PauseGuard::require_not_paused(&env)?; - PauseGuard::require_token_not_paused(&env, &id)?; + Self::internal_transfer_token(&env, id, new_user) + } - if FractionalizationModule::is_fractionalized(&env, &id) { + fn internal_transfer_token(env: &Env, id: BytesN<32>, new_user: Address) -> Result<(), Error> { + PauseGuard::require_token_not_paused(env, &id)?; + + if FractionalizationModule::is_fractionalized(env, &id) { return Err(Error::TokenFractionalized); } @@ -168,6 +202,19 @@ impl MembershipTokenContract { Ok(()) } + pub fn batch_transfer_tokens( + env: Env, + params: Vec, + ) -> Result<(), Error> { + PauseGuard::require_not_paused(&env)?; + + for p in params.iter() { + Self::internal_transfer_token(&env, p.id, p.new_user)?; + } + + Ok(()) + } + pub fn approve( env: Env, token_id: BytesN<32>, @@ -415,13 +462,6 @@ impl MembershipTokenContract { description: String, attributes: Map, ) -> Result<(), Error> { - // Verify token exists - let token: MembershipToken = env - .storage() - .persistent() - .get(&DataKey::Token(token_id.clone())) - .ok_or(Error::TokenNotFound)?; - // Require authorization from admin or token owner let admin: Address = env .storage() @@ -429,6 +469,23 @@ impl MembershipTokenContract { .get(&DataKey::Admin) .ok_or(Error::AdminNotSet)?; + Self::internal_set_token_metadata(&env, &admin, token_id, description, attributes) + } + + fn internal_set_token_metadata( + env: &Env, + admin: &Address, + token_id: BytesN<32>, + description: String, + attributes: Map, + ) -> Result<(), Error> { + // Verify token exists + let token: MembershipToken = env + .storage() + .persistent() + .get(&DataKey::Token(token_id.clone())) + .ok_or(Error::TokenNotFound)?; + // Check if caller is admin or token owner if env.ledger().sequence() > 0 { // In tests, we might not have proper auth @@ -477,7 +534,7 @@ impl MembershipTokenContract { // Remove old attribute indexes for key in existing_metadata.attributes.keys() { if let Some(value) = existing_metadata.attributes.get(key.clone()) { - Self::remove_from_metadata_index(&env, &key, &value, &token_id); + Self::remove_from_metadata_index(env, &key, &value, &token_id); } } } @@ -485,7 +542,7 @@ impl MembershipTokenContract { // Add new attribute indexes for key in attributes.keys() { if let Some(value) = attributes.get(key.clone()) { - Self::add_to_metadata_index(&env, &key, &value, &token_id); + Self::add_to_metadata_index(env, &key, &value, &token_id); } } @@ -508,7 +565,7 @@ impl MembershipTokenContract { .storage() .persistent() .get(&DataKey::MetadataHistory(token_id.clone())) - .unwrap_or_else(|| Vec::new(&env)); + .unwrap_or_else(|| Vec::new(env)); history.push_back(metadata_update); @@ -525,6 +582,23 @@ impl MembershipTokenContract { Ok(()) } + pub fn batch_set_token_metadata( + env: Env, + params: Vec, + ) -> Result<(), Error> { + let admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .ok_or(Error::AdminNotSet)?; + + for p in params.iter() { + Self::internal_set_token_metadata(&env, &admin, p.id, p.description, p.attributes)?; + } + + Ok(()) + } + /// Gets metadata for a token. /// /// # Arguments diff --git a/contracts/manage_hub/src/types.rs b/contracts/manage_hub/src/types.rs index 4adace6b..97d9a573 100644 --- a/contracts/manage_hub/src/types.rs +++ b/contracts/manage_hub/src/types.rs @@ -3,10 +3,33 @@ use soroban_sdk::{contracttype, Address, BytesN, String, Vec}; // Re-export types from common_types for consistency pub use common_types::MembershipStatus; pub use common_types::{ - SubscriptionTier, TierChangeRequest, TierChangeStatus, TierChangeType, TierFeature, TierLevel, - TierPromotion, + MetadataValue, SubscriptionTier, TierChangeRequest, TierChangeStatus, TierChangeType, + TierFeature, TierLevel, TierPromotion, }; +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct BatchMintParams { + pub id: BytesN<32>, + pub user: Address, + pub expiry_date: u64, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct BatchTransferParams { + pub id: BytesN<32>, + pub new_user: Address, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct BatchUpdateParams { + pub id: BytesN<32>, + pub description: String, + pub attributes: soroban_sdk::Map, +} + #[contracttype] #[derive(Clone, Debug, PartialEq)] pub enum AttendanceAction { diff --git a/contracts/manage_hub/src/validation.rs b/contracts/manage_hub/src/validation.rs new file mode 100644 index 00000000..ff42b8b7 --- /dev/null +++ b/contracts/manage_hub/src/validation.rs @@ -0,0 +1,22 @@ +use crate::errors::Error; + +/// Maximum number of operations allowed in a single batch to prevent gas limits or DoS +pub const MAX_BATCH_SIZE: u32 = 50; + +pub struct BatchValidator; + +impl BatchValidator { + /// Validates that the batch size is within acceptable limits. + /// + /// # Arguments + /// * `size` - The number of items in the batch + /// + /// # Errors + /// * `BatchSizeExceeded` - If size is 0 or greater than MAX_BATCH_SIZE + pub fn validate_batch_size(size: u32) -> Result<(), Error> { + if size == 0 || size > MAX_BATCH_SIZE { + return Err(Error::Unauthorized); + } + Ok(()) + } +}