diff --git a/contracts/settlement/src/lib.rs b/contracts/settlement/src/lib.rs
index 86ad38d..38cfd94 100644
--- a/contracts/settlement/src/lib.rs
+++ b/contracts/settlement/src/lib.rs
@@ -1,135 +1,12 @@
#![no_std]
-use soroban_sdk::{contract, contractimpl, contracttype, token, Address, BytesN, Env, Symbol, Vec};
+use soroban_sdk::{contract, contractimpl, token, Address, BytesN, Env, String, Symbol, Vec};
mod errors;
pub use errors::SettlementError;
-/// Maximum number of items allowed in a single `batch_receive_payment` call.
-pub const MAX_BATCH_SIZE: u32 = 50;
-
-/// Maximum number of developer balances returned per page in paginated queries.
-pub const MAX_DEVELOPER_BALANCES_PAGE_SIZE: u32 = 100;
-
-/// Persistent storage keys for settlement contract
-#[contracttype]
-#[derive(Clone, Debug, PartialEq)]
-pub enum StorageKey {
- Admin,
- Vault,
- PendingAdmin,
- PendingVault,
- DeveloperIndex,
- DeveloperBalance(Address),
- GlobalPool,
- Usdc,
- DailyWithdrawCap(Address),
- WithdrawalToday(Address),
- ContractVersion,
-}
-
-/// Developer balance record in settlement contract
-#[contracttype]
-#[derive(Clone, Debug, PartialEq)]
-pub struct DeveloperBalance {
- pub address: Address,
- pub balance: i128,
-}
-
-/// Global pool balance tracking.
-///
-/// `last_updated` is set to `env.ledger().timestamp()` on every
-/// `receive_payment` call that credits the pool (`to_pool = true`).
-/// It is also set at `init` time. It is **not** updated when payments
-/// are routed to individual developer balances.
-#[contracttype]
-#[derive(Clone, Debug, PartialEq)]
-pub struct GlobalPool {
- pub total_balance: i128,
- /// Ledger timestamp of the last pool credit. Useful for analytics
- /// and staleness checks.
- pub last_updated: u64,
-}
-
-/// Tracks a developer's cumulative withdrawal amount for a given epoch day.
-///
-/// `day` is `timestamp / 86400` (UTC epoch day). When the current call's day
-/// differs from the stored day the accumulator is silently reset.
-#[contracttype]
-#[derive(Clone, Debug, PartialEq)]
-pub struct DailyWithdrawState {
- pub day: u64,
- pub amount: i128,
-}
-
-/// Payment received event
-#[contracttype]
-#[derive(Clone, Debug, PartialEq)]
-pub struct PaymentReceivedEvent {
- pub from_vault: Address,
- pub amount: i128,
- pub to_pool: bool, // true if credited to global pool, false if to specific developer
- pub developer: Option
, // developer address if credited to specific developer
-}
-
-/// Balance credited event
-#[contracttype]
-#[derive(Clone, Debug, PartialEq)]
-pub struct BalanceCreditedEvent {
- pub developer: Address,
- pub amount: i128,
- pub new_balance: i128,
-}
-
-/// Emitted when a new vault address is proposed via `propose_vault()`.
-#[contracttype]
-#[derive(Clone, Debug, PartialEq)]
-pub struct VaultProposedEvent {
- pub current_vault: Address,
- pub proposed_vault: Address,
-}
-
-/// Emitted when the proposed vault is accepted via `accept_vault()`.
-#[contracttype]
-#[derive(Clone, Debug, PartialEq)]
-pub struct VaultAcceptedEvent {
- pub old_vault: Address,
- pub new_vault: Address,
- pub accepted_by: Address,
-}
-
-/// Emitted when a developer withdraws their balance.
-#[contracttype]
-#[derive(Clone, Debug, PartialEq)]
-pub struct DeveloperWithdrawEvent {
- pub developer: Address,
- pub amount: i128,
- pub remaining_balance: i128,
- pub to: Address,
-}
-
-/// Emitted when the admin sets or changes a developer's daily withdrawal cap.
-#[contracttype]
-#[derive(Clone, Debug, PartialEq)]
-pub struct DailyWithdrawCapChanged {
- pub developer: Address,
- pub new_cap: i128,
-}
-
-/// Emitted when an admin force-credits a developer balance (escape hatch).
-#[contracttype]
-#[derive(Clone, Debug, PartialEq)]
-pub struct DeveloperForceCreditedEvent {
- pub developer: Address,
- pub amount: i128,
- pub reason: Symbol,
- pub new_balance: i128,
-}
-
-/// Maximum byte length for the `reason` Symbol in `force_credit_developer`.
-/// The Soroban SDK enforces a 32-byte limit on Symbol values at construction;
-/// this constant is used for explicit defense-in-depth validation.
-pub const MAX_REASON_LENGTH: u32 = 32;
+mod types;
+pub use types::*;
#[contract]
@@ -180,16 +57,17 @@ impl CalloraSettlement {
///
/// # Arguments
/// * `caller` - Must be authorized vault address or admin
- /// * `amount` - Payment amount in USDC micro-units; must be > 0
+ /// * `amount` - Payment amount in token micro-units; must be > 0
/// * `to_pool` - If true, credit global pool; if false, credit a specific developer
/// * `developer` - Required when `to_pool=false`; ignored when `to_pool=true`
+ /// * `token` - The token contract address for this payment
///
/// # Access Control
/// Only the registered vault address or admin can call this function.
///
/// # Persistent Storage Operations
/// When crediting to developer balance:
- /// - Performs O(1) point-read from persistent storage for the developer
+ /// - Performs O(1) point-read from persistent storage for the developer + token
/// - Updates the specific developer's balance in persistent storage
/// - Extends persistent TTL for the developer's balance entry
/// - Adds developer to index if not already present
@@ -208,6 +86,7 @@ impl CalloraSettlement {
amount: i128,
to_pool: bool,
developer: Option,
+ token: Address,
) {
caller.require_auth();
Self::require_authorized_caller(env.clone(), caller.clone());
@@ -233,34 +112,33 @@ impl CalloraSettlement {
amount,
to_pool: true,
developer: None,
+ token: token.clone(),
},
);
} else {
let dev_address = developer
.unwrap_or_else(|| env.panic_with_error(SettlementError::DeveloperRequired));
+ // Per-token balance key: (developer, token)
+ let balance_key = StorageKey::DeveloperBalance(dev_address.clone(), token.clone());
+
// Read current balance from persistent storage
let current_balance: i128 = env
.storage()
.persistent()
- .get(&StorageKey::DeveloperBalance(dev_address.clone()))
+ .get(&balance_key)
.unwrap_or(0i128);
let new_balance = current_balance
.checked_add(amount)
.unwrap_or_else(|| env.panic_with_error(SettlementError::DeveloperOverflow));
// Write to persistent storage with TTL extension
- env.storage().persistent().set(
- &StorageKey::DeveloperBalance(dev_address.clone()),
- &new_balance,
- );
+ env.storage().persistent().set(&balance_key, &new_balance);
// Extend TTL for the developer's balance entry (persistent storage live for 1 year)
- env.storage().persistent().extend_ttl(
- &StorageKey::DeveloperBalance(dev_address.clone()),
- 50000,
- 50000,
- );
+ env.storage()
+ .persistent()
+ .extend_ttl(&balance_key, 50000, 50000);
// Add developer to index in sorted order if not already present
let mut index: Vec = inst
@@ -276,6 +154,7 @@ impl CalloraSettlement {
amount,
to_pool: false,
developer: Some(dev_address.clone()),
+ token: token.clone(),
},
);
env.events().publish(
@@ -284,6 +163,7 @@ impl CalloraSettlement {
developer: dev_address,
amount,
new_balance,
+ token,
},
);
}
@@ -294,6 +174,7 @@ impl CalloraSettlement {
/// # Arguments
/// * `caller` - Must be the registered vault address or admin
/// * `items` - Vec of `(developer_address, amount)` pairs; 1–[`MAX_BATCH_SIZE`] entries
+ /// * `token` - The token contract address for this batch payment
///
/// # Access Control
/// Only the registered vault address or admin can call this function.
@@ -313,7 +194,12 @@ impl CalloraSettlement {
/// * `"batch too large"` — more than [`MAX_BATCH_SIZE`] items
/// * `"amount must be positive"` — any amount ≤ 0
/// * `"developer balance overflow"` — `i128` overflow on any developer balance
- pub fn batch_receive_payment(env: Env, caller: Address, items: Vec<(Address, i128)>) {
+ pub fn batch_receive_payment(
+ env: Env,
+ caller: Address,
+ items: Vec<(Address, i128)>,
+ token: Address,
+ ) {
caller.require_auth();
Self::require_authorized_caller(env.clone(), caller.clone());
@@ -331,20 +217,21 @@ impl CalloraSettlement {
for item in items.iter() {
let (dev, amount) = item;
+ let balance_key = StorageKey::DeveloperBalance(dev.clone(), token.clone());
let current: i128 = env
.storage()
.persistent()
- .get(&StorageKey::DeveloperBalance(dev.clone()))
+ .get(&balance_key)
.unwrap_or(0);
let new_balance = current
.checked_add(amount)
.unwrap_or_else(|| env.panic_with_error(SettlementError::DeveloperOverflow));
env.storage()
.persistent()
- .set(&StorageKey::DeveloperBalance(dev.clone()), &new_balance);
+ .set(&balance_key, &new_balance);
env.storage()
.persistent()
- .extend_ttl(&StorageKey::DeveloperBalance(dev.clone()), 50000, 50000);
+ .extend_ttl(&balance_key, 50000, 50000);
// Add to index in sorted order if not already present
let mut index: Vec = inst
.get(&StorageKey::DeveloperIndex)
@@ -355,8 +242,9 @@ impl CalloraSettlement {
(events::event_balance_credited(&env), dev.clone()),
BalanceCreditedEvent {
developer: dev.clone(),
- amount: amount,
+ amount,
new_balance,
+ token: token.clone(),
},
);
}
@@ -386,26 +274,27 @@ impl CalloraSettlement {
.unwrap_or_else(|| env.panic_with_error(SettlementError::NotInitialized))
}
- /// Get developer balance
+ /// Get developer balance for a specific token.
///
- /// Performs a direct O(1) persistent storage lookup for the specified developer's balance.
- /// This is the preferred method for querying individual balances as it uses point storage.
+ /// Performs a direct O(1) persistent storage lookup for the specified
+ /// developer's balance denominated in `token`.
///
/// # Arguments
/// * `developer` - Developer address to query
+ /// * `token` - Token contract address
///
/// # Returns
- /// Balance in USDC micro-units, or 0 if no balance recorded
+ /// Balance in token micro-units, or 0 if no balance recorded
///
/// # Safety
/// Safe for all use cases; uses persistent storage with TTL.
- pub fn get_developer_balance(env: Env, developer: Address) -> i128 {
+ pub fn get_developer_balance(env: Env, developer: Address, token: Address) -> i128 {
if !env.storage().instance().has(&StorageKey::Admin) {
env.panic_with_error(SettlementError::NotInitialized);
}
env.storage()
.persistent()
- .get(&StorageKey::DeveloperBalance(developer))
+ .get(&StorageKey::DeveloperBalance(developer, token))
.unwrap_or(0)
}
@@ -434,56 +323,145 @@ impl CalloraSettlement {
.ok_or(SettlementError::UsdcTokenNotConfigured)
}
- /// Withdraw developer balance as USDC to a designated recipient (defaults to the developer).
-///
-/// Requires the developer to authorize the request and the requested amount
-/// to be positive and covered by the tracked developer balance.
-///
-/// # Arguments
-/// * `developer` - Address of the developer withdrawing their balance
-/// * `amount` - Amount to withdraw in USDC micro-units
-/// * `to` - Optional recipient address; if `None`, defaults to `developer`
-///
-/// # Errors
-/// - `AmountNotPositive` if amount is ≤ 0
-/// - `InsufficientDeveloperBalance` if developer balance < amount
-/// - `DailyWithdrawCapExceeded` if daily cap is exceeded
-/// - `DeveloperBalanceUnderflow` if subtraction underflows
-/// - `UsdcTokenNotConfigured` if USDC token not set
-/// - `InsufficientContractBalance` if contract has insufficient USDC
-/// - Panics if `to` is the contract's own address
-pub fn withdraw_developer_balance(
- env: Env,
- developer: Address,
- amount: i128,
- to: Option,
-) -> Result<(), SettlementError> {
- developer.require_auth();
- if amount <= 0 {
- return Err(SettlementError::AmountNotPositive);
- }
+ /// Migrate a developer's balance from the legacy single-token format
+ /// `DeveloperBalanceV1(dev)` to the new per-token format
+ /// `DeveloperBalance(dev, usdc_token)`.
+ ///
+ /// After migration, the old entry is removed from storage. This is a
+ /// one-way, idempotent operation: calling it again for the same developer
+ /// will see a zero legacy balance and be a no-op.
+ ///
+ /// # Arguments
+ /// * `caller` – Must be the current admin.
+ /// * `developer` – The developer address whose balance to migrate.
+ ///
+ /// # Errors
+ /// * `SettlementError::Unauthorized` – caller is not admin.
+ /// * `SettlementError::UsdcTokenNotConfigured` – no USDC token has been set
+ /// via `set_usdc_token`, which is required because the legacy format was
+ /// single-token (USDC).
+ ///
+ /// # Events
+ /// Does not emit an event; the state change is observable via balance reads.
+ pub fn migrate_developer_balance(
+ env: Env,
+ caller: Address,
+ developer: Address,
+ ) -> Result<(), SettlementError> {
+ caller.require_auth();
+ let admin = Self::get_admin(env.clone());
+ if caller != admin {
+ return Err(SettlementError::Unauthorized);
+ }
- let recipient = to.unwrap_or_else(|| developer.clone());
- let contract_address = env.current_contract_address();
- if recipient == contract_address {
- panic!("invalid recipient: cannot withdraw to contract itself");
- }
+ let usdc = Self::get_usdc_token(env.clone())?;
+ let legacy_key = StorageKey::DeveloperBalanceV1(developer.clone());
+ let pers = env.storage().persistent();
+
+ // Read legacy balance.
+ let balance: i128 = pers.get(&legacy_key).unwrap_or(0);
+ if balance == 0 {
+ // Nothing to migrate — still ok, idempotent.
+ return Ok(());
+ }
- let current_balance: i128 = env
- .storage()
- .persistent()
- .get(&StorageKey::DeveloperBalance(developer.clone()))
- .unwrap_or(0);
- if amount > current_balance {
- return Err(SettlementError::InsufficientDeveloperBalance);
+ // Write new per-token entry.
+ let new_key = StorageKey::DeveloperBalance(developer.clone(), usdc);
+ pers.set(&new_key, &balance);
+ pers.extend_ttl(&new_key, 50000, 50000);
+
+ // Remove legacy entry.
+ pers.remove(&legacy_key);
+
+ Ok(())
}
- let cap: i128 = env
- .storage()
- .persistent()
- .get(&StorageKey::DailyWithdrawCap(developer.clone()))
- .unwrap_or(0);
- if cap > 0 {
+ /// Withdraw developer balance as a specific token to a designated recipient
+ /// (defaults to the developer).
+ ///
+ /// Requires the developer to authorize the request and the requested amount
+ /// to be positive and covered by the tracked developer balance.
+ ///
+ /// # Arguments
+ /// * `developer` - Address of the developer withdrawing their balance
+ /// * `amount` - Amount to withdraw in token micro-units
+ /// * `to` - Optional recipient address; if `None`, defaults to `developer`
+ /// * `token` - The token contract address to withdraw
+ ///
+ /// # Errors
+ /// - `AmountNotPositive` if amount is ≤ 0
+ /// - `InsufficientDeveloperBalance` if developer balance < amount
+ /// - `DailyWithdrawCapExceeded` if daily cap is exceeded
+ /// - `DeveloperBalanceUnderflow` if subtraction underflows
+ /// - `InsufficientContractBalance` if contract has insufficient token balance
+ /// - Panics if `to` is the contract's own address
+ pub fn withdraw_developer_balance(
+ env: Env,
+ developer: Address,
+ amount: i128,
+ to: Option,
+ token: Address,
+ ) -> Result<(), SettlementError> {
+ developer.require_auth();
+ if amount <= 0 {
+ return Err(SettlementError::AmountNotPositive);
+ }
+
+ let recipient = to.unwrap_or_else(|| developer.clone());
+ let contract_address = env.current_contract_address();
+ if recipient == contract_address {
+ panic!("invalid recipient: cannot withdraw to contract itself");
+ }
+
+ let balance_key = StorageKey::DeveloperBalance(developer.clone(), token.clone());
+ let current_balance: i128 = env
+ .storage()
+ .persistent()
+ .get(&balance_key)
+ .unwrap_or(0);
+ if amount > current_balance {
+ return Err(SettlementError::InsufficientDeveloperBalance);
+ }
+
+ let cap: i128 = env
+ .storage()
+ .persistent()
+ .get(&StorageKey::DailyWithdrawCap(developer.clone()))
+ .unwrap_or(0);
+ if cap > 0 {
+ let today = env.ledger().timestamp() / 86400;
+ let mut daily = env
+ .storage()
+ .persistent()
+ .get::<_, DailyWithdrawState>(&StorageKey::WithdrawalToday(developer.clone()))
+ .unwrap_or(DailyWithdrawState { day: today, amount: 0 });
+ if daily.day != today {
+ daily.day = today;
+ daily.amount = 0;
+ }
+ if daily.amount.checked_add(amount).is_none_or(|sum| sum > cap) {
+ return Err(SettlementError::DailyWithdrawCapExceeded);
+ }
+ }
+
+ let new_balance = current_balance
+ .checked_sub(amount)
+ .ok_or(SettlementError::DeveloperBalanceUnderflow)?;
+
+ let token_client = token::Client::new(&env, &token);
+
+ if token_client.balance(&contract_address) < amount {
+ return Err(SettlementError::InsufficientContractBalance);
+ }
+
+ token_client.transfer(&contract_address, &recipient, &amount);
+
+ env.storage().persistent().set(&balance_key, &new_balance);
+ env.storage()
+ .persistent()
+ .extend_ttl(&balance_key, 50000, 50000);
+
+ // Update daily withdrawal accumulator
let today = env.ledger().timestamp() / 86400;
let mut daily = env
.storage()
@@ -494,65 +472,27 @@ pub fn withdraw_developer_balance(
daily.day = today;
daily.amount = 0;
}
- if daily.amount.checked_add(amount).is_none_or(|sum| sum > cap) {
- return Err(SettlementError::DailyWithdrawCapExceeded);
- }
- }
-
- let new_balance = current_balance
- .checked_sub(amount)
- .ok_or(SettlementError::DeveloperBalanceUnderflow)?;
-
- let usdc_address = Self::get_usdc_token(env.clone())?;
- let usdc = token::Client::new(&env, &usdc_address);
+ daily.amount = daily.amount.saturating_add(amount);
+ env.storage()
+ .persistent()
+ .set(&StorageKey::WithdrawalToday(developer.clone()), &daily);
+ env.storage()
+ .persistent()
+ .extend_ttl(&StorageKey::WithdrawalToday(developer.clone()), 50000, 50000);
- if usdc.balance(&contract_address) < amount {
- return Err(SettlementError::InsufficientContractBalance);
- }
+ env.events().publish(
+ (events::event_developer_withdraw(&env), developer.clone()),
+ DeveloperWithdrawEvent {
+ developer,
+ amount,
+ remaining_balance: new_balance,
+ to: recipient,
+ token,
+ },
+ );
- usdc.transfer(&contract_address, &recipient, &amount);
-
- env.storage().persistent().set(
- &StorageKey::DeveloperBalance(developer.clone()),
- &new_balance,
- );
- env.storage().persistent().extend_ttl(
- &StorageKey::DeveloperBalance(developer.clone()),
- 50000,
- 50000,
- );
-
- // Update daily withdrawal accumulator
- let today = env.ledger().timestamp() / 86400;
- let mut daily = env
- .storage()
- .persistent()
- .get::<_, DailyWithdrawState>(&StorageKey::WithdrawalToday(developer.clone()))
- .unwrap_or(DailyWithdrawState { day: today, amount: 0 });
- if daily.day != today {
- daily.day = today;
- daily.amount = 0;
+ Ok(())
}
- daily.amount = daily.amount.saturating_add(amount);
- env.storage()
- .persistent()
- .set(&StorageKey::WithdrawalToday(developer.clone()), &daily);
- env.storage()
- .persistent()
- .extend_ttl(&StorageKey::WithdrawalToday(developer.clone()), 50000, 50000);
-
- env.events().publish(
- (events::event_developer_withdraw(&env), developer.clone()),
- DeveloperWithdrawEvent {
- developer,
- amount,
- remaining_balance: new_balance,
- to: recipient,
- },
- );
-
- Ok(())
-}
/// Set the daily withdrawal cap for a developer (admin only).
///
@@ -606,17 +546,19 @@ pub fn withdraw_developer_balance(
}
}
- /// Admin-only escape hatch to manually credit a developer balance.
+ /// Admin-only escape hatch to manually credit a developer balance for a
+ /// specific token.
///
/// This function is designed for operational edge cases where a developer
/// must be credited outside the normal `receive_payment` flow (e.g.,
/// off-chain payment reconciliation, dispute resolution). It does **not**
- /// move on-ledger USDC and is treated as an audited administrative inflow.
+ /// move on-ledger tokens and is treated as an audited administrative inflow.
///
/// # Arguments
/// * `caller` - Must be the current admin address.
/// * `developer` - Address of the developer to credit.
- /// * `amount` - Amount in USDC micro-units; must be `> 0`.
+ /// * `amount` - Amount in token micro-units; must be `> 0`.
+ /// * `token` - The token contract address for this credit.
/// * `reason` - On-chain reason code (Symbol); used for auditability.
/// The Soroban SDK enforces a 32-byte maximum on Symbol values at
/// construction, so a reason Symbol received here is always ≤ 32 bytes.
@@ -628,12 +570,13 @@ pub fn withdraw_developer_balance(
///
/// # Events
/// Emits `developer_force_credited` with
- /// `(developer, amount, reason, new_balance)`.
+ /// `(developer, amount, token, reason, new_balance)`.
pub fn force_credit_developer(
env: Env,
caller: Address,
developer: Address,
amount: i128,
+ token: Address,
reason: Symbol,
) {
caller.require_auth();
@@ -645,25 +588,20 @@ pub fn withdraw_developer_balance(
env.panic_with_error(SettlementError::AmountNotPositive);
}
+ let balance_key = StorageKey::DeveloperBalance(developer.clone(), token.clone());
let current_balance: i128 = env
.storage()
.persistent()
- .get(&StorageKey::DeveloperBalance(developer.clone()))
+ .get(&balance_key)
.unwrap_or(0i128);
let new_balance = current_balance
.checked_add(amount)
.unwrap_or_else(|| env.panic_with_error(SettlementError::DeveloperOverflow));
+ env.storage().persistent().set(&balance_key, &new_balance);
env.storage()
.persistent()
- .set(&StorageKey::DeveloperBalance(developer.clone()), &new_balance);
- env.storage()
- .persistent()
- .extend_ttl(
- &StorageKey::DeveloperBalance(developer.clone()),
- 50000,
- 50000,
- );
+ .extend_ttl(&balance_key, 50000, 50000);
let mut index: Vec = env
.storage()
@@ -688,7 +626,7 @@ pub fn withdraw_developer_balance(
);
}
- /// Get all developer balances (admin only)
+ /// Get all developer balances for a specific token (admin only).
///
/// **CRITICAL**: Uses developer index for iteration; order is based on index insertion order.
/// Use this function only for administrative queries or reporting purposes.
@@ -697,6 +635,7 @@ pub fn withdraw_developer_balance(
///
/// # Arguments
/// * `caller` - Must be the current admin address.
+ /// * `token` - Token contract address to query balances for.
///
/// # Access Control
/// Only the current admin can call this function.
@@ -726,6 +665,7 @@ pub fn withdraw_developer_balance(
pub fn get_all_developer_balances(
env: Env,
caller: Address,
+ token: Address,
) -> Result, SettlementError> {
caller.require_auth();
let admin = Self::get_admin(env.clone());
@@ -748,26 +688,34 @@ pub fn withdraw_developer_balance(
let balance: i128 = env
.storage()
.persistent()
- .get(&StorageKey::DeveloperBalance(address.clone()))
+ .get(&StorageKey::DeveloperBalance(address.clone(), token.clone()))
.unwrap_or(0i128);
result.push_back(DeveloperBalance {
address: address.clone(),
+ token: token.clone(),
balance,
});
}
Ok(result)
}
- /// Get a paginated slice of developer balances (admin only).
+ /// Get a paginated slice of developer balances for a token (admin only).
///
/// This method avoids expensive full-index iteration by returning
/// a bounded window of developer balance records. Use it for
/// admin dashboards and off-chain pagination.
+ ///
+ /// # Arguments
+ /// * `caller` - Must be the current admin address.
+ /// * `start` - Zero-based start index.
+ /// * `limit` - Maximum records to return; capped at 100.
+ /// * `token` - Token contract address to query balances for.
pub fn get_developer_balances_page(
env: Env,
caller: Address,
start: u32,
limit: u32,
+ token: Address,
) -> Result, SettlementError> {
caller.require_auth();
let admin = Self::get_admin(env.clone());
@@ -794,10 +742,11 @@ pub fn withdraw_developer_balance(
let balance = env
.storage()
.persistent()
- .get(&StorageKey::DeveloperBalance(address.clone()))
+ .get(&StorageKey::DeveloperBalance(address.clone(), token.clone()))
.unwrap_or(0);
result.push_back(DeveloperBalance {
address: address.clone(),
+ token: token.clone(),
balance,
});
}
@@ -809,7 +758,7 @@ pub fn withdraw_developer_balance(
Ok(result)
}
- /// Cursor-based paginated developer balances (admin only).
+ /// Cursor-based paginated developer balances for a specific token (admin only).
///
/// Returns up to `limit` developer balance records starting **after** the
/// supplied `cursor` address (exclusive), or from the beginning of the
@@ -825,6 +774,7 @@ pub fn withdraw_developer_balance(
/// subsequent pages.
/// * `limit` – Maximum records to return; capped at
/// [`MAX_DEVELOPER_BALANCES_PAGE_SIZE`] (100).
+ /// * `token` – Token contract address to query balances for.
///
/// # Returns
/// `(page, next_cursor)` where:
@@ -839,19 +789,12 @@ pub fn withdraw_developer_balance(
/// # Errors
/// * [`SettlementError::NotInitialized`] – contract not yet initialised.
/// * [`SettlementError::Unauthorized`] – caller is not the admin.
- ///
- /// # Example
- /// ```text
- /// // First page
- /// let (page1, next) = client.get_developer_balances_cursor(&admin, &None, &10u32);
- /// // Second page
- /// let (page2, next) = client.get_developer_balances_cursor(&admin, &next, &10u32);
- /// ```
pub fn get_developer_balances_cursor(
env: Env,
caller: Address,
cursor: Option,
limit: u32,
+ token: Address,
) -> (Vec, Option) {
caller.require_auth();
let admin = Self::get_admin(env.clone());
@@ -892,10 +835,11 @@ pub fn withdraw_developer_balance(
let balance: i128 = env
.storage()
.persistent()
- .get(&StorageKey::DeveloperBalance(address.clone()))
+ .get(&StorageKey::DeveloperBalance(address.clone(), token.clone()))
.unwrap_or(0i128);
result.push_back(DeveloperBalance {
address: address.clone(),
+ token: token.clone(),
balance,
});
last_address = Some(address.clone());
@@ -1222,3 +1166,6 @@ mod test_invariant;
#[cfg(test)]
mod test_error_codes;
+
+#[cfg(test)]
+mod test_multi_asset;
diff --git a/contracts/settlement/src/test.rs b/contracts/settlement/src/test.rs
index e5c6881..f37bc65 100644
--- a/contracts/settlement/src/test.rs
+++ b/contracts/settlement/src/test.rs
@@ -6,7 +6,7 @@ mod settlement_tests {
use soroban_sdk::testutils::{Address as _, Ledger as _, Events as _};
use soroban_sdk::{token, Address, Env, Error, InvokeError, Symbol, BytesN, TryFromVal};
- fn setup_contract() -> (Env, Address, Address, Address, Address) {
+ fn setup_contract() -> (Env, Address, Address, Address, Address, Address) {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
@@ -15,7 +15,8 @@ mod settlement_tests {
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
let third_party = Address::generate(&env);
- (env, addr, admin, vault, third_party)
+ let token = Address::generate(&env);
+ (env, addr, admin, vault, third_party, token)
}
fn is_error, E: Into>(
@@ -50,6 +51,7 @@ mod settlement_tests {
let developer = Address::generate(&env);
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
+ let token = Address::generate(&env);
client.init(&admin, &vault);
@@ -68,9 +70,9 @@ mod settlement_tests {
assert_eq!(global_pool.total_balance, 0);
assert_eq!(global_pool.last_updated, 1_700_000_000);
- let all_balances = client.get_all_developer_balances(&admin);
+ let all_balances = client.get_all_developer_balances(&admin, &token);
assert_eq!(all_balances.len(), 0);
- assert_eq!(client.get_developer_balance(&developer), 0);
+ assert_eq!(client.get_developer_balance(&developer, &token), 0);
}
#[test]
#[should_panic(expected = "invalid config: admin and vault_address must be distinct")]
@@ -136,8 +138,9 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
- client.receive_payment(&vault, &1000i128, &true, &None);
+ client.receive_payment(&vault, &1000i128, &true, &None, &token);
let global_pool = client.get_global_pool();
assert_eq!(global_pool.total_balance, 1000i128);
@@ -153,10 +156,11 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
- client.receive_payment(&vault, &500i128, &false, &Some(developer.clone()));
+ client.receive_payment(&vault, &500i128, &false, &Some(developer.clone()), &token);
- assert_eq!(client.get_developer_balance(&developer), 500i128);
+ assert_eq!(client.get_developer_balance(&developer, &token), 500i128);
assert_eq!(client.get_global_pool().total_balance, 0);
}
@@ -170,11 +174,12 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
- client.receive_payment(&vault, &100i128, &false, &Some(developer.clone()));
- client.receive_payment(&vault, &150i128, &false, &Some(developer.clone()));
+ client.receive_payment(&vault, &100i128, &false, &Some(developer.clone()), &token);
+ client.receive_payment(&vault, &150i128, &false, &Some(developer.clone()), &token);
- assert_eq!(client.get_developer_balance(&developer), 250i128);
+ assert_eq!(client.get_developer_balance(&developer, &token), 250i128);
}
#[test]
@@ -187,8 +192,9 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
- let balance = client.get_developer_balance(&developer);
+ let balance = client.get_developer_balance(&developer, &token);
assert_eq!(balance, 0);
}
@@ -201,8 +207,9 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
- let all = client.get_all_developer_balances(&admin);
+ let all = client.get_all_developer_balances(&admin, &token);
assert_eq!(all.len(), 0);
}
@@ -216,7 +223,8 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
- client.receive_payment(&admin, &100i128, &true, &None);
+ let token = Address::generate(&env);
+ client.receive_payment(&admin, &100i128, &true, &None, &token);
}
#[test]
@@ -230,10 +238,11 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
- client.receive_payment(&admin, &200i128, &false, &Some(developer.clone()));
+ client.receive_payment(&admin, &200i128, &false, &Some(developer.clone()), &token);
- assert_eq!(client.get_developer_balance(&developer), 200i128);
+ assert_eq!(client.get_developer_balance(&developer, &token), 200i128);
}
#[test]
@@ -245,9 +254,10 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
- client.receive_payment(&vault, &400i128, &true, &None);
- client.receive_payment(&vault, &600i128, &true, &None);
+ client.receive_payment(&vault, &400i128, &true, &None, &token);
+ client.receive_payment(&vault, &600i128, &true, &None, &token);
assert_eq!(client.get_global_pool().total_balance, 1000i128);
}
@@ -262,8 +272,9 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
- assert_eq!(client.get_developer_balance(&stranger), 0i128);
+ assert_eq!(client.get_developer_balance(&stranger, &token), 0i128);
}
#[test]
@@ -279,12 +290,12 @@ mod settlement_tests {
client.init(&admin, &vault);
client.set_usdc_token(&admin, &usdc_address);
- client.receive_payment(&vault, &100i128, &false, &Some(developer.clone()));
+ client.receive_payment(&vault, &100i128, &false, &Some(developer.clone()), &usdc_address);
usdc_admin_client.mint(&addr, &100i128);
- let result = client.try_withdraw_developer_balance(&developer, &100i128, &None);
+ let result = client.try_withdraw_developer_balance(&developer, &100i128, &None, &usdc_address);
assert!(result.is_ok());
- assert_eq!(client.get_developer_balance(&developer), 0i128);
+ assert_eq!(client.get_developer_balance(&developer, &usdc_address), 0i128);
assert_eq!(
token::Client::new(&env, &usdc_address).balance(&addr),
0i128
@@ -308,12 +319,12 @@ mod settlement_tests {
client.init(&admin, &vault);
client.set_usdc_token(&admin, &usdc_address);
- client.receive_payment(&vault, &100i128, &false, &Some(developer.clone()));
+ client.receive_payment(&vault, &100i128, &false, &Some(developer.clone()), &usdc_address);
usdc_admin_client.mint(&addr, &100i128);
- let result = client.try_withdraw_developer_balance(&developer, &101i128, &None);
+ let result = client.try_withdraw_developer_balance(&developer, &101i128, &None, &usdc_address);
assert!(result.is_err());
- assert_eq!(client.get_developer_balance(&developer), 100i128);
+ assert_eq!(client.get_developer_balance(&developer, &usdc_address), 100i128);
}
#[test]
@@ -325,11 +336,12 @@ mod settlement_tests {
let developer = Address::generate(&env);
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
+ let token = Address::generate(&env);
client.init(&admin, &vault);
- let zero_result = client.try_withdraw_developer_balance(&developer, &0i128, &None);
- let negative_result = client.try_withdraw_developer_balance(&developer, &-1i128, &None);
+ let zero_result = client.try_withdraw_developer_balance(&developer, &0i128, &None, &token);
+ let negative_result = client.try_withdraw_developer_balance(&developer, &-1i128, &None, &token);
assert!(zero_result.is_err());
assert!(negative_result.is_err());
@@ -351,10 +363,10 @@ mod settlement_tests {
client.init(&admin, &vault);
client.set_usdc_token(&admin, &usdc_address);
- client.receive_payment(&vault, &200i128, &false, &Some(developer.clone()));
+ client.receive_payment(&vault, &200i128, &false, &Some(developer.clone()), &usdc_address);
usdc_admin_client.mint(&addr, &200i128);
- let result = client.try_withdraw_developer_balance(&developer, &200i128, &None);
+ let result = client.try_withdraw_developer_balance(&developer, &200i128, &None, &usdc_address);
assert!(result.is_ok());
let events = env.events().all();
@@ -392,12 +404,12 @@ mod settlement_tests {
client.init(&admin, &vault);
client.set_usdc_token(&admin, &usdc_address);
- client.receive_payment(&vault, &150i128, &false, &Some(developer.clone()));
+ client.receive_payment(&vault, &150i128, &false, &Some(developer.clone()), &usdc_address);
usdc_admin_client.mint(&addr, &150i128);
- let result = client.try_withdraw_developer_balance(&developer, &150i128, &Some(custodial.clone()));
+ let result = client.try_withdraw_developer_balance(&developer, &150i128, &Some(custodial.clone()), &usdc_address);
assert!(result.is_ok());
- assert_eq!(client.get_developer_balance(&developer), 0i128);
+ assert_eq!(client.get_developer_balance(&developer, &usdc_address), 0i128);
assert_eq!(
token::Client::new(&env, &usdc_address).balance(&addr),
0i128
@@ -425,10 +437,10 @@ mod settlement_tests {
client.init(&admin, &vault);
client.set_usdc_token(&admin, &usdc_address);
- client.receive_payment(&vault, &200i128, &false, &Some(developer.clone()));
+ client.receive_payment(&vault, &200i128, &false, &Some(developer.clone()), &usdc_address);
usdc_admin_client.mint(&addr, &200i128);
- let result = client.try_withdraw_developer_balance(&developer, &200i128, &Some(custodial.clone()));
+ let result = client.try_withdraw_developer_balance(&developer, &200i128, &Some(custodial.clone()), &usdc_address);
assert!(result.is_ok());
let events = env.events().all();
@@ -459,14 +471,14 @@ mod settlement_tests {
let developer = Address::generate(&env);
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
- let (usdc_address, usdc_admin_client) = create_usdc(&env, &admin);
+ let (usdc_address, _, usdc_admin_client) = create_usdc(&env, &admin);
client.init(&admin, &vault);
client.set_usdc_token(&admin, &usdc_address);
- client.receive_payment(&vault, &100i128, &false, &Some(developer.clone()));
+ client.receive_payment(&vault, &100i128, &false, &Some(developer.clone()), &usdc_address);
usdc_admin_client.mint(&addr, &100i128);
- client.withdraw_developer_balance(&developer, &100i128, &Some(addr.clone()));
+ client.withdraw_developer_balance(&developer, &100i128, &Some(addr.clone()), &usdc_address);
}
#[test]
@@ -480,12 +492,13 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
- client.receive_payment(&vault, &300i128, &false, &Some(dev1.clone()));
- client.receive_payment(&vault, &200i128, &false, &Some(dev2.clone()));
- client.receive_payment(&vault, &150i128, &false, &Some(dev1.clone()));
+ client.receive_payment(&vault, &300i128, &false, &Some(dev1.clone()), &token);
+ client.receive_payment(&vault, &200i128, &false, &Some(dev2.clone()), &token);
+ client.receive_payment(&vault, &150i128, &false, &Some(dev1.clone()), &token);
- let all = client.get_all_developer_balances(&admin);
+ let all = client.get_all_developer_balances(&admin, &token);
assert_eq!(all.len(), 2);
let mut dev1_seen = false;
let mut dev2_seen = false;
@@ -513,13 +526,9 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
- let all = client.get_all_developer_balances(&admin);
- assert_eq!(all.len(), 0);
- }
-
- #[test]
- fn test_get_developer_balances_page() {
+ let all = client.get_all_developer_balances(&admin, &token);
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
@@ -530,12 +539,13 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
- client.receive_payment(&vault, &100i128, &false, &Some(dev1.clone()));
- client.receive_payment(&vault, &200i128, &false, &Some(dev2.clone()));
- client.receive_payment(&vault, &300i128, &false, &Some(dev3.clone()));
+ client.receive_payment(&vault, &100i128, &false, &Some(dev1.clone()), &token);
+ client.receive_payment(&vault, &200i128, &false, &Some(dev2.clone()), &token);
+ client.receive_payment(&vault, &300i128, &false, &Some(dev3.clone()), &token);
- let page = client.get_developer_balances_page(&admin, &1u32, &2u32);
+ let page = client.get_developer_balances_page(&admin, &1u32, &2u32, &token);
assert_eq!(page.len(), 2);
assert_eq!(page.get(0).unwrap().address, dev2);
assert_eq!(page.get(1).unwrap().address, dev3);
@@ -550,14 +560,15 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
for _ in 0..105 {
let developer = Address::generate(&env);
- client.receive_payment(&vault, &1i128, &false, &Some(developer));
+ client.receive_payment(&vault, &1i128, &false, &Some(developer), &token);
}
// limit higher than MAX should be capped at MAX_DEVELOPER_BALANCES_PAGE_SIZE (100)
- let page = client.get_developer_balances_page(&admin, &0u32, &200u32);
+ let page = client.get_developer_balances_page(&admin, &0u32, &200u32, &token);
assert_eq!(page.len(), 100);
}
@@ -570,13 +581,14 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
for _ in 0..101 {
let developer = Address::generate(&env);
- client.receive_payment(&vault, &1i128, &false, &Some(developer));
+ client.receive_payment(&vault, &1i128, &false, &Some(developer), &token);
}
- let all = client.get_all_developer_balances(&admin);
+ let all = client.get_all_developer_balances(&admin, &token);
assert_eq!(all.len(), 101);
}
@@ -951,10 +963,11 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
// Add some balance
- client.receive_payment(&vault, &1000i128, &false, &Some(developer.clone()));
- let dev_balance_before = client.get_developer_balance(&developer);
+ client.receive_payment(&vault, &1000i128, &false, &Some(developer.clone()), &token);
+ let dev_balance_before = client.get_developer_balance(&developer, &token);
let pool_before = client.get_global_pool();
// Rotate admin
@@ -962,7 +975,7 @@ mod settlement_tests {
client.accept_admin();
// State preserved
- assert_eq!(client.get_developer_balance(&developer), dev_balance_before);
+ assert_eq!(client.get_developer_balance(&developer, &token), dev_balance_before);
assert_eq!(
client.get_global_pool().total_balance,
pool_before.total_balance
@@ -1065,8 +1078,9 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
- let result = client.try_receive_payment(&vault, &0i128, &true, &None);
+ let result = client.try_receive_payment(&vault, &0i128, &true, &None, &token);
assert!(is_error(result, SettlementError::AmountNotPositive));
}
@@ -1079,8 +1093,9 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
- let result = client.try_receive_payment(&vault, &-1i128, &true, &None);
+ let result = client.try_receive_payment(&vault, &-1i128, &true, &None, &token);
assert!(is_error(result, SettlementError::AmountNotPositive));
}
@@ -1093,6 +1108,7 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
env.as_contract(&addr, || {
let inst = env.storage().instance();
@@ -1103,7 +1119,7 @@ mod settlement_tests {
inst.set(&crate::StorageKey::GlobalPool, &pool);
});
- let result = client.try_receive_payment(&vault, &1i128, &true, &None);
+ let result = client.try_receive_payment(&vault, &1i128, &true, &None, &token);
assert!(is_error(result, SettlementError::PoolOverflow));
}
@@ -1117,15 +1133,16 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
env.as_contract(&addr, || {
env.storage().persistent().set(
- &crate::StorageKey::DeveloperBalance(developer.clone()),
+ &crate::StorageKey::DeveloperBalance(developer.clone(), token.clone()),
&i128::MAX,
);
});
- let result = client.try_receive_payment(&vault, &1i128, &false, &Some(developer));
+ let result = client.try_receive_payment(&vault, &1i128, &false, &Some(developer), &token);
assert!(is_error(result, SettlementError::DeveloperOverflow));
}
@@ -1138,8 +1155,9 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
- let result = client.try_receive_payment(&vault, &100i128, &false, &None);
+ let result = client.try_receive_payment(&vault, &100i128, &false, &None, &token);
assert!(is_error(result, SettlementError::DeveloperRequired));
}
@@ -1153,8 +1171,9 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
- let result = client.try_receive_payment(&vault, &100i128, &true, &Some(developer));
+ let result = client.try_receive_payment(&vault, &100i128, &true, &Some(developer), &token);
assert!(is_error(result, SettlementError::DeveloperMustBeNone));
}
@@ -1191,7 +1210,7 @@ mod settlement_tests {
];
for case in cases {
- let (env, addr, admin, vault, third_party) = setup_contract();
+ let (env, addr, admin, vault, third_party, token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let caller = match case.role {
CallerRole::Vault => vault,
@@ -1199,7 +1218,7 @@ mod settlement_tests {
CallerRole::ThirdParty => third_party,
};
- let result = client.try_receive_payment(&caller, &100i128, &true, &None);
+ let result = client.try_receive_payment(&caller, &100i128, &true, &None, &token);
if case.should_succeed {
assert!(result.is_ok(), "expected success for case: {}", case.name);
@@ -1228,8 +1247,9 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
- client.receive_payment(&vault, &1000i128, &true, &None);
+ client.receive_payment(&vault, &1000i128, &true, &None, &token);
let events = env.events().all();
let ev = events
@@ -1265,11 +1285,11 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
- client.receive_payment(&vault, &500i128, &false, &Some(developer.clone()));
+ client.receive_payment(&vault, &500i128, &false, &Some(developer.clone()), &token);
let events = env.events().all();
-
let pr_ev = events
.iter()
.find(|e| {
@@ -1316,9 +1336,10 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
- client.receive_payment(&vault, &300i128, &false, &Some(developer.clone()));
- client.receive_payment(&vault, &200i128, &false, &Some(developer.clone()));
+ client.receive_payment(&vault, &300i128, &false, &Some(developer.clone()), &token);
+ client.receive_payment(&vault, &200i128, &false, &Some(developer.clone()), &token);
// grab the last balance_credited event
let events = env.events().all();
@@ -1350,13 +1371,14 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
// Rotate admin
client.set_admin(&admin, &new_admin);
client.accept_admin();
// Vault can still send payments
- client.receive_payment(&vault, &1000i128, &true, &None);
+ client.receive_payment(&vault, &1000i128, &true, &None, &token);
assert_eq!(client.get_global_pool().total_balance, 1000i128);
}
@@ -1371,17 +1393,18 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
// Update vault
client.propose_vault(&admin, &new_vault);
client.accept_vault(&new_vault);
// Old vault cannot send payments
- let result = client.try_receive_payment(&vault, &1000i128, &true, &None);
+ let result = client.try_receive_payment(&vault, &1000i128, &true, &None, &token);
assert!(is_error(result, SettlementError::Unauthorized));
// New vault can send payments
- client.receive_payment(&new_vault, &1000i128, &true, &None);
+ client.receive_payment(&new_vault, &1000i128, &true, &None, &token);
assert_eq!(client.get_global_pool().total_balance, 1000i128);
}
@@ -1397,19 +1420,20 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
// Credit developer
- client.receive_payment(&vault, &500i128, &false, &Some(developer.clone()));
+ client.receive_payment(&vault, &500i128, &false, &Some(developer.clone()), &token);
// Rotate admin
client.set_admin(&admin, &new_admin);
client.accept_admin();
// Balance still accessible
- assert_eq!(client.get_developer_balance(&developer), 500i128);
+ assert_eq!(client.get_developer_balance(&developer, &token), 500i128);
// Admin can still view all balances
- let all_balances = client.get_all_developer_balances(&new_admin);
+ let all_balances = client.get_all_developer_balances(&new_admin, &token);
assert_eq!(all_balances.len(), 1);
assert_eq!(all_balances.get(0).unwrap().balance, 500i128);
}
@@ -1426,20 +1450,21 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
// Some payments from old vault
- client.receive_payment(&vault, &100i128, &false, &Some(developer.clone()));
+ client.receive_payment(&vault, &100i128, &false, &Some(developer.clone()), &token);
// Update vault
client.propose_vault(&admin, &new_vault);
client.accept_vault(&new_vault);
// More payments from new vault
- client.receive_payment(&new_vault, &150i128, &false, &Some(developer.clone()));
- client.receive_payment(&new_vault, &200i128, &false, &Some(developer.clone()));
+ client.receive_payment(&new_vault, &150i128, &false, &Some(developer.clone()), &token);
+ client.receive_payment(&new_vault, &200i128, &false, &Some(developer.clone()), &token);
// Total should accumulate correctly
- assert_eq!(client.get_developer_balance(&developer), 450i128);
+ assert_eq!(client.get_developer_balance(&developer, &token), 450i128);
}
#[test]
@@ -1455,9 +1480,10 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
// Initial payment
- client.receive_payment(&vault, &1000i128, &true, &None);
+ client.receive_payment(&vault, &1000i128, &true, &None, &token);
let pool_before = client.get_global_pool();
assert_eq!(pool_before.last_updated, 1_700_000_000);
@@ -1467,7 +1493,7 @@ mod settlement_tests {
env.ledger().set_timestamp(1_700_000_100);
// New payment updates timestamp
- client.receive_payment(&vault, &500i128, &true, &None);
+ client.receive_payment(&vault, &500i128, &true, &None, &token);
let pool_after = client.get_global_pool();
assert_eq!(pool_after.last_updated, 1_700_000_100);
assert_eq!(pool_after.total_balance, 1500i128);
@@ -1483,6 +1509,7 @@ mod settlement_tests {
let vault = Address::generate(&env);
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
+ let token = Address::generate(&env);
env.ledger().set_timestamp(1_000);
client.init(&admin, &vault);
@@ -1490,14 +1517,14 @@ mod settlement_tests {
// Advance time and credit pool � last_updated must change
env.ledger().set_timestamp(2_000);
- client.receive_payment(&vault, &100i128, &true, &None);
+ client.receive_payment(&vault, &100i128, &true, &None, &token);
let pool = client.get_global_pool();
assert_eq!(pool.last_updated, 2_000);
assert_eq!(pool.total_balance, 100i128);
// Advance again � each credit stamps the new time
env.ledger().set_timestamp(3_000);
- client.receive_payment(&vault, &50i128, &true, &None);
+ client.receive_payment(&vault, &50i128, &true, &None, &token);
let pool2 = client.get_global_pool();
assert_eq!(pool2.last_updated, 3_000);
assert_eq!(pool2.total_balance, 150i128);
@@ -1514,24 +1541,25 @@ mod settlement_tests {
let developer = Address::generate(&env);
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
+ let token = Address::generate(&env);
env.ledger().set_timestamp(1_000);
client.init(&admin, &vault);
env.ledger().set_timestamp(5_000);
- client.receive_payment(&vault, &200i128, &false, &Some(developer.clone()));
+ client.receive_payment(&vault, &200i128, &false, &Some(developer.clone()), &token);
// Pool timestamp must still be the init timestamp
assert_eq!(client.get_global_pool().last_updated, 1_000);
assert_eq!(client.get_global_pool().total_balance, 0);
- assert_eq!(client.get_developer_balance(&developer), 200i128);
+ assert_eq!(client.get_developer_balance(&developer, &token), 200i128);
}
// --- Authorization Matrix Tests ---
#[test]
fn test_set_admin_authorization_matrix() {
- let (env, addr, admin, vault, third_party) = setup_contract();
+ let (env, addr, admin, vault, third_party, _token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let new_admin = Address::generate(&env);
@@ -1549,7 +1577,7 @@ mod settlement_tests {
#[test]
fn test_set_vault_authorization_matrix() {
- let (env, addr, admin, vault, third_party) = setup_contract();
+ let (env, addr, admin, vault, third_party, _token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let new_vault = Address::generate(&env);
@@ -1567,7 +1595,7 @@ mod settlement_tests {
#[test]
fn test_accept_vault_rejects_unauthorized_caller() {
- let (env, addr, admin, vault, third_party) = setup_contract();
+ let (env, addr, admin, vault, third_party, _token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let new_vault = Address::generate(&env);
@@ -1580,7 +1608,7 @@ mod settlement_tests {
#[test]
fn test_accept_vault_allows_admin_to_finalize() {
- let (env, addr, admin, vault, _third_party) = setup_contract();
+ let (env, addr, admin, vault, _third_party, _token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let new_vault = Address::generate(&env);
@@ -1607,7 +1635,7 @@ mod settlement_tests {
#[test]
fn test_accept_admin_authorization_matrix() {
- let (env, addr, admin, _vault, _third_party) = setup_contract();
+ let (env, addr, admin, _vault, _third_party, _token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let new_admin = Address::generate(&env);
@@ -1620,18 +1648,18 @@ mod settlement_tests {
#[test]
fn test_get_all_developer_balances_authorization_matrix() {
- let (env, addr, admin, vault, third_party) = setup_contract();
+ let (env, addr, admin, vault, third_party, token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
// Admin can call
- let _ = client.get_all_developer_balances(&admin);
+ let _ = client.get_all_developer_balances(&admin, &token);
// Vault cannot call
- let result = client.try_get_all_developer_balances(&vault);
+ let result = client.try_get_all_developer_balances(&vault, &token);
assert!(is_error(result, SettlementError::Unauthorized));
// Third party cannot call
- let result = client.try_get_all_developer_balances(&third_party);
+ let result = client.try_get_all_developer_balances(&third_party, &token);
assert!(is_error(result, SettlementError::Unauthorized));
}
@@ -1639,7 +1667,7 @@ mod settlement_tests {
#[test]
fn test_batch_receive_payment_credits_multiple_developers() {
- let (env, addr, _admin, vault, _third_party) = setup_contract();
+ let (env, addr, _admin, vault, _third_party, token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let dev1 = Address::generate(&env);
let dev2 = Address::generate(&env);
@@ -1648,54 +1676,54 @@ mod settlement_tests {
items.push_back((dev1.clone(), 100i128));
items.push_back((dev2.clone(), 200i128));
- client.batch_receive_payment(&vault, &items);
+ client.batch_receive_payment(&vault, &items, &token);
- assert_eq!(client.get_developer_balance(&dev1), 100i128);
- assert_eq!(client.get_developer_balance(&dev2), 200i128);
+ assert_eq!(client.get_developer_balance(&dev1, &token), 100i128);
+ assert_eq!(client.get_developer_balance(&dev2, &token), 200i128);
}
#[test]
fn test_batch_receive_payment_accumulates_existing_balance() {
- let (env, addr, _admin, vault, _third_party) = setup_contract();
+ let (env, addr, _admin, vault, _third_party, token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let dev = Address::generate(&env);
- client.receive_payment(&vault, &50i128, &false, &Some(dev.clone()));
+ client.receive_payment(&vault, &50i128, &false, &Some(dev.clone()), &token);
let mut items = soroban_sdk::Vec::new(&env);
items.push_back((dev.clone(), 75i128));
- client.batch_receive_payment(&vault, &items);
+ client.batch_receive_payment(&vault, &items, &token);
- assert_eq!(client.get_developer_balance(&dev), 125i128);
+ assert_eq!(client.get_developer_balance(&dev, &token), 125i128);
}
#[test]
fn test_batch_receive_payment_admin_caller_allowed() {
- let (env, addr, admin, _vault, _third_party) = setup_contract();
+ let (env, addr, admin, _vault, _third_party, token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let dev = Address::generate(&env);
let mut items = soroban_sdk::Vec::new(&env);
items.push_back((dev.clone(), 300i128));
- client.batch_receive_payment(&admin, &items);
+ client.batch_receive_payment(&admin, &items, &token);
- assert_eq!(client.get_developer_balance(&dev), 300i128);
+ assert_eq!(client.get_developer_balance(&dev, &token), 300i128);
}
#[test]
fn test_batch_receive_payment_rejects_empty_batch() {
- let (env, addr, _admin, vault, _third_party) = setup_contract();
+ let (env, addr, _admin, vault, _third_party, token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let items: soroban_sdk::Vec<(Address, i128)> = soroban_sdk::Vec::new(&env);
- let result = client.try_batch_receive_payment(&vault, &items);
+ let result = client.try_batch_receive_payment(&vault, &items, &token);
assert!(result.is_err());
}
#[test]
fn test_batch_receive_payment_rejects_oversized_batch() {
use crate::MAX_BATCH_SIZE;
- let (env, addr, _admin, vault, _third_party) = setup_contract();
+ let (env, addr, _admin, vault, _third_party, token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let dev = Address::generate(&env);
@@ -1703,63 +1731,63 @@ mod settlement_tests {
for _ in 0..=MAX_BATCH_SIZE {
items.push_back((dev.clone(), 1i128));
}
- let result = client.try_batch_receive_payment(&vault, &items);
+ let result = client.try_batch_receive_payment(&vault, &items, &token);
assert!(result.is_err());
}
#[test]
fn test_batch_receive_payment_rejects_zero_amount() {
- let (env, addr, _admin, vault, _third_party) = setup_contract();
+ let (env, addr, _admin, vault, _third_party, token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let dev = Address::generate(&env);
let mut items = soroban_sdk::Vec::new(&env);
items.push_back((dev.clone(), 0i128));
- let result = client.try_batch_receive_payment(&vault, &items);
+ let result = client.try_batch_receive_payment(&vault, &items, &token);
assert!(result.is_err());
}
#[test]
fn test_batch_receive_payment_rejects_negative_amount() {
- let (env, addr, _admin, vault, _third_party) = setup_contract();
+ let (env, addr, _admin, vault, _third_party, token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let dev = Address::generate(&env);
let mut items = soroban_sdk::Vec::new(&env);
items.push_back((dev.clone(), -1i128));
- let result = client.try_batch_receive_payment(&vault, &items);
+ let result = client.try_batch_receive_payment(&vault, &items, &token);
assert!(result.is_err());
}
#[test]
fn test_batch_receive_payment_unauthorized_caller_rejected() {
- let (env, addr, _admin, _vault, third_party) = setup_contract();
+ let (env, addr, _admin, _vault, third_party, token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let dev = Address::generate(&env);
let mut items = soroban_sdk::Vec::new(&env);
items.push_back((dev.clone(), 100i128));
- let result = client.try_batch_receive_payment(&third_party, &items);
+ let result = client.try_batch_receive_payment(&third_party, &items, &token);
assert!(is_error(result, SettlementError::Unauthorized));
}
#[test]
fn test_batch_receive_payment_single_item() {
- let (env, addr, _admin, vault, _third_party) = setup_contract();
+ let (env, addr, _admin, vault, _third_party, token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let dev = Address::generate(&env);
let mut items = soroban_sdk::Vec::new(&env);
items.push_back((dev.clone(), 999i128));
- client.batch_receive_payment(&vault, &items);
+ client.batch_receive_payment(&vault, &items, &token);
- assert_eq!(client.get_developer_balance(&dev), 999i128);
+ assert_eq!(client.get_developer_balance(&dev, &token), 999i128);
}
#[test]
fn test_batch_receive_payment_max_batch_size_accepted() {
use crate::MAX_BATCH_SIZE;
- let (env, addr, _admin, vault, _third_party) = setup_contract();
+ let (env, addr, _admin, vault, _third_party, token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let mut items = soroban_sdk::Vec::new(&env);
@@ -1769,10 +1797,10 @@ mod settlement_tests {
devs.push(dev.clone());
items.push_back((dev, 1i128));
}
- client.batch_receive_payment(&vault, &items);
+ client.batch_receive_payment(&vault, &items, &token);
for dev in &devs {
- assert_eq!(client.get_developer_balance(dev), 1i128);
+ assert_eq!(client.get_developer_balance(dev, &token), 1i128);
}
}
@@ -1780,19 +1808,19 @@ mod settlement_tests {
#[test]
fn test_force_credit_developer_happy_path() {
- let (env, addr, admin, _vault, _third_party) = setup_contract();
+ let (env, addr, admin, _vault, _third_party, token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let developer = Address::generate(&env);
let reason = Symbol::new(&env, "offline_settlement");
- client.force_credit_developer(&admin, &developer, &1000i128, &reason);
+ client.force_credit_developer(&admin, &developer, &1000i128, &token, &reason);
- assert_eq!(client.get_developer_balance(&developer), 1000i128);
+ assert_eq!(client.get_developer_balance(&developer, &token), 1000i128);
}
#[test]
fn test_force_credit_developer_accumulates() {
- let (env, addr, admin, _vault, _third_party) = setup_contract();
+ let (env, addr, admin, _vault, _third_party, token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let developer = Address::generate(&env);
@@ -1800,37 +1828,39 @@ mod settlement_tests {
&admin,
&developer,
&500i128,
+ &token,
&Symbol::new(&env, "first"),
);
client.force_credit_developer(
&admin,
&developer,
&300i128,
+ &token,
&Symbol::new(&env, "second"),
);
- assert_eq!(client.get_developer_balance(&developer), 800i128);
+ assert_eq!(client.get_developer_balance(&developer, &token), 800i128);
}
#[test]
fn test_force_credit_developer_unauthorized() {
- let (env, addr, _admin, vault, third_party) = setup_contract();
+ let (env, addr, _admin, vault, third_party, token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let developer = Address::generate(&env);
let reason = Symbol::new(&env, "unauthorized_test");
let vault_result =
- client.try_force_credit_developer(&vault, &developer, &100i128, &reason);
+ client.try_force_credit_developer(&vault, &developer, &100i128, &token, &reason);
assert!(is_error(vault_result, SettlementError::Unauthorized));
let third_party_result =
- client.try_force_credit_developer(&third_party, &developer, &100i128, &reason);
+ client.try_force_credit_developer(&third_party, &developer, &100i128, &token, &reason);
assert!(is_error(third_party_result, SettlementError::Unauthorized));
}
#[test]
fn test_force_credit_developer_zero_amount() {
- let (env, addr, admin, _vault, _third_party) = setup_contract();
+ let (env, addr, admin, _vault, _third_party, token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let developer = Address::generate(&env);
@@ -1838,6 +1868,7 @@ mod settlement_tests {
&admin,
&developer,
&0i128,
+ &token,
&Symbol::new(&env, "zero"),
);
assert!(is_error(result, SettlementError::AmountNotPositive));
@@ -1845,7 +1876,7 @@ mod settlement_tests {
#[test]
fn test_force_credit_developer_negative_amount() {
- let (env, addr, admin, _vault, _third_party) = setup_contract();
+ let (env, addr, admin, _vault, _third_party, token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let developer = Address::generate(&env);
@@ -1853,6 +1884,7 @@ mod settlement_tests {
&admin,
&developer,
&-1i128,
+ &token,
&Symbol::new(&env, "negative"),
);
assert!(is_error(result, SettlementError::AmountNotPositive));
@@ -1863,12 +1895,12 @@ mod settlement_tests {
use soroban_sdk::testutils::Events as _;
use soroban_sdk::IntoVal;
- let (env, addr, admin, _vault, _third_party) = setup_contract();
+ let (env, addr, admin, _vault, _third_party, token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let developer = Address::generate(&env);
let reason = Symbol::new(&env, "dispute_resolution");
- client.force_credit_developer(&admin, &developer, &2500i128, &reason);
+ client.force_credit_developer(&admin, &developer, &2500i128, &token, &reason);
let events = env.events().all();
let ev = events
@@ -1893,22 +1925,22 @@ mod settlement_tests {
#[test]
fn test_force_credit_developer_repeated_reason() {
- let (env, addr, admin, _vault, _third_party) = setup_contract();
+ let (env, addr, admin, _vault, _third_party, token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let developer1 = Address::generate(&env);
let developer2 = Address::generate(&env);
let reason = Symbol::new(&env, "bulk_reconciliation");
- client.force_credit_developer(&admin, &developer1, &100i128, &reason);
- client.force_credit_developer(&admin, &developer2, &200i128, &reason);
+ client.force_credit_developer(&admin, &developer1, &100i128, &token, &reason);
+ client.force_credit_developer(&admin, &developer2, &200i128, &token, &reason);
- assert_eq!(client.get_developer_balance(&developer1), 100i128);
- assert_eq!(client.get_developer_balance(&developer2), 200i128);
+ assert_eq!(client.get_developer_balance(&developer1, &token), 100i128);
+ assert_eq!(client.get_developer_balance(&developer2, &token), 200i128);
}
#[test]
fn test_force_credit_developer_overflow() {
- let (env, addr, admin, _vault, _third_party) = setup_contract();
+ let (env, addr, admin, _vault, _third_party, token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let developer = Address::generate(&env);
@@ -1916,7 +1948,7 @@ mod settlement_tests {
env.storage()
.persistent()
.set(
- &crate::StorageKey::DeveloperBalance(developer.clone()),
+ &crate::StorageKey::DeveloperBalance(developer.clone(), token.clone()),
&i128::MAX,
);
});
@@ -1925,6 +1957,7 @@ mod settlement_tests {
&admin,
&developer,
&1i128,
+ &token,
&Symbol::new(&env, "overflow"),
);
assert!(is_error(result, SettlementError::DeveloperOverflow));
@@ -1936,7 +1969,7 @@ mod settlement_tests {
/// Includes overflow-boundary cases near i128::MAX.
#[test]
fn test_conservation_invariant_randomized() {
- let (env, addr, admin, vault, _third_party) = setup_contract();
+ let (env, addr, admin, vault, _third_party, token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let mut developers = std::vec::Vec::new();
@@ -1959,11 +1992,11 @@ mod settlement_tests {
let amount = (next_rand() % 1_000_000) as i128 + 1;
if to_pool {
- client.receive_payment(&vault, &amount, &true, &None);
+ client.receive_payment(&vault, &amount, &true, &None, &token);
} else {
let dev_idx = (next_rand() % 10) as usize;
if let Some(developer) = developers.get(dev_idx) {
- client.receive_payment(&vault, &amount, &false, &Some(developer.clone()));
+ client.receive_payment(&vault, &amount, &false, &Some(developer.clone()), &token);
}
}
total_credited += amount;
@@ -1978,12 +2011,12 @@ mod settlement_tests {
let half_remaining = remaining / 2;
// Large credit to pool
- client.receive_payment(&vault, &half_remaining, &true, &None);
+ client.receive_payment(&vault, &half_remaining, &true, &None, &token);
total_credited += half_remaining;
// Large credit to a developer
if let Some(developer) = developers.get(0) {
- client.receive_payment(&vault, &half_remaining, &false, &Some(developer.clone()));
+ client.receive_payment(&vault, &half_remaining, &false, &Some(developer.clone()), &token);
total_credited += half_remaining;
}
}
@@ -1992,7 +2025,7 @@ mod settlement_tests {
let pool = client.get_global_pool();
let mut sum_dev_balances: i128 = 0;
- let all_balances = client.get_all_developer_balances(&admin);
+ let all_balances = client.get_all_developer_balances(&admin, &token);
for record in all_balances.iter() {
sum_dev_balances += record.balance;
}
@@ -2005,7 +2038,7 @@ mod settlement_tests {
}
#[test]
fn test_upgrade_and_get_version() {
- let (env, addr, admin, _vault, _third_party) = setup_contract();
+ let (env, addr, admin, _vault, _third_party, _token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
assert_eq!(client.get_version(), None);
@@ -2038,18 +2071,18 @@ mod settlement_tests {
client.init(&admin, &vault);
client.set_usdc_token(&admin, &usdc_address);
client.set_daily_withdraw_cap(&admin, &developer, &500i128);
- client.receive_payment(&vault, &1000i128, &false, &Some(developer.clone()));
+ client.receive_payment(&vault, &1000i128, &false, &Some(developer.clone()), &usdc_address);
usdc_admin_client.mint(&addr, &1000i128);
// First withdrawal of 300 should succeed (under 500 cap)
- let result = client.try_withdraw_developer_balance(&developer, &300i128);
+ let result = client.try_withdraw_developer_balance(&developer, &300i128, &None, &usdc_address);
assert!(result.is_ok());
- assert_eq!(client.get_developer_balance(&developer), 700i128);
+ assert_eq!(client.get_developer_balance(&developer, &usdc_address), 700i128);
// Second withdrawal of 300 would push total to 600 (over 500 cap)
- let result = client.try_withdraw_developer_balance(&developer, &300i128);
+ let result = client.try_withdraw_developer_balance(&developer, &300i128, &None, &usdc_address);
assert!(is_error(result, SettlementError::DailyWithdrawCapExceeded));
- assert_eq!(client.get_developer_balance(&developer), 700i128);
+ assert_eq!(client.get_developer_balance(&developer, &usdc_address), 700i128);
}
#[test]
@@ -2066,20 +2099,20 @@ mod settlement_tests {
client.init(&admin, &vault);
client.set_usdc_token(&admin, &usdc_address);
client.set_daily_withdraw_cap(&admin, &developer, &500i128);
- client.receive_payment(&vault, &1000i128, &false, &Some(developer.clone()));
+ client.receive_payment(&vault, &1000i128, &false, &Some(developer.clone()), &usdc_address);
usdc_admin_client.mint(&addr, &1000i128);
// Withdraw 200 + 200 = 400, still under 500
- assert!(client.try_withdraw_developer_balance(&developer, &200i128).is_ok());
- assert!(client.try_withdraw_developer_balance(&developer, &200i128).is_ok());
- assert_eq!(client.get_developer_balance(&developer), 600i128);
+ assert!(client.try_withdraw_developer_balance(&developer, &200i128, &None, &usdc_address).is_ok());
+ assert!(client.try_withdraw_developer_balance(&developer, &200i128, &None, &usdc_address).is_ok());
+ assert_eq!(client.get_developer_balance(&developer, &usdc_address), 600i128);
// Third withdrawal of 100 would push to 500 (exact cap — allowed)
- assert!(client.try_withdraw_developer_balance(&developer, &100i128).is_ok());
- assert_eq!(client.get_developer_balance(&developer), 500i128);
+ assert!(client.try_withdraw_developer_balance(&developer, &100i128, &None, &usdc_address).is_ok());
+ assert_eq!(client.get_developer_balance(&developer, &usdc_address), 500i128);
// Fourth withdrawal of 1 would exceed cap
- let result = client.try_withdraw_developer_balance(&developer, &1i128);
+ let result = client.try_withdraw_developer_balance(&developer, &1i128, &None, &usdc_address);
assert!(is_error(result, SettlementError::DailyWithdrawCapExceeded));
}
@@ -2098,11 +2131,11 @@ mod settlement_tests {
client.set_usdc_token(&admin, &usdc_address);
// Cap = 0 explicitly means unlimited
client.set_daily_withdraw_cap(&admin, &developer, &0i128);
- client.receive_payment(&vault, &1000i128, &false, &Some(developer.clone()));
+ client.receive_payment(&vault, &1000i128, &false, &Some(developer.clone()), &usdc_address);
usdc_admin_client.mint(&addr, &1000i128);
- assert!(client.try_withdraw_developer_balance(&developer, &1000i128).is_ok());
- assert_eq!(client.get_developer_balance(&developer), 0i128);
+ assert!(client.try_withdraw_developer_balance(&developer, &1000i128, &None, &usdc_address).is_ok());
+ assert_eq!(client.get_developer_balance(&developer, &usdc_address), 0i128);
}
#[test]
@@ -2119,11 +2152,11 @@ mod settlement_tests {
client.init(&admin, &vault);
client.set_usdc_token(&admin, &usdc_address);
// No cap set at all — should be unlimited
- client.receive_payment(&vault, &1000i128, &false, &Some(developer.clone()));
+ client.receive_payment(&vault, &1000i128, &false, &Some(developer.clone()), &usdc_address);
usdc_admin_client.mint(&addr, &1000i128);
- assert!(client.try_withdraw_developer_balance(&developer, &1000i128).is_ok());
- assert_eq!(client.get_developer_balance(&developer), 0i128);
+ assert!(client.try_withdraw_developer_balance(&developer, &1000i128, &None, &usdc_address).is_ok());
+ assert_eq!(client.get_developer_balance(&developer, &usdc_address), 0i128);
}
#[test]
@@ -2142,15 +2175,15 @@ mod settlement_tests {
client.init(&admin, &vault);
client.set_usdc_token(&admin, &usdc_address);
client.set_daily_withdraw_cap(&admin, &developer, &500i128);
- client.receive_payment(&vault, &1000i128, &false, &Some(developer.clone()));
+ client.receive_payment(&vault, &1000i128, &false, &Some(developer.clone()), &usdc_address);
usdc_admin_client.mint(&addr, &1000i128);
// Withdraw 400 on day 0
- assert!(client.try_withdraw_developer_balance(&developer, &400i128).is_ok());
- assert_eq!(client.get_developer_balance(&developer), 600i128);
+ assert!(client.try_withdraw_developer_balance(&developer, &400i128, &None, &usdc_address).is_ok());
+ assert_eq!(client.get_developer_balance(&developer, &usdc_address), 600i128);
// Another 200 would exceed the 500 cap
- let result = client.try_withdraw_developer_balance(&developer, &200i128);
+ let result = client.try_withdraw_developer_balance(&developer, &200i128, &None, &usdc_address);
assert!(is_error(result, SettlementError::DailyWithdrawCapExceeded));
// Advance to day 1
@@ -2159,13 +2192,13 @@ mod settlement_tests {
usdc_admin_client.mint(&addr, &500i128);
// Withdrawal should succeed now (cap resets)
- assert!(client.try_withdraw_developer_balance(&developer, &500i128).is_ok());
- assert_eq!(client.get_developer_balance(&developer), 100i128);
+ assert!(client.try_withdraw_developer_balance(&developer, &500i128, &None, &usdc_address).is_ok());
+ assert_eq!(client.get_developer_balance(&developer, &usdc_address), 100i128);
}
#[test]
fn test_set_daily_withdraw_cap_unauthorized() {
- let (env, addr, _admin, vault, third_party) = setup_contract();
+ let (env, addr, _admin, vault, third_party, _token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let developer = Address::generate(&env);
@@ -2215,7 +2248,7 @@ mod settlement_tests {
#[test]
fn test_get_daily_withdraw_cap_returns_zero_when_unset() {
- let (env, addr, _admin, _vault, _third_party) = setup_contract();
+ let (env, addr, _admin, _vault, _third_party, _token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let developer = Address::generate(&env);
@@ -2225,7 +2258,7 @@ mod settlement_tests {
#[test]
fn test_get_withdrawal_today_returns_zero_after_no_withdrawals() {
- let (env, addr, _admin, _vault, _third_party) = setup_contract();
+ let (env, addr, _admin, _vault, _third_party, _token) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let developer = Address::generate(&env);
@@ -2247,15 +2280,15 @@ mod settlement_tests {
client.init(&admin, &vault);
client.set_usdc_token(&admin, &usdc_address);
client.set_daily_withdraw_cap(&admin, &developer, &1000i128);
- client.receive_payment(&vault, &1000i128, &false, &Some(developer.clone()));
+ client.receive_payment(&vault, &1000i128, &false, &Some(developer.clone()), &usdc_address);
usdc_admin_client.mint(&addr, &1000i128);
assert_eq!(client.get_withdrawal_today(&developer), 0i128);
- client.withdraw_developer_balance(&developer, &300i128);
+ client.withdraw_developer_balance(&developer, &300i128, &None, &usdc_address);
assert_eq!(client.get_withdrawal_today(&developer), 300i128);
- client.withdraw_developer_balance(&developer, &200i128);
+ client.withdraw_developer_balance(&developer, &200i128, &None, &usdc_address);
assert_eq!(client.get_withdrawal_today(&developer), 500i128);
}
@@ -2276,20 +2309,20 @@ mod settlement_tests {
client.set_usdc_token(&admin, &usdc_address);
client.set_daily_withdraw_cap(&admin, &dev1, &500i128);
// dev2 has no cap (unlimited)
- client.receive_payment(&vault, &1000i128, &false, &Some(dev1.clone()));
- client.receive_payment(&vault, &500i128, &false, &Some(dev2.clone()));
+ client.receive_payment(&vault, &1000i128, &false, &Some(dev1.clone()), &usdc_address);
+ client.receive_payment(&vault, &500i128, &false, &Some(dev2.clone()), &usdc_address);
usdc_admin_client.mint(&addr, &1500i128);
// dev1 hits cap at 500
- assert!(client.try_withdraw_developer_balance(&dev1, &300i128).is_ok());
+ assert!(client.try_withdraw_developer_balance(&dev1, &300i128, &None, &usdc_address).is_ok());
// Still within cap (300 < 500)
- assert!(client.try_withdraw_developer_balance(&dev1, &200i128).is_ok());
+ assert!(client.try_withdraw_developer_balance(&dev1, &200i128, &None, &usdc_address).is_ok());
// Exceeds cap (300 + 200 + 1 > 500)
- let result = client.try_withdraw_developer_balance(&dev1, &1i128);
+ let result = client.try_withdraw_developer_balance(&dev1, &1i128, &None, &usdc_address);
assert!(is_error(result, SettlementError::DailyWithdrawCapExceeded));
// dev2 can still withdraw (no cap)
- assert!(client.try_withdraw_developer_balance(&dev2, &500i128).is_ok());
+ assert!(client.try_withdraw_developer_balance(&dev2, &500i128, &None, &usdc_address).is_ok());
}
// ── cursor-based pagination tests ────────────────────────────────────────
@@ -2306,15 +2339,16 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
let dev1 = Address::generate(&env);
let dev2 = Address::generate(&env);
let dev3 = Address::generate(&env);
- client.receive_payment(&vault, &100i128, &false, &Some(dev1.clone()));
- client.receive_payment(&vault, &200i128, &false, &Some(dev2.clone()));
- client.receive_payment(&vault, &300i128, &false, &Some(dev3.clone()));
+ client.receive_payment(&vault, &100i128, &false, &Some(dev1.clone()), &token);
+ client.receive_payment(&vault, &200i128, &false, &Some(dev2.clone()), &token);
+ client.receive_payment(&vault, &300i128, &false, &Some(dev3.clone()), &token);
- let (page, next) = client.get_developer_balances_cursor(&admin, &None, &2u32);
+ let (page, next) = client.get_developer_balances_cursor(&admin, &None, &2u32, &token);
assert_eq!(page.len(), 2, "first page must contain exactly limit records");
// next_cursor must point at the last record on this page so the caller
@@ -2337,21 +2371,22 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
let dev1 = Address::generate(&env);
let dev2 = Address::generate(&env);
let dev3 = Address::generate(&env);
- client.receive_payment(&vault, &10i128, &false, &Some(dev1.clone()));
- client.receive_payment(&vault, &20i128, &false, &Some(dev2.clone()));
- client.receive_payment(&vault, &30i128, &false, &Some(dev3.clone()));
+ client.receive_payment(&vault, &10i128, &false, &Some(dev1.clone()), &token);
+ client.receive_payment(&vault, &20i128, &false, &Some(dev2.clone()), &token);
+ client.receive_payment(&vault, &30i128, &false, &Some(dev3.clone()), &token);
// Page 1
- let (page1, next1) = client.get_developer_balances_cursor(&admin, &None, &2u32);
+ let (page1, next1) = client.get_developer_balances_cursor(&admin, &None, &2u32, &token);
assert_eq!(page1.len(), 2);
assert!(next1.is_some());
// Page 2 — use next_cursor from page 1
- let (page2, next2) = client.get_developer_balances_cursor(&admin, &next1, &2u32);
+ let (page2, next2) = client.get_developer_balances_cursor(&admin, &next1, &2u32, &token);
assert_eq!(page2.len(), 1, "last page must contain the remaining record");
// Reached the end of the index.
assert!(next2.is_none(), "next_cursor must be None on the last page");
@@ -2376,21 +2411,22 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
let dev1 = Address::generate(&env);
let dev2 = Address::generate(&env);
- client.receive_payment(&vault, &1i128, &false, &Some(dev1.clone()));
- client.receive_payment(&vault, &2i128, &false, &Some(dev2.clone()));
+ client.receive_payment(&vault, &1i128, &false, &Some(dev1.clone()), &token);
+ client.receive_payment(&vault, &2i128, &false, &Some(dev2.clone()), &token);
// Exhaust the index with a large limit to find the last cursor.
- let (full_page, last_cursor) = client.get_developer_balances_cursor(&admin, &None, &100u32);
+ let (full_page, last_cursor) = client.get_developer_balances_cursor(&admin, &None, &100u32, &token);
assert_eq!(full_page.len(), 2);
assert!(last_cursor.is_none());
// Use the address of the last record as the cursor — nothing should follow.
let last_addr = full_page.get(full_page.len() - 1).unwrap().address;
let (empty_page, next) =
- client.get_developer_balances_cursor(&admin, &Some(last_addr), &10u32);
+ client.get_developer_balances_cursor(&admin, &Some(last_addr), &10u32, &token);
assert_eq!(empty_page.len(), 0, "page after last cursor must be empty");
assert!(next.is_none());
}
@@ -2406,27 +2442,28 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
// Pre-populate three developers so the sorted index is stable.
let dev_a = Address::generate(&env);
let dev_b = Address::generate(&env);
let dev_c = Address::generate(&env);
- client.receive_payment(&vault, &1i128, &false, &Some(dev_a.clone()));
- client.receive_payment(&vault, &1i128, &false, &Some(dev_b.clone()));
- client.receive_payment(&vault, &1i128, &false, &Some(dev_c.clone()));
+ client.receive_payment(&vault, &1i128, &false, &Some(dev_a.clone()), &token);
+ client.receive_payment(&vault, &1i128, &false, &Some(dev_b.clone()), &token);
+ client.receive_payment(&vault, &1i128, &false, &Some(dev_c.clone()), &token);
// Fetch first page (limit=1) to get the cursor.
let (page1, cursor_after_first) =
- client.get_developer_balances_cursor(&admin, &None, &1u32);
+ client.get_developer_balances_cursor(&admin, &None, &1u32, &token);
assert_eq!(page1.len(), 1);
let first_addr = page1.get(0).unwrap().address.clone();
// Credit the first developer again — this must NOT shift remaining pages.
- client.receive_payment(&vault, &999i128, &false, &Some(first_addr.clone()));
+ client.receive_payment(&vault, &999i128, &false, &Some(first_addr.clone()), &token);
// Continue pagination from the saved cursor.
let (page2, _) =
- client.get_developer_balances_cursor(&admin, &cursor_after_first, &10u32);
+ client.get_developer_balances_cursor(&admin, &cursor_after_first, &10u32, &token);
assert_eq!(page2.len(), 2, "two records must remain after the cursor");
// The first developer must not appear again in page2.
@@ -2450,16 +2487,17 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
// Insert more developers than MAX_DEVELOPER_BALANCES_PAGE_SIZE.
for _ in 0..(MAX_DEVELOPER_BALANCES_PAGE_SIZE + 10) {
let dev = Address::generate(&env);
- client.receive_payment(&vault, &1i128, &false, &Some(dev));
+ client.receive_payment(&vault, &1i128, &false, &Some(dev), &token);
}
// Request more than the cap.
let (page, _) =
- client.get_developer_balances_cursor(&admin, &None, &(MAX_DEVELOPER_BALANCES_PAGE_SIZE + 50));
+ client.get_developer_balances_cursor(&admin, &None, &(MAX_DEVELOPER_BALANCES_PAGE_SIZE + 50), &token);
assert_eq!(
page.len(),
MAX_DEVELOPER_BALANCES_PAGE_SIZE,
@@ -2477,11 +2515,12 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
let dev = Address::generate(&env);
- client.receive_payment(&vault, &1i128, &false, &Some(dev));
+ client.receive_payment(&vault, &1i128, &false, &Some(dev), &token);
- let (page, next) = client.get_developer_balances_cursor(&admin, &None, &0u32);
+ let (page, next) = client.get_developer_balances_cursor(&admin, &None, &0u32, &token);
assert_eq!(page.len(), 0);
assert!(next.is_none());
}
@@ -2496,8 +2535,9 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
- let (page, next) = client.get_developer_balances_cursor(&admin, &None, &10u32);
+ let (page, next) = client.get_developer_balances_cursor(&admin, &None, &10u32, &token);
assert_eq!(page.len(), 0);
assert!(next.is_none());
}
@@ -2512,8 +2552,9 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
- let result = client.try_get_developer_balances_cursor(&vault, &None, &10u32);
+ let result = client.try_get_developer_balances_cursor(&vault, &None, &10u32, &token);
assert!(
is_error(result, SettlementError::Unauthorized),
"non-admin caller must be rejected with Unauthorized"
@@ -2531,18 +2572,19 @@ mod settlement_tests {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
+ let token = Address::generate(&env);
// Generate addresses in arbitrary order and credit them.
let mut devs: std::vec::Vec = (0..5).map(|_| Address::generate(&env)).collect();
for dev in &devs {
- client.receive_payment(&vault, &1i128, &false, &Some(dev.clone()));
+ client.receive_payment(&vault, &1i128, &false, &Some(dev.clone()), &token);
}
// Collect all balances via cursor pagination.
let mut cursor_pages: std::vec::Vec = std::vec::Vec::new();
let mut next: Option = None;
loop {
- let (page, nc) = client.get_developer_balances_cursor(&admin, &next, &2u32);
+ let (page, nc) = client.get_developer_balances_cursor(&admin, &next, &2u32, &token);
for r in page.iter() {
cursor_pages.push(r.address.clone());
}
diff --git a/contracts/settlement/src/test_invariant.rs b/contracts/settlement/src/test_invariant.rs
index 06c51e9..a7a4629 100644
--- a/contracts/settlement/src/test_invariant.rs
+++ b/contracts/settlement/src/test_invariant.rs
@@ -33,6 +33,8 @@
extern crate std;
+use std::boxed::Box;
+
use proptest::prelude::*;
use soroban_sdk::testutils::Address as _;
use soroban_sdk::{token, Address, Env, Vec};
@@ -189,7 +191,7 @@ fn make_usdc<'a>(
}
fn setup_env() -> (
- Env,
+ &'static Env,
Address, // contract address
CalloraSettlementClient<'static>,
Address, // admin
@@ -231,13 +233,14 @@ fn check_invariant(
env: &Env,
client: &CalloraSettlementClient<'_>,
admin: &Address,
+ token: &Address,
expected_dev_total: i128,
expected_pool_total: i128,
trace: &Trace,
step: u32,
) {
// Sum all developer balances via the paginated view.
- let balances = client.get_all_developer_balances(admin);
+ let balances = client.get_all_developer_balances(admin, token);
let dev_sum: i128 = balances.iter().map(|b| b.balance).sum();
let pool = client.get_global_pool().total_balance;
@@ -306,7 +309,7 @@ fn run_trace(seed: u64) {
let mut expected_pool_total: i128 = 0;
// Check invariant at t=0 (empty state).
- check_invariant(env, &client, &admin, 0, 0, &trace, 0);
+ check_invariant(env, &client, &admin, &usdc_addr, 0, 0, &trace, 0);
for step in 1..=TRACE_LENGTH {
let op = (rng.next_u64() % OP_COUNT) as u8;
@@ -315,7 +318,7 @@ fn run_trace(seed: u64) {
x if x == Op::ReceiveDev as u8 => {
let dev = devs[rng.gen_usize(0, DEV_POOL_SIZE - 1)].clone();
let amount = rng.gen_i128(1, AMOUNT_CAP);
- client.receive_payment(&vault, &amount, &false, &Some(dev.clone()));
+ client.receive_payment(&vault, &amount, &false, &Some(dev.clone()), &usdc_addr);
expected_dev_total = expected_dev_total
.checked_add(amount)
.expect("test tally overflow");
@@ -328,7 +331,7 @@ fn run_trace(seed: u64) {
x if x == Op::ReceivePool as u8 => {
let amount = rng.gen_i128(1, AMOUNT_CAP);
- client.receive_payment(&vault, &amount, &true, &None);
+ client.receive_payment(&vault, &amount, &true, &None, &usdc_addr);
expected_pool_total = expected_pool_total
.checked_add(amount)
.expect("test tally overflow");
@@ -345,7 +348,7 @@ fn run_trace(seed: u64) {
items.push_back((dev, amount));
batch_total = batch_total.checked_add(amount).expect("batch tally overflow");
}
- client.batch_receive_payment(&vault, &items);
+ client.batch_receive_payment(&vault, &items, &usdc_addr);
expected_dev_total = expected_dev_total
.checked_add(batch_total)
.expect("test tally overflow");
@@ -359,10 +362,10 @@ fn run_trace(seed: u64) {
x if x == Op::Withdraw as u8 => {
// Pick a developer who has a positive balance.
let dev = devs[rng.gen_usize(0, DEV_POOL_SIZE - 1)].clone();
- let current: i128 = client.get_developer_balance(&dev);
+ let current: i128 = client.get_developer_balance(&dev, &usdc_addr);
if current > 0 {
let amount = rng.gen_i128(1, current.min(AMOUNT_CAP));
- let result = client.try_withdraw_developer_balance(&dev, &amount, &None);
+ let result = client.try_withdraw_developer_balance(&dev, &amount, &None, &usdc_addr);
if result.is_ok() {
expected_dev_total = expected_dev_total
.checked_sub(amount)
@@ -395,6 +398,7 @@ fn run_trace(seed: u64) {
env,
&client,
&admin,
+ &usdc_addr,
expected_dev_total,
expected_pool_total,
&trace,
@@ -441,14 +445,14 @@ fn test_invariant_pool_only() {
let amounts = [100i128, 200, 300, 50, 1];
let mut expected_pool: i128 = 0;
for (i, &amount) in amounts.iter().enumerate() {
- client.receive_payment(&vault, &amount, &true, &None);
+ client.receive_payment(&vault, &amount, &true, &None, &usdc_addr);
expected_pool += amount;
let pool = client.get_global_pool().total_balance;
assert_eq!(
pool, expected_pool,
"pool invariant failed at step {i}: expected {expected_pool}, got {pool}"
);
- let dev_sum: i128 = client.get_all_developer_balances(&admin).iter().map(|b| b.balance).sum();
+ let dev_sum: i128 = client.get_all_developer_balances(&admin, &usdc_addr).iter().map(|b| b.balance).sum();
assert_eq!(dev_sum, 0, "no developer should have a balance (step {i})");
}
}
@@ -475,21 +479,21 @@ fn test_invariant_single_dev_full_withdraw() {
client.set_usdc_token(&admin, &usdc_addr);
// Credit the developer.
- client.receive_payment(&vault, &1_000, &false, &Some(dev.clone()));
- client.receive_payment(&vault, &2_000, &false, &Some(dev.clone()));
- client.receive_payment(&vault, &500, &false, &Some(dev.clone()));
+ client.receive_payment(&vault, &1_000, &false, &Some(dev.clone()), &usdc_addr);
+ client.receive_payment(&vault, &2_000, &false, &Some(dev.clone()), &usdc_addr);
+ client.receive_payment(&vault, &500, &false, &Some(dev.clone()), &usdc_addr);
- let balance = client.get_developer_balance(&dev);
+ let balance = client.get_developer_balance(&dev, &usdc_addr);
assert_eq!(balance, 3_500);
- let dev_sum: i128 = client.get_all_developer_balances(&admin).iter().map(|b| b.balance).sum();
+ let dev_sum: i128 = client.get_all_developer_balances(&admin, &usdc_addr).iter().map(|b| b.balance).sum();
assert_eq!(dev_sum, 3_500, "dev sum before withdraw");
// Full withdraw.
- client.withdraw_developer_balance(&dev, &3_500, &None);
+ client.withdraw_developer_balance(&dev, &3_500, &None, &usdc_addr);
let dev_sum_after: i128 = client
- .get_all_developer_balances(&admin)
+ .get_all_developer_balances(&admin, &usdc_addr)
.iter()
.map(|b| b.balance)
.sum();
@@ -522,11 +526,11 @@ fn test_invariant_batch_duplicate_dev() {
let mut items: Vec<(Address, i128)> = Vec::new(env);
items.push_back((dev.clone(), 100));
items.push_back((dev.clone(), 200));
- client.batch_receive_payment(&vault, &items);
+ client.batch_receive_payment(&vault, &items, &usdc_addr);
- let dev_sum: i128 = client.get_all_developer_balances(&admin).iter().map(|b| b.balance).sum();
+ let dev_sum: i128 = client.get_all_developer_balances(&admin, &usdc_addr).iter().map(|b| b.balance).sum();
assert_eq!(dev_sum, 300, "batch duplicate dev: expected 300, got {dev_sum}");
- assert_eq!(client.get_developer_balance(&dev), 300);
+ assert_eq!(client.get_developer_balance(&dev, &usdc_addr), 300);
}
/// Edge case: interleaved developer and pool payments preserve the full conservation invariant.
@@ -565,14 +569,14 @@ fn test_invariant_interleaved_dev_and_pool() {
for &(to_pool, amount, is_dev1) in ops {
if to_pool {
- client.receive_payment(&vault, &amount, &true, &None);
+ client.receive_payment(&vault, &amount, &true, &None, &usdc_addr);
exp_pool += amount;
} else {
let dev = if is_dev1 { dev1.clone() } else { dev2.clone() };
- client.receive_payment(&vault, &amount, &false, &Some(dev));
+ client.receive_payment(&vault, &amount, &false, &Some(dev), &usdc_addr);
exp_dev += amount;
}
- let dev_sum: i128 = client.get_all_developer_balances(&admin).iter().map(|b| b.balance).sum();
+ let dev_sum: i128 = client.get_all_developer_balances(&admin, &usdc_addr).iter().map(|b| b.balance).sum();
let pool = client.get_global_pool().total_balance;
assert_eq!(dev_sum, exp_dev, "dev sum mismatch");
assert_eq!(pool, exp_pool, "pool mismatch");
diff --git a/contracts/settlement/src/test_multi_asset.rs b/contracts/settlement/src/test_multi_asset.rs
new file mode 100644
index 0000000..813dc38
--- /dev/null
+++ b/contracts/settlement/src/test_multi_asset.rs
@@ -0,0 +1,323 @@
+extern crate std;
+
+use crate::{CalloraSettlement, CalloraSettlementClient, SettlementError, StorageKey};
+use soroban_sdk::testutils::{Address as _, Ledger as _};
+use soroban_sdk::{token, Address, Env, Symbol};
+
+fn create_token<'a>(
+ env: &'a Env,
+ admin: &Address,
+) -> (Address, token::Client<'a>, token::StellarAssetClient<'a>) {
+ let contract_address = env.register_stellar_asset_contract_v2(admin.clone());
+ let address = contract_address.address();
+ let client = token::Client::new(env, &address);
+ let admin_client = token::StellarAssetClient::new(env, &address);
+ (address, client, admin_client)
+}
+
+/// Acceptance: two different tokens can be tracked independently for the same developer.
+#[test]
+fn test_two_tokens_independent_balances() {
+ let env = Env::default();
+ env.mock_all_auths();
+ env.ledger().set_timestamp(1_700_000_000);
+
+ let admin = Address::generate(&env);
+ let vault = Address::generate(&env);
+ let developer = Address::generate(&env);
+ let addr = env.register(CalloraSettlement, ());
+ let client = CalloraSettlementClient::new(&env, &addr);
+
+ let (token_a, _, _) = create_token(&env, &admin);
+ let (token_b, _, _) = create_token(&env, &admin);
+
+ client.init(&admin, &vault);
+
+ // Credit token_a to developer
+ client.receive_payment(&vault, &1000i128, &false, &Some(developer.clone()), &token_a);
+ // Credit token_b to developer
+ client.receive_payment(&vault, &2000i128, &false, &Some(developer.clone()), &token_b);
+
+ // Balances are independent per token
+ assert_eq!(
+ client.get_developer_balance(&developer, &token_a),
+ 1000i128
+ );
+ assert_eq!(
+ client.get_developer_balance(&developer, &token_b),
+ 2000i128
+ );
+
+ // get_all_developer_balances filters by token
+ let all_a = client.get_all_developer_balances(&admin, &token_a);
+ assert_eq!(all_a.len(), 1);
+ assert_eq!(all_a.get(0).unwrap().balance, 1000i128);
+ assert_eq!(all_a.get(0).unwrap().token, token_a);
+
+ let all_b = client.get_all_developer_balances(&admin, &token_b);
+ assert_eq!(all_b.len(), 1);
+ assert_eq!(all_b.get(0).unwrap().balance, 2000i128);
+ assert_eq!(all_b.get(0).unwrap().token, token_b);
+}
+
+/// Acceptance: receives independently for two developers across two tokens.
+#[test]
+fn test_two_tokens_two_developers() {
+ let env = Env::default();
+ env.mock_all_auths();
+ env.ledger().set_timestamp(1_700_000_000);
+
+ let admin = Address::generate(&env);
+ let vault = Address::generate(&env);
+ let dev1 = Address::generate(&env);
+ let dev2 = Address::generate(&env);
+ let addr = env.register(CalloraSettlement, ());
+ let client = CalloraSettlementClient::new(&env, &addr);
+
+ let (token_a, _, _) = create_token(&env, &admin);
+ let (token_b, _, _) = create_token(&env, &admin);
+
+ client.init(&admin, &vault);
+
+ // dev1 gets token_a, dev2 gets token_b
+ client.receive_payment(&vault, &100i128, &false, &Some(dev1.clone()), &token_a);
+ client.receive_payment(&vault, &200i128, &false, &Some(dev2.clone()), &token_b);
+
+ assert_eq!(client.get_developer_balance(&dev1, &token_a), 100i128);
+ assert_eq!(client.get_developer_balance(&dev2, &token_b), 200i128);
+ // Cross-token queries return zero
+ assert_eq!(client.get_developer_balance(&dev1, &token_b), 0i128);
+ assert_eq!(client.get_developer_balance(&dev2, &token_a), 0i128);
+}
+
+/// Acceptance: withdraw asserts token — can only withdraw the same token as credited.
+#[test]
+fn test_withdraw_asserts_token() {
+ let env = Env::default();
+ env.mock_all_auths();
+ env.ledger().set_timestamp(1_700_000_000);
+
+ let admin = Address::generate(&env);
+ let vault = Address::generate(&env);
+ let developer = Address::generate(&env);
+ let recipient = Address::generate(&env);
+ let addr = env.register(CalloraSettlement, ());
+ let client = CalloraSettlementClient::new(&env, &addr);
+
+ let (token_a, _, token_a_sac) = create_token(&env, &admin);
+ let (token_b, token_b_client, token_b_sac) = create_token(&env, &admin);
+
+ client.init(&admin, &vault);
+
+ // Credit both tokens to developer
+ client.receive_payment(&vault, &500i128, &false, &Some(developer.clone()), &token_a);
+ client.receive_payment(&vault, &300i128, &false, &Some(developer.clone()), &token_b);
+
+ // Fund the settlement contract with both tokens so withdrawal succeeds.
+ token_a_sac.mint(&addr, &1000i128);
+ token_b_sac.mint(&addr, &1000i128);
+
+ // Withdraw token_a — succeeds, uses token_a's contract
+ let result = client.try_withdraw_developer_balance(
+ &developer,
+ &200i128,
+ &Some(recipient.clone()),
+ &token_a,
+ );
+ assert!(result.is_ok());
+ assert_eq!(client.get_developer_balance(&developer, &token_a), 300i128);
+ assert_eq!(token_b_client.balance(&recipient), 0i128); // token_b not touched
+
+ // Withdraw token_b — succeeds, uses token_b's contract
+ let result = client.try_withdraw_developer_balance(
+ &developer,
+ &100i128,
+ &Some(recipient.clone()),
+ &token_b,
+ );
+ assert!(result.is_ok());
+ assert_eq!(client.get_developer_balance(&developer, &token_b), 200i128);
+
+ // Cannot withdraw token_a balance when passing token_b (wrong token assertion)
+ // token_a balance is 300, trying to withdraw 300 but passing token_b address
+ // This should check balance for token_b (which is 200) and reject.
+ let result = client.try_withdraw_developer_balance(
+ &developer,
+ &300i128,
+ &Some(recipient.clone()),
+ &token_b,
+ );
+ assert!(result.is_err()); // InsufficientDeveloperBalance for token_b
+
+ // Cannot withdraw token_b balance when passing token_a (301 > token_a's 300 balance)
+ let result = client.try_withdraw_developer_balance(
+ &developer,
+ &301i128,
+ &Some(recipient.clone()),
+ &token_a,
+ );
+ assert!(result.is_err()); // InsufficientDeveloperBalance for token_a
+}
+
+/// Acceptance: migration helper converts legacy single-USDC entry to per-token format.
+#[test]
+fn test_migrate_developer_balance() {
+ let env = Env::default();
+ env.mock_all_auths();
+ env.ledger().set_timestamp(1_700_000_000);
+
+ let admin = Address::generate(&env);
+ let vault = Address::generate(&env);
+ let developer = Address::generate(&env);
+ let addr = env.register(CalloraSettlement, ());
+ let client = CalloraSettlementClient::new(&env, &addr);
+
+ let (usdc, _, _) = create_token(&env, &admin);
+
+ client.init(&admin, &vault);
+ client.set_usdc_token(&admin, &usdc);
+
+ // Write a legacy single-token balance using the old storage key directly
+ env.as_contract(&addr, || {
+ let legacy_key = StorageKey::DeveloperBalanceV1(developer.clone());
+ env.storage().persistent().set(&legacy_key, &999i128);
+ env.storage()
+ .persistent()
+ .extend_ttl(&legacy_key, 50000, 50000);
+ });
+
+ // Before migration, new per-token read returns 0
+ assert_eq!(
+ client.get_developer_balance(&developer, &usdc),
+ 0i128,
+ "legacy balance not yet migrated"
+ );
+
+ // Run migration
+ let result = client.try_migrate_developer_balance(&admin, &developer);
+ assert!(result.is_ok(), "migration should succeed");
+
+ // After migration, new per-token read returns the migrated value
+ assert_eq!(
+ client.get_developer_balance(&developer, &usdc),
+ 999i128,
+ "migrated balance should be 999"
+ );
+
+ // Legacy entry is removed
+ let legacy_exists = env.as_contract(&addr, || {
+ env.storage()
+ .persistent()
+ .has(&StorageKey::DeveloperBalanceV1(developer.clone()))
+ });
+ assert!(!legacy_exists, "legacy entry should be removed");
+}
+
+/// Migration is idempotent — running twice is a no-op.
+#[test]
+fn test_migrate_developer_balance_idempotent() {
+ let env = Env::default();
+ env.mock_all_auths();
+ env.ledger().set_timestamp(1_700_000_000);
+
+ let admin = Address::generate(&env);
+ let vault = Address::generate(&env);
+ let developer = Address::generate(&env);
+ let addr = env.register(CalloraSettlement, ());
+ let client = CalloraSettlementClient::new(&env, &addr);
+
+ let (usdc, _, _) = create_token(&env, &admin);
+
+ client.init(&admin, &vault);
+ client.set_usdc_token(&admin, &usdc);
+
+ // Write legacy balance
+ env.as_contract(&addr, || {
+ env.storage()
+ .persistent()
+ .set(&StorageKey::DeveloperBalanceV1(developer.clone()), &555i128);
+ env.storage()
+ .persistent()
+ .extend_ttl(&StorageKey::DeveloperBalanceV1(developer.clone()), 50000, 50000);
+ });
+
+ // First migration
+ assert!(client.try_migrate_developer_balance(&admin, &developer).is_ok());
+ assert_eq!(client.get_developer_balance(&developer, &usdc), 555i128);
+
+ // Second migration — idempotent, no error, balance unchanged
+ assert!(client.try_migrate_developer_balance(&admin, &developer).is_ok());
+ assert_eq!(client.get_developer_balance(&developer, &usdc), 555i128);
+}
+
+/// Migration requires admin authorization.
+#[test]
+fn test_migrate_developer_balance_unauthorized() {
+ let env = Env::default();
+ env.mock_all_auths();
+ env.ledger().set_timestamp(1_700_000_000);
+
+ let admin = Address::generate(&env);
+ let vault = Address::generate(&env);
+ let developer = Address::generate(&env);
+ let addr = env.register(CalloraSettlement, ());
+ let client = CalloraSettlementClient::new(&env, &addr);
+
+ let (usdc, _, _) = create_token(&env, &admin);
+
+ client.init(&admin, &vault);
+ client.set_usdc_token(&admin, &usdc);
+
+ // Non-admin tries to migrate
+ let attacker = Address::generate(&env);
+ let result = client.try_migrate_developer_balance(&attacker, &developer);
+ assert!(result.is_err());
+}
+
+/// Migration fails if USDC token not configured.
+#[test]
+fn test_migrate_developer_balance_no_usdc() {
+ let env = Env::default();
+ env.mock_all_auths();
+ env.ledger().set_timestamp(1_700_000_000);
+
+ let admin = Address::generate(&env);
+ let vault = Address::generate(&env);
+ let developer = Address::generate(&env);
+ let addr = env.register(CalloraSettlement, ());
+ let client = CalloraSettlementClient::new(&env, &addr);
+
+ client.init(&admin, &vault);
+ // Do NOT set USDC token
+
+ let result = client.try_migrate_developer_balance(&admin, &developer);
+ assert!(result.is_err());
+}
+
+/// Batch receive payment per-token works correctly.
+#[test]
+fn test_batch_receive_payment_with_token() {
+ let env = Env::default();
+ env.mock_all_auths();
+ env.ledger().set_timestamp(1_700_000_000);
+
+ let admin = Address::generate(&env);
+ let vault = Address::generate(&env);
+ let dev1 = Address::generate(&env);
+ let dev2 = Address::generate(&env);
+ let addr = env.register(CalloraSettlement, ());
+ let client = CalloraSettlementClient::new(&env, &addr);
+
+ let (token, _, _) = create_token(&env, &admin);
+
+ client.init(&admin, &vault);
+
+ let mut items: soroban_sdk::Vec<(Address, i128)> = soroban_sdk::Vec::new(&env);
+ items.push_back((dev1.clone(), 100i128));
+ items.push_back((dev2.clone(), 200i128));
+
+ client.batch_receive_payment(&vault, &items, &token);
+
+ assert_eq!(client.get_developer_balance(&dev1, &token), 100i128);
+ assert_eq!(client.get_developer_balance(&dev2, &token), 200i128);
+}
diff --git a/contracts/settlement/src/test_views.rs b/contracts/settlement/src/test_views.rs
index d742c72..018ef40 100644
--- a/contracts/settlement/src/test_views.rs
+++ b/contracts/settlement/src/test_views.rs
@@ -43,11 +43,12 @@ fn test_get_global_pool_uninitialized() {
fn test_get_developer_balance_uninitialized() {
let env = Env::default();
let dev = Address::generate(&env);
+ let token = Address::generate(&env);
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
assert!(is_not_initialized(
- client.try_get_developer_balance(&dev)
+ client.try_get_developer_balance(&dev, &token)
));
}
@@ -58,9 +59,10 @@ fn test_get_all_developer_balances_uninitialized() {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
let dummy = Address::generate(&env);
+ let token = Address::generate(&env);
assert!(is_not_initialized(
- client.try_get_all_developer_balances(&dummy)
+ client.try_get_all_developer_balances(&dummy, &token)
));
}
@@ -72,13 +74,14 @@ fn test_get_developer_balance_returns_zero_when_not_stored() {
let admin = Address::generate(&env);
let vault = Address::generate(&env);
let dev = Address::generate(&env);
+ let token = Address::generate(&env);
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
- let balance = client.get_developer_balance(&dev);
+ let balance = client.get_developer_balance(&dev, &token);
assert_eq!(balance, 0);
}
@@ -91,8 +94,9 @@ fn test_get_developer_balances_cursor_uninitialized() {
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
let dummy = Address::generate(&env);
+ let token = Address::generate(&env);
- let result = client.try_get_developer_balances_cursor(&dummy, &None, &10u32);
+ let result = client.try_get_developer_balances_cursor(&dummy, &None, &10u32, &token);
assert!(
is_not_initialized(result),
"expected NotInitialized before init"
diff --git a/contracts/settlement/src/types.rs b/contracts/settlement/src/types.rs
new file mode 100644
index 0000000..b39708c
--- /dev/null
+++ b/contracts/settlement/src/types.rs
@@ -0,0 +1,156 @@
+use soroban_sdk::{contracttype, Address, Symbol};
+
+/// The maximum message length in bytes allowed for `broadcast` calls.
+pub const MAX_MESSAGE_LEN: u32 = 256;
+
+/// Maximum number of items allowed in a single `batch_receive_payment` call.
+pub const MAX_BATCH_SIZE: u32 = 50;
+
+/// Maximum number of developer balance records returned in a single
+/// non-cursor-based query (gas guard).
+pub const MAX_DEVELOPER_BALANCES_PAGE_SIZE: u32 = 100;
+
+/// Persistent storage keys for settlement contract.
+///
+/// # Migration note
+/// Discriminant 5 was the original `DeveloperBalance(Address)` (single-token, now
+/// `DeveloperBalanceV1` — kept for migration only). New per-token entries use
+/// `DeveloperBalance(Address, Address)` at discriminant 6.
+#[contracttype]
+#[derive(Clone, Debug, PartialEq)]
+pub enum StorageKey {
+ Admin,
+ Vault,
+ PendingAdmin,
+ PendingVault,
+ DeveloperIndex,
+ /// Legacy single-token balance — kept for migration reads. Do NOT use for
+ /// new writes.
+ DeveloperBalanceV1(Address),
+ /// Per-token developer balance `(developer, token)`.
+ DeveloperBalance(Address, Address),
+ GlobalPool,
+ Usdc,
+ DailyWithdrawCap(Address),
+ WithdrawalToday(Address),
+ ContractVersion,
+}
+
+/// Severity levels for admin broadcast messages.
+#[contracttype]
+#[derive(Clone, Debug, PartialEq)]
+pub enum Severity {
+ Info,
+ Warn,
+ Crit,
+}
+
+/// Payload for the `admin_broadcast` event.
+#[contracttype]
+#[derive(Clone, Debug, PartialEq)]
+pub struct AdminBroadcast {
+ pub severity: Severity,
+ pub message: soroban_sdk::String,
+}
+
+/// Developer balance record in settlement contract.
+#[contracttype]
+#[derive(Clone, Debug, PartialEq)]
+pub struct DeveloperBalance {
+ pub address: Address,
+ pub token: Address,
+ pub balance: i128,
+}
+
+/// Global pool balance tracking.
+///
+/// `last_updated` is set to `env.ledger().timestamp()` on every
+/// `receive_payment` call that credits the pool (`to_pool = true`).
+/// It is also set at `init` time. It is **not** updated when payments
+/// are routed to individual developer balances.
+#[contracttype]
+#[derive(Clone, Debug, PartialEq)]
+pub struct GlobalPool {
+ pub total_balance: i128,
+ /// Ledger timestamp of the last pool credit. Useful for analytics
+ /// and staleness checks.
+ pub last_updated: u64,
+}
+
+/// Tracks a developer's cumulative withdrawal amount for a given epoch day.
+///
+/// `day` is `timestamp / 86400` (UTC epoch day). When the current call's day
+/// differs from the stored day the accumulator is silently reset.
+#[contracttype]
+#[derive(Clone, Debug, PartialEq)]
+pub struct DailyWithdrawState {
+ pub day: u64,
+ pub amount: i128,
+}
+
+/// Payment received event.
+#[contracttype]
+#[derive(Clone, Debug, PartialEq)]
+pub struct PaymentReceivedEvent {
+ pub from_vault: Address,
+ pub amount: i128,
+ pub to_pool: bool,
+ pub developer: Option,
+ pub token: Address,
+}
+
+/// Balance credited event.
+#[contracttype]
+#[derive(Clone, Debug, PartialEq)]
+pub struct BalanceCreditedEvent {
+ pub developer: Address,
+ pub amount: i128,
+ pub new_balance: i128,
+ pub token: Address,
+}
+
+/// Emitted when a new vault address is proposed via `propose_vault()`.
+#[contracttype]
+#[derive(Clone, Debug, PartialEq)]
+pub struct VaultProposedEvent {
+ pub current_vault: Address,
+ pub proposed_vault: Address,
+}
+
+/// Emitted when the proposed vault is accepted via `accept_vault()`.
+#[contracttype]
+#[derive(Clone, Debug, PartialEq)]
+pub struct VaultAcceptedEvent {
+ pub old_vault: Address,
+ pub new_vault: Address,
+ pub accepted_by: Address,
+}
+
+/// Emitted when a developer withdraws their balance.
+#[contracttype]
+#[derive(Clone, Debug, PartialEq)]
+pub struct DeveloperWithdrawEvent {
+ pub developer: Address,
+ pub amount: i128,
+ pub remaining_balance: i128,
+ pub to: Address,
+ pub token: Address,
+}
+
+/// Emitted when the admin sets or changes a developer's daily withdrawal cap.
+#[contracttype]
+#[derive(Clone, Debug, PartialEq)]
+pub struct DailyWithdrawCapChanged {
+ pub developer: Address,
+ pub new_cap: i128,
+}
+
+/// Emitted when an admin force-credits a developer balance (escape hatch).
+#[contracttype]
+#[derive(Clone, Debug, PartialEq)]
+pub struct DeveloperForceCreditedEvent {
+ pub developer: Address,
+ pub amount: i128,
+ pub reason: Symbol,
+ pub new_balance: i128,
+}
diff --git a/contracts/vault/src/lib.rs b/contracts/vault/src/lib.rs
index 4684fb5..0ba9017 100644
--- a/contracts/vault/src/lib.rs
+++ b/contracts/vault/src/lib.rs
@@ -196,6 +196,7 @@ trait Settlement {
amount: i128,
to_pool: bool,
developer: Option,
+ token: Address,
);
}
@@ -835,6 +836,7 @@ impl CalloraVault {
&amount,
&true, // to_pool = true: credit global pool
&None, // no specific developer
+ &ut, // token
);
// Now that external operations succeeded, update internal state
@@ -942,6 +944,7 @@ impl CalloraVault {
&total,
&true, // to_pool = true: credit global pool
&None, // no specific developer
+ &ut, // token
);
// Now that external operations succeeded, update internal state
diff --git a/contracts/vault/src/test_reentrancy.rs b/contracts/vault/src/test_reentrancy.rs
index b0a54aa..4509188 100644
--- a/contracts/vault/src/test_reentrancy.rs
+++ b/contracts/vault/src/test_reentrancy.rs
@@ -78,6 +78,7 @@ impl MaliciousSettlement {
_amount: i128,
_to_pool: bool,
_developer: Option,
+ _token: Address,
) {
let vault_addr: Option = env
.storage()
diff --git a/tests/e2e_full_cycle.rs b/tests/e2e_full_cycle.rs
index 78eb2d5..96ab538 100644
--- a/tests/e2e_full_cycle.rs
+++ b/tests/e2e_full_cycle.rs
@@ -101,7 +101,7 @@ use soroban_sdk::{vec, Env, Symbol};
/// every checkpoint computes the conservation invariant identically.
fn settlement_total(h: &Harness, devs: &[soroban_sdk::Address]) -> i128 {
let pool = h.settlement.get_global_pool().total_balance;
- let dev_sum: i128 = devs.iter().map(|d| h.settlement.get_developer_balance(d)).sum();
+ let dev_sum: i128 = devs.iter().map(|d| h.settlement.get_developer_balance(d, &h.usdc_id)).sum();
pool + dev_sum
}
@@ -220,9 +220,9 @@ fn e2e_full_cycle() {
// ------------------------------------------------------------------
let dev_a_credit: i128 = 4_000_000;
h.settlement
- .receive_payment(&h.owner, &dev_a_credit, &false, &Some(h.dev_a.clone()));
+ .receive_payment(&h.owner, &dev_a_credit, &false, &Some(h.dev_a.clone()), &h.usdc_id);
- assert_eq!(h.settlement.get_developer_balance(&h.dev_a), dev_a_credit);
+ assert_eq!(h.settlement.get_developer_balance(&h.dev_a, &h.usdc_id), dev_a_credit);
assert_eq!(
h.settlement.get_global_pool().total_balance,
total_deducted,
@@ -237,10 +237,10 @@ fn e2e_full_cycle() {
h.settlement.set_usdc_token(&h.owner, &h.usdc_id);
let dev_a_withdraw: i128 = 1_500_000;
h.settlement
- .withdraw_developer_balance(&h.dev_a, &dev_a_withdraw);
+ .withdraw_developer_balance(&h.dev_a, &dev_a_withdraw, &h.usdc_id);
assert_eq!(
- h.settlement.get_developer_balance(&h.dev_a),
+ h.settlement.get_developer_balance(&h.dev_a, &h.usdc_id),
dev_a_credit - dev_a_withdraw
);
assert_eq!(h.usdc.balance(&h.dev_a), dev_a_withdraw);