This document describes the implementation of batch remittance creation for SwiftRemit, which allows high-volume senders to create multiple remittances in a single atomic transaction, reducing ledger fees.
Added BatchCreateEntry struct to represent individual entries in a batch:
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BatchCreateEntry {
/// Address of the agent who will receive the payout
pub agent: Address,
/// Amount to send (in USDC)
pub amount: i128,
/// Optional expiry timestamp (seconds since epoch) for settlement
pub expiry: Option<u64>,
}Added batch_create_remittances function with the following features:
- Atomic Operation: All entries are validated before any state changes occur
- Single Token Transfer: Total amount is transferred in one transaction
- Batch Size Limit: Maximum 100 entries per batch (MAX_BATCH_SIZE)
- Comprehensive Validation: Each entry is validated individually
- Daily Limit Enforcement: Each entry checks daily send limits
pub fn batch_create_remittances(
env: Env,
sender: Address,
entries: Vec<BatchCreateEntry>,
) -> Result<Vec<u64>, ContractError>-
Batch Size Validation: Checks that batch is not empty and doesn't exceed MAX_BATCH_SIZE (100)
-
Pre-validation Phase:
- Validates all entries before any state changes
- Checks agent registration for each entry
- Validates amounts are positive
- Ensures sender is not blacklisted
- Enforces daily send limits for each entry
- Accumulates total amount with overflow protection
-
Token Transfer:
- Transfers total amount in a single token transfer
- Reduces ledger fees compared to individual transfers
-
Remittance Creation:
- Creates all remittances with sequential IDs
- Calculates fees for each entry individually
- Sets up payout commitments
- Initializes transfer states
- Indexes remittances under sender for queries
-
Counter Update: Updates remittance counter once at the end
Created comprehensive test suite covering:
- Success Case: Multiple entries with different agents and amounts
- Partial Failure: Atomic rollback when one entry fails validation
- Oversized Batch: Rejection of batches exceeding 100 entries
- Empty Batch: Rejection of empty batch
- Invalid Amount: Rejection when any entry has zero/negative amount
- Maximum Size: Successful creation of exactly 100 entries
- Different Amounts: Verification of fee calculation for varying amounts
Checked off "Batch remittance processing" in the roadmap section.
✅ Batch size limited to MAX_BATCH_SIZE (100)
- Implemented validation in
batch_create_remittancesfunction - Returns
ContractError::InvalidBatchSizeif batch is empty or exceeds 100
✅ Atomic: all succeed or all fail
- All entries are validated before any state changes
- If any validation fails, no remittances are created
- Token transfer only occurs after all validations pass
✅ Single token transfer for the total amount
- Total amount is accumulated during validation
- Single
token_client.transfer()call with total amount - Reduces ledger fees compared to N individual transfers
✅ Unit tests cover: success, partial failure, oversized batch
test_batch_create_success: Multiple entries succeedtest_batch_create_partial_failure: Atomic rollback on validation failuretest_batch_create_oversized: Rejection of >100 entries- Additional tests for edge cases
✅ README roadmap item checked off
- Updated README.md to mark batch remittance processing as complete
The function returns appropriate errors for various failure scenarios:
ContractError::InvalidBatchSize: Batch is empty or exceeds 100 entriesContractError::InvalidAmount: Any entry has zero or negative amountContractError::AgentNotRegistered: Any agent is not registeredContractError::UserBlacklisted: Sender is blacklistedContractError::DailySendLimitExceeded: Total amount exceeds daily limitContractError::Overflow: Arithmetic overflow in amount calculationContractError::MigrationInProgress: Contract migration is in progress
// Create batch entries
let mut entries = Vec::new(&env);
entries.push_back(BatchCreateEntry {
agent: agent1,
amount: 100_000_000, // 100 USDC
expiry: None,
});
entries.push_back(BatchCreateEntry {
agent: agent2,
amount: 200_000_000, // 200 USDC
expiry: Some(env.ledger().timestamp() + 3600),
});
// Execute batch creation
let remittance_ids = batch_create_remittances(
env.clone(),
sender.clone(),
entries
)?;
// Returns vector of created remittance IDs
assert_eq!(remittance_ids.len(), 2);- Cost Reduction: Single token transfer instead of N transfers reduces ledger fees
- Atomicity: All-or-nothing execution ensures data consistency
- Efficiency: Batch processing is more efficient than individual calls
- Validation: Comprehensive pre-validation prevents partial state changes
- Scalability: Supports up to 100 entries per batch
The implementation includes 7 comprehensive unit tests covering:
- Success scenarios with multiple entries
- Atomic rollback on validation failures
- Batch size limits (empty, oversized, maximum)
- Invalid amounts
- Different fee calculations
All tests follow the existing test patterns in the codebase and use the Soroban testing framework.
Potential improvements for future iterations:
- Support for corridor-specific fees in batch entries
- Idempotency key support for batch operations
- Batch settlement confirmation
- Batch cancellation
- Performance optimizations for large batches