From f59e7105995f1d8072e7d6d856779871bea95557 Mon Sep 17 00:00:00 2001 From: Nimi <120312489+emmyoat@users.noreply.github.com> Date: Wed, 22 Jul 2026 22:43:17 +0100 Subject: [PATCH 1/6] Create comprehensive EVENTS.md documenting all event topics, types, and emission sites --- docs/EVENTS.md | 513 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 513 insertions(+) create mode 100644 docs/EVENTS.md diff --git a/docs/EVENTS.md b/docs/EVENTS.md new file mode 100644 index 0000000..661079b --- /dev/null +++ b/docs/EVENTS.md @@ -0,0 +1,513 @@ +# AnchorNet Event Reference + +This document catalogs every event topic emitted by the AnchorNet contract, including argument types, emitting entrypoints, and examples of the raw event structure as Soroban emits it. + +Indexers subscribe to these events to keep an off-chain view of pool liquidity in sync with on-chain state. All events are emitted by functions in [`src/events.rs`](../src/events.rs) and invoked from [`src/lib.rs`](../src/lib.rs). + +## Event Structure + +Soroban events are composed of: +- **Topics**: a tuple of indexed values (SearchableByTopics in the event header) +- **Data**: a single unindexed payload value + +Both topics and data are Soroban SDK types (e.g., `Address`, `Symbol`, `u32`, `i128`, `u64`, etc.). + +--- + +## Event Catalog + +### 1. **init** — Contract Initialization + +| Property | Value | +|----------|-------| +| **Topics** | `("init",)` | +| **Data** | `Address` (admin) | +| **Emitted by** | [`initialize`](../src/lib.rs#L41) | +| **Emitted from** | [`initialized`](../src/events.rs#L25) | + +Emitted once when the contract is initialized. The data contains the address of the initial administrator. + +**Raw Event Example:** +```rust +topics = ("init",) +data = Address { account_id: "..." } +``` + +--- + +### 2. **admin** — Administrator Changed + +| Property | Value | +|----------|-------| +| **Topics** | `("admin", path)` where `path` is `"direct"` or `"accept"` | +| **Data** | `Address` (new_admin) | +| **Emitted by** | [`set_admin`](../src/lib.rs#L70) or [`accept_admin`](../src/lib.rs#L109) | +| **Emitted from** | [`admin_changed`](../src/events.rs#L32) | + +Emitted when the administrator is transferred. The second topic indicates the transfer method: +- `"direct"` — single-step transfer via `set_admin` +- `"accept"` — two-step transfer accepted via `accept_admin` + +**Raw Event Examples:** + +Single-step transfer: +```rust +topics = ("admin", "direct") +data = Address { account_id: "..." } +``` + +Two-step accepted transfer: +```rust +topics = ("admin", "accept") +data = Address { account_id: "..." } +``` + +--- + +### 3. **propose** — Admin Transfer Proposed + +| Property | Value | +|----------|-------| +| **Topics** | `("propose",)` | +| **Data** | `Address` (candidate) | +| **Emitted by** | [`propose_admin`](../src/lib.rs#L87) | +| **Emitted from** | [`admin_proposed`](../src/events.rs#L43) | + +Emitted when an admin transfer is proposed via the two-step flow. The data contains the proposed next administrator. + +**Raw Event Example:** +```rust +topics = ("propose",) +data = Address { account_id: "..." } +``` + +--- + +### 4. **anchor** — Anchor Registered + +| Property | Value | +|----------|-------| +| **Topics** | `("anchor", anchor)` | +| **Data** | `()` (unit) | +| **Emitted by** | [`register_anchor`](../src/lib.rs#L358) or [`register_anchors`](../src/lib.rs#L376) | +| **Emitted from** | [`anchor_registered`](../src/events.rs#L49) | + +Emitted when an anchor is registered as an approved liquidity provider. The second topic is the registered anchor's address. + +**Raw Event Example:** +```rust +topics = ("anchor", Address { account_id: "..." }) +data = () +``` + +--- + +### 5. **deanchor** — Anchor Deregistered + +| Property | Value | +|----------|-------| +| **Topics** | `("deanchor", anchor)` | +| **Data** | `()` (unit) | +| **Emitted by** | [`deregister_anchor`](../src/lib.rs#L433) | +| **Emitted from** | [`anchor_removed`](../src/events.rs#L93) | + +Emitted when an anchor is removed from the approved set. Existing pool liquidity is unaffected. The second topic is the deregistered anchor's address. + +**Raw Event Example:** +```rust +topics = ("deanchor", Address { account_id: "..." }) +data = () +``` + +--- + +### 6. **provide** — Liquidity Provided + +| Property | Value | +|----------|-------| +| **Topics** | `("provide", provider, asset)` | +| **Data** | `i128` (amount) | +| **Emitted by** | [`provide_liquidity`](../src/lib.rs#L447) or [`provide_liquidity_multi`](../src/lib.rs#L470) | +| **Emitted from** | [`liquidity_provided`](../src/events.rs#L55) | + +Emitted when an anchor provides liquidity to a pool. Topics include the provider address and asset symbol; data is the amount provided. + +**Raw Event Example:** +```rust +topics = ("provide", Address { account_id: "..." }, Symbol::short("USDC")) +data = 1_000_000 // 1 USDC (assuming 6 decimals) +``` + +--- + +### 7. **onboarded** — Asset Onboarded + +| Property | Value | +|----------|-------| +| **Topics** | `("onboarded", asset)` | +| **Data** | `()` (unit) | +| **Emitted by** | [`provide_liquidity`](../src/lib.rs#L447) or [`provide_liquidity_multi`](../src/lib.rs#L470) (indirectly via `do_provide`) | +| **Emitted from** | [`asset_onboarded`](../src/events.rs#L63) | + +Emitted when an asset receives liquidity for the first time and is onboarded to the contract. The second topic is the asset symbol. + +**Raw Event Example:** +```rust +topics = ("onboarded", Symbol::short("USDC")) +data = () +``` + +--- + +### 8. **withdraw** — Liquidity Withdrawn + +| Property | Value | +|----------|-------| +| **Topics** | `("withdraw", provider, asset)` | +| **Data** | `i128` (amount) | +| **Emitted by** | [`withdraw_liquidity`](../src/lib.rs#L549), [`withdraw_all_liquidity`](../src/lib.rs#L624), or [`withdraw_liquidity_multi`](../src/lib.rs#L578) | +| **Emitted from** | [`liquidity_withdrawn`](../src/events.rs#L75) | + +Emitted when an anchor withdraws liquidity from a pool. Topics include the provider address and asset symbol; data is the amount withdrawn. + +**Note:** Both `withdraw_liquidity` and `withdraw_all_liquidity` emit this event via the same internal code path (`do_withdraw`), guaranteeing identical topic/data shapes for equivalent withdrawals. This parity is enforced by a regression test (`test_withdraw_event_parity` in `src/test.rs`). + +**Raw Event Example:** +```rust +topics = ("withdraw", Address { account_id: "..." }, Symbol::short("USDC")) +data = 500_000 // 0.5 USDC +``` + +--- + +### 9. **paused** — Paused Flag Changed + +| Property | Value | +|----------|-------| +| **Topics** | `("paused",)` | +| **Data** | `bool` (paused) | +| **Emitted by** | [`pause`](../src/lib.rs#L163) or [`unpause`](../src/lib.rs#L173) | +| **Emitted from** | [`paused_changed`](../src/events.rs#L83) | + +Emitted when the contract is paused or unpaused. Data is `true` for pause, `false` for unpause. + +**Raw Event Examples:** + +Pause: +```rust +topics = ("paused",) +data = true +``` + +Unpause: +```rust +topics = ("paused",) +data = false +``` + +--- + +### 10. **fee** — Global Protocol Fee Changed + +| Property | Value | +|----------|-------| +| **Topics** | `("fee",)` | +| **Data** | `u32` (bps — basis points) | +| **Emitted by** | [`set_fee`](../src/lib.rs#L196) | +| **Emitted from** | [`fee_changed`](../src/events.rs#L88) | + +Emitted when the global protocol fee is updated. Data is the new fee rate in basis points (max 1000 = 10%). + +**Raw Event Example:** +```rust +topics = ("fee",) +data = 50 // 0.5% (50 bps) +``` + +--- + +### 11. **waiver** — Anchor Fee Waiver Changed + +| Property | Value | +|----------|-------| +| **Topics** | `("waiver", anchor)` | +| **Data** | `bool` (waived) | +| **Emitted by** | [`set_fee_waiver`](../src/lib.rs#L235) | +| **Emitted from** | [`fee_waiver_changed`](../src/events.rs#L116) | + +Emitted when an anchor is granted or revoked a protocol fee waiver. Data is `true` for waived, `false` for revoked. + +**Raw Event Examples:** + +Grant waiver: +```rust +topics = ("waiver", Address { account_id: "..." }) +data = true +``` + +Revoke waiver: +```rust +topics = ("waiver", Address { account_id: "..." }) +data = false +``` + +--- + +### 12. **assetfee** — Asset-Specific Fee Override Set + +| Property | Value | +|----------|-------| +| **Topics** | `("assetfee", asset)` | +| **Data** | `u32` (bps — basis points) | +| **Emitted by** | [`set_asset_fee`](../src/lib.rs#L254) | +| **Emitted from** | [`asset_fee_changed`](../src/events.rs#L166) | + +Emitted when an asset-specific fee override is set, independent of the global rate. Data is the override fee rate in basis points. + +**Raw Event Example:** +```rust +topics = ("assetfee", Symbol::short("BTC")) +data = 100 // 1% (100 bps) for this asset only +``` + +--- + +### 13. **feeclear** — Asset Fee Override Cleared + +| Property | Value | +|----------|-------| +| **Topics** | `("feeclear", asset)` | +| **Data** | `()` (unit) | +| **Emitted by** | [`clear_asset_fee`](../src/lib.rs#L266) | +| **Emitted from** | [`asset_fee_cleared`](../src/events.rs#L173) | + +Emitted when an asset's fee override is cleared, reverting it to the global fee. The second topic is the asset symbol. + +**Raw Event Example:** +```rust +topics = ("feeclear", Symbol::short("BTC")) +data = () +``` + +--- + +### 14. **settle** — Settlement Opened + +| Property | Value | +|----------|-------| +| **Topics** | `("settle", anchor, asset)` | +| **Data** | `u64` (settlement_id) | +| **Emitted by** | [`open_settlement`](../src/lib.rs#L640) | +| **Emitted from** | [`settlement_opened`](../src/events.rs#L99) | + +Emitted when a settlement is opened, reserving pool liquidity. Topics include the anchor that opened it and the asset being settled; data is the newly assigned settlement ID. + +**Raw Event Example:** +```rust +topics = ("settle", Address { account_id: "..." }, Symbol::short("USDC")) +data = 1 // settlement ID +``` + +--- + +### 15. **executed** — Settlement Executed + +| Property | Value | +|----------|-------| +| **Topics** | `("executed", id)` | +| **Data** | `()` (unit) | +| **Emitted by** | [`execute_settlement`](../src/lib.rs#L700) | +| **Emitted from** | [`settlement_executed`](../src/events.rs#L105) | + +Emitted when a pending settlement is finalized. The reserved liquidity is considered released to the anchor, and its fee is accrued. The second topic is the settlement ID. + +**Raw Event Example:** +```rust +topics = ("executed", 1) +data = () +``` + +--- + +### 16. **cancelled** — Settlement Cancelled + +| Property | Value | +|----------|-------| +| **Topics** | `("cancelled", id)` | +| **Data** | `()` (unit) | +| **Emitted by** | [`cancel_settlement`](../src/lib.rs#L720) | +| **Emitted from** | [`settlement_cancelled`](../src/events.rs#L110) | + +Emitted when a pending settlement is cancelled. The reserved liquidity is returned to the pool. The second topic is the settlement ID. + +**Raw Event Example:** +```rust +topics = ("cancelled", 1) +data = () +``` + +--- + +### 17. **expired** — Settlement Expired and Reclaimed + +| Property | Value | +|----------|-------| +| **Topics** | `("expired", id)` | +| **Data** | `()` (unit) | +| **Emitted by** | [`cancel_expired_settlement`](../src/lib.rs#L760) | +| **Emitted from** | [`settlement_expired`](../src/events.rs#L135) | + +Emitted when a pending settlement is reclaimed after exceeding the configured expiry window. The reserved liquidity is returned to the pool. The second topic is the settlement ID. This entrypoint requires no authorization; anyone may call it. + +**Raw Event Example:** +```rust +topics = ("expired", 1) +data = () +``` + +--- + +### 18. **expiry** — Settlement Expiry Window Changed + +| Property | Value | +|----------|-------| +| **Topics** | `("expiry",)` | +| **Data** | `u32` (ledgers) | +| **Emitted by** | [`set_settlement_expiry_ledgers`](../src/lib.rs#L294) | +| **Emitted from** | [`settlement_expiry_changed`](../src/events.rs#L129) | + +Emitted when the settlement expiry window is updated. Data is the new window in ledgers (zero disables expiry). + +**Raw Event Example:** +```rust +topics = ("expiry",) +data = 86_400 // ~6 days of ledgers (assuming 10-second blocks) +``` + +--- + +### 19. **collect** — Fees Collected + +| Property | Value | +|----------|-------| +| **Topics** | `("collect", asset)` | +| **Data** | `i128` (amount) | +| **Emitted by** | [`collect_fees`](../src/lib.rs#L343) | +| **Emitted from** | [`fees_collected`](../src/events.rs#L122) | + +Emitted when accrued protocol fees for an asset are collected and the balance is reset to zero. Topics include the asset symbol; data is the collected amount. + +**Raw Event Example:** +```rust +topics = ("collect", Symbol::short("USDC")) +data = 50_000 // 0.05 USDC (50,000 stroops) +``` + +--- + +### 20. **minliq** — Minimum Liquidity Floor Changed + +| Property | Value | +|----------|-------| +| **Topics** | `("minliq", asset)` | +| **Data** | `i128` (floor) | +| **Emitted by** | [`set_min_liquidity`](../src/lib.rs#L507) | +| **Emitted from** | [`min_liquidity_changed`](../src/events.rs#L141) | + +Emitted when the minimum liquidity floor for an asset is configured. The floor prevents the pool from being withdrawn below this level. Topics include the asset symbol; data is the new floor value (zero disables). + +**Raw Event Example:** +```rust +topics = ("minliq", Symbol::short("USDC")) +data = 1_000_000_000 // 1,000 USDC (assuming 6 decimals) +``` + +--- + +### 21. **maxamt** — Maximum Settlement Amount Changed + +| Property | Value | +|----------|-------| +| **Topics** | `("maxamt", asset)` | +| **Data** | `i128` (amount) | +| **Emitted by** | [`set_max_settlement_amount`](../src/lib.rs#L527) | +| **Emitted from** | [`max_settlement_amount_changed`](../src/events.rs#L159) | + +Emitted when the maximum settlement amount for an asset is configured. This caps the amount a single `open_settlement` call may reserve. Topics include the asset symbol; data is the new cap (zero disables). + +**Raw Event Example:** +```rust +topics = ("maxamt", Symbol::short("BTC")) +data = 100_000_000 // 1 BTC (assuming 8 decimals) +``` + +--- + +### 22. **operator** — Operator Appointed + +| Property | Value | +|----------|-------| +| **Topics** | `("operator",)` | +| **Data** | `Address` (operator) | +| **Emitted by** | [`set_operator`](../src/lib.rs#L129) | +| **Emitted from** | [`operator_changed`](../src/events.rs#L147) | + +Emitted when an operator is appointed. The operator may pause/unpause but cannot change fees, admin, or other privileged settings. Data is the operator's address. + +**Raw Event Example:** +```rust +topics = ("operator",) +data = Address { account_id: "..." } +``` + +--- + +### 23. **op_clear** — Operator Role Revoked + +| Property | Value | +|----------|-------| +| **Topics** | `("op_clear",)` | +| **Data** | `()` (unit) | +| **Emitted by** | [`clear_operator`](../src/lib.rs#L139) | +| **Emitted from** | [`operator_cleared`](../src/events.rs#L153) | + +Emitted when the operator role is revoked, returning the contract to an operator-less state. + +**Raw Event Example:** +```rust +topics = ("op_clear",) +data = () +``` + +--- + +## Adding New Events + +When adding a new event: + +1. **Define the event function** in `src/events.rs` following the existing pattern: + - Name it `event_name()` and document topics/data in the doc comment + - Use `symbol_short!()` for topic symbols + - Emit via `env.events().publish(topics, data)` + +2. **Call it from the corresponding entrypoint** in `src/lib.rs` exactly once per logical operation + +3. **Update this document** with a new section following the template above, including: + - Topics structure (all indices in the tuple) + - Data type and value + - Emitting entrypoint and helper function + - A raw event example + +4. **Verify parity** if the event is emitted from multiple paths (see `test_withdraw_event_parity` in `src/test.rs` for a reference implementation) + +--- + +## Indexer Integration + +Off-chain indexers subscribe to these topics and maintain a replicated view of pool state. For each event: +- Parse the topic tuple to identify event type and parameters +- Extract the data payload +- Update off-chain state accordingly + +Topics are indexed and searchable; data is always unindexed. This structure allows efficient filtering by, e.g., `("provide", provider_address, asset_symbol)` without scanning all event data. + From 16c407d320dfd6b473ccbd4f0c01daee7193a373 Mon Sep 17 00:00:00 2001 From: Nimi <120312489+emmyoat@users.noreply.github.com> Date: Wed, 22 Jul 2026 22:46:21 +0100 Subject: [PATCH 2/6] Update README.md to link to EVENTS.md and mention events documentation --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c92026f..e3f91ef 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ cargo test - [`docs/ADMIN.md`](docs/ADMIN.md) – privileged admin/operator roles, lifecycle, and security properties - [`docs/ERRORS.md`](docs/ERRORS.md) – stable error-code reference and originating entrypoints - [`docs/PAGINATION.md`](docs/PAGINATION.md) – stable pagination semantics reference and worked examples +- [`docs/EVENTS.md`](docs/EVENTS.md) – event topics, argument types, and indexer integration guide - `src/types.rs` – on-chain data types (`Pool`) - `src/storage.rs` – storage keys and TTL-aware accessors - `src/events.rs` – event publishing helpers @@ -61,10 +62,10 @@ state. | `list_anchors(start, limit)` | – | Page through currently registered anchors | | `anchor_count()` | – | Read the number of currently registered anchors | | `provide_liquidity(provider, asset, amount)` | provider | Add liquidity to a pool | -| `provide_liquidity_multi(provider, requests)` | provider | Add liquidity to several assets in one call and authorization; validates the whole batch (no duplicate assets) before applying any of it | +| `provide_liquidity_multi(provider, requests)` | provider | Add liquidity to several assets in one call and authorization; validates the whole batch (no duplicate assets) before applying any of i[...] | `withdraw_liquidity(provider, asset, amount)` | provider | Remove liquidity from a pool | | `withdraw_all_liquidity(provider, asset)` | provider | Withdraw a provider's entire balance in one call | -| `withdraw_liquidity_multi(provider, requests)` | provider | Withdraw from several assets in one call and authorization; validates the whole batch (no duplicate assets) before applying any of it | +| `withdraw_liquidity_multi(provider, requests)` | provider | Withdraw from several assets in one call and authorization; validates the whole batch (no duplicate assets) before applying any of it [...] | `deregister_anchor(anchor)` | admin | Remove an anchor from the approved set | | `pool(asset)` | – | Read aggregate pool state | | `total_liquidity(asset)` | – | Read total liquidity for an asset | @@ -89,7 +90,7 @@ state. | `is_operator(address)` | – | Check whether an address is the currently appointed operator | | `extend_instance_ttl(caller)` | admin or operator | Extend the contract instance/code TTL so it survives long inactivity | -> **Note:** `extend_instance_ttl` only refreshes the **instance** storage bucket. Persistent entries (e.g., `Anchor`, `Pool`, `Balance`, etc.) have independent TTLs managed by per‑key `extend` calls on read/write. +> **Note:** `extend_instance_ttl` only refreshes the **instance** storage bucket. Persistent entries (e.g., `Anchor`, `Pool`, `Balance`, etc.) have independent TTLs managed by per‑key `extend` c[...] | `set_fee(bps)` | admin | Set the protocol fee in basis points (max 1000) | | `fee()` / `quote_fee(asset, amount)` | – | Read the global fee rate / preview the effective fee for an asset | @@ -189,6 +190,10 @@ verify the boundary without reading individual doc comments. ### Events +For detailed event documentation, including argument shapes, emission sites, and indexer integration guidance, see [`docs/EVENTS.md`](docs/EVENTS.md). + +**Event topics at a glance:** + - `("init",)` – contract initialized - `("admin",)` – administrator changed (via `set_admin` or `accept_admin`) - `("propose",)` – admin transfer proposed @@ -205,6 +210,9 @@ verify the boundary without reading individual doc comments. - `("expiry",)` – settlement expiry window changed - `("collect", asset)` – fees collected - `("minliq", asset)` – minimum liquidity floor configured +- `("maxamt", asset)` – maximum settlement amount configured +- `("assetfee", asset)` – asset-specific fee override set (data: `u32` bps) +- `("feeclear", asset)` – asset-specific fee override cleared - `("operator",)` – operator appointed or replaced - `("op_clear",)` – operator role revoked From 3fce5b5e54603547b2537af39f3989699dd55b3a Mon Sep 17 00:00:00 2001 From: emmyoat Date: Wed, 22 Jul 2026 23:46:16 +0100 Subject: [PATCH 3/6] docs & tests: add risk parameter event topics to README and unit tests --- README.md | 3 ++ src/test.rs | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/README.md b/README.md index c92026f..a0e44a8 100644 --- a/README.md +++ b/README.md @@ -205,6 +205,9 @@ verify the boundary without reading individual doc comments. - `("expiry",)` – settlement expiry window changed - `("collect", asset)` – fees collected - `("minliq", asset)` – minimum liquidity floor configured +- `("maxamt", asset)` – maximum settlement amount configured +- `("assetfee", asset)` – asset-specific fee override set (data: `u32` bps) +- `("feeclear", asset)` – asset-specific fee override cleared - `("operator",)` – operator appointed or replaced - `("op_clear",)` – operator role revoked diff --git a/src/test.rs b/src/test.rs index 4495dcb..9c61eec 100644 --- a/src/test.rs +++ b/src/test.rs @@ -2607,6 +2607,30 @@ fn test_set_min_liquidity_updates_value() { assert_eq!(client.min_liquidity(&asset), 200); } +#[test] +fn test_set_min_liquidity_emits_event() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin) = setup(&env); + let asset = symbol_short!("USDC"); + + client.initialize(&admin); + client.set_min_liquidity(&asset, &200); + + let events = env.events().all(); + assert_eq!( + events, + vec![ + &env, + ( + client.address.clone(), + (symbol_short!("minliq"), asset.clone()).into_val(&env), + 200i128.into_val(&env), + ), + ] + ); +} + #[test] fn test_set_min_liquidity_rejects_negative_floor() { let env = Env::default(); @@ -3133,6 +3157,30 @@ fn test_set_max_settlement_amount_updates_value() { assert_eq!(client.max_settlement_amount(&asset), 500); } +#[test] +fn test_set_max_settlement_amount_emits_event() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin) = setup(&env); + let asset = symbol_short!("USDC"); + + client.initialize(&admin); + client.set_max_settlement_amount(&asset, &500); + + let events = env.events().all(); + assert_eq!( + events, + vec![ + &env, + ( + client.address.clone(), + (symbol_short!("maxamt"), asset.clone()).into_val(&env), + 500i128.into_val(&env), + ), + ] + ); +} + #[test] fn test_set_max_settlement_amount_rejects_negative_value() { let env = Env::default(); @@ -3243,6 +3291,56 @@ fn test_clear_asset_fee_reverts_to_global() { assert_eq!(client.asset_fee(&asset), 100); } +#[test] +fn test_set_asset_fee_emits_event() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin) = setup(&env); + let asset = symbol_short!("USDC"); + + client.initialize(&admin); + client.set_asset_fee(&asset, &250); + + let events = env.events().all(); + assert_eq!( + events, + vec![ + &env, + ( + client.address.clone(), + (symbol_short!("assetfee"), asset.clone()).into_val(&env), + 250u32.into_val(&env), + ), + ] + ); +} + +#[test] +fn test_clear_asset_fee_emits_event() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin) = setup(&env); + let asset = symbol_short!("USDC"); + + client.initialize(&admin); + client.set_asset_fee(&asset, &250); + + client.clear_asset_fee(&asset); + + let events = env.events().all(); + assert_eq!( + events, + vec![ + &env, + ( + client.address.clone(), + (symbol_short!("feeclear"), asset.clone()).into_val(&env), + ().into_val(&env), + ), + ] + ); +} + #[test] fn test_asset_fee_override_is_per_asset() { let env = Env::default(); From b0996e9f3688ce6140032ca3bd944834238510a2 Mon Sep 17 00:00:00 2001 From: AnchorNet Dev Date: Thu, 23 Jul 2026 00:08:31 +0100 Subject: [PATCH 4/6] feat: add anchor_status tri-state read and status tracking --- README.md | 1 + src/lib.rs | 9 ++++++- src/storage.rs | 18 +++++++++++++- src/test.rs | 66 +++++++++++++++++++++++++++++++++++++++++++++++++- src/types.rs | 12 +++++++++ 5 files changed, 103 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a0e44a8..cf87f4a 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ state. | `register_anchor(anchor)` | admin | Approve an anchor as a liquidity provider | | `register_anchors(anchors)` | admin | Approve a batch of anchors atomically in one call | | `is_anchor(anchor)` | – | Check whether an address is registered | +| `anchor_status(anchor)` | – | Read the registration status (`NeverRegistered`, `Active`, or `Deregistered`) of an address | | `list_anchors(start, limit)` | – | Page through currently registered anchors | | `anchor_count()` | – | Read the number of currently registered anchors | | `provide_liquidity(provider, asset, amount)` | provider | Add liquidity to a pool | diff --git a/src/lib.rs b/src/lib.rs index 833440b..17b20f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ mod storage; mod types; pub use error::Error; -pub use types::{ContractInfo, Pool, Settlement, SettlementStatus}; +pub use types::{AnchorStatus, ContractInfo, Pool, Settlement, SettlementStatus}; /// Maximum protocol fee that can be configured: 1000 bps (10%). const MAX_FEE_BPS: u32 = 1_000; @@ -397,6 +397,13 @@ impl AnchornetContract { storage::is_anchor(&env, &anchor) } + /// Returns the registration status ([`AnchorStatus`]) of `anchor`: + /// [`AnchorStatus::NeverRegistered`], [`AnchorStatus::Active`], or + /// [`AnchorStatus::Deregistered`]. + pub fn anchor_status(env: Env, anchor: Address) -> AnchorStatus { + storage::anchor_status(&env, &anchor) + } + /// Returns up to `limit` currently registered anchors, in registration /// order, scanning the registration history starting at list index /// `start` (0-based). Anchors that have been diff --git a/src/storage.rs b/src/storage.rs index 4b7cdcb..5613d15 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -27,7 +27,7 @@ use soroban_sdk::{contracttype, Address, Env, Symbol, Vec}; -use crate::types::{Pool, Settlement}; +use crate::types::{AnchorStatus, Pool, Settlement}; const DAY_IN_LEDGERS: u32 = 17_280; /// How long an entry's TTL is extended to on access (~30 days). @@ -196,6 +196,22 @@ pub fn is_anchor(env: &Env, anchor: &Address) -> bool { env.storage().persistent().get(&key).unwrap_or(false) } +/// Reads the registration status of `anchor`. +pub fn anchor_status(env: &Env, anchor: &Address) -> AnchorStatus { + let key = DataKey::Anchor(anchor.clone()); + match env.storage().persistent().get::(&key) { + Some(true) => { + extend(env, &key); + AnchorStatus::Active + } + Some(false) => { + extend(env, &key); + AnchorStatus::Deregistered + } + None => AnchorStatus::NeverRegistered, + } +} + /// Marks `anchor` as registered. pub fn set_anchor(env: &Env, anchor: &Address) { set_anchor_flag(env, anchor, true); diff --git a/src/test.rs b/src/test.rs index 9c61eec..5da70c3 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,5 +1,8 @@ use crate::storage::DataKey; -use crate::{AnchornetContract, AnchornetContractClient, Error, SettlementStatus, BPS_DENOMINATOR}; +use crate::{ + AnchornetContract, AnchornetContractClient, AnchorStatus, Error, SettlementStatus, + BPS_DENOMINATOR, +}; use proptest::prelude::*; use soroban_sdk::{ symbol_short, @@ -102,6 +105,35 @@ fn test_register_anchor() { assert!(client.is_anchor(&anchor)); } +#[test] +fn test_anchor_status_lifecycle() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin) = setup(&env); + let anchor = Address::generate(&env); + + client.initialize(&admin); + + // Initial state: NeverRegistered + assert_eq!(client.anchor_status(&anchor), AnchorStatus::NeverRegistered); + assert!(!client.is_anchor(&anchor)); + + // Register anchor: Active + client.register_anchor(&anchor); + assert_eq!(client.anchor_status(&anchor), AnchorStatus::Active); + assert!(client.is_anchor(&anchor)); + + // Deregister anchor: Deregistered + client.deregister_anchor(&anchor); + assert_eq!(client.anchor_status(&anchor), AnchorStatus::Deregistered); + assert!(!client.is_anchor(&anchor)); + + // Re-register anchor: Active + client.register_anchor(&anchor); + assert_eq!(client.anchor_status(&anchor), AnchorStatus::Active); + assert!(client.is_anchor(&anchor)); +} + #[test] fn test_register_anchor_twice_fails() { let env = Env::default(); @@ -4839,6 +4871,38 @@ fn test_is_anchor_read_bumps_ttl() { ); } +#[test] +fn test_anchor_status_read_bumps_ttl() { + let env = Env::default(); + let (client, _admin, anchor, _asset) = funded(&env, 1_000); + + let key = DataKey::Anchor(anchor.clone()); + + // Active state TTL bump check + advance_ledger(&env, TTL_DECAY_LEDGERS); + let before_active = persistent_ttl(&env, &client.address, &key); + assert_eq!(client.anchor_status(&anchor), AnchorStatus::Active); + let after_active = persistent_ttl(&env, &client.address, &key); + assert!( + after_active > before_active, + "anchor_status read in Active state did not bump TTL: before={before_active}, after={after_active}" + ); + + // Deregistered state TTL bump check + env.mock_all_auths(); + client.deregister_anchor(&anchor); + assert_eq!(client.anchor_status(&anchor), AnchorStatus::Deregistered); + + advance_ledger(&env, TTL_DECAY_LEDGERS); + let before_dereg = persistent_ttl(&env, &client.address, &key); + assert_eq!(client.anchor_status(&anchor), AnchorStatus::Deregistered); + let after_dereg = persistent_ttl(&env, &client.address, &key); + assert!( + after_dereg > before_dereg, + "anchor_status read in Deregistered state did not bump TTL: before={before_dereg}, after={after_dereg}" + ); +} + #[test] fn test_max_settlement_amount_read_bumps_ttl() { let env = Env::default(); diff --git a/src/types.rs b/src/types.rs index 3bd7419..fedf91a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -43,6 +43,18 @@ pub enum SettlementStatus { Expired, } +/// Registration status of an anchor address. +#[contracttype] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum AnchorStatus { + /// The address has never been registered as an anchor. + NeverRegistered, + /// The address is currently registered as an active anchor. + Active, + /// The address was previously registered as an anchor but has been deregistered. + Deregistered, +} + /// A request to draw `amount` of `asset` liquidity for cross-anchor settlement. #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] From d7ce5f26e1a6c44364a3134e2e6ce5ee580bb724 Mon Sep 17 00:00:00 2001 From: AnchorNet Dev Date: Thu, 23 Jul 2026 00:13:38 +0100 Subject: [PATCH 5/6] style: remove comments from anchor_status code --- src/lib.rs | 3 --- src/storage.rs | 1 - src/test.rs | 6 ------ src/types.rs | 4 ---- 4 files changed, 14 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 17b20f2..fab8f73 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -397,9 +397,6 @@ impl AnchornetContract { storage::is_anchor(&env, &anchor) } - /// Returns the registration status ([`AnchorStatus`]) of `anchor`: - /// [`AnchorStatus::NeverRegistered`], [`AnchorStatus::Active`], or - /// [`AnchorStatus::Deregistered`]. pub fn anchor_status(env: Env, anchor: Address) -> AnchorStatus { storage::anchor_status(&env, &anchor) } diff --git a/src/storage.rs b/src/storage.rs index 5613d15..52094e5 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -196,7 +196,6 @@ pub fn is_anchor(env: &Env, anchor: &Address) -> bool { env.storage().persistent().get(&key).unwrap_or(false) } -/// Reads the registration status of `anchor`. pub fn anchor_status(env: &Env, anchor: &Address) -> AnchorStatus { let key = DataKey::Anchor(anchor.clone()); match env.storage().persistent().get::(&key) { diff --git a/src/test.rs b/src/test.rs index 5da70c3..3e5fb27 100644 --- a/src/test.rs +++ b/src/test.rs @@ -114,21 +114,17 @@ fn test_anchor_status_lifecycle() { client.initialize(&admin); - // Initial state: NeverRegistered assert_eq!(client.anchor_status(&anchor), AnchorStatus::NeverRegistered); assert!(!client.is_anchor(&anchor)); - // Register anchor: Active client.register_anchor(&anchor); assert_eq!(client.anchor_status(&anchor), AnchorStatus::Active); assert!(client.is_anchor(&anchor)); - // Deregister anchor: Deregistered client.deregister_anchor(&anchor); assert_eq!(client.anchor_status(&anchor), AnchorStatus::Deregistered); assert!(!client.is_anchor(&anchor)); - // Re-register anchor: Active client.register_anchor(&anchor); assert_eq!(client.anchor_status(&anchor), AnchorStatus::Active); assert!(client.is_anchor(&anchor)); @@ -4878,7 +4874,6 @@ fn test_anchor_status_read_bumps_ttl() { let key = DataKey::Anchor(anchor.clone()); - // Active state TTL bump check advance_ledger(&env, TTL_DECAY_LEDGERS); let before_active = persistent_ttl(&env, &client.address, &key); assert_eq!(client.anchor_status(&anchor), AnchorStatus::Active); @@ -4888,7 +4883,6 @@ fn test_anchor_status_read_bumps_ttl() { "anchor_status read in Active state did not bump TTL: before={before_active}, after={after_active}" ); - // Deregistered state TTL bump check env.mock_all_auths(); client.deregister_anchor(&anchor); assert_eq!(client.anchor_status(&anchor), AnchorStatus::Deregistered); diff --git a/src/types.rs b/src/types.rs index fedf91a..4a35f7f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -43,15 +43,11 @@ pub enum SettlementStatus { Expired, } -/// Registration status of an anchor address. #[contracttype] #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum AnchorStatus { - /// The address has never been registered as an anchor. NeverRegistered, - /// The address is currently registered as an active anchor. Active, - /// The address was previously registered as an anchor but has been deregistered. Deregistered, } From f17a0056caedc09db4acd6ce89a7a04ae7b2570a Mon Sep 17 00:00:00 2001 From: AnchorNet Dev Date: Thu, 23 Jul 2026 17:14:44 +0100 Subject: [PATCH 6/6] docs: add public API compatibility checklist --- README.md | 6 +- docs/PUBLIC_API_CHECKLIST.md | 72 +++++++++++++++++++++ src/lib.rs | 15 +++-- src/test.rs | 117 ++++++++++++++++++++++++++++++----- 4 files changed, 187 insertions(+), 23 deletions(-) create mode 100644 docs/PUBLIC_API_CHECKLIST.md diff --git a/README.md b/README.md index cf87f4a..2e91b7b 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ cargo test - [`docs/ADMIN.md`](docs/ADMIN.md) – privileged admin/operator roles, lifecycle, and security properties - [`docs/ERRORS.md`](docs/ERRORS.md) – stable error-code reference and originating entrypoints - [`docs/PAGINATION.md`](docs/PAGINATION.md) – stable pagination semantics reference and worked examples +- [`docs/PUBLIC_API_CHECKLIST.md`](docs/PUBLIC_API_CHECKLIST.md) – public API compatibility checklist for reviewing contract signature and type changes - `src/types.rs` – on-chain data types (`Pool`) - `src/storage.rs` – storage keys and TTL-aware accessors - `src/events.rs` – event publishing helpers @@ -231,8 +232,9 @@ it without an off-chain registry. 1. Fork the repo and create a branch from `main`. 2. Make changes; keep formatting with `cargo fmt --all`. -3. Run `cargo fmt --all -- --check`, `cargo build`, and `cargo test`. -4. Open a pull request. CI will run format check, build, and tests. +3. If modifying public contract functions, parameters, return types, data structures, error codes, or events, review and complete the [`Public API Compatibility Checklist`](docs/PUBLIC_API_CHECKLIST.md). +4. Run `cargo fmt --all -- --check`, `cargo build`, and `cargo test`. +5. Open a pull request. CI will run format check, build, and tests. ## License diff --git a/docs/PUBLIC_API_CHECKLIST.md b/docs/PUBLIC_API_CHECKLIST.md new file mode 100644 index 0000000..3324b8e --- /dev/null +++ b/docs/PUBLIC_API_CHECKLIST.md @@ -0,0 +1,72 @@ +# Public API Compatibility Checklist + +This checklist must be reviewed by contributors and maintainers before proposing or merging any changes to the public contract interface, function signatures, data structures, event payloads, or error codes in `anchornet-contracts`. + +Because the contracts serve as the core state and execution layer for off-chain SDKs, mobile applications, and indexers, uncoordinated changes to the public contract API can cause severe breakage across downstream repositories. + +--- + +## 1. API Compatibility Assessment + +- [ ] **Identify the Change Type:** + - **Non-breaking:** Adding a new optional function/entrypoint, adding a new event variant without altering existing ones. + - **Breaking:** Modifying existing function names, changing parameter counts or types, changing return types, removing functions, altering data structure layouts (`Pool`, storage keys), changing event topic/data schema, or renumbering/removing error codes. +- [ ] **Function Signatures:** + - Are function names, argument order, parameter types, and return types unchanged for existing public entrypoints? + - If a function signature was altered, is there a backward-compatible alternative or migration path? +- [ ] **Data Types & Storage Layout:** + - Are on-chain structs (e.g. in `src/types.rs`) backward compatible with existing contract state? + - Do storage key definitions (`src/storage.rs`) preserve existing key encoding and TTL behavior? +- [ ] **Errors & Events:** + - Are existing error code numerical values in `src/error.rs` preserved without renumbering or removing variants? + - Are emitted event topics and data payloads (`src/events.rs`) consistent with existing off-chain indexer expectations? + +--- + +## 2. Downstream SDK & Mobile Impact + +- [ ] **SDK Bindings:** + - Has the impact on Soroban SDK client libraries (TypeScript, Rust, etc.) been evaluated? + - Will generated SDK client method signatures require major version bumps? +- [ ] **Mobile Integration:** + - Will mobile applications (e.g., PocketPay Android / iOS apps) break if deployed against this contract version without a client update? + - Is a deprecation strategy or dual-version support required for active mobile app releases? +- [ ] **Cross-Repo Coordination:** + - Have corresponding issues or pull requests been opened in dependent repositories (SDKs, mobile apps, indexers)? + - Are breaking changes coordinated with cross-repository release timelines? + +--- + +## 3. Tests & Verification + +- [ ] **Unit & Integration Tests:** + - Are new unit tests added in `src/test.rs` covering modified or newly added entrypoints? + - Do all existing unit tests pass without suppressing or removing critical assertions? +- [ ] **Snapshot & Property Tests:** + - Have proptest regressions (`proptest-regressions/`) and snapshot tests (`test_snapshots/`) been verified or updated? +- [ ] **Local Build & Test Run:** + - Has the contract been compiled (`cargo build`) and verified via `cargo test` and `cargo fmt --all -- --check`? + +--- + +## 4. Documentation & Reference Updates + +- [ ] **`README.md` Interface Reference:** + - Is the Contract Interface table updated with any new or modified entrypoints, authorization requirements, and descriptions? + - Is the Operator Permission Boundary table updated if new gated functions were introduced? + - Is the Events list updated if new event topics or payloads are published? +- [ ] **`docs/ERRORS.md`:** + - Is `docs/ERRORS.md` updated if error codes were added or public entrypoint mappings changed? +- [ ] **`docs/ADMIN.md` & `docs/PAGINATION.md`:** + - Are administrative role specifications or pagination semantics updated if affected? + +--- + +## 5. Migration Notes & Changelog + +- [ ] **`CHANGELOG.md`:** + - Is the change documented in `CHANGELOG.md` under breaking changes or new features? +- [ ] **Migration Notes:** + - Are upgrading instructions and breaking change summaries documented for SDK developers and mobile integrators? +- [ ] **Contract Versioning:** + - Is the `version()` entrypoint or contract metadata updated if required for this release? diff --git a/src/lib.rs b/src/lib.rs index fab8f73..c13d2a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -223,10 +223,7 @@ impl AnchornetContract { if amount <= 0 { return Err(Error::InvalidAmount); } - Self::calculate_fee( - amount, - Self::effective_fee_bps(&env, &asset), - ) + Self::calculate_fee(amount, Self::effective_fee_bps(&env, &asset)) } /// Grants or revokes a protocol fee waiver for `anchor`. While waived, @@ -729,7 +726,10 @@ impl AnchornetContract { } let mut pool = storage::get_pool(&env, &settlement.asset); - pool.total = pool.total.checked_add(settlement.amount).ok_or(Error::Overflow)?; + pool.total = pool + .total + .checked_add(settlement.amount) + .ok_or(Error::Overflow)?; storage::set_pool(&env, &settlement.asset, &pool); settlement.status = SettlementStatus::Cancelled; @@ -780,7 +780,10 @@ impl AnchornetContract { } let mut pool = storage::get_pool(&env, &settlement.asset); - pool.total = pool.total.checked_add(settlement.amount).ok_or(Error::Overflow)?; + pool.total = pool + .total + .checked_add(settlement.amount) + .ok_or(Error::Overflow)?; storage::set_pool(&env, &settlement.asset, &pool); settlement.status = SettlementStatus::Expired; diff --git a/src/test.rs b/src/test.rs index 3e5fb27..d1ad336 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,6 +1,6 @@ use crate::storage::DataKey; use crate::{ - AnchornetContract, AnchornetContractClient, AnchorStatus, Error, SettlementStatus, + AnchorStatus, AnchornetContract, AnchornetContractClient, Error, SettlementStatus, BPS_DENOMINATOR, }; use proptest::prelude::*; @@ -1255,11 +1255,20 @@ fn test_list_settlements_by_anchor_and_asset_empty_for_unknown() { let stranger = Address::generate(&env); let other_asset = symbol_short!("EURC"); - assert_eq!(client.list_settlements_by_anchor_and_asset(&stranger, &asset, &1, &10).len(), 0); - assert_eq!(client.list_settlements_by_anchor_and_asset(&anchor, &other_asset, &1, &10).len(), 0); + assert_eq!( + client + .list_settlements_by_anchor_and_asset(&stranger, &asset, &1, &10) + .len(), + 0 + ); + assert_eq!( + client + .list_settlements_by_anchor_and_asset(&anchor, &other_asset, &1, &10) + .len(), + 0 + ); } - #[test] fn test_version() { let env = Env::default(); @@ -2359,8 +2368,22 @@ fn test_clear_operator() { assert_eq!(err, Error::NoOperator); assert!(!client.is_operator(&operator)); - assert_operator_rejected!(env, client, operator, "pause", (), client.try_pause(&operator)); - assert_operator_rejected!(env, client, operator, "unpause", (), client.try_unpause(&operator)); + assert_operator_rejected!( + env, + client, + operator, + "pause", + (), + client.try_pause(&operator) + ); + assert_operator_rejected!( + env, + client, + operator, + "unpause", + (), + client.try_unpause(&operator) + ); assert_operator_rejected!( env, client, @@ -2369,7 +2392,7 @@ fn test_clear_operator() { (), client.try_extend_instance_ttl(&operator) ); - + // Admin can still act client.pause(&admin); assert!(client.is_paused()); @@ -2940,11 +2963,7 @@ fn test_settlement_age_rejects_unknown_id() { let env = Env::default(); let (client, _admin, _anchor, _asset) = funded(&env, 1_000); - let err = client - .try_settlement_age(&99) - .err() - .unwrap() - .unwrap(); + let err = client.try_settlement_age(&99).err().unwrap().unwrap(); assert_eq!(err, Error::SettlementNotFound); } @@ -4451,7 +4470,9 @@ fn test_list_settlements_by_anchor_and_asset_start_past_end_returns_empty() { client.open_settlement(&anchor, &asset, &100); assert_eq!( - client.list_settlements_by_anchor_and_asset(&anchor, &asset, &3, &10).len(), + client + .list_settlements_by_anchor_and_asset(&anchor, &asset, &3, &10) + .len(), 0 ); assert_eq!( @@ -4469,11 +4490,15 @@ fn test_list_settlements_by_anchor_and_asset_limit_zero_returns_empty() { client.open_settlement(&anchor, &asset, &100); assert_eq!( - client.list_settlements_by_anchor_and_asset(&anchor, &asset, &1, &0).len(), + client + .list_settlements_by_anchor_and_asset(&anchor, &asset, &1, &0) + .len(), 0 ); assert_eq!( - client.list_settlements_by_anchor_and_asset(&anchor, &asset, &0, &0).len(), + client + .list_settlements_by_anchor_and_asset(&anchor, &asset, &0, &0) + .len(), 0 ); } @@ -5084,3 +5109,65 @@ fn test_get_fees_accrued_read_on_unconfigured_asset_is_safe() { let never_settled = symbol_short!("EURC"); assert_eq!(client.fees_accrued(&never_settled), 0); } + +#[test] +fn test_withdraw_liquidity_multi_atomic_rejection_on_min_liquidity_floor_violation() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin) = setup(&env); + let anchor = Address::generate(&env); + let ast1 = symbol_short!("AST1"); + let ast2 = symbol_short!("AST2"); + + client.initialize(&admin); + client.register_anchor(&anchor); + + client.provide_liquidity(&anchor, &ast1, &1_000); + client.provide_liquidity(&anchor, &ast2, &1_000); + + client.set_min_liquidity(&ast2, &700); + + let requests = vec![&env, (ast1.clone(), 500), (ast2.clone(), 400)]; + let err = client + .try_withdraw_liquidity_multi(&anchor, &requests) + .err() + .unwrap() + .unwrap(); + + assert_eq!(err, Error::BelowMinLiquidity); + assert_eq!(client.balance(&anchor, &ast1), 1_000); + assert_eq!(client.balance(&anchor, &ast2), 1_000); + assert_eq!(client.total_liquidity(&ast1), 1_000); + assert_eq!(client.total_liquidity(&ast2), 1_000); +} + +#[test] +fn test_open_settlement_enforces_per_asset_max_settlement_amount_cap() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin) = setup(&env); + let anchor = Address::generate(&env); + let ast1 = symbol_short!("AST1"); + let ast2 = symbol_short!("AST2"); + + client.initialize(&admin); + client.register_anchor(&anchor); + + client.provide_liquidity(&anchor, &ast1, &1_000); + client.provide_liquidity(&anchor, &ast2, &1_000); + + client.set_max_settlement_amount(&ast1, &500); + + let err = client + .try_open_settlement(&anchor, &ast1, &600) + .err() + .unwrap() + .unwrap(); + assert_eq!(err, Error::AboveMaxSettlementAmount); + + let id1 = client.open_settlement(&anchor, &ast1, &500); + assert_eq!(id1, 1); + + let id2 = client.open_settlement(&anchor, &ast2, &600); + assert_eq!(id2, 2); +}