diff --git a/.github/workflows/contracts.yml b/.github/workflows/contracts.yml index 7e122c28..07794ccf 100644 --- a/.github/workflows/contracts.yml +++ b/.github/workflows/contracts.yml @@ -22,9 +22,13 @@ permissions: jobs: test: - name: cargo test -p quittance-contracts-example + name: Build & Test Contracts runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 30 + defaults: + run: + working-directory: contracts + steps: - name: Check out repository uses: actions/checkout@v4 @@ -36,6 +40,22 @@ jobs: with: workspaces: contracts - - name: cargo test -p quittance-contracts-example - working-directory: contracts - run: cargo test -p quittance-contracts-example + - name: Cache Cargo registry & per-crate targets + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + contracts/*/target + key: ${{ runner.os }}-cargo-${{ hashFiles('contracts/**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Check formatting + run: make fmt + + - name: Run clippy + run: make clippy + + - name: Run tests + run: make test diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index f8569f92..4f0a69d3 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -11,10 +11,17 @@ members = [ [workspace.package] edition = "2021" version = "0.1.0" +rust-version = "1.81" license = "MIT" publish = false +[workspace.dependencies] +soroban-sdk = "22" + [profile.release] opt-level = "z" lto = true +debug = false +strip = "symbols" +panic = "abort" codegen-units = 1 diff --git a/contracts/Makefile b/contracts/Makefile index eb84dcaa..1940b8e4 100644 --- a/contracts/Makefile +++ b/contracts/Makefile @@ -1,34 +1,60 @@ # Quittance contracts -- local build/test shortcuts. # -# Run `make test` from this directory to execute the workspace test -# suite. All targets are thin wrappers around `cargo` so they work -# identically in CI (.github/workflows/contracts.yml). +# Instead of relying on a workspace member list, every target iterates +# over all subdirectories that contain a Cargo.toml. This means new +# contract crates are picked up automatically and each crate uses its +# own dependency versions without fighting workspace constraints. +# +# Run `make test` from this directory to execute every contract test. + +CONTRACT_DIRS := $(shell find . -mindepth 2 -maxdepth 2 -name Cargo.toml -exec dirname {} \; | sort) -.PHONY: help build test check fmt clippy clean +.PHONY: help all build test check fmt clippy clean help: @echo "Available targets:" - @echo " make build - cargo build --workspace" - @echo " make test - cargo test --workspace" - @echo " make check - cargo check --workspace --all-targets" - @echo " make fmt - cargo fmt --all -- --check" - @echo " make clippy - cargo clippy --workspace -- -D warnings" - @echo " make clean - cargo clean" + @echo " make all - fmt → clippy → check → build → test" + @echo " make build - cargo build in every contract crate" + @echo " make test - cargo test in every contract crate" + @echo " make check - cargo check in every contract crate" + @echo " make fmt - cargo fmt in every contract crate" + @echo " make clippy - cargo clippy in every contract crate" + @echo " make clean - cargo clean in every contract crate" + +# ── Default: run all checks ──────────────────────────────────────── +all: fmt clippy check build test build: - cargo build --workspace + @status=0; for dir in $(CONTRACT_DIRS); do \ + echo "==> Building $$dir..."; \ + (cd "$$dir" && cargo build) || status=1; \ + done; exit $$status test: - cargo test --workspace + @status=0; for dir in $(CONTRACT_DIRS); do \ + echo "==> Testing $$dir..."; \ + (cd "$$dir" && cargo test) || status=1; \ + done; exit $$status check: - cargo check --workspace --all-targets + @status=0; for dir in $(CONTRACT_DIRS); do \ + echo "==> Checking $$dir..."; \ + (cd "$$dir" && cargo check --all-targets) || status=1; \ + done; exit $$status fmt: - cargo fmt --all -- --check + @status=0; for dir in $(CONTRACT_DIRS); do \ + echo "==> Format $$dir..."; \ + (cd "$$dir" && cargo fmt -- --check) || status=1; \ + done; exit $$status clippy: - cargo clippy --workspace -- -D warnings + @status=0; for dir in $(CONTRACT_DIRS); do \ + echo "==> Clippy $$dir..."; \ + (cd "$$dir" && cargo clippy --all-targets -- -D warnings) || status=1; \ + done; exit $$status clean: - cargo clean + @for dir in $(CONTRACT_DIRS); do \ + (cd "$$dir" && cargo clean); \ + done diff --git a/contracts/README.md b/contracts/README.md index 3a18882c..5fed9bfb 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -29,7 +29,27 @@ New contract crates should be added as additional `[workspace] members` in ```bash cd contracts -make test # equivalent to: cargo test --workspace + +# Run all checks (fmt, clippy, check, build, test) +make all + +# Run only unit tests +make test + +# Build all crates +make build + +# Check that all crates compile (faster than a full build) +make check + +# Check formatting +make fmt + +# Run clippy lints +make clippy + +# Clean build artifacts +make clean ``` Direct equivalents (no make): @@ -40,6 +60,7 @@ cargo build --workspace cargo check --workspace --all-targets cargo test --workspace cargo fmt --all -- --check +cargo clippy --workspace --all-targets -- -D warnings ``` To target a single crate: @@ -50,12 +71,9 @@ cargo test -p quittance-contracts-example ## Continuous integration -`.github/workflows/contracts.yml` runs `cargo test -p quittance-contracts-example` +`.github/workflows/contracts.yml` runs `cargo test --workspace` (via `make test`) on every push or pull request that changes files under `contracts/**` -(or the workflow file itself). The `example` crate is the only currently -testable workspace member; `init_once`, `max_amount`, and `min_amount` -are scoped per the maintainer's "remaining files are in the right lane -for #57 / #229" note. The workflow is path-filtered so PRs that only +(or the workflow file itself). The workflow is path-filtered so PRs that only touch `frontend/`, `backend/`, `db/`, or the deploy docs do not trigger it and cannot fail it. diff --git a/contracts/amount_scale/Cargo.toml b/contracts/amount_scale/Cargo.toml index 6e5ebbb1..2538004f 100644 --- a/contracts/amount_scale/Cargo.toml +++ b/contracts/amount_scale/Cargo.toml @@ -1,3 +1,5 @@ +[workspace] + [package] name = "quittance-amount-scale" version = "0.1.0" diff --git a/contracts/amount_scale/src/lib.rs b/contracts/amount_scale/src/lib.rs index 378438a8..c33896e7 100644 --- a/contracts/amount_scale/src/lib.rs +++ b/contracts/amount_scale/src/lib.rs @@ -196,7 +196,10 @@ mod tests { #[test] fn from_stroops_truncates_fractions() { // 12.3456789 XLM exactly: - assert_eq!(from_stroops(123_456_789 * STROOPS_PER_UNIT), Some(12_345_678)); + assert_eq!( + from_stroops(123_456_789 * STROOPS_PER_UNIT), + Some(12_345_678) + ); // Anything below 1 display unit floors to 0: assert_eq!(from_stroops(1), Some(0)); assert_eq!(from_stroops(STROOPS_PER_UNIT - 1), Some(0)); @@ -230,7 +233,10 @@ mod tests { #[test] fn remainder_stroops_sub_unit_residue() { assert_eq!(remainder_stroops(STROOPS_PER_UNIT + 500_000), Some(500_000)); - assert_eq!(remainder_stroops(123_456_789 * STROOPS_PER_UNIT + 1), Some(1)); + assert_eq!( + remainder_stroops(123_456_789 * STROOPS_PER_UNIT + 1), + Some(1) + ); } #[test] diff --git a/contracts/asset_allowlist/Cargo.toml b/contracts/asset_allowlist/Cargo.toml index 3c390133..eceebdfe 100644 --- a/contracts/asset_allowlist/Cargo.toml +++ b/contracts/asset_allowlist/Cargo.toml @@ -1,3 +1,5 @@ +[workspace] + [package] name = "asset-allowlist" version = "0.1.0" diff --git a/contracts/auth_one_address/Cargo.toml b/contracts/auth_one_address/Cargo.toml index e0a5e86d..6d6b842b 100644 --- a/contracts/auth_one_address/Cargo.toml +++ b/contracts/auth_one_address/Cargo.toml @@ -1,3 +1,5 @@ +[workspace] + [package] name = "auth-one-address" version = "0.1.0" diff --git a/contracts/destination_guard/src/lib.rs b/contracts/destination_guard/src/lib.rs index 29cedbf6..01d4b07c 100644 --- a/contracts/destination_guard/src/lib.rs +++ b/contracts/destination_guard/src/lib.rs @@ -245,11 +245,17 @@ mod tests { // pick a prefix that is in the alphabet but reserved for a // different StrKey family — `S` for secret seed, `M` for muxed. bad.replace_range(0..1, "S"); - assert_eq!(check_destination(&bad), Err(DestinationError::InvalidPrefix)); + assert_eq!( + check_destination(&bad), + Err(DestinationError::InvalidPrefix) + ); let mut bad = ZERO_STRKEY.to_string(); bad.replace_range(0..1, "M"); - assert_eq!(check_destination(&bad), Err(DestinationError::InvalidPrefix)); + assert_eq!( + check_destination(&bad), + Err(DestinationError::InvalidPrefix) + ); } #[test] @@ -260,7 +266,10 @@ mod tests { // `InvalidPrefix`, not `InvalidCharacter`. let mut bad = ZERO_STRKEY.to_string(); bad.replace_range(0..1, "g"); - assert_eq!(check_destination(&bad), Err(DestinationError::InvalidPrefix)); + assert_eq!( + check_destination(&bad), + Err(DestinationError::InvalidPrefix) + ); } #[test] @@ -269,7 +278,10 @@ mod tests { // set, so the prefix check fires first. let mut bad = ZERO_STRKEY.to_string(); bad.replace_range(0..1, "c"); - assert_eq!(check_destination(&bad), Err(DestinationError::InvalidPrefix)); + assert_eq!( + check_destination(&bad), + Err(DestinationError::InvalidPrefix) + ); } // ── character alphabet ─────────────────────────────────────────── @@ -354,7 +366,11 @@ mod tests { assert!(!is_stellar_base32(c)); } for c in b'a'..=b'z' { - assert!(!is_stellar_base32(c), "lowercase {} should be rejected", c as char); + assert!( + !is_stellar_base32(c), + "lowercase {} should be rejected", + c as char + ); } for &c in &[b'-', b'_', b' ', b'\t', b'\n', b'!', b'?', b'.', b','] { assert!(!is_stellar_base32(c), "{:?} should be rejected", c as char); @@ -421,19 +437,17 @@ mod tests { // `S` is reserved for secret-seed StrKeys. Derived // from `ZERO_STRKEY` so future fixture edits can't drift // the literal. - (format!("S{}", &ZERO_STRKEY[1..]), - Err(DestinationError::InvalidPrefix)), + ( + format!("S{}", &ZERO_STRKEY[1..]), + Err(DestinationError::InvalidPrefix), + ), // Digit `0` in a non-prefix position trips the // alphabet scan. (bad_char, Err(DestinationError::InvalidCharacter)), ]; for (input, expected) in &samples { let actual = check_destination(input); - assert_eq!( - actual, *expected, - "check_destination({:?}) mismatch", - input - ); + assert_eq!(actual, *expected, "check_destination({:?}) mismatch", input); } } @@ -448,7 +462,10 @@ mod tests { // WrongLength: takes precedence over InvalidPrefix (a non-56 // string cannot have its first character meaningfully tested). let short_non_g = "G"; // 1 char, valid base32, valid prefix candidate. - assert_eq!(check_destination(short_non_g), Err(DestinationError::WrongLength)); + assert_eq!( + check_destination(short_non_g), + Err(DestinationError::WrongLength) + ); // InvalidPrefix: takes precedence over InvalidCharacter when // both apply (we never get to alphabet scan). diff --git a/contracts/error_codes/src/lib.rs b/contracts/error_codes/src/lib.rs index 97b47e49..f0be34ac 100644 --- a/contracts/error_codes/src/lib.rs +++ b/contracts/error_codes/src/lib.rs @@ -31,7 +31,6 @@ use soroban_sdk::contracterror; #[repr(u32)] pub enum ErrorCode { // ── General (1–99) ────────────────────────────────────────── - /// An unexpected internal error occurred. InternalError = 1, /// The caller does not have permission for this operation. @@ -44,7 +43,6 @@ pub enum ErrorCode { AlreadyExists = 5, // ── Invoice (100–199) ─────────────────────────────────────── - /// The invoice id does not match any known invoice. InvoiceNotFound = 100, /// The invoice has already been paid. @@ -55,7 +53,6 @@ pub enum ErrorCode { InvoiceCancelled = 103, // ── Payment verification (200–299) ────────────────────────── - /// The transferred amount does not match the invoice amount. PaymentAmountMismatch = 200, /// The payment destination does not match the invoice seller. @@ -68,14 +65,12 @@ pub enum ErrorCode { PaymentNotConfirmed = 204, // ── Asset (300–399) ───────────────────────────────────────── - /// The asset is not in the allowlist and is not accepted. AssetNotSupported = 300, /// The seller has not established a trustline for this asset. AssetNotTrusted = 301, // ── Amount / scale (400–499) ──────────────────────────────── - /// The provided amount is zero or negative. InvalidAmount = 400, /// The amount exceeds the representable range. @@ -84,14 +79,12 @@ pub enum ErrorCode { ScaleMismatch = 402, // ── Binding / permission (500–599) ────────────────────────── - /// The seller address does not match the stored binding. SellerMismatch = 500, /// The binding contract has not been initialised. BindingNotInitialized = 501, // ── Initialisation (600–699) ──────────────────────────────── - /// The contract has not been initialised yet. NotInitialized = 600, /// The contract has already been initialised. @@ -123,9 +116,7 @@ impl ErrorCode { ErrorCode::PaymentDestinationMismatch => { "The payment destination does not match the invoice seller." } - ErrorCode::PaymentMemoMismatch => { - "The transaction memo does not match the invoice id." - } + ErrorCode::PaymentMemoMismatch => "The transaction memo does not match the invoice id.", ErrorCode::PaymentAssetMismatch => { "The payment asset does not match the invoice asset." } @@ -144,12 +135,8 @@ impl ErrorCode { ErrorCode::AmountOverflow => "The amount exceeds the representable range.", ErrorCode::ScaleMismatch => "The asset scale for the two operands does not match.", - ErrorCode::SellerMismatch => { - "The seller address does not match the stored binding." - } - ErrorCode::BindingNotInitialized => { - "The binding contract has not been initialised." - } + ErrorCode::SellerMismatch => "The seller address does not match the stored binding.", + ErrorCode::BindingNotInitialized => "The binding contract has not been initialised.", ErrorCode::NotInitialized => "The contract has not been initialised yet.", ErrorCode::AlreadyInitialized => "The contract has already been initialised.", diff --git a/contracts/error_codes/src/tests.rs b/contracts/error_codes/src/tests.rs index 5902df26..e49fd1da 100644 --- a/contracts/error_codes/src/tests.rs +++ b/contracts/error_codes/src/tests.rs @@ -142,12 +142,18 @@ fn message_matches_expected_value() { use ErrorCode::*; // General - assert_eq!(InternalError.message(), "An unexpected internal error occurred."); + assert_eq!( + InternalError.message(), + "An unexpected internal error occurred." + ); assert_eq!( Unauthorized.message(), "The caller does not have permission for this operation." ); - assert_eq!(InvalidArgument.message(), "One or more arguments are invalid."); + assert_eq!( + InvalidArgument.message(), + "One or more arguments are invalid." + ); assert_eq!(NotFound.message(), "The requested resource was not found."); assert_eq!( AlreadyExists.message(), @@ -159,9 +165,18 @@ fn message_matches_expected_value() { InvoiceNotFound.message(), "The invoice id does not match any known invoice." ); - assert_eq!(InvoiceAlreadyPaid.message(), "The invoice has already been paid."); - assert_eq!(InvoiceExpired.message(), "The invoice settlement window has expired."); - assert_eq!(InvoiceCancelled.message(), "The invoice was cancelled before settlement."); + assert_eq!( + InvoiceAlreadyPaid.message(), + "The invoice has already been paid." + ); + assert_eq!( + InvoiceExpired.message(), + "The invoice settlement window has expired." + ); + assert_eq!( + InvoiceCancelled.message(), + "The invoice was cancelled before settlement." + ); // Payment assert_eq!( @@ -196,9 +211,18 @@ fn message_matches_expected_value() { ); // Amount / scale - assert_eq!(InvalidAmount.message(), "The provided amount is zero or negative."); - assert_eq!(AmountOverflow.message(), "The amount exceeds the representable range."); - assert_eq!(ScaleMismatch.message(), "The asset scale for the two operands does not match."); + assert_eq!( + InvalidAmount.message(), + "The provided amount is zero or negative." + ); + assert_eq!( + AmountOverflow.message(), + "The amount exceeds the representable range." + ); + assert_eq!( + ScaleMismatch.message(), + "The asset scale for the two operands does not match." + ); // Binding / permission assert_eq!( @@ -211,8 +235,14 @@ fn message_matches_expected_value() { ); // Initialisation - assert_eq!(NotInitialized.message(), "The contract has not been initialised yet."); - assert_eq!(AlreadyInitialized.message(), "The contract has already been initialised."); + assert_eq!( + NotInitialized.message(), + "The contract has not been initialised yet." + ); + assert_eq!( + AlreadyInitialized.message(), + "The contract has already been initialised." + ); } #[test] @@ -220,10 +250,7 @@ fn error_code_derives_copy_clone_debug_eq_partial_ord_ord() { let a = ErrorCode::NotFound; let b = a; let _c = b.clone(); - assert_eq!( - alloc::format!("{:?}", a), - "NotFound" - ); + assert_eq!(alloc::format!("{:?}", a), "NotFound"); assert_eq!(a, a); assert!(a > ErrorCode::InternalError); } @@ -233,10 +260,6 @@ fn from_u32_roundtrip() { for code in all_variants() { let n = code.clone() as u32; let reconstructed: ErrorCode = unsafe { core::mem::transmute(n) }; - assert_eq!( - code, reconstructed, - "roundtrip failed for code {}", - n - ); + assert_eq!(code, reconstructed, "roundtrip failed for code {}", n); } } diff --git a/contracts/event_invoice_created/src/lib.rs b/contracts/event_invoice_created/src/lib.rs index 9456738c..ba3267eb 100644 --- a/contracts/event_invoice_created/src/lib.rs +++ b/contracts/event_invoice_created/src/lib.rs @@ -88,12 +88,7 @@ pub fn topic(env: &Env) -> Symbol { /// /// Off-chain consumers can subscribe to a specific invoice id, seller, /// or payer by matching the corresponding topic position. -pub fn topics( - env: &Env, - invoice_id: &String, - seller: &Address, - payer: &Address, -) -> Vec { +pub fn topics(env: &Env, invoice_id: &String, seller: &Address, payer: &Address) -> Vec { Vec::from_array( env, [ diff --git a/contracts/event_invoice_created/src/test.rs b/contracts/event_invoice_created/src/test.rs index 6e038190..2174ec19 100644 --- a/contracts/event_invoice_created/src/test.rs +++ b/contracts/event_invoice_created/src/test.rs @@ -179,12 +179,7 @@ fn data_handles_zero_and_large_values() { let decoded_zero: (i128, Address, u64) = zero.into_val(&env); assert_eq!(decoded_zero, (0_i128, asset.clone(), 0_u64)); - let big: Val = data( - &env, - i128::MAX / 2, - &asset, - u64::MAX, - ); + let big: Val = data(&env, i128::MAX / 2, &asset, u64::MAX); let decoded_big: (i128, Address, u64) = big.into_val(&env); assert_eq!(decoded_big, (i128::MAX / 2, asset, u64::MAX)); } diff --git a/contracts/event_invoice_paid/Cargo.toml b/contracts/event_invoice_paid/Cargo.toml index 381e8d82..efa7ddd8 100644 --- a/contracts/event_invoice_paid/Cargo.toml +++ b/contracts/event_invoice_paid/Cargo.toml @@ -1,3 +1,5 @@ +[workspace] + [package] name = "event_invoice_paid" version = "0.1.0" diff --git a/contracts/event_invoice_paid/src/lib.rs b/contracts/event_invoice_paid/src/lib.rs index 6348e619..743084f7 100644 --- a/contracts/event_invoice_paid/src/lib.rs +++ b/contracts/event_invoice_paid/src/lib.rs @@ -85,12 +85,7 @@ pub fn topic(env: &Env) -> Symbol { /// /// Off-chain consumers can subscribe to a specific invoice id, payer, /// or seller by matching the corresponding topic position. -pub fn topics( - env: &Env, - invoice_id: &String, - payer: &Address, - seller: &Address, -) -> Vec { +pub fn topics(env: &Env, invoice_id: &String, payer: &Address, seller: &Address) -> Vec { Vec::from_array( env, [ diff --git a/contracts/event_invoice_paid/src/test.rs b/contracts/event_invoice_paid/src/test.rs index 9135af79..6549cc24 100644 --- a/contracts/event_invoice_paid/src/test.rs +++ b/contracts/event_invoice_paid/src/test.rs @@ -198,12 +198,7 @@ fn data_handles_zero_and_large_values() { let decoded_zero: (i128, Address, u64) = zero.into_val(&env); assert_eq!(decoded_zero, (0_i128, asset.clone(), 0_u64)); - let big: Val = data( - &env, - i128::MAX / 2, - &asset, - u64::MAX, - ); + let big: Val = data(&env, i128::MAX / 2, &asset, u64::MAX); let decoded_big: (i128, Address, u64) = big.into_val(&env); assert_eq!(decoded_big, (i128::MAX / 2, asset, u64::MAX)); } diff --git a/contracts/expiry_check/src/lib.rs b/contracts/expiry_check/src/lib.rs index 7ae51efc..c31aa9a0 100644 --- a/contracts/expiry_check/src/lib.rs +++ b/contracts/expiry_check/src/lib.rs @@ -381,10 +381,7 @@ mod tests { #[test] fn require_active_err_at_boundary_inclusive() { // now == expiry must be rejected (inclusive boundary). - assert_eq!( - require_active(NOW, EQUAL), - Err(ExpiryError::AlreadyExpired), - ); + assert_eq!(require_active(NOW, EQUAL), Err(ExpiryError::AlreadyExpired),); assert_eq!(require_active(0, 0), Err(ExpiryError::AlreadyExpired)); assert_eq!( require_active(u64::MAX, u64::MAX), @@ -398,11 +395,11 @@ mod tests { require_active(NOW, BEFORE), Err(ExpiryError::AlreadyExpired), ); + assert_eq!(require_active(AFTER, NOW), Err(ExpiryError::AlreadyExpired),); assert_eq!( - require_active(AFTER, NOW), - Err(ExpiryError::AlreadyExpired), + require_active(u64::MAX, 0), + Err(ExpiryError::AlreadyExpired) ); - assert_eq!(require_active(u64::MAX, 0), Err(ExpiryError::AlreadyExpired)); } #[test] @@ -591,7 +588,9 @@ mod tests { // are exposed under the documented names. We deliberately // do NOT call them here — `env.ledger()` panics on a bare // Env::default() without the testutils feature. - let _fns: (fn(&Env, u64) -> bool, fn(&Env, u64) -> Result<(), ExpiryError>) = - (is_expired_env, require_active_env); + let _fns: ( + fn(&Env, u64) -> bool, + fn(&Env, u64) -> Result<(), ExpiryError>, + ) = (is_expired_env, require_active_env); } } diff --git a/contracts/fee_bps_clamp/Cargo.toml b/contracts/fee_bps_clamp/Cargo.toml index 712dab8e..bbd34392 100644 --- a/contracts/fee_bps_clamp/Cargo.toml +++ b/contracts/fee_bps_clamp/Cargo.toml @@ -1,3 +1,5 @@ +[workspace] + [package] name = "quittance-fee-bps-clamp" version = "0.1.0" diff --git a/contracts/invoice_claim/src/lib.rs b/contracts/invoice_claim/src/lib.rs index 102b6907..5488c057 100644 --- a/contracts/invoice_claim/src/lib.rs +++ b/contracts/invoice_claim/src/lib.rs @@ -187,8 +187,9 @@ mod tests { #[test] fn deterministic_across_calls() { let (seller, amount, memo, expiry) = sample(); - let hashes: Vec = - (0..10).map(|_| compute(seller, amount, memo, expiry)).collect(); + let hashes: Vec = (0..10) + .map(|_| compute(seller, amount, memo, expiry)) + .collect(); for h in &hashes[1..] { assert_eq!(hashes[0], *h); } diff --git a/contracts/memo_collision/src/lib.rs b/contracts/memo_collision/src/lib.rs index 0b0aa269..849c30a0 100644 --- a/contracts/memo_collision/src/lib.rs +++ b/contracts/memo_collision/src/lib.rs @@ -305,10 +305,7 @@ mod tests { #[test] fn default_is_equivalent_to_new() { - assert_eq!( - MemoCollisionGuard::default(), - MemoCollisionGuard::new() - ); + assert_eq!(MemoCollisionGuard::default(), MemoCollisionGuard::new()); } #[test] diff --git a/contracts/memo_validator/src/lib.rs b/contracts/memo_validator/src/lib.rs index eded6089..c57f0ddb 100644 --- a/contracts/memo_validator/src/lib.rs +++ b/contracts/memo_validator/src/lib.rs @@ -178,10 +178,7 @@ mod test { // Test a representative sample of allowed characters. let chars = [" ", "A", "z", "0", "9", "!", "~"]; for ch in chars { - assert!( - validate(&env, &contract_id, ch), - "expected valid: '{ch}'" - ); + assert!(validate(&env, &contract_id, ch), "expected valid: '{ch}'"); } } diff --git a/contracts/meta_info/Cargo.toml b/contracts/meta_info/Cargo.toml index 10dd25e1..a0f238cc 100644 --- a/contracts/meta_info/Cargo.toml +++ b/contracts/meta_info/Cargo.toml @@ -1,3 +1,5 @@ +[workspace] + [package] name = "meta-info" version = "0.1.0" diff --git a/contracts/meta_info/src/lib.rs b/contracts/meta_info/src/lib.rs index 3c72f57c..9dfaf59e 100644 --- a/contracts/meta_info/src/lib.rs +++ b/contracts/meta_info/src/lib.rs @@ -108,13 +108,7 @@ mod test { let v = client.version(); assert_ne!(n, v, "name and version must be different strings"); - assert!( - !n.is_empty(), - "name must not be empty" - ); - assert!( - !v.is_empty(), - "version must not be empty" - ); + assert!(!n.is_empty(), "name must not be empty"); + assert!(!v.is_empty(), "version must not be empty"); } } diff --git a/contracts/network_passphrase/src/lib.rs b/contracts/network_passphrase/src/lib.rs index 5a4b4f16..a74d486c 100644 --- a/contracts/network_passphrase/src/lib.rs +++ b/contracts/network_passphrase/src/lib.rs @@ -66,7 +66,6 @@ pub enum Network { Public, } - /// Returns `true` when `passphrase` is exactly the Stellar Testnet passphrase. pub fn is_testnet_passphrase(passphrase: &str) -> bool { passphrase == TESTNET_PASSPHRASE @@ -252,4 +251,3 @@ mod tests { assert!(!is_public_passphrase("not-a-passphrase")); } } - diff --git a/contracts/paid_status/src/lib.rs b/contracts/paid_status/src/lib.rs index b7365eae..9a05ef53 100644 --- a/contracts/paid_status/src/lib.rs +++ b/contracts/paid_status/src/lib.rs @@ -335,7 +335,10 @@ mod tests { PaidStatus::Pending.message(), "Invoice has been issued but no payment has been confirmed." ); - assert_eq!(PaidStatus::Paid.message(), "Payment confirmed on the ledger."); + assert_eq!( + PaidStatus::Paid.message(), + "Payment confirmed on the ledger." + ); assert_eq!( PaidStatus::Expired.message(), "Settlement window elapsed without a confirmed payment." diff --git a/contracts/proof_meta/Cargo.toml b/contracts/proof_meta/Cargo.toml index 46e0c31b..000fc420 100644 --- a/contracts/proof_meta/Cargo.toml +++ b/contracts/proof_meta/Cargo.toml @@ -1,3 +1,5 @@ +[workspace] + [package] name = "quittance-proof-meta" version = "0.1.0" diff --git a/contracts/quittance_receipt_hash/Cargo.toml b/contracts/quittance_receipt_hash/Cargo.toml index c41c992b..a29a072f 100644 --- a/contracts/quittance_receipt_hash/Cargo.toml +++ b/contracts/quittance_receipt_hash/Cargo.toml @@ -1,3 +1,5 @@ +[workspace] + [package] name = "quittance-receipt-hash" version = "0.1.0" diff --git a/contracts/quittance_receipt_hash/examples/canonical_sample.rs b/contracts/quittance_receipt_hash/examples/canonical_sample.rs index 0817774d..80dfeb7b 100644 --- a/contracts/quittance_receipt_hash/examples/canonical_sample.rs +++ b/contracts/quittance_receipt_hash/examples/canonical_sample.rs @@ -7,9 +7,7 @@ //! `pinned_canonical_sample_hash` if you intentionally change the //! encoding. -use quittance_receipt_hash::{ - compute_hex, Asset, DomainSeparator, ReceiptFieldsBuilder, -}; +use quittance_receipt_hash::{compute_hex, Asset, DomainSeparator, ReceiptFieldsBuilder}; fn main() { let fields = ReceiptFieldsBuilder::default() diff --git a/contracts/quittance_receipt_hash/src/domain.rs b/contracts/quittance_receipt_hash/src/domain.rs index 80c97c53..42eca68b 100644 --- a/contracts/quittance_receipt_hash/src/domain.rs +++ b/contracts/quittance_receipt_hash/src/domain.rs @@ -23,7 +23,9 @@ impl DomainSeparator { /// Build a domain separator from an arbitrary label. pub fn new(label: &str) -> Self { - Self { bytes: label.as_bytes().to_vec() } + Self { + bytes: label.as_bytes().to_vec(), + } } /// Return the default Quittance receipt hash domain separator. diff --git a/contracts/quittance_receipt_hash/src/encoding.rs b/contracts/quittance_receipt_hash/src/encoding.rs index 543e843a..3c7b3971 100644 --- a/contracts/quittance_receipt_hash/src/encoding.rs +++ b/contracts/quittance_receipt_hash/src/encoding.rs @@ -42,10 +42,7 @@ pub(crate) const VERSION: u32 = 1; /// field("invoice_id", encode_optional(invoice_id)) /// ) /// ``` -pub(crate) fn build_preimage( - domain: &DomainSeparator, - fields: &ReceiptFields, -) -> Vec { +pub(crate) fn build_preimage(domain: &DomainSeparator, fields: &ReceiptFields) -> Vec { // 512 bytes is enough for the typical Quittance receipt; worst case we // reallocate once. The preimage is bounded because every field value is // length-prefixed by the writer and limited by Stellar / UUID sizes. @@ -142,7 +139,10 @@ mod tests { #[test] fn optional_empty_some_differs_from_none() { - assert_ne!(encode_optional(&None), encode_optional(&Some(String::new()))); + assert_ne!( + encode_optional(&None), + encode_optional(&Some(String::new())) + ); } #[test] diff --git a/contracts/quittance_receipt_hash/src/lib.rs b/contracts/quittance_receipt_hash/src/lib.rs index ef3faf1c..f78eb578 100644 --- a/contracts/quittance_receipt_hash/src/lib.rs +++ b/contracts/quittance_receipt_hash/src/lib.rs @@ -16,5 +16,5 @@ mod hash; mod receipt; pub use crate::domain::DomainSeparator; -pub use crate::hash::{ReceiptHash, compute, compute_hex}; +pub use crate::hash::{compute, compute_hex, ReceiptHash}; pub use crate::receipt::{Asset, BuildError, ReceiptFields, ReceiptFieldsBuilder}; diff --git a/contracts/quittance_receipt_hash/src/receipt.rs b/contracts/quittance_receipt_hash/src/receipt.rs index fe607d56..18cb60f5 100644 --- a/contracts/quittance_receipt_hash/src/receipt.rs +++ b/contracts/quittance_receipt_hash/src/receipt.rs @@ -190,9 +190,7 @@ impl ReceiptFieldsBuilder { /// field has not been set. pub fn build(self) -> Result { Ok(ReceiptFields { - network_passphrase: self - .network_passphrase - .ok_or(BuildError::MissingNetwork)?, + network_passphrase: self.network_passphrase.ok_or(BuildError::MissingNetwork)?, tx_hash: self.tx_hash.ok_or(BuildError::MissingTxHash)?, ledger: self.ledger.ok_or(BuildError::MissingLedger)?, seller: self.seller.ok_or(BuildError::MissingSeller)?, diff --git a/contracts/quittance_receipt_hash/tests/edge_cases.rs b/contracts/quittance_receipt_hash/tests/edge_cases.rs index df7cce81..5755b008 100644 --- a/contracts/quittance_receipt_hash/tests/edge_cases.rs +++ b/contracts/quittance_receipt_hash/tests/edge_cases.rs @@ -23,10 +23,7 @@ fn native_xlm_differs_from_xlm_with_issuer() { // them. They MUST hash differently because on chain they are different // assets even if both speak "XLM". let domain = DomainSeparator::quittance_v1(); - let native = compute( - &domain, - &basic().asset(Asset::native()).build().unwrap(), - ); + let native = compute(&domain, &basic().asset(Asset::native()).build().unwrap()); let pseudo = compute( &domain, &basic() @@ -47,16 +44,8 @@ fn length_prefix_blocks_concatenation_collision() { // "seller" (whose values look the same length) never hash to the same // preimage even if the names were swapped. let domain = DomainSeparator::quittance_v1(); - let f_a = basic() - .memo("ABC") - .invoice_id("DEF") - .build() - .unwrap(); - let f_b = basic() - .memo("DEF") - .invoice_id("ABC") - .build() - .unwrap(); + let f_a = basic().memo("ABC").invoice_id("DEF").build().unwrap(); + let f_b = basic().memo("DEF").invoice_id("ABC").build().unwrap(); assert_ne!(compute(&domain, &f_a), compute(&domain, &f_b)); } @@ -105,10 +94,7 @@ fn testnet_and_public_passphrase_differ() { fn compute_hex_matches_compute_to_hex() { let domain = DomainSeparator::quittance_v1(); let f = basic().memo("hello").build().unwrap(); - assert_eq!( - compute_hex(&domain, &f), - compute(&domain, &f).to_hex() - ); + assert_eq!(compute_hex(&domain, &f), compute(&domain, &f).to_hex()); } #[test] diff --git a/contracts/seller_bind/Cargo.toml b/contracts/seller_bind/Cargo.toml index 6c184d34..95bbd6f2 100644 --- a/contracts/seller_bind/Cargo.toml +++ b/contracts/seller_bind/Cargo.toml @@ -1,3 +1,5 @@ +[workspace] + [package] name = "seller-bind" version = "0.1.0" diff --git a/contracts/status_transitions/src/lib.rs b/contracts/status_transitions/src/lib.rs index 9903ad6d..e38a1083 100644 --- a/contracts/status_transitions/src/lib.rs +++ b/contracts/status_transitions/src/lib.rs @@ -76,7 +76,10 @@ impl InvoiceStatus { /// ``` #[must_use] pub fn is_terminal(self) -> bool { - matches!(self, InvoiceStatus::Paid | InvoiceStatus::Expired | InvoiceStatus::Cancelled) + matches!( + self, + InvoiceStatus::Paid | InvoiceStatus::Expired | InvoiceStatus::Cancelled + ) } } @@ -105,9 +108,18 @@ pub struct Transition { /// transition rules). Removing a row is also a breaking change (it /// tightens them). Callers should pin the major version. pub const ALLOWED_TRANSITIONS: &[Transition] = &[ - Transition { from: InvoiceStatus::Pending, to: InvoiceStatus::Paid }, - Transition { from: InvoiceStatus::Pending, to: InvoiceStatus::Expired }, - Transition { from: InvoiceStatus::Pending, to: InvoiceStatus::Cancelled }, + Transition { + from: InvoiceStatus::Pending, + to: InvoiceStatus::Paid, + }, + Transition { + from: InvoiceStatus::Pending, + to: InvoiceStatus::Expired, + }, + Transition { + from: InvoiceStatus::Pending, + to: InvoiceStatus::Cancelled, + }, ]; /// Returns `true` if the transition `from → to` is listed in @@ -173,8 +185,8 @@ pub fn allowed_targets(from: InvoiceStatus) -> &'static [InvoiceStatus] { InvoiceStatus::Expired, InvoiceStatus::Cancelled, ], - InvoiceStatus::Paid => &[], - InvoiceStatus::Expired => &[], + InvoiceStatus::Paid => &[], + InvoiceStatus::Expired => &[], InvoiceStatus::Cancelled => &[], } } @@ -206,9 +218,8 @@ const fn build_full_matrix() -> [(InvoiceStatus, InvoiceStatus, bool); 16] { InvoiceStatus::Cancelled, ]; - let mut matrix: [(InvoiceStatus, InvoiceStatus, bool); 16] = [ - (InvoiceStatus::Pending, InvoiceStatus::Pending, false); 16 - ]; + let mut matrix: [(InvoiceStatus, InvoiceStatus, bool); 16] = + [(InvoiceStatus::Pending, InvoiceStatus::Pending, false); 16]; let mut i: usize = 0; while i < statuses.len() { @@ -267,7 +278,8 @@ mod tests { assert!( !(a.0 == b.0 && a.1 == b.1), "duplicate pair in FULL_TRANSITION_MATRIX at indices {i} and {j}: ({:?}, {:?})", - a.0, a.1, + a.0, + a.1, ); } } @@ -323,7 +335,10 @@ mod tests { #[test] fn is_allowed_denies_cancelled_to_cancelled() { - assert!(!is_allowed(InvoiceStatus::Cancelled, InvoiceStatus::Cancelled)); + assert!(!is_allowed( + InvoiceStatus::Cancelled, + InvoiceStatus::Cancelled + )); } // ── is_allowed: denied exits from terminal states ────────────────── @@ -355,12 +370,18 @@ mod tests { #[test] fn is_allowed_denies_expired_to_cancelled() { - assert!(!is_allowed(InvoiceStatus::Expired, InvoiceStatus::Cancelled)); + assert!(!is_allowed( + InvoiceStatus::Expired, + InvoiceStatus::Cancelled + )); } #[test] fn is_allowed_denies_cancelled_to_pending() { - assert!(!is_allowed(InvoiceStatus::Cancelled, InvoiceStatus::Pending)); + assert!(!is_allowed( + InvoiceStatus::Cancelled, + InvoiceStatus::Pending + )); } #[test] @@ -370,7 +391,10 @@ mod tests { #[test] fn is_allowed_denies_cancelled_to_expired() { - assert!(!is_allowed(InvoiceStatus::Cancelled, InvoiceStatus::Expired)); + assert!(!is_allowed( + InvoiceStatus::Cancelled, + InvoiceStatus::Expired + )); } // ── allowed_targets ──────────────────────────────────────────────── @@ -483,7 +507,10 @@ mod tests { #[test] fn debug_format_is_human_readable() { let s = format!("{:?}", InvoiceStatus::Pending); - assert!(s.contains("Pending"), "Debug output should contain variant name, got: {s}"); + assert!( + s.contains("Pending"), + "Debug output should contain variant name, got: {s}" + ); } #[test] @@ -535,9 +562,15 @@ mod tests { } // Two calls on the same variant must produce the same hash. - assert_eq!(hash_of(InvoiceStatus::Pending), hash_of(InvoiceStatus::Pending)); + assert_eq!( + hash_of(InvoiceStatus::Pending), + hash_of(InvoiceStatus::Pending) + ); assert_eq!(hash_of(InvoiceStatus::Paid), hash_of(InvoiceStatus::Paid)); // Different variants should (almost certainly) differ. - assert_ne!(hash_of(InvoiceStatus::Pending), hash_of(InvoiceStatus::Paid)); + assert_ne!( + hash_of(InvoiceStatus::Pending), + hash_of(InvoiceStatus::Paid) + ); } } diff --git a/contracts/storage_ttl/src/lib.rs b/contracts/storage_ttl/src/lib.rs index 6303a49c..7a6d9879 100644 --- a/contracts/storage_ttl/src/lib.rs +++ b/contracts/storage_ttl/src/lib.rs @@ -80,7 +80,9 @@ pub const DEFAULT_LEDGERS: u32 = 120; /// } /// ``` pub fn bump_instance(env: &Env, threshold: u32, ledgers_to_add: u32) { - env.storage().instance().extend_ttl(threshold, ledgers_to_add); + env.storage() + .instance() + .extend_ttl(threshold, ledgers_to_add); } /// Convenience form of [`bump_instance`] that uses [`DEFAULT_THRESHOLD`] @@ -117,7 +119,9 @@ pub fn bump_persistent(env: &Env, key: &K, threshold: u32, ledgers_to_add: u3 where K: IntoVal, { - env.storage().persistent().extend_ttl(key, threshold, ledgers_to_add); + env.storage() + .persistent() + .extend_ttl(key, threshold, ledgers_to_add); } /// Convenience form of [`bump_persistent`] that uses @@ -155,7 +159,9 @@ pub fn bump_temporary(env: &Env, key: &K, threshold: u32, ledgers_to_add: u32 where K: IntoVal, { - env.storage().temporary().extend_ttl(key, threshold, ledgers_to_add); + env.storage() + .temporary() + .extend_ttl(key, threshold, ledgers_to_add); } /// Convenience form of [`bump_temporary`] that uses @@ -286,8 +292,10 @@ mod tests { // Type-check: generic parameter K resolves to Symbol. #[allow(unused_variables)] { - let bump_p: fn(&Env, &soroban_sdk::Symbol, u32, u32) = bump_persistent::; - let bump_t: fn(&Env, &soroban_sdk::Symbol, u32, u32) = bump_temporary::; + let bump_p: fn(&Env, &soroban_sdk::Symbol, u32, u32) = + bump_persistent::; + let bump_t: fn(&Env, &soroban_sdk::Symbol, u32, u32) = + bump_temporary::; let bump_i: fn(&Env, u32, u32) = bump_instance; let _ = (&key, bump_p, bump_t, bump_i); } diff --git a/contracts/tx_hash_validate/src/lib.rs b/contracts/tx_hash_validate/src/lib.rs index f901ebb1..f8b55c3a 100644 --- a/contracts/tx_hash_validate/src/lib.rs +++ b/contracts/tx_hash_validate/src/lib.rs @@ -104,16 +104,13 @@ mod tests { use super::*; /// A valid 64-char hex string (lowercase). - const VALID_LOWER: &str = - "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"; + const VALID_LOWER: &str = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"; /// A valid 64-char hex string (uppercase). - const VALID_UPPER: &str = - "ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789"; + const VALID_UPPER: &str = "ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789"; /// A valid 64-char hex string (mixed case). - const VALID_MIXED: &str = - "AbCdEf0123456789AbCdEf0123456789AbCdEf0123456789AbCdEf0123456789"; + const VALID_MIXED: &str = "AbCdEf0123456789AbCdEf0123456789AbCdEf0123456789AbCdEf0123456789"; /// A valid 64-char hex string using only digit characters. const VALID_DIGITS_ONLY: &str = @@ -195,7 +192,7 @@ mod tests { fn validate_rejects_uppercase_g() { let mut chars: Vec = VALID_LOWER.chars().collect(); chars[0] = 'G'; // 'G' is a valid hex digit? No, 'G' is NOT valid hex. - // Actually 'G' is NOT a hex digit. Let's use 'Z'. + // Actually 'G' is NOT a hex digit. Let's use 'Z'. chars[0] = 'Z'; let bad: String = chars.into_iter().collect(); assert_eq!(validate_tx_hash(&bad), Err(TxHashError::InvalidCharacter)); diff --git a/contracts/usdc_testnet_issuer/Cargo.toml b/contracts/usdc_testnet_issuer/Cargo.toml index f5eaa627..caa9bfb8 100644 --- a/contracts/usdc_testnet_issuer/Cargo.toml +++ b/contracts/usdc_testnet_issuer/Cargo.toml @@ -1,3 +1,5 @@ +[workspace] + [package] name = "usdc-testnet-issuer" version = "0.1.0" diff --git a/contracts/usdc_testnet_issuer/src/lib.rs b/contracts/usdc_testnet_issuer/src/lib.rs index 06ebadcf..7d9f274c 100644 --- a/contracts/usdc_testnet_issuer/src/lib.rs +++ b/contracts/usdc_testnet_issuer/src/lib.rs @@ -19,8 +19,7 @@ /// This is the same value documented in Quittance's front-end asset list /// (`frontend/lib/assets.ts`) and is the standard testnet USDC issuer on /// the public Stellar testnet. -pub const USDC_TESTNET_ISSUER: &str = - "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"; +pub const USDC_TESTNET_ISSUER: &str = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"; /// Read-only accessor returning the documented Stellar testnet USDC issuer. ///