Skip to content

Commit 5115a76

Browse files
feat(onchain): add get_distributor_list accessor for indexers (#232)
Add a public view function get_distributor_list that returns a sorted Vec<Address> of currently registered distributor addresses. This provides a source-of-truth accessor for off-chain indexers and audit dashboards, eliminating the need to maintain parallel distributor lists. Changes: - src/lib.rs: add get_distributor_list view function - tests: 6 test cases covering empty, single, multiple, removal, duplicate idempotency, and sorted ordering - README.md: document the new function - docs/onchain/api.md: new high-level API reference Closes #232
1 parent 3a7d872 commit 5115a76

4 files changed

Lines changed: 168 additions & 0 deletions

File tree

app/onchain/contracts/aid_escrow/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ expires and is refunded.
3535
| `migrate(env, new_version)` | Admin | Performs version-specific migrations. |
3636
| `add_distributor(env, addr)` | Admin | Grants distributor privileges to an address. |
3737
| `remove_distributor(env, addr)` | Admin | Revokes distributor privileges. |
38+
| `get_distributor_list(env)` || Returns a sorted list of registered distributor addresses. |
3839
| `set_config(env, config)` | Admin | Updates contract configuration (min amount, max expiry, allowed tokens). |
3940
| `get_config(env)` || Returns the current config. |
4041
| `pause(env)` | Admin | Pauses the contract (blocks package creation and claims). |

app/onchain/contracts/aid_escrow/src/lib.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,22 @@ impl AidEscrow {
408408
Ok(())
409409
}
410410

411+
/// Returns the list of currently registered distributor addresses.
412+
///
413+
/// Returns an empty `Vec` if no distributors have been added.
414+
/// The result is sorted for deterministic ordering, making it suitable
415+
/// for off-chain indexers and audit dashboards.
416+
pub fn get_distributor_list(env: Env) -> Vec<Address> {
417+
let distributors: Map<Address, bool> = env
418+
.storage()
419+
.instance()
420+
.get(&KEY_DISTRIBUTORS)
421+
.unwrap_or(Map::new(&env));
422+
let mut keys = distributors.keys();
423+
keys.sort();
424+
keys
425+
}
426+
411427
/// Admin-only. Updates the global contract configuration.
412428
///
413429
/// # Arguments

app/onchain/contracts/aid_escrow/tests/aid_escrow_tests.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,101 @@ mod token_decimal_normalization {
643643
assert!(result.is_ok());
644644
}
645645
}
646+
647+
// ===========================================================================
648+
// get_distributor_list — Tests
649+
// ===========================================================================
650+
651+
mod get_distributor_list {
652+
use super::*;
653+
654+
#[test]
655+
fn returns_empty_vec_after_init() {
656+
let t = TestSetup::new();
657+
let list = t.client.get_distributor_list();
658+
assert_eq!(list.len(), 0);
659+
}
660+
661+
#[test]
662+
fn returns_single_distributor_after_add() {
663+
let t = TestSetup::new();
664+
let addr = Address::generate(&t.env);
665+
t.client.add_distributor(&addr);
666+
let list = t.client.get_distributor_list();
667+
assert_eq!(list.len(), 1);
668+
assert_eq!(list.get(0).unwrap(), addr);
669+
}
670+
671+
#[test]
672+
fn returns_empty_after_add_then_remove() {
673+
let t = TestSetup::new();
674+
let addr = Address::generate(&t.env);
675+
t.client.add_distributor(&addr);
676+
t.client.remove_distributor(&addr);
677+
let list = t.client.get_distributor_list();
678+
assert_eq!(list.len(), 0);
679+
}
680+
681+
#[test]
682+
fn returns_sorted_list_after_multiple_adds() {
683+
let t = TestSetup::new();
684+
let addr1 = Address::generate(&t.env);
685+
let addr2 = Address::generate(&t.env);
686+
let addr3 = Address::generate(&t.env);
687+
688+
// Add in non-sorted order
689+
t.client.add_distributor(&addr3);
690+
t.client.add_distributor(&addr1);
691+
t.client.add_distributor(&addr2);
692+
693+
let list = t.client.get_distributor_list();
694+
assert_eq!(list.len(), 3);
695+
696+
// Verify the list is sorted by checking each element's relative ordering.
697+
// Since Address implements Ord via the host, we compare adjacent pairs.
698+
assert!(list.get(0).unwrap() <= list.get(1).unwrap());
699+
assert!(list.get(1).unwrap() <= list.get(2).unwrap());
700+
701+
// All three addresses must be present
702+
let mut found = [false; 3];
703+
for i in 0..3 {
704+
let addr = list.get(i).unwrap();
705+
if addr == addr1 { found[0] = true; }
706+
if addr == addr2 { found[1] = true; }
707+
if addr == addr3 { found[2] = true; }
708+
}
709+
assert!(found.iter().all(|&f| f), "all addresses must be present");
710+
}
711+
712+
#[test]
713+
fn reflects_removal_in_list() {
714+
let t = TestSetup::new();
715+
let addr1 = Address::generate(&t.env);
716+
let addr2 = Address::generate(&t.env);
717+
718+
t.client.add_distributor(&addr1);
719+
t.client.add_distributor(&addr2);
720+
assert_eq!(t.client.get_distributor_list().len(), 2);
721+
722+
t.client.remove_distributor(&addr1);
723+
let list = t.client.get_distributor_list();
724+
assert_eq!(list.len(), 1);
725+
assert_eq!(list.get(0).unwrap(), addr2);
726+
}
727+
728+
#[test]
729+
fn duplicate_add_is_idempotent() {
730+
let t = TestSetup::new();
731+
let addr = Address::generate(&t.env);
732+
733+
t.client.add_distributor(&addr);
734+
t.client.add_distributor(&addr); // duplicate add
735+
736+
let list = t.client.get_distributor_list();
737+
assert_eq!(list.len(), 1, "duplicate add must not create duplicate entry");
738+
assert_eq!(list.get(0).unwrap(), addr);
739+
}
740+
}
646741
#[test]
647742
fn test_claim_with_proof_oversized_fails() {
648743
let env = Env::default();

docs/onchain/api.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Onchain API Reference
2+
3+
ChainForge onchain contracts, deployed on Stellar Soroban.
4+
5+
## Contracts
6+
7+
### AidEscrow
8+
9+
**Contract ID (Testnet):** `CDSBJ27PKTNFTRW6OKPCVXDRUSSRUIQUG6DW5PUTKLDXTDT23NQIS6JG`
10+
11+
Full contract documentation: [`app/onchain/contracts/aid_escrow/README.md`](../../app/onchain/contracts/aid_escrow/README.md)
12+
13+
#### Admin & Config
14+
15+
| Function | Auth | Description |
16+
|---|---|---|
17+
| `init(env, admin)` | None (once) | Initializes the contract with an admin address and default config. |
18+
| `get_admin(env)` || Returns the current admin address. |
19+
| `get_version(env)` || Returns the current contract version. |
20+
| `migrate(env, new_version)` | Admin | Performs version-specific migrations. |
21+
| `add_distributor(env, addr)` | Admin | Grants distributor privileges to an address. |
22+
| `remove_distributor(env, addr)` | Admin | Revokes distributor privileges. |
23+
| `get_distributor_list(env)` || Returns a sorted list of registered distributor addresses. |
24+
| `set_config(env, config)` | Admin | Updates contract configuration. |
25+
| `get_config(env)` || Returns the current config. |
26+
| `pause(env)` | Admin | Pauses the contract. |
27+
| `unpause(env)` | Admin | Unpauses the contract. |
28+
| `is_paused(env)` || Returns true if the contract is paused. |
29+
30+
#### Funding & Packages
31+
32+
| Function | Auth | Description |
33+
|---|---|---|
34+
| `fund(env, token, from, amount)` | Funder | Transfers tokens into the contract balance. |
35+
| `create_package(env, operator, id, recipient, amount, token, expires_at, metadata)` | Admin / Distributor | Creates a single aid package. |
36+
| `batch_create_packages(env, operator, recipients, amounts, token, expires_in, metadatas)` | Admin / Distributor | Creates multiple packages in one transaction. |
37+
| `claim(env, id)` | Recipient | Recipient claims the package. |
38+
| `claim_with_proof(env, id, claimant, proof)` | Claimant | Claim with Merkle allowlist proof. |
39+
| `disburse(env, id)` | Admin | Admin manually disburses a package. |
40+
| `revoke(env, id)` | Admin | Admin revokes a package. |
41+
| `refund(env, id)` | Admin | Refunds an expired/cancelled package. |
42+
| `cancel_package(env, package_id)` | Admin | Cancels a package. |
43+
| `extend_expiration(env, package_id, additional_time)` | Admin | Extends expiration time. |
44+
45+
#### Queries
46+
47+
| Function | Auth | Description |
48+
|---|---|---|
49+
| `get_package(env, id)` || Returns full package details. |
50+
| `view_package_status(env, id)` || Returns only package status. |
51+
| `get_aggregates(env, token)` || Returns aggregate stats for a token. |
52+
| `get_total_locked(env, token)` || Returns total locked amount for a token. |
53+
| `get_total_claimed(env, token)` || Returns total claimed amount for a token. |
54+
| `get_recipient_package_count(env, recipient)` || Returns package count for a recipient. |
55+
| `list_recipient_packages(env, recipient, cursor, limit)` || Paginated list of recipient package IDs. |
56+
| `withdraw_surplus(env, to, amount, token)` | Admin | Withdraws surplus (unlocked) tokens. |

0 commit comments

Comments
 (0)