From cbf1bd1c5f02561f56c58619ee39f62b0f6fd1b7 Mon Sep 17 00:00:00 2001 From: maxw06 <58760380+maxw06@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:00:54 +0800 Subject: [PATCH 1/9] ci: add per-package rustdoc workflow --- .github/workflows/rustdoc.yml | 82 +++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .github/workflows/rustdoc.yml diff --git a/.github/workflows/rustdoc.yml b/.github/workflows/rustdoc.yml new file mode 100644 index 000000000..306e36b08 --- /dev/null +++ b/.github/workflows/rustdoc.yml @@ -0,0 +1,82 @@ +name: Rustdoc + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + paths: + - ".github/workflows/rustdoc.yml" + - "Cargo.toml" + - "Cargo.lock" + - "quicklendx-contracts/**" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: rustdoc-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + +jobs: + cargo-doc: + name: cargo doc (${{ matrix.package }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + package: + - quicklendx-contracts + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + run: | + rustup show + rustup target add wasm32v1-none + + - name: Generate rustdoc + run: cargo doc -p "${{ matrix.package }}" --no-deps + + - name: Stage package docs + run: | + mkdir -p "public/${{ matrix.package }}" + cp -R target/doc/. "public/${{ matrix.package }}/" + + - name: Upload package rustdoc artifact + uses: actions/upload-artifact@v4 + with: + name: rustdoc-${{ matrix.package }} + path: public/${{ matrix.package }} + retention-days: 14 + + - name: Upload GitHub Pages artifact + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: actions/upload-pages-artifact@v3 + with: + path: public + + deploy-pages: + name: Publish rustdoc to GitHub Pages + needs: cargo-doc + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Configure GitHub Pages + uses: actions/configure-pages@v5 + + - name: Deploy rustdoc + id: deployment + uses: actions/deploy-pages@v4 From 367701e85b10c9250c67de8f679c4d30da41105c Mon Sep 17 00:00:00 2001 From: maxw06 <58760380+maxw06@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:01:43 +0800 Subject: [PATCH 2/9] ci: publish per-package rustdoc --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5ef418b6a..9bfe258dd 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ • docs/ : Project-wide design, implementation, and audit documentation. • docs/VESTING.md /docs/VESTING.md: Vesting model, edge cases, and admin protections. • docs/QUERIES.md /docs/QUERIES.md: Catalog of common read-only entrypoints with concrete invocation examples and return values — the quickest way to find the query you need. + • docs/RUSTDOC.md : Published contract API reference and local rustdoc generation instructions. • docs/contracts/platform-fee-ops.md /docs/contracts/platform-fee-ops.md: Admin operations playbook for managing fee rates, treasury rotation, and revenue splits. • docs/RUNBOOK_INCIDENT_RESPONSE.md : Operator playbook for unexpected contract behavior and incident-mode recovery. • docs/INVESTOR_TIER.md : How the investor risk score, tier, and investment limit are computed — math, thresholds, and worked examples. @@ -65,6 +66,7 @@ npm run dev - [Invoice Lifecycle](docs/INVOICE_LIFECYCLE.md): State diagram and entrypoint reference — Pending → Verified → Funded → Settled/Defaulted. - [Dispute Lifecycle](file:///c:/Users/HP/quicklendx-protocol/docs/DISPUTE.md): Who can open, who resolves, timeout behaviour, and fund implications. - [`docs/QUERIES.md`](docs/QUERIES.md): Catalog of common read-only entrypoints with concrete invocation examples and return values — the quickest way to find the query you need. +- [`docs/RUSTDOC.md`](docs/RUSTDOC.md): Published contract API reference and local rustdoc generation instructions. - `docs/INVESTOR_TIER.md`: How the investor risk score, tier, and investment limit are computed — math, thresholds, and worked examples. - `docs/KYC.md`: Business KYC vs investor KYC, what each gates. - `quicklendx-contracts/README.md`: Smart contract-specific documentation. From 77756f691687812ee77430494454ba6a18ec64fd Mon Sep 17 00:00:00 2001 From: maxw06 <58760380+maxw06@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:01:45 +0800 Subject: [PATCH 3/9] ci: publish per-package rustdoc --- docs/RUSTDOC.md | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/docs/RUSTDOC.md b/docs/RUSTDOC.md index 1ea1eec6b..6a223defa 100644 --- a/docs/RUSTDOC.md +++ b/docs/RUSTDOC.md @@ -6,21 +6,23 @@ ## Where to find the docs -The CI pipeline auto-publishes rustdoc for the **latest tagged release** to GitHub Pages every time a tag is pushed: +The CI pipeline auto-publishes rustdoc for the latest `main` branch build to GitHub Pages on every push to `main`: ``` -https://.github.io/quicklendx/quicklendx_contracts/ +https://.github.io/quicklendx-protocol/quicklendx-contracts/quicklendx_contracts/ ``` -Replace `` with the GitHub organisation that owns this repository (e.g. `quicklendx-labs`). The URL is stable across releases — it always points to the most recently tagged version. +Replace `` with the GitHub organisation that owns this repository (e.g. `quicklendx-labs`). The URL is stable across `main` updates and points to the most recently published `main` build. -> **Tip:** Bookmark the top-level index at the URL above. Each tagged release overwrites the previous one, so you are always reading the current stable API. +> **Tip:** Bookmark the top-level index at the URL above. Each successful `main` +> publish overwrites the previous one, so you are always reading the current +> branch API. --- ## What is covered -The published docs are generated from the `quicklendx-contracts/` crate with `cargo doc`. They include: +The published docs are generated from each Cargo package with `cargo doc --no-deps`. Today the workspace package list contains `quicklendx-contracts`; future packages should be added to the `package` matrix in `.github/workflows/rustdoc.yml`. The generated docs include: | Section | What you will find | |---|---| @@ -76,27 +78,30 @@ match result { ## Generating the docs locally -If you need to browse the docs offline, or you are reviewing a branch that has not been tagged yet: +If you need to browse the docs offline, or you are reviewing a branch that has not been published yet: ```bash -cd quicklendx-contracts - -# Generate HTML docs (output: target/doc/) -cargo doc --no-deps --target wasm32-unknown-unknown +# Generate HTML docs for the contract package (output: target/doc/) +cargo doc -p quicklendx-contracts --no-deps # Open in your default browser (macOS / Linux) open target/doc/quicklendx_contracts/index.html ``` -Omit `--target wasm32-unknown-unknown` if you only need to read the docs and do not need to verify the WASM build at the same time. +The workflow also runs this command for pull requests that touch the contract +package, but it publishes only for pushes to `main`. --- ## Staying up to date -The published URL is updated automatically on every tag push — no manual step is needed. To be notified of new releases, watch the repository on GitHub and select **Releases only**. +The published URL is updated automatically on every successful `main` push — +no manual step is needed. -If the published URL returns a 404, the most likely cause is that no tag has been pushed yet for the current development cycle. Generate the docs locally using the command above, or check the repository's Actions tab to confirm the publish workflow ran successfully. +If the published URL returns a 404, the most likely cause is that GitHub Pages +has not been enabled for the repository yet or the latest publish workflow has +not completed successfully. Generate the docs locally using the command above, +or check the repository's Actions tab for the `Rustdoc` workflow. --- From 75225522ea021628395f397892beacfc0a7c6202 Mon Sep 17 00:00:00 2001 From: maxw06 <58760380+maxw06@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:01:46 +0800 Subject: [PATCH 4/9] ci: publish per-package rustdoc --- quicklendx-contracts/src/idempotency.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quicklendx-contracts/src/idempotency.rs b/quicklendx-contracts/src/idempotency.rs index 06dc4089a..01fa332e3 100644 --- a/quicklendx-contracts/src/idempotency.rs +++ b/quicklendx-contracts/src/idempotency.rs @@ -1,4 +1,5 @@ -use crate::storage::{bump_persistent, extend_persistent_ttl}; +use crate::storage::extend_persistent_ttl; +use soroban_sdk::{symbol_short, Address, Bytes, BytesN, Env, Symbol}; /// Storage key for the idempotency map. pub const IDEMPOTENCY_MAP_KEY: Symbol = symbol_short!("idem_map"); From ab6710ff3e834003ba40c0ab04e13c24cf8c5cf8 Mon Sep 17 00:00:00 2001 From: maxw06 <58760380+maxw06@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:01:50 +0800 Subject: [PATCH 5/9] ci: publish per-package rustdoc --- quicklendx-contracts/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/quicklendx-contracts/src/lib.rs b/quicklendx-contracts/src/lib.rs index a6d543454..ada64a92e 100644 --- a/quicklendx-contracts/src/lib.rs +++ b/quicklendx-contracts/src/lib.rs @@ -64,6 +64,7 @@ use crate::idempotency::{idempotency_key, idempotency_exists, store_idempotency} pub mod bench; pub mod admin; pub mod analytics; +pub mod address_summary; pub mod audit; pub mod backpressure; pub mod backup; @@ -84,6 +85,7 @@ pub mod fees; pub mod freshness; pub mod governance; pub mod health; +pub mod idempotency; pub mod incident; pub mod init; pub mod invariants; From 4cbe90de8d80d8465106a64532947b4df6bd636a Mon Sep 17 00:00:00 2001 From: maxw06 <58760380+maxw06@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:01:51 +0800 Subject: [PATCH 6/9] ci: publish per-package rustdoc --- quicklendx-contracts/src/profits.rs | 81 ++++++++--------------------- 1 file changed, 23 insertions(+), 58 deletions(-) diff --git a/quicklendx-contracts/src/profits.rs b/quicklendx-contracts/src/profits.rs index 4b16d577a..f215af4a8 100644 --- a/quicklendx-contracts/src/profits.rs +++ b/quicklendx-contracts/src/profits.rs @@ -529,41 +529,12 @@ pub fn validate_calculation_inputs( // Yield Calculation // ============================================================================ -pub fn compute_yield(amount: i128, rate_bps: i128, duration_days: i128) -> i128 { - let safe_amount = amount.max(0); - let safe_rate = rate_bps.max(0); - let safe_days = duration_days.max(0); - - if safe_amount == 0 || safe_rate == 0 || safe_days == 0 { - return 0; - } - - let days_in_year = 365i128; - let denominator = BPS_DENOMINATOR.saturating_mul(days_in_year); - - safe_amount - .saturating_mul(safe_rate) - .saturating_mul(safe_days) - / denominator -} - /// Compute the simple interest yield on a principal amount. /// /// # Formula /// ```text /// yield = amount * rate_bps * duration_days / (BPS_DENOMINATOR * 365) /// ``` -pub fn compute_yield(amount: i128, rate_bps: u32, duration_days: u32) -> i128 { - let safe_amount = amount.max(0); - let safe_rate = rate_bps as i128; - let safe_days = duration_days as i128; - - let numerator = safe_amount - .saturating_mul(safe_rate) - .saturating_mul(safe_days); - let denominator = BPS_DENOMINATOR.saturating_mul(365); - numerator / denominator -} /// /// All arithmetic uses `saturating_mul` / integer division to stay within /// `i128` bounds without panicking and to preserve `#![no_std]` discipline. @@ -577,6 +548,29 @@ pub fn compute_yield(amount: i128, rate_bps: u32, duration_days: u32) -> i128 { /// For fixed `rate_bps` and `duration_days`, `yield` is non-decreasing in `amount`. /// For fixed `amount` and `duration_days`, `yield` is non-decreasing in `rate_bps`. /// For fixed `amount` and `rate_bps`, `yield` is non-decreasing in `duration_days`. +/// +/// # Returns +/// Simple interest yield (non-negative). +pub fn compute_yield(amount: i128, rate_bps: R, duration_days: D) -> i128 +where + R: Into, + D: Into, +{ + let safe_amount = amount.max(0); + let safe_rate = rate_bps.into().max(0); + let safe_days = duration_days.into().max(0); + + if safe_amount == 0 || safe_rate == 0 || safe_days == 0 { + return 0; + } + + let numerator = safe_amount + .saturating_mul(safe_rate) + .saturating_mul(safe_days); + let denominator = BPS_DENOMINATOR.saturating_mul(365); + numerator / denominator +} + /// Compute the expected return on a principal amount. /// /// # Returns @@ -586,35 +580,6 @@ pub fn compute_expected_return(amount: i128, rate_bps: u32, duration_days: u32) amount.max(0).saturating_add(yield_amount) } -/// Compute the simple interest yield on a principal amount. -/// -/// # Formula -/// ```text -/// yield = amount * rate_bps * duration_days / (BPS_DENOMINATOR * 365) -/// ``` -/// -/// All arithmetic uses `saturating_mul` / integer division to stay within -/// `i128` bounds without panicking and to preserve `#![no_std]` discipline. -/// -/// # Arguments -/// * `amount` — Principal (must be >= 0; negative input returns 0) -/// * `rate_bps` — Annual rate in basis points, e.g. 500 = 5 % -/// * `duration_days` — Holding period in days -/// -/// # Returns -/// Simple interest yield (non-negative). -pub fn compute_yield(amount: i128, rate_bps: i128, duration_days: i128) -> i128 { - if amount <= 0 || rate_bps <= 0 || duration_days <= 0 { - return 0; - } - // amount * rate_bps * duration_days / (10_000 * 365) - let numerator = amount - .saturating_mul(rate_bps) - .saturating_mul(duration_days); - let denominator: i128 = BPS_DENOMINATOR.saturating_mul(365); - numerator / denominator -} - /// A single ledger-delta entry for time-weighted average calculations. /// /// Each entry records the `balance` held for `duration_ledgers` ledgers. From 1d9ea704d058327d2b2309195fe7d750d14a6241 Mon Sep 17 00:00:00 2001 From: maxw06 <58760380+maxw06@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:10:03 +0800 Subject: [PATCH 7/9] fix: keep contract build green for rustdoc job --- quicklendx-contracts/src/errors.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quicklendx-contracts/src/errors.rs b/quicklendx-contracts/src/errors.rs index f8558911b..58f39dc03 100644 --- a/quicklendx-contracts/src/errors.rs +++ b/quicklendx-contracts/src/errors.rs @@ -283,7 +283,8 @@ impl From for Symbol { QuickLendXError::MaintenanceModeActive => symbol_short!("MAINT"), QuickLendXError::ArithmeticOverflow => symbol_short!("ARITH_OF"), QuickLendXError::DuplicateDefaultTransition => symbol_short!("DEF_DUP"), - QuickLendXError::BackupVersionUnsupported => symbol_short!("BKP_VER") + QuickLendXError::BackupVersionUnsupported => symbol_short!("BKP_VER"), + QuickLendXError::InvalidLedgerSequence => symbol_short!("LED_SEQ"), } } } From 0d32566bf5583f859e09f0149d6e53b75c735556 Mon Sep 17 00:00:00 2001 From: maxw06 <58760380+maxw06@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:10:05 +0800 Subject: [PATCH 8/9] fix: keep contract build green for rustdoc job --- quicklendx-contracts/src/profits.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quicklendx-contracts/src/profits.rs b/quicklendx-contracts/src/profits.rs index f215af4a8..bf1f37f63 100644 --- a/quicklendx-contracts/src/profits.rs +++ b/quicklendx-contracts/src/profits.rs @@ -576,7 +576,7 @@ where /// # Returns /// Total expected return (principal + yield) pub fn compute_expected_return(amount: i128, rate_bps: u32, duration_days: u32) -> i128 { - let yield_amount = compute_yield(amount, rate_bps.into(), duration_days.into()); + let yield_amount = compute_yield(amount, rate_bps, duration_days); amount.max(0).saturating_add(yield_amount) } From 817796fa78719c18d2a231c2c87668c5f580c58c Mon Sep 17 00:00:00 2001 From: maxw06 <58760380+maxw06@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:17:23 +0800 Subject: [PATCH 9/9] ci: use wasm32v1 target for wasm size check --- .../scripts/check-wasm-size.sh | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/quicklendx-contracts/scripts/check-wasm-size.sh b/quicklendx-contracts/scripts/check-wasm-size.sh index 74da301ca..8c7f6d185 100755 --- a/quicklendx-contracts/scripts/check-wasm-size.sh +++ b/quicklendx-contracts/scripts/check-wasm-size.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # WASM build and size budget regression checks for QuickLendX contracts. # -# Builds the contract for Soroban (wasm32v1-none or wasm32-unknown-unknown) +# Builds the contract for Soroban (wasm32v1-none) # and applies a three-tier size classification: # # OK : size <= WARN_BYTES (90 % of hard limit) – healthy @@ -26,6 +26,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONTRACTS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +TARGET_DIR="${CARGO_TARGET_DIR:-$(cd "$CONTRACTS_DIR/.." && pwd)/target}" cd "$CONTRACTS_DIR" # ── Budget constants ─────────────────────────────────────────────────────────── @@ -49,18 +50,18 @@ if [[ "$CHECK_ONLY" == false ]]; then stellar contract build --verbose WASM_PATH="target/wasm32v1-none/release/$WASM_NAME" else - echo "Stellar CLI not found; using cargo wasm32-unknown-unknown." + echo "Stellar CLI not found; using cargo wasm32v1-none." [[ -f "$HOME/.cargo/env" ]] && source "$HOME/.cargo/env" - rustup target add wasm32-unknown-unknown 2>/dev/null || true - cargo build --target wasm32-unknown-unknown --release --lib - WASM_PATH="target/wasm32-unknown-unknown/release/$WASM_NAME" + rustup target add wasm32v1-none 2>/dev/null || true + CARGO_TARGET_DIR="$TARGET_DIR" cargo build --target wasm32v1-none --release --lib + WASM_PATH="$TARGET_DIR/wasm32v1-none/release/$WASM_NAME" fi else # --check-only: probe both target directories for an existing artifact - if [[ -f "target/wasm32v1-none/release/$WASM_NAME" ]]; then - WASM_PATH="target/wasm32v1-none/release/$WASM_NAME" - elif [[ -f "target/wasm32-unknown-unknown/release/$WASM_NAME" ]]; then - WASM_PATH="target/wasm32-unknown-unknown/release/$WASM_NAME" + if [[ -f "$TARGET_DIR/wasm32v1-none/release/$WASM_NAME" ]]; then + WASM_PATH="$TARGET_DIR/wasm32v1-none/release/$WASM_NAME" + elif [[ -f "$TARGET_DIR/wasm32-unknown-unknown/release/$WASM_NAME" ]]; then + WASM_PATH="$TARGET_DIR/wasm32-unknown-unknown/release/$WASM_NAME" else echo "::error::--check-only specified but no WASM artifact found; run without --check-only first." exit 1