Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions .github/workflows/contracts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
7 changes: 7 additions & 0 deletions contracts/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
58 changes: 42 additions & 16 deletions contracts/Makefile
Original file line number Diff line number Diff line change
@@ -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
30 changes: 24 additions & 6 deletions contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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:
Expand All @@ -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.

Expand Down
2 changes: 2 additions & 0 deletions contracts/amount_scale/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[workspace]

[package]
name = "quittance-amount-scale"
version = "0.1.0"
Expand Down
10 changes: 8 additions & 2 deletions contracts/amount_scale/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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]
Expand Down
2 changes: 2 additions & 0 deletions contracts/asset_allowlist/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[workspace]

[package]
name = "asset-allowlist"
version = "0.1.0"
Expand Down
2 changes: 2 additions & 0 deletions contracts/auth_one_address/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[workspace]

[package]
name = "auth-one-address"
version = "0.1.0"
Expand Down
43 changes: 30 additions & 13 deletions contracts/destination_guard/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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 ───────────────────────────────────────────
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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).
Expand Down
Loading
Loading