Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions contracts/manage_hub/src/batch.rs
Original file line number Diff line number Diff line change
@@ -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<BatchMintParams>) -> 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<BatchTransferParams>) -> 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<BatchUpdateParams>) -> 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(())
}
}
29 changes: 24 additions & 5 deletions contracts/manage_hub/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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;

Expand All @@ -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<BatchMintParams>) -> Result<(), Error> {
BatchModule::batch_mint(env, params)
}

/// Transfers multiple tokens in a single transaction.
pub fn batch_transfer(env: Env, params: Vec<BatchTransferParams>) -> Result<(), Error> {
BatchModule::batch_transfer(env, params)
}

/// Updates metadata for multiple tokens in a single transaction.
pub fn batch_update(env: Env, params: Vec<BatchUpdateParams>) -> Result<(), Error> {
BatchModule::batch_update(env, params)
}

pub fn issue_token(
env: Env,
id: BytesN<32>,
Expand Down
98 changes: 86 additions & 12 deletions contracts/manage_hub/src/membership_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -121,12 +131,36 @@ impl MembershipTokenContract {
Ok(())
}

pub fn batch_issue_tokens(
env: Env,
params: Vec<crate::types::BatchMintParams>,
) -> 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);
}

Expand Down Expand Up @@ -168,6 +202,19 @@ impl MembershipTokenContract {
Ok(())
}

pub fn batch_transfer_tokens(
env: Env,
params: Vec<crate::types::BatchTransferParams>,
) -> 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>,
Expand Down Expand Up @@ -415,20 +462,30 @@ impl MembershipTokenContract {
description: String,
attributes: Map<String, MetadataValue>,
) -> 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()
.instance()
.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<String, MetadataValue>,
) -> 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
Expand Down Expand Up @@ -477,15 +534,15 @@ 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);
}
}
}

// 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);
}
}

Expand All @@ -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);

Expand All @@ -525,6 +582,23 @@ impl MembershipTokenContract {
Ok(())
}

pub fn batch_set_token_metadata(
env: Env,
params: Vec<crate::types::BatchUpdateParams>,
) -> 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
Expand Down
27 changes: 25 additions & 2 deletions contracts/manage_hub/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, MetadataValue>,
}

#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub enum AttendanceAction {
Expand Down
22 changes: 22 additions & 0 deletions contracts/manage_hub/src/validation.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
}