diff --git a/app/onchain/contracts/aid_escrow/README.md b/app/onchain/contracts/aid_escrow/README.md
new file mode 100644
index 0000000..811e300
--- /dev/null
+++ b/app/onchain/contracts/aid_escrow/README.md
@@ -0,0 +1,250 @@
+# aid_escrow — Soroban Smart Contract
+
+A Soroban smart contract that implements a **pool-based aid escrow** on the Stellar network. NGOs and distributors fund a shared token pool, create claimable packages for individual recipients, and recipients (or admins) redeem them on-chain. Every state change is recorded as an immutable ledger event.
+
+---
+
+## How It Works
+
+```
+Funder ──fund()──► Contract Pool
+ │
+Operator ──create_package()──► Package (Created, funds locked)
+ │
+Recipient ──claim()──► Package (Claimed, funds transferred)
+```
+
+1. A funder calls `fund()` to deposit tokens into the contract.
+2. An admin or distributor calls `create_package()` (or `batch_create_packages()`) to lock a portion of the pool for a specific recipient.
+3. The recipient calls `claim()` to receive their tokens. Alternatively, the admin can push funds via `disburse()`.
+4. Unclaimed packages can be cancelled (`revoke` / `cancel_package`) or refunded to the admin after expiry.
+
+---
+
+## Package State Machine
+
+```
+Created ──claim() / disburse()──► Claimed
+Created ──revoke() / cancel_package()──► Cancelled ──refund()──► Refunded
+Created ──(expires_at passed)──► Expired ──refund()──► Refunded
+```
+
+A package in `Claimed` or `Refunded` status is terminal and cannot be modified.
+
+---
+
+## Public Functions
+
+### Initialisation
+
+#### `init(env, admin)`
+Initialises the contract. Must be called exactly once.
+
+| Argument | Type | Description |
+|----------|-----------|--------------------------------------|
+| `admin` | `Address` | Address granted admin privileges. |
+
+Errors: `AlreadyInitialized`
+
+---
+
+### Configuration
+
+#### `get_admin(env) → Address`
+Returns the stored admin address.
+
+Errors: `NotInitialized`
+
+#### `set_config(env, config)`
+Replaces the contract configuration. Admin only.
+
+| Argument | Type | Description |
+|----------|----------|-----------------------------------------------------------------------------|
+| `config` | `Config` | `min_amount` (i128), `max_expires_in` (u64 seconds, 0 = unlimited), `allowed_tokens` (Vec\
, empty = all allowed). |
+
+Errors: `NotInitialized`, `NotAuthorized`, `InvalidAmount`
+
+#### `get_config(env) → Config`
+Returns the current configuration (read-only, no auth required).
+
+#### `add_distributor(env, addr)` / `remove_distributor(env, addr)`
+Grants or revokes distributor privileges. Admin only. Distributors may call `create_package` and `batch_create_packages`.
+
+Errors: `NotInitialized`, `NotAuthorized`
+
+#### `pause(env)` / `unpause(env)`
+Halts or resumes `fund`, `create_package`, `batch_create_packages`, and `claim`. Admin only.
+
+Errors: `NotInitialized`, `NotAuthorized`
+
+#### `is_paused(env) → bool`
+Returns `true` when the contract is paused (read-only).
+
+---
+
+### Funding
+
+#### `fund(env, token, from, amount)`
+Deposits tokens into the contract pool.
+
+| Argument | Type | Description |
+|----------|-----------|--------------------------------------------------|
+| `token` | `Address` | SEP-41 token contract address. |
+| `from` | `Address` | Funding address; must authorise this call. |
+| `amount` | `i128` | Amount to deposit (must be > 0). |
+
+Errors: `InvalidAmount`
+
+---
+
+### Package Management
+
+#### `create_package(env, operator, id, recipient, amount, token, expires_at) → u64`
+Creates a single aid package and locks funds from the pool.
+
+| Argument | Type | Description |
+|--------------|-----------|----------------------------------------------------------------------|
+| `operator` | `Address` | Admin or distributor; must authorise this call. |
+| `id` | `u64` | Caller-supplied unique package ID. |
+| `recipient` | `Address` | Address entitled to claim the package. |
+| `amount` | `i128` | Token amount to lock (must be ≥ `config.min_amount`). |
+| `token` | `Address` | SEP-41 token contract address. |
+| `expires_at` | `u64` | Unix timestamp after which the package expires (`0` = no expiry). |
+
+Returns the package `id` on success.
+
+Errors: `ContractPaused`, `NotAuthorized`, `InvalidAmount`, `InvalidState`, `PackageIdExists`, `InsufficientFunds`
+
+#### `batch_create_packages(env, operator, recipients, amounts, token, expires_in) → Vec`
+Creates multiple packages in one transaction using auto-incremented IDs.
+
+| Argument | Type | Description |
+|--------------|-----------------|----------------------------------------------------------------|
+| `operator` | `Address` | Admin or distributor; must authorise this call. |
+| `recipients` | `Vec` | Ordered list of recipient addresses. |
+| `amounts` | `Vec` | Ordered list of amounts, one per recipient. |
+| `token` | `Address` | SEP-41 token contract address. |
+| `expires_in` | `u64` | Seconds from now until all packages in this batch expire. |
+
+Returns a `Vec` of the created package IDs.
+
+Errors: `ContractPaused`, `NotAuthorized`, `MismatchedArrays`, `InvalidAmount`, `InsufficientFunds`
+
+---
+
+### Recipient Actions
+
+#### `claim(env, id)`
+Recipient claims their package. Transfers locked tokens to the recipient.
+
+| Argument | Type | Description |
+|----------|-------|--------------------------|
+| `id` | `u64` | Package ID to claim. |
+
+The recipient address stored in the package must authorise this call.
+
+Errors: `ContractPaused`, `PackageNotFound`, `PackageNotActive`, `PackageExpired`
+
+---
+
+### Admin Actions
+
+#### `disburse(env, id)`
+Admin pushes funds to the recipient without requiring the recipient's signature. Useful for field operations.
+
+Errors: `NotInitialized`, `NotAuthorized`, `PackageNotFound`, `PackageNotActive`
+
+#### `revoke(env, id)`
+Cancels a `Created` package and returns its funds to the pool.
+
+Errors: `NotInitialized`, `NotAuthorized`, `PackageNotFound`, `InvalidState`
+
+#### `cancel_package(env, package_id)`
+Cancels a `Created` package that has not yet expired. Funds return to the pool.
+
+Errors: `NotInitialized`, `NotAuthorized`, `PackageNotFound`, `PackageNotActive`, `PackageExpired`
+
+#### `refund(env, id)`
+Transfers the package amount back to the admin. Only valid for `Expired` or `Cancelled` packages.
+
+Errors: `NotInitialized`, `NotAuthorized`, `PackageNotFound`, `InvalidState`
+
+#### `extend_expiration(env, package_id, additional_time)`
+Extends the expiry of a `Created` package by `additional_time` seconds. Cannot extend unbounded packages (`expires_at == 0`).
+
+Errors: `NotInitialized`, `NotAuthorized`, `PackageNotFound`, `PackageNotActive`, `InvalidAmount`, `InvalidState`, `PackageExpired`
+
+#### `withdraw_surplus(env, to, amount, token)`
+Withdraws unallocated (surplus) tokens from the contract to `to`. Surplus = contract balance − total locked.
+
+Errors: `NotInitialized`, `NotAuthorized`, `InvalidAmount`, `InsufficientSurplus`
+
+---
+
+### Read-Only Queries
+
+#### `get_package(env, id) → Package`
+Returns the full `Package` struct for the given ID.
+
+Errors: `PackageNotFound`
+
+#### `get_aggregates(env, token) → Aggregates`
+Returns aggregate token statistics across all packages:
+
+| Field | Description |
+|--------------------------|----------------------------------------------------------|
+| `total_committed` | Sum of amounts in `Created` packages. |
+| `total_claimed` | Sum of amounts in `Claimed` packages. |
+| `total_expired_cancelled`| Sum of amounts in `Expired`, `Cancelled`, or `Refunded`. |
+
+---
+
+## Error Reference
+
+| Code | Variant | When it is returned |
+|------|------------------------|------------------------------------------------------------------------|
+| 1 | `NotInitialized` | Contract has not been initialised via `init`. |
+| 2 | `AlreadyInitialized` | `init` was called more than once. |
+| 3 | `NotAuthorized` | Caller lacks the required role (admin or distributor). |
+| 4 | `InvalidAmount` | Amount is ≤ 0, below `min_amount`, or `additional_time` is 0. |
+| 5 | `PackageNotFound` | No package exists for the given ID. |
+| 6 | `PackageNotActive` | Package status is not `Created`. |
+| 7 | `PackageExpired` | Package has passed its `expires_at` timestamp. |
+| 8 | `PackageNotExpired` | Operation requires the package to be expired, but it is not. |
+| 9 | `InsufficientFunds` | Contract pool balance cannot cover the requested lock amount. |
+| 10 | `PackageIdExists` | A package with the supplied ID already exists. |
+| 11 | `InvalidState` | General state violation (token not allowed, expiry out of range, etc). |
+| 12 | `MismatchedArrays` | `recipients` and `amounts` arrays have different lengths. |
+| 13 | `InsufficientSurplus` | Requested withdrawal exceeds unallocated contract balance. |
+| 14 | `ContractPaused` | Contract is paused; mutating operations are blocked. |
+
+---
+
+## Events
+
+| Event | Emitted by | Key fields |
+|--------------------------|-------------------------------------|-----------------------------------------|
+| `FundEvent` | `fund` | `from`, `token`, `amount` |
+| `PackageCreatedEvent` | `create_package`, `batch_create_packages` | `id`, `recipient`, `amount` |
+| `ClaimedEvent` | `claim` | `id`, `recipient`, `amount` |
+| `DisbursedEvent` | `disburse` | `id`, `admin`, `amount` |
+| `RevokedEvent` | `revoke`, `cancel_package` | `id`, `admin`, `amount` |
+| `RefundedEvent` | `refund` | `id`, `admin`, `amount` |
+| `BatchCreatedEvent` | `batch_create_packages` | `ids`, `admin`, `total_amount` |
+| `ExtendedEvent` | `extend_expiration` | `id`, `admin`, `old_expires_at`, `new_expires_at` |
+| `SurplusWithdrawnEvent` | `withdraw_surplus` | `to`, `token`, `amount` |
+| `ContractPausedEvent` | `pause` | `admin` |
+| `ContractUnpausedEvent` | `unpause` | `admin` |
+
+---
+
+## Building & Testing
+
+```bash
+# From app/onchain/
+make build # cargo build --target wasm32-unknown-unknown --release
+make test # cargo test
+make deploy # see scripts/deploy.sh for env vars required
+```
+
+See [`app/onchain/README.md`](../../README.md) for full CLI setup and deployment instructions.
diff --git a/app/onchain/contracts/aid_escrow/src/lib.rs b/app/onchain/contracts/aid_escrow/src/lib.rs
index 3684b31..bfb38db 100644
--- a/app/onchain/contracts/aid_escrow/src/lib.rs
+++ b/app/onchain/contracts/aid_escrow/src/lib.rs
@@ -178,6 +178,13 @@ pub struct AidEscrow;
impl AidEscrow {
// --- Admin & Config ---
+ /// Initialises the contract. Must be called exactly once before any other function.
+ ///
+ /// # Arguments
+ /// * `admin` - The address that will own the contract and hold admin privileges.
+ ///
+ /// # Errors
+ /// * [`Error::AlreadyInitialized`] – contract has already been initialised.
pub fn init(env: Env, admin: Address) -> Result<(), Error> {
if env.storage().instance().has(&KEY_ADMIN) {
return Err(Error::AlreadyInitialized);
@@ -193,6 +200,10 @@ impl AidEscrow {
Ok(())
}
+ /// Returns the current admin address.
+ ///
+ /// # Errors
+ /// * [`Error::NotInitialized`] – contract has not been initialised yet.
pub fn get_admin(env: Env) -> Result {
env.storage()
.instance()
@@ -241,6 +252,14 @@ impl AidEscrow {
Ok(())
}
+ /// Revokes distributor privileges from `addr`.
+ ///
+ /// # Arguments
+ /// * `addr` - Address to demote.
+ ///
+ /// # Errors
+ /// * [`Error::NotInitialized`] – contract not initialised.
+ /// * [`Error::NotAuthorized`] – caller is not the admin.
pub fn remove_distributor(env: Env, addr: Address) -> Result<(), Error> {
let admin = Self::get_admin(env.clone())?;
admin.require_auth();
@@ -258,6 +277,15 @@ impl AidEscrow {
Ok(())
}
+ /// Updates the contract configuration.
+ ///
+ /// # Arguments
+ /// * `config` - New [`Config`] value containing `min_amount`, `max_expires_in`, and `allowed_tokens`.
+ ///
+ /// # Errors
+ /// * [`Error::NotInitialized`] – contract not initialised.
+ /// * [`Error::NotAuthorized`] – caller is not the admin.
+ /// * [`Error::InvalidAmount`] – `config.min_amount` is ≤ 0.
pub fn set_config(env: Env, config: Config) -> Result<(), Error> {
let admin = Self::get_admin(env.clone())?;
admin.require_auth();
@@ -270,6 +298,11 @@ impl AidEscrow {
Ok(())
}
+ /// Pauses the contract, blocking `create_package`, `batch_create_packages`, and `claim`.
+ ///
+ /// # Errors
+ /// * [`Error::NotInitialized`] – contract not initialised.
+ /// * [`Error::NotAuthorized`] – caller is not the admin.
pub fn pause(env: Env) -> Result<(), Error> {
let admin = Self::get_admin(env.clone())?;
admin.require_auth();
@@ -278,6 +311,11 @@ impl AidEscrow {
Ok(())
}
+ /// Resumes normal contract operation after a pause.
+ ///
+ /// # Errors
+ /// * [`Error::NotInitialized`] – contract not initialised.
+ /// * [`Error::NotAuthorized`] – caller is not the admin.
pub fn unpause(env: Env) -> Result<(), Error> {
let admin = Self::get_admin(env.clone())?;
admin.require_auth();
@@ -286,10 +324,12 @@ impl AidEscrow {
Ok(())
}
+ /// Returns `true` if the contract is currently paused.
pub fn is_paused(env: Env) -> bool {
env.storage().instance().get(&KEY_PAUSED).unwrap_or(false)
}
+ /// Returns the current [`Config`]. Falls back to safe defaults if not yet set.
pub fn get_config(env: Env) -> Config {
env.storage().instance().get(&KEY_CONFIG).unwrap_or(Config {
min_amount: 1,
@@ -300,9 +340,15 @@ impl AidEscrow {
// --- Funding & Packages ---
- /// Funds the contract (Pool Model).
- /// Transfers `amount` of `token` from `from` to this contract.
- /// This increases the contract's balance, allowing new packages to be created.
+ /// Deposits tokens into the contract pool so packages can be created against them.
+ ///
+ /// # Arguments
+ /// * `token` - SEP-41 token contract address.
+ /// * `from` - Address funding the contract; must authorise this call.
+ /// * `amount` - Amount to transfer (must be > 0).
+ ///
+ /// # Errors
+ /// * [`Error::InvalidAmount`] – `amount` is ≤ 0.
pub fn fund(env: Env, token: Address, from: Address, amount: i128) -> Result<(), Error> {
if amount <= 0 {
return Err(Error::InvalidAmount);
@@ -325,8 +371,27 @@ impl AidEscrow {
Ok(())
}
- /// Creates a package with a specific ID.
- /// Locks funds from the available pool (Contract Balance - Total Locked).
+ /// Creates a single aid package and locks the specified funds for the recipient.
+ ///
+ /// The caller must be the admin or an authorised distributor. Funds are reserved
+ /// from the contract's unallocated pool; the package cannot be created if the pool
+ /// has insufficient balance.
+ ///
+ /// # Arguments
+ /// * `operator` - Admin or distributor address; must authorise this call.
+ /// * `id` - Caller-supplied unique package identifier.
+ /// * `recipient` - Address that may claim the package.
+ /// * `amount` - Token amount to lock (must be ≥ `config.min_amount`).
+ /// * `token` - SEP-41 token contract address.
+ /// * `expires_at` - Unix timestamp after which the package expires (`0` = no expiry).
+ ///
+ /// # Errors
+ /// * [`Error::ContractPaused`] – contract is paused.
+ /// * [`Error::NotAuthorized`] – `operator` is neither admin nor distributor.
+ /// * [`Error::InvalidAmount`] – `amount` ≤ 0 or below `min_amount`.
+ /// * [`Error::InvalidState`] – token not in `allowed_tokens`, or expiry violates `max_expires_in`.
+ /// * [`Error::PackageIdExists`] – a package with `id` already exists.
+ /// * [`Error::InsufficientFunds`] – contract pool cannot cover the new package.
pub fn create_package(
env: Env,
operator: Address,
@@ -418,8 +483,24 @@ impl AidEscrow {
Ok(id)
}
- /// Creates multiple packages in a single transaction for multiple recipients.
- /// Uses an auto-incrementing counter for package IDs.
+ /// Creates multiple aid packages in a single transaction using auto-incremented IDs.
+ ///
+ /// Package IDs are assigned sequentially from the internal counter. Each package
+ /// shares the same `token` and expiry window (`expires_in` seconds from now).
+ ///
+ /// # Arguments
+ /// * `operator` - Admin or distributor address; must authorise this call.
+ /// * `recipients` - Ordered list of recipient addresses.
+ /// * `amounts` - Ordered list of amounts, one per recipient (must be > 0 each).
+ /// * `token` - SEP-41 token contract address.
+ /// * `expires_in` - Seconds from the current ledger timestamp until expiry.
+ ///
+ /// # Errors
+ /// * [`Error::ContractPaused`] – contract is paused.
+ /// * [`Error::NotAuthorized`] – `operator` is neither admin nor distributor.
+ /// * [`Error::MismatchedArrays`] – `recipients` and `amounts` have different lengths.
+ /// * [`Error::InvalidAmount`] – any individual amount is ≤ 0.
+ /// * [`Error::InsufficientFunds`] – contract pool cannot cover all packages.
pub fn batch_create_packages(
env: Env,
operator: Address,
@@ -531,7 +612,20 @@ impl AidEscrow {
// --- Recipient Actions ---
- /// Recipient claims the package.
+ /// Allows the designated recipient to claim their aid package.
+ ///
+ /// The recipient must authorise this call. On success the package status transitions
+ /// to `Claimed`, the locked funds are released, and the token amount is transferred
+ /// directly to the recipient.
+ ///
+ /// # Arguments
+ /// * `id` - Package identifier to claim.
+ ///
+ /// # Errors
+ /// * [`Error::ContractPaused`] – contract is paused.
+ /// * [`Error::PackageNotFound`] – no package exists with `id`.
+ /// * [`Error::PackageNotActive`] – package is not in `Created` status.
+ /// * [`Error::PackageExpired`] – package has passed its `expires_at` timestamp.
pub fn claim(env: Env, id: u64) -> Result<(), Error> {
Self::check_paused(&env)?;
let key = (symbol_short!("pkg"), id);
@@ -587,7 +681,19 @@ impl AidEscrow {
// --- Admin Actions ---
- /// Admin manually triggers disbursement (overrides recipient claim need, strictly checks status).
+ /// Admin-initiated disbursement — pushes funds to the recipient without requiring their signature.
+ ///
+ /// Useful when the recipient cannot sign (e.g. field operations). Transitions the package
+ /// to `Claimed` and transfers funds to the recipient address on record.
+ ///
+ /// # Arguments
+ /// * `id` - Package identifier to disburse.
+ ///
+ /// # Errors
+ /// * [`Error::NotInitialized`] – contract not initialised.
+ /// * [`Error::NotAuthorized`] – caller is not the admin.
+ /// * [`Error::PackageNotFound`] – no package exists with `id`.
+ /// * [`Error::PackageNotActive`] – package is not in `Created` status.
pub fn disburse(env: Env, id: u64) -> Result<(), Error> {
let admin = Self::get_admin(env.clone())?;
admin.require_auth();
@@ -631,7 +737,19 @@ impl AidEscrow {
Ok(())
}
- /// Admin revokes a package (Cancels it). Funds are effectively unlocked but remain in contract pool.
+ /// Cancels a `Created` package and returns its locked funds to the pool.
+ ///
+ /// Unlike `cancel_package`, this does not check whether the package has expired,
+ /// making it suitable for administrative clean-up of any active package.
+ ///
+ /// # Arguments
+ /// * `id` - Package identifier to revoke.
+ ///
+ /// # Errors
+ /// * [`Error::NotInitialized`] – contract not initialised.
+ /// * [`Error::NotAuthorized`] – caller is not the admin.
+ /// * [`Error::PackageNotFound`] – no package exists with `id`.
+ /// * [`Error::InvalidState`] – package is not in `Created` status.
pub fn revoke(env: Env, id: u64) -> Result<(), Error> {
let admin = Self::get_admin(env.clone())?;
admin.require_auth();
@@ -667,6 +785,19 @@ impl AidEscrow {
Ok(())
}
+ /// Transfers a package's tokens back to the admin after the package is `Expired` or `Cancelled`.
+ ///
+ /// If the package is still in `Created` status but its `expires_at` has passed, this
+ /// function will auto-transition it to `Expired` before processing the refund.
+ ///
+ /// # Arguments
+ /// * `id` - Package identifier to refund.
+ ///
+ /// # Errors
+ /// * [`Error::NotInitialized`] – contract not initialised.
+ /// * [`Error::NotAuthorized`] – caller is not the admin.
+ /// * [`Error::PackageNotFound`] – no package exists with `id`.
+ /// * [`Error::InvalidState`] – package is `Claimed`, already `Refunded`, or `Created` but not yet expired.
pub fn refund(env: Env, id: u64) -> Result<(), Error> {
let admin = Self::get_admin(env.clone())?;
admin.require_auth();
@@ -720,8 +851,17 @@ impl AidEscrow {
Ok(())
}
- /// Admin-only package cancellation.
- /// Requirements: Admin auth, existing package, status must be 'Created'.
+ /// Cancels a `Created` package that has not yet expired, returning funds to the pool.
+ ///
+ /// # Arguments
+ /// * `package_id` - Package identifier to cancel.
+ ///
+ /// # Errors
+ /// * [`Error::NotInitialized`] – contract not initialised.
+ /// * [`Error::NotAuthorized`] – caller is not the admin.
+ /// * [`Error::PackageNotFound`] – no package exists with `package_id`.
+ /// * [`Error::PackageNotActive`] – package is not in `Created` status.
+ /// * [`Error::PackageExpired`] – package has already passed its `expires_at` timestamp.
pub fn cancel_package(env: Env, package_id: u64) -> Result<(), Error> {
// 1. Only the admin can cancel (check stored admin and require_auth)
let admin = Self::get_admin(env.clone())?;
@@ -765,10 +905,23 @@ impl AidEscrow {
Ok(())
}
- /// Admin-only package expiration extension.
- /// Requirements: Admin auth, existing package, status must be 'Created', additional_time > 0.
- /// Behavior: Adds additional_time to the package's expires_at timestamp.
- /// Cannot extend unbounded packages (expires_at == 0).
+ /// Extends the expiry of a `Created` package by `additional_time` seconds.
+ ///
+ /// Cannot be used on unbounded packages (`expires_at == 0`). The resulting
+ /// `expires_at` must still satisfy `config.max_expires_in` if that limit is set.
+ ///
+ /// # Arguments
+ /// * `package_id` - Package identifier to extend.
+ /// * `additional_time` - Seconds to add to the current `expires_at` (must be > 0).
+ ///
+ /// # Errors
+ /// * [`Error::NotInitialized`] – contract not initialised.
+ /// * [`Error::NotAuthorized`] – caller is not the admin.
+ /// * [`Error::PackageNotFound`] – no package exists with `package_id`.
+ /// * [`Error::PackageNotActive`] – package is not in `Created` status.
+ /// * [`Error::InvalidAmount`] – `additional_time` is 0.
+ /// * [`Error::InvalidState`] – package is unbounded or new expiry violates `max_expires_in`.
+ /// * [`Error::PackageExpired`] – package has already expired.
pub fn extend_expiration(env: Env, package_id: u64, additional_time: u64) -> Result<(), Error> {
// 1. Only the admin can extend (check stored admin and require_auth)
let admin = Self::get_admin(env.clone())?;
@@ -827,9 +980,21 @@ impl AidEscrow {
Ok(())
}
- /// Admin-only function to withdraw surplus (unallocated) funds from the contract.
- /// Requirements: Admin auth, valid amount, sufficient surplus available.
- /// Behavior: Transfers amount of token from contract to the specified address.
+ /// Withdraws surplus (unallocated) tokens from the contract to `to`.
+ ///
+ /// Surplus = contract token balance − total locked amount. Only tokens not
+ /// committed to any active package may be withdrawn.
+ ///
+ /// # Arguments
+ /// * `to` - Destination address for the withdrawn tokens.
+ /// * `amount` - Amount to withdraw (must be > 0).
+ /// * `token` - SEP-41 token contract address.
+ ///
+ /// # Errors
+ /// * [`Error::NotInitialized`] – contract not initialised.
+ /// * [`Error::NotAuthorized`] – caller is not the admin.
+ /// * [`Error::InvalidAmount`] – `amount` is ≤ 0.
+ /// * [`Error::InsufficientSurplus`] – requested amount exceeds available surplus.
pub fn withdraw_surplus(
env: Env,
to: Address,
@@ -924,6 +1089,13 @@ impl AidEscrow {
}
}
+ /// Returns the full [`Package`] struct for the given ID.
+ ///
+ /// # Arguments
+ /// * `id` - Package identifier to look up.
+ ///
+ /// # Errors
+ /// * [`Error::PackageNotFound`] – no package exists with `id`.
pub fn get_package(env: Env, id: u64) -> Result {
let key = (symbol_short!("pkg"), id);
env.storage()