Important
Target Audience: Downstream Integrators, Indexer Service Developers, and Client API Engineers. This document specifies the exact read guarantees, ordering rules, snapshot consistency, cursor stability, and concurrency semantics for all paginated query entrypoints on the QuickLendX Soroban smart contracts.
Read-only entrypoints on QuickLendX smart contracts allow off-chain clients, indexers, and frontends to inspect protocol state without submitting mutating transactions.
Every paginated query endpoint follows strict runtime invariants to guarantee deterministic execution, zero panic risk, and resource-bounded execution on the Soroban host.
| Semantic Dimension | Guarantee Provided | Behavior & Enforcement |
|---|---|---|
| Snapshot Consistency | Single-Ledger Atomic | Within a single RPC query invocation, all state storage accesses are executed against a frozen, consistent snapshot of the ledger at sequence |
| Cross-Request Isolation | Stateless Read Independent | Each subsequent paged query (e.g., fetching Page 2 after Page 1) is an independent RPC call evaluated at the current ledger sequence |
| Ordering Determinism | Stable Index Order | Results are returned in deterministic index order (insertion sequence or explicit rank score). Page slicing never reorders or skips items within a single page slice. |
| Query Hard Cap | Clamped to 50 Items | No endpoint returns more than MAX_QUERY_LIMIT = 50 items. Requests specifying limit > 50 are automatically clamped to 50. |
| Overflow Protection | Empty Slice (No Panic) | If offset >= total_count or if offset approaches u32::MAX, endpoints return an empty list (Vec::new()) and has_more = false. |
When calling a paged endpoint such as get_business_invoices_paged(business, offset = 0, limit = 20):
- The entire evaluation occurs within ledger sequence
$L$ . - The set of invoices returned, their statuses, and
has_morerepresent a point-in-time atomic state.
Because Soroban contract reads are stateless across RPC requests, state mutations can occur on-chain between fetching Page 1 at ledger
-
Insertion Shift:
- If a new invoice is created by the business at
$L_{1.5}$ , item positions shift right. -
Result: Fetching Page 2 with
offset = 20at$L_2$ might re-read the last element of Page 1.
- If a new invoice is created by the business at
-
Deletion / Status Transition Shift:
- If an invoice transitions status or is pruned between requests, item positions shift left.
- Result: Fetching Page 2 might skip an item that shifted from index 20 to index 19.
Downstream indexers and API clients MUST handle potential multi-page drift:
- Client-Side Deduplication: Key processed entities by unique ID (
invoice_id: BytesN<32>orbid_id: BytesN<32>). - State Verification: Re-verify record status or version if operating across multi-block indexing jobs.
- Index-Based Ingestion: For full historical replication, stream contract events (
emit_invoice_created,emit_bid_placed) rather than polling paginated queries.
All contract query handlers utilize the internal quicklendx_contracts::pagination module:
Requested: (offset, limit)
│
▼
cap_query_limit(limit) ──► Clamped to min(limit, 50)
│
▼
calculate_safe_bounds(offset, clamped_limit, total_count)
│
▼
[start_idx, end_idx) ──► Guaranteed 0 <= start <= end <= total_count
-
cap_query_limit(limit)$\le 50$ . -
start = min(offset, total_count). -
end = min(start + capped_limit, total_count). -
has_more = (start + effective_limit < total_count).
Retrieves all invoices created by a specific business address, sorted by creation sequence.
pub fn get_business_invoices_paged(
env: Env,
business: Address,
offset: u32,
limit: u32,
) -> Result<(Vec<Invoice>, bool), QuickLendXError>Invocation Parameters:
business:G...(Stellar Address)offset:0(First page)limit:10
Response Output Structure:
[
[
{
"id": "0x1a2b3c...",
"owner": "G...BUSINESS",
"amount": 1000000000,
"status": "Verified",
"created_at": 1740000000
}
],
true // has_more flag indicating page 2 is available
]Retrieves all bids placed on a specific invoice, ordered deterministically by bid ranking (highest yield rate & tier first).
pub fn get_bids_for_invoice_paged(
env: Env,
invoice_id: BytesN<32>,
offset: u32,
limit: u32,
) -> Result<(Vec<Bid>, bool), QuickLendXError>Ordering Semantic: Bids are ordered by deterministic ranking rules (bid_ranking.rs). Pagination preserves this exact ranking order across pages.
Filter & rank search across active invoices.
pub fn search_invoices_paged(
env: Env,
query: InvoiceSearchQuery,
offset: u32,
limit: u32,
) -> Result<(Vec<Invoice>, bool), QuickLendXError>Ordering Semantic: Filtered search matches are sorted deterministically by match relevance rank, with tie-breaking by invoice ID string byte comparison.
The following Rust example demonstrates how to implement a safe paginated reader inside a custom Soroban client or contract:
use soroban_sdk::{contracttype, Address, Env, Vec};
use quicklendx_contracts::pagination::{cap_query_limit, calculate_safe_bounds, MAX_QUERY_LIMIT};
use quicklendx_contracts::errors::QuickLendXError;
/// Client helper: Safely iterates pages from a contract endpoint without panicking.
pub fn fetch_all_items_safely<T: Clone>(
env: &Env,
all_items: &Vec<T>,
requested_offset: u32,
requested_limit: u32,
) -> (Vec<T>, bool) {
let total_count = all_items.len();
// Step 1: Calculate safe bounds using protocol pagination logic
let (start, end) = calculate_safe_bounds(requested_offset, requested_limit, total_count);
let mut page_result = Vec::new(env);
if start < end {
for idx in start..end {
if let Some(item) = all_items.get(idx) {
page_result.push_back(item);
}
}
}
let effective_limit = end.saturating_sub(start);
let has_more = start.saturating_add(effective_limit) < total_count;
(page_result, has_more)
}- Default Page Limit:
50(hard capped). - Page Offset: 0-indexed (
offset = 0means start of collection). - End of Results Indicator:
has_more == falseor returned array length is0. - Concurrency Strategy: Treat multi-page fetches as eventually consistent; deduplicate items client-side by entity ID.