Skip to content

Commit 57de44f

Browse files
authored
Merge pull request #39 from ACodehunter/feature/batch-mint-issue-12
feat(token): add batch_mint() function for multi-recipient minting
2 parents e7f4f3d + 5a69f37 commit 57de44f

3 files changed

Lines changed: 388 additions & 24 deletions

File tree

contracts/token/src/lib.rs

Lines changed: 110 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,7 @@ mod test;
2020
mod proptest;
2121

2222
use soroban_sdk::token::TokenInterface;
23-
use soroban_sdk::{
24-
contract, contracterror, contractimpl, contracttype, Address, BytesN, Env, String,
25-
};
26-
27-
/// Errors returned by the token contract.
28-
#[contracterror]
29-
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
30-
#[repr(u32)]
31-
pub enum TokenError {
32-
/// The contract was initialized more than once.
33-
AlreadyInitialized = 1,
34-
/// The contract has not been initialized yet.
35-
NotInitialized = 2,
36-
/// The source account does not have enough tokens.
37-
InsufficientBalance = 3,
38-
/// The approved allowance is too small for the requested action.
39-
InsufficientAllowance = 4,
40-
/// The provided amount is invalid for this operation.
41-
InvalidAmount = 5,
42-
/// The contract is currently paused.
43-
ContractPaused = 6,
44-
}
23+
use soroban_sdk::{contract, contractimpl, contracttype, vec, Address, Env, String, Vec};
4524
use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String, Vec};
4625
use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env, String};
4726
use bc_forge_admin::{self as admin, Role};
@@ -94,6 +73,14 @@ pub enum TokenAction {
9473
Unpause,
9574
}
9675

76+
/// Represents a mint recipient with address and amount.
77+
#[derive(Clone)]
78+
#[contracttype]
79+
pub struct Recipient {
80+
pub address: Address,
81+
pub amount: i128,
82+
}
83+
9784
// ─────────────────────────────────────────────────────────────────────────────
9885
// Contract Definition
9986
// ─────────────────────────────────────────────────────────────────────────────
@@ -409,6 +396,52 @@ impl BcForgeToken {
409396
events::emit_clawback(&env, &claw_admin, &from, &to, amount);
410397
}
411398

399+
/// Mints tokens to multiple recipients in a single transaction. Admin-only.
400+
///
401+
/// # Arguments
402+
/// * `recipients` - Vector of (address, amount) pairs.
403+
///
404+
/// # Panics
405+
/// Panics if caller is not admin, contract is paused, any amount is non-positive,
406+
/// or if the recipients list is empty.
407+
///
408+
/// # Note
409+
/// All mints are atomic - if any recipient has an invalid amount, the entire batch reverts.
410+
pub fn batch_mint(env: Env, recipients: Vec<Recipient>) {
411+
bc_forge_lifecycle::require_not_paused(&env);
412+
413+
let admin = Self::read_admin(&env);
414+
admin.require_auth();
415+
416+
if recipients.is_empty() {
417+
panic!("recipients list cannot be empty");
418+
}
419+
420+
// First pass: validate all amounts are positive
421+
for i in 0..recipients.len() {
422+
let recipient = recipients.get(i).expect("recipient should exist");
423+
if recipient.amount <= 0 {
424+
panic!("mint amount must be positive for all recipients");
425+
}
426+
}
427+
428+
// Second pass: perform all mints and calculate total
429+
let mut total_minted: i128 = 0;
430+
for i in 0..recipients.len() {
431+
let recipient = recipients.get(i).expect("recipient should exist");
432+
let balance = Self::read_balance(&env, &recipient.address) + recipient.amount;
433+
Self::write_balance(&env, &recipient.address, balance);
434+
total_minted += recipient.amount;
435+
436+
// Emit individual mint event per recipient
437+
events::emit_mint(&env, &admin, &recipient.address, recipient.amount, balance, Self::read_supply(&env) + total_minted);
438+
}
439+
440+
// Update total supply atomically once at the end
441+
let new_supply = Self::read_supply(&env) + total_minted;
442+
Self::write_supply(&env, new_supply);
443+
}
444+
412445
/// Transfers the admin role to a new address. Current admin-only.
413446
///
414447
/// ⚠️ DEPRECATED: Use propose_owner() + accept_ownership() for safer two-step transfer.
@@ -525,6 +558,61 @@ impl BcForgeToken {
525558
Self::read_pending_admin(&env)
526559
}
527560

561+
/// Proposes a new admin for two-step ownership transfer. Current admin-only.
562+
///
563+
/// # Arguments
564+
/// * `new_admin` - The address to propose as the new admin.
565+
///
566+
/// # Panics
567+
/// Panics if caller is not the current admin.
568+
pub fn propose_owner(env: Env, new_admin: Address) {
569+
let admin = Self::read_admin(&env);
570+
admin.require_auth();
571+
572+
env.storage().instance().set(&DataKey::PendingAdmin, &new_admin);
573+
events::emit_ownership_proposed(&env, &admin, &new_admin);
574+
}
575+
576+
/// Accepts pending ownership transfer. Only the pending admin can call this.
577+
///
578+
/// # Panics
579+
/// Panics if there is no pending admin or if caller is not the pending admin.
580+
pub fn accept_ownership(env: Env) {
581+
let pending_admin = Self::read_pending_admin(&env)
582+
.expect("no pending ownership transfer");
583+
584+
pending_admin.require_auth();
585+
586+
let old_admin = Self::read_admin(&env);
587+
env.storage().instance().set(&DataKey::Admin, &pending_admin);
588+
env.storage().instance().remove(&DataKey::PendingAdmin);
589+
590+
events::emit_ownership_accepted(&env, &old_admin, &pending_admin);
591+
}
592+
593+
/// Cancels a pending ownership transfer. Current admin-only.
594+
///
595+
/// # Panics
596+
/// Panics if caller is not the current admin or if there is no pending transfer.
597+
pub fn cancel_transfer(env: Env) {
598+
let admin = Self::read_admin(&env);
599+
admin.require_auth();
600+
601+
let pending_admin = Self::read_pending_admin(&env)
602+
.expect("no pending ownership transfer");
603+
604+
env.storage().instance().remove(&DataKey::PendingAdmin);
605+
events::emit_ownership_cancelled(&env, &admin, &pending_admin);
606+
}
607+
608+
/// Returns the pending admin address if there is a pending transfer.
609+
///
610+
/// # Returns
611+
/// Some(Address) if there is a pending admin, None otherwise.
612+
pub fn pending_owner(env: Env) -> Option<Address> {
613+
Self::read_pending_admin(&env)
614+
}
615+
528616
/// Returns the total token supply.
529617
pub fn supply(env: Env) -> i128 {
530618
Self::panic_on_err(&env, Self::ensure_initialized(&env));
@@ -677,8 +765,6 @@ impl TokenInterface for BcForgeToken {
677765

678766
Self::move_balance(&env, &from, &to, amount);
679767
Self::write_allowance(&env, &from, &spender, allowance - amount, 0); // Keep original expiration
680-
let _ = Self::panic_on_err(&env, Self::move_balance(&env, &from, &to, amount));
681-
Self::write_allowance(&env, &from, &spender, allowance - amount);
682768
events::emit_transfer_from(&env, &spender, &from, &to, amount, allowance - amount);
683769
}
684770

0 commit comments

Comments
 (0)