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 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. 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. --- 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 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"), } } } 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"); 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; diff --git a/quicklendx-contracts/src/profits.rs b/quicklendx-contracts/src/profits.rs index 4b16d577a..bf1f37f63 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,44 +548,38 @@ 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`. -/// Compute the expected return on a principal amount. -/// -/// # 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()); - 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 { +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; } - // 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); + + 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 +/// 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, duration_days); + amount.max(0).saturating_add(yield_amount) +} + /// A single ledger-delta entry for time-weighted average calculations. /// /// Each entry records the `balance` held for `duration_ledgers` ledgers.