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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/onchain/contracts/aid_escrow/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ expires and is refunded.
| `migrate(env, new_version)` | Admin | Performs version-specific migrations. |
| `add_distributor(env, addr)` | Admin | Grants distributor privileges to an address. |
| `remove_distributor(env, addr)` | Admin | Revokes distributor privileges. |
| `get_distributor_list(env)` | — | Returns a sorted list of registered distributor addresses. |
| `set_config(env, config)` | Admin | Updates contract configuration (min amount, max expiry, allowed tokens). |
| `get_config(env)` | — | Returns the current config. |
| `pause(env)` | Admin | Pauses the contract (blocks package creation and claims). |
Expand Down
31 changes: 31 additions & 0 deletions app/onchain/contracts/aid_escrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,37 @@ impl AidEscrow {
Ok(())
}

/// Returns the list of currently registered distributor addresses.
///
/// Returns an empty `Vec` if no distributors have been added.
/// The result is sorted for deterministic ordering, making it suitable
/// for off-chain indexers and audit dashboards.
pub fn get_distributor_list(env: Env) -> Vec<Address> {
let distributors: Map<Address, bool> = env
.storage()
.instance()
.get(&KEY_DISTRIBUTORS)
.unwrap_or(Map::new(&env));
let mut keys = distributors.keys();
// Manual sort — soroban_sdk::Vec does not provide a sort() method.
// Bubble sort is acceptable because distributor lists are expected to
// be small (typically < 50 entries).
let len = keys.len();
if len > 1 {
for i in 0..(len - 1) {
for j in 0..(len - i - 1) {
let a = keys.get(j).unwrap();
let b = keys.get(j + 1).unwrap();
if a > b {
keys.set(j, b);
keys.set(j + 1, a);
}
}
}
}
keys
}

/// Admin-only. Updates the global contract configuration.
///
/// # Arguments
Expand Down
105 changes: 105 additions & 0 deletions app/onchain/contracts/aid_escrow/tests/aid_escrow_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,111 @@ mod token_decimal_normalization {
assert!(result.is_ok());
}
}

// ===========================================================================
// get_distributor_list — Tests
// ===========================================================================

mod get_distributor_list {
use super::*;

#[test]
fn returns_empty_vec_after_init() {
let t = TestSetup::new();
let list = t.client.get_distributor_list();
assert_eq!(list.len(), 0);
}

#[test]
fn returns_single_distributor_after_add() {
let t = TestSetup::new();
let addr = Address::generate(&t.env);
t.client.add_distributor(&addr);
let list = t.client.get_distributor_list();
assert_eq!(list.len(), 1);
assert_eq!(list.get(0).unwrap(), addr);
}

#[test]
fn returns_empty_after_add_then_remove() {
let t = TestSetup::new();
let addr = Address::generate(&t.env);
t.client.add_distributor(&addr);
t.client.remove_distributor(&addr);
let list = t.client.get_distributor_list();
assert_eq!(list.len(), 0);
}

#[test]
fn returns_sorted_list_after_multiple_adds() {
let t = TestSetup::new();
let addr1 = Address::generate(&t.env);
let addr2 = Address::generate(&t.env);
let addr3 = Address::generate(&t.env);

// Add in non-sorted order
t.client.add_distributor(&addr3);
t.client.add_distributor(&addr1);
t.client.add_distributor(&addr2);

let list = t.client.get_distributor_list();
assert_eq!(list.len(), 3);

// Verify the list is sorted by checking each element's relative ordering.
// Since Address implements Ord via the host, we compare adjacent pairs.
assert!(list.get(0).unwrap() <= list.get(1).unwrap());
assert!(list.get(1).unwrap() <= list.get(2).unwrap());

// All three addresses must be present
let mut found = [false; 3];
for i in 0..3 {
let addr = list.get(i).unwrap();
if addr == addr1 {
found[0] = true;
}
if addr == addr2 {
found[1] = true;
}
if addr == addr3 {
found[2] = true;
}
}
assert!(found.iter().all(|&f| f), "all addresses must be present");
}

#[test]
fn reflects_removal_in_list() {
let t = TestSetup::new();
let addr1 = Address::generate(&t.env);
let addr2 = Address::generate(&t.env);

t.client.add_distributor(&addr1);
t.client.add_distributor(&addr2);
assert_eq!(t.client.get_distributor_list().len(), 2);

t.client.remove_distributor(&addr1);
let list = t.client.get_distributor_list();
assert_eq!(list.len(), 1);
assert_eq!(list.get(0).unwrap(), addr2);
}

#[test]
fn duplicate_add_is_idempotent() {
let t = TestSetup::new();
let addr = Address::generate(&t.env);

t.client.add_distributor(&addr);
t.client.add_distributor(&addr); // duplicate add

let list = t.client.get_distributor_list();
assert_eq!(
list.len(),
1,
"duplicate add must not create duplicate entry"
);
assert_eq!(list.get(0).unwrap(), addr);
}
}
#[test]
fn test_claim_with_proof_oversized_fails() {
let env = Env::default();
Expand Down
56 changes: 56 additions & 0 deletions docs/onchain/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Onchain API Reference

ChainForge onchain contracts, deployed on Stellar Soroban.

## Contracts

### AidEscrow

**Contract ID (Testnet):** `CDSBJ27PKTNFTRW6OKPCVXDRUSSRUIQUG6DW5PUTKLDXTDT23NQIS6JG`

Full contract documentation: [`app/onchain/contracts/aid_escrow/README.md`](../../app/onchain/contracts/aid_escrow/README.md)

#### Admin & Config

| Function | Auth | Description |
|---|---|---|
| `init(env, admin)` | None (once) | Initializes the contract with an admin address and default config. |
| `get_admin(env)` | — | Returns the current admin address. |
| `get_version(env)` | — | Returns the current contract version. |
| `migrate(env, new_version)` | Admin | Performs version-specific migrations. |
| `add_distributor(env, addr)` | Admin | Grants distributor privileges to an address. |
| `remove_distributor(env, addr)` | Admin | Revokes distributor privileges. |
| `get_distributor_list(env)` | — | Returns a sorted list of registered distributor addresses. |
| `set_config(env, config)` | Admin | Updates contract configuration. |
| `get_config(env)` | — | Returns the current config. |
| `pause(env)` | Admin | Pauses the contract. |
| `unpause(env)` | Admin | Unpauses the contract. |
| `is_paused(env)` | — | Returns true if the contract is paused. |

#### Funding & Packages

| Function | Auth | Description |
|---|---|---|
| `fund(env, token, from, amount)` | Funder | Transfers tokens into the contract balance. |
| `create_package(env, operator, id, recipient, amount, token, expires_at, metadata)` | Admin / Distributor | Creates a single aid package. |
| `batch_create_packages(env, operator, recipients, amounts, token, expires_in, metadatas)` | Admin / Distributor | Creates multiple packages in one transaction. |
| `claim(env, id)` | Recipient | Recipient claims the package. |
| `claim_with_proof(env, id, claimant, proof)` | Claimant | Claim with Merkle allowlist proof. |
| `disburse(env, id)` | Admin | Admin manually disburses a package. |
| `revoke(env, id)` | Admin | Admin revokes a package. |
| `refund(env, id)` | Admin | Refunds an expired/cancelled package. |
| `cancel_package(env, package_id)` | Admin | Cancels a package. |
| `extend_expiration(env, package_id, additional_time)` | Admin | Extends expiration time. |

#### Queries

| Function | Auth | Description |
|---|---|---|
| `get_package(env, id)` | — | Returns full package details. |
| `view_package_status(env, id)` | — | Returns only package status. |
| `get_aggregates(env, token)` | — | Returns aggregate stats for a token. |
| `get_total_locked(env, token)` | — | Returns total locked amount for a token. |
| `get_total_claimed(env, token)` | — | Returns total claimed amount for a token. |
| `get_recipient_package_count(env, recipient)` | — | Returns package count for a recipient. |
| `list_recipient_packages(env, recipient, cursor, limit)` | — | Paginated list of recipient package IDs. |
| `withdraw_surplus(env, to, amount, token)` | Admin | Withdraws surplus (unlocked) tokens. |
Loading