From ab8efd648441a45a0269d8e9c25db763f592a3b8 Mon Sep 17 00:00:00 2001 From: Ryjen1 Date: Sat, 28 Mar 2026 16:27:08 +0100 Subject: [PATCH 1/3] feat(escrow): add compact summary read for indexer efficiency --- README.md | 1 + escrow/src/lib.rs | 60 ++++++++++++++++++++++++ escrow/src/test.rs | 113 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 172 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 147c9f0..c89980b 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Soroban smart contracts for **LiquiFact** on Stellar. This repository contains t |--------|---------| | `init` | Create escrow (admin auth). Sets `funding_target = amount`. Binds **`funding_token`**, **`treasury`**, optional **`registry`**, optional **`yield_tiers`** ([`YieldTier`](escrow/src/lib.rs) Soroban [`Vec`]); validates **`invoice_id`** string (length ≤ 32, charset `[A-Za-z0-9_]`). | | `get_escrow` / `get_version` / `get_legal_hold` | Read state. | +| `get_escrow_summary` | Compact read view for indexers/callers: status, amounts/targets, maturity, and funding-close ledger timestamp/sequence (when funded). | | `get_funding_token` / `get_treasury` / `get_registry_ref` | Immutable funding asset, treasury for dust recovery, optional registry hint (`None` if unset at init). | | `get_contribution` | Per-investor funded principal. | | `update_funding_target` | Admin, open state only; target ≥ `funded_amount`. | diff --git a/escrow/src/lib.rs b/escrow/src/lib.rs index e437e71..159f458 100644 --- a/escrow/src/lib.rs +++ b/escrow/src/lib.rs @@ -189,6 +189,36 @@ pub struct FundingCloseSnapshot { pub closed_at_ledger_sequence: u32, } +/// Compact read view for indexers and cross-contract callers. +/// +/// This struct intentionally contains only primitive, non-address fields so it can be fetched +/// efficiently and decoded without pulling the full [`InvoiceEscrow`] (which includes `Address` +/// and `Symbol` fields). +/// +/// ## Stability across versions +/// +/// The field order and types are part of the contract interface. Treat this struct as +/// **append-only**: future versions may add new optional fields at the end, but existing fields +/// will not be reordered or repurposed. +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct EscrowSummary { + /// Current escrow status: `0 = open`, `1 = funded`, `2 = settled`, `3 = withdrawn`. + pub status: u32, + /// Invoice amount set at init (base units of the funding token). + pub amount: i128, + /// Funding target (may be updated by admin while status is `open`). + pub funding_target: i128, + /// Total funded principal credited so far (base units of the funding token). + pub funded_amount: i128, + /// Maturity ledger timestamp (seconds). `0` means no maturity gate. + pub maturity: u64, + /// Ledger timestamp when the escrow first became funded; [`None`] until then. + pub funding_close_timestamp: Option, + /// Ledger sequence when the escrow first became funded; [`None`] until then. + pub funding_close_sequence: Option, +} + // --- Events --- #[contractevent] @@ -560,6 +590,36 @@ impl LiquifactEscrow { .unwrap_or_else(|| panic!("Escrow not initialized")) } + /// Compact summary read for external indexers and cross-contract callers. + /// + /// Returns a small struct containing the most commonly-indexed numeric fields from + /// [`InvoiceEscrow`], plus the ledger time metadata from [`FundingCloseSnapshot`] (when present). + pub fn get_escrow_summary(env: Env) -> EscrowSummary { + let escrow = Self::get_escrow(env.clone()); + let snap: Option = env + .storage() + .instance() + .get::(&DataKey::FundingCloseSnapshot); + + let (funding_close_timestamp, funding_close_sequence) = match snap { + Some(s) => ( + Some(s.closed_at_ledger_timestamp), + Some(s.closed_at_ledger_sequence), + ), + None => (None, None), + }; + + EscrowSummary { + status: escrow.status, + amount: escrow.amount, + funding_target: escrow.funding_target, + funded_amount: escrow.funded_amount, + maturity: escrow.maturity, + funding_close_timestamp, + funding_close_sequence, + } + } + pub fn get_version(env: Env) -> u32 { env.storage().instance().get(&DataKey::Version).unwrap_or(0) } diff --git a/escrow/src/test.rs b/escrow/src/test.rs index 090e466..a867faa 100644 --- a/escrow/src/test.rs +++ b/escrow/src/test.rs @@ -1,11 +1,12 @@ use super::{ - external_calls, LiquifactEscrow, LiquifactEscrowClient, YieldTier, MAX_DUST_SWEEP_AMOUNT, - SCHEMA_VERSION, + external_calls, EscrowSummary, LiquifactEscrow, LiquifactEscrowClient, YieldTier, + MAX_DUST_SWEEP_AMOUNT, SCHEMA_VERSION, }; use soroban_sdk::{ symbol_short, testutils::{Address as _, Ledger as _}, token::StellarAssetClient, + xdr::{FromXdr, ToXdr}, Address, Env, String, Vec as SorobanVec, }; @@ -153,6 +154,114 @@ fn test_get_escrow_uninitialized_panics() { client.get_escrow(); } +// --- summary read (#101) --- + +#[test] +#[should_panic(expected = "Escrow not initialized")] +fn test_get_escrow_summary_uninitialized_panics() { + let env = Env::default(); + let client = deploy(&env); + client.get_escrow_summary(); +} + +#[test] +fn test_get_escrow_summary_open_roundtrip_and_compact_xdr() { + let env = Env::default(); + let (client, admin, sme) = setup(&env); + client.init( + &admin, + &String::from_str(&env, "SUM001"), + &sme, + &TARGET, + &800i64, + &1000u64, + &Address::generate(&env), + &None, + &Address::generate(&env), + &None, + ); + + let summary = client.get_escrow_summary(); + assert_eq!( + summary, + EscrowSummary { + status: 0, + amount: TARGET, + funding_target: TARGET, + funded_amount: 0, + maturity: 1000u64, + funding_close_timestamp: None, + funding_close_sequence: None, + } + ); + + let bytes = summary.clone().to_xdr(&env); + let rt = EscrowSummary::from_xdr(&env, &bytes).expect("xdr roundtrip"); + assert_eq!(rt, summary); + + let escrow_bytes = client.get_escrow().to_xdr(&env); + assert!( + bytes.len() < escrow_bytes.len(), + "expected summary to be more compact than full InvoiceEscrow" + ); +} + +#[test] +fn test_get_escrow_summary_reflects_updated_funding_target() { + let env = Env::default(); + let (client, admin, sme) = setup(&env); + client.init( + &admin, + &String::from_str(&env, "SUM002"), + &sme, + &5_000i128, + &800i64, + &0u64, + &Address::generate(&env), + &None, + &Address::generate(&env), + &None, + ); + client.update_funding_target(&10_000i128); + + let summary = client.get_escrow_summary(); + assert_eq!(summary.amount, 5_000i128); + assert_eq!(summary.funding_target, 10_000i128); + assert_eq!(summary.status, 0); + assert_eq!(summary.funding_close_timestamp, None); + assert_eq!(summary.funding_close_sequence, None); +} + +#[test] +fn test_get_escrow_summary_includes_funding_close_metadata_when_funded() { + let env = Env::default(); + let (client, admin, sme) = setup(&env); + let investor = Address::generate(&env); + client.init( + &admin, + &String::from_str(&env, "SUM003"), + &sme, + &TARGET, + &800i64, + &0u64, + &Address::generate(&env), + &None, + &Address::generate(&env), + &None, + ); + + env.ledger().set_timestamp(4242); + env.ledger().set_sequence_number(77); + + client.fund(&investor, &TARGET); + + let summary = client.get_escrow_summary(); + assert_eq!(summary.status, 1); + assert_eq!(summary.funded_amount, TARGET); + assert_eq!(summary.funding_close_timestamp, Some(4242)); + assert_eq!(summary.funding_close_sequence, Some(77)); +} + // --- fund --- #[test] From 304d1a21da74e6cc8f6fb7e7e44f73a77e06464a Mon Sep 17 00:00:00 2001 From: Ryjen1 Date: Tue, 31 Mar 2026 10:38:05 +0100 Subject: [PATCH 2/3] fix(ci): pin rust toolchain for formatting --- .github/workflows/ci.yml | 3 ++- rust-toolchain.toml | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 rust-toolchain.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0e2878..ed17677 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,8 +13,9 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master with: + toolchain: 1.93.0 components: rustfmt, llvm-tools-preview - name: Cache cargo registry and build diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..609e284 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.93.0" +profile = "minimal" From ceace99db8d0c4b445255971497a8108092c4a6d Mon Sep 17 00:00:00 2001 From: Ryjen1 Date: Tue, 31 Mar 2026 10:46:48 +0100 Subject: [PATCH 3/3] fix(test): resolve merged escrow test drift --- escrow/src/test.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/escrow/src/test.rs b/escrow/src/test.rs index 289b2a5..45ab4bf 100644 --- a/escrow/src/test.rs +++ b/escrow/src/test.rs @@ -7,7 +7,6 @@ use soroban_sdk::{ testutils::{Address as _, Ledger as _}, token::StellarAssetClient, xdr::{FromXdr, ToXdr}, - Address, Env, String, Vec as SorobanVec, Address, BytesN, Env, String, Vec as SorobanVec, }; @@ -192,6 +191,8 @@ fn test_get_escrow_summary_open_roundtrip_and_compact_xdr() { &None, &Address::generate(&env), &None, + &None, + &None, ); let summary = client.get_escrow_summary(); @@ -234,6 +235,8 @@ fn test_get_escrow_summary_reflects_updated_funding_target() { &None, &Address::generate(&env), &None, + &None, + &None, ); client.update_funding_target(&10_000i128); @@ -261,6 +264,8 @@ fn test_get_escrow_summary_includes_funding_close_metadata_when_funded() { &None, &Address::generate(&env), &None, + &None, + &None, ); env.ledger().set_timestamp(4242);