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
39 changes: 39 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Normalize line endings so a Windows checkout cannot commit CRLF into files a
# Linux runner executes or diffs (scripts/deploy.sh fails on `\r`).
#
# `text=auto` is the safety net for any file type not listed below; the
# explicit entries force LF in the index AND in the working tree, whatever a
# contributor's core.autocrlf says.
* text=auto

# Source and docs the issue requires to be LF.
*.sh text eol=lf
*.rs text eol=lf
*.ts text eol=lf
*.md text eol=lf

# Everything else this repo executes or parses. A CR breaks make recipes, shell
# shebangs and YAML/TOML just as it breaks the files above.
*.cjs text eol=lf
*.mjs text eol=lf
*.js text eol=lf
*.json text eol=lf
*.toml text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
Makefile text eol=lf
.gitattributes text eol=lf
.gitignore text eol=lf

# Lockfiles are generated by cargo / npm. GitHub collapses their diffs and
# excludes them from language statistics.
Cargo.lock text eol=lf linguist-generated=true
package-lock.json text eol=lf linguist-generated=true
sdk/package-lock.json text eol=lf linguist-generated=true

# Never touch binary payloads.
*.png binary
*.jpg binary
*.gif binary
*.ico binary
*.wasm binary
44 changes: 44 additions & 0 deletions .github/workflows/commitlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Commitlint

# Lints every commit a pull request adds against commitlint.config.cjs, which
# implements the "Commit Style" section of CONTRIBUTING.md.
on:
pull_request:
branches: [main, develop]

permissions:
contents: read

jobs:
commitlint:
name: Conventional Commits
runs-on: ubuntu-latest
# Dependabot titles its own commits ("Bump x from 1 to 2").
if: github.actor != 'dependabot[bot]'
steps:
- uses: actions/checkout@v7
with:
# The range base..head needs the PR's history, not a shallow clone.
fetch-depth: 0

- uses: actions/setup-node@v7
with:
node-version: "22"
cache: "npm"
cache-dependency-path: sdk/package-lock.json

- name: Install dependencies
working-directory: sdk
run: npm ci

- name: Lint pull request commits
working-directory: sdk
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
npx --no-install commitlint \
--config ../commitlint.config.cjs \
--from "$BASE_SHA" \
--to "$HEAD_SHA" \
--verbose
23 changes: 23 additions & 0 deletions .github/workflows/sdk-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,32 @@ on:
- "sdk/**"
- "contracts/oracle-adapter/**"
- "scripts/generate-oracle-parity-table.cjs"
- "scripts/pre-commit-eslint.sh"
- "scripts/check-unused-deps.sh"
- "**/Cargo.toml"
- ".gitattributes"
- ".pre-commit-config.yaml"
- "commitlint.config.cjs"
- "CONTRIBUTING.md"
- ".github/workflows/commitlint.yml"
- ".github/workflows/unused-deps.yml"
- ".github/workflows/sdk-ci.yml"
pull_request:
branches: [main, develop]
paths:
- "sdk/**"
- "contracts/oracle-adapter/**"
- "scripts/generate-oracle-parity-table.cjs"
- "scripts/pre-commit-eslint.sh"
- "scripts/check-unused-deps.sh"
- "**/Cargo.toml"
- ".gitattributes"
- ".pre-commit-config.yaml"
- "commitlint.config.cjs"
- "CONTRIBUTING.md"
- ".github/workflows/commitlint.yml"
- ".github/workflows/unused-deps.yml"
- ".github/workflows/sdk-ci.yml"

jobs:
oracle-parity:
Expand Down Expand Up @@ -63,6 +82,10 @@ jobs:
working-directory: sdk
run: npm run typecheck

- name: Lint
working-directory: sdk
run: npm run lint

- name: Run tests
working-directory: sdk
run: npm test
Expand Down
55 changes: 55 additions & 0 deletions .github/workflows/unused-deps.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Unused Dependencies

on:
schedule:
# Weekly on Mondays at 07:00 UTC. A dependency can become unused without a
# Cargo.toml change (a refactor that drops the last `use`), so the check
# also runs on a clock.
- cron: "0 7 * * 1"
workflow_dispatch:
push:
branches: [main]
paths: &unused-deps-paths
- "contracts/**"
- "tools/**"
- "Cargo.toml"
- "Cargo.lock"
- "scripts/check-unused-deps.sh"
- ".github/workflows/unused-deps.yml"
pull_request:
branches: [main, develop]
paths: *unused-deps-paths

permissions:
contents: read

env:
# Pinned so the check is the same build on every run. Keep in step with
# MACHETE_VERSION in scripts/check-unused-deps.sh.
CARGO_MACHETE_VERSION: "0.7.0"

jobs:
cargo-machete:
name: cargo-machete (unused Rust dependencies)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Install Rust toolchain
run: rustup toolchain install && rustc --version

# Also caches ~/.cargo/bin, so cargo-machete is compiled once, not per run.
- name: Cache cargo registry and binaries
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: ${{ runner.os }}-machete-${{ env.CARGO_MACHETE_VERSION }}

- name: Install cargo-machete
run: cargo install cargo-machete --locked --version "$CARGO_MACHETE_VERSION"

# Proves the job can fail: an unused dependency must exit non-zero.
- name: Self-test
run: bash scripts/check-unused-deps.sh --self-test

- name: Check workspace for unused dependencies
run: bash scripts/check-unused-deps.sh
32 changes: 32 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Local pre-commit hooks: the same formatting gates CI runs, before a commit
# exists. Install once per clone:
#
# pipx install pre-commit # or: pip install --user pre-commit
# make sdk-install # eslint lives in sdk/node_modules
# make hooks # = pre-commit install
#
# Run against every file instead of only staged ones:
# pre-commit run --all-files
#
# Both hooks are `language: system`: they use the Rust toolchain pinned by
# rust-toolchain.toml and the eslint installed by `npm ci`, so the local check
# and the CI check are the same binaries.
minimum_pre_commit_version: "3.0.0"

repos:
- repo: local
hooks:
- id: cargo-fmt
name: cargo fmt --check
# cargo fmt formats a whole workspace; passing it individual staged
# paths would skip its edition and config handling.
entry: cargo fmt --all -- --check
language: system
files: \.rs$
pass_filenames: false

- id: eslint
name: eslint (sdk)
entry: bash scripts/pre-commit-eslint.sh
language: system
files: ^sdk/src/.*\.ts$
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Repository tooling, no contract or SDK API change (#85, #89, #90, #92):
- `.gitattributes` forces LF for `*.sh`, `*.rs`, `*.ts`, `*.md` and the other files the repo executes or parses, and marks `Cargo.lock` and the npm lockfile as generated, so a Windows checkout can no longer commit CRLF into `scripts/deploy.sh` (#85).
- `.pre-commit-config.yaml` runs `cargo fmt --all -- --check` on staged `.rs` files and eslint on staged `sdk/src` files; install with `make hooks`. The SDK now ships an `eslint.config.mjs` and the eslint dev dependencies its `lint` script always assumed, and `npm run lint` is a step in SDK CI (#89).
- `commitlint.config.cjs` and a `Commitlint` workflow check every pull request commit against the types and scopes in CONTRIBUTING.md; `make commitlint` runs the same check locally (#90).
- A weekly and per-pull-request `Unused Dependencies` workflow runs a pinned `cargo-machete` over the workspace and fails on an unused dependency; `make unused-deps` runs it locally. A manual audit of every workspace manifest against its sources found no unused dependency to remove (#92).

- `rwa-asset`: holder count tracking (#146). `holder_count()` returns the number of addresses currently holding a strictly positive balance. The counter is incremented when a balance crosses 0 → >0 (first receive) and decremented when a balance hits exactly 0 (full burn or full send). `set_holder_cap(cap)` lets the admin set an optional ceiling on the holder count (Reg D style); any `mint`, `transfer`, `transfer_from` or `forced_transfer` that would add a new holder when the count already equals the cap is rejected with `ExceedsHolderCap` (error 25). `holder_count_cap()` returns the configured ceiling or `None`. The new `HolderCapSet` event is emitted by `set_holder_cap`, including when the cap is removed. A property test (`tests/test_holder_count.rs`) verifies the invariant `holder_count == count(addresses with balance > 0)` after every step in a 50-operation sequence of mints, burns, and transfers, both with and without an active cap.

- `rwa-asset`: `set_governance`, `propose_cap_increase`/`approve_cap_increase`/`cancel_cap_increase` and `pending_cap_increase`. `update_metadata` still refuses to loosen a supply cap by itself (IR-02); raising one now requires the admin to propose a value and a separately configured `governance` address to approve it, mirroring `propose_admin`/`accept_admin`'s two-step shape. See [ADR-007](docs/architecture/007-governance-controlled-supply-cap.md) for why this is a second address rather than the `governance` contract itself (#148).
Expand Down
68 changes: 67 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,49 @@ make sdk-install
make sdk-test
```

### Git hooks (recommended)

Formatting failures should never reach CI. `.pre-commit-config.yaml` runs
`cargo fmt --all -- --check` when you stage a `.rs` file and `eslint` when you
stage a file under `sdk/src/`, and rejects the commit if either fails.

```bash
pipx install pre-commit # or: pip install --user pre-commit
make hooks # npm ci in sdk/, then `pre-commit install`
```

Run the hooks on the whole tree with `pre-commit run --all-files`. Skip them
once with `git commit --no-verify`; CI runs the same checks, so a skipped hook
only moves the failure to your pull request.

### Unused dependencies

An unused crate grows the WASM and the audit surface. CI runs
[cargo-machete](https://github.com/bnjbvr/cargo-machete) on every pull request
that touches Rust code or a manifest, and weekly on `main`
(`.github/workflows/unused-deps.yml`).

```bash
cargo install cargo-machete --locked --version 0.7.0
make unused-deps # fails and lists the crate if a dependency is unused
make unused-deps-selftest # proves the check can fail
```

If cargo-machete reports a false positive (a crate used only through a macro it
cannot see), list it under `[package.metadata.cargo-machete] ignored = ["name"]`
in that crate's `Cargo.toml` and say why in the pull request.

### Line endings

`.gitattributes` forces LF for `*.sh`, `*.rs`, `*.ts`, `*.md` and the other
files the repo executes or parses, whatever your `core.autocrlf` is. Windows
contributors do not need to configure anything. To check a clone:

```bash
git ls-files --eol # the i/ column must never say crlf or mixed
git add --renormalize . # re-normalizes files committed before .gitattributes
```

### Running a single contract's tests

```bash
Expand Down Expand Up @@ -193,6 +236,10 @@ make lint

# TypeScript — checks src/ and tests/
cd sdk && npm run typecheck
cd sdk && npm run lint # eslint, config in sdk/eslint.config.mjs

# Unused Rust dependencies
make unused-deps
```

CI will reject PRs that fail formatting or linting.
Expand All @@ -217,7 +264,26 @@ We follow [Conventional Commits](https://www.conventionalcommits.org/):

**Types:** `feat`, `fix`, `docs`, `test`, `refactor`, `chore`, `perf`

**Scopes:** `rwa-asset`, `registry`, `compliance`, `governance`, `oracle-adapter`, `sdk`, `ci`, `docs`
**Scopes:** `rwa-asset`, `registry`, `compliance`, `governance`, `oracle-adapter`, `sdk`, `ci`, `docs`, `common`, `benchmark`, `readme`

The scope is optional, but if you give one it must be on this list. Use the
contract's name for a change to that contract, `common` for `contracts/common`,
`benchmark` for `tools/benchmark`, `sdk` for `sdk/`, `ci` for workflows and
tooling, and `docs` or `readme` for documentation. Add `!` before the colon for a
breaking change (`feat(sdk)!: ...`).

**Enforced:** the `Commitlint` workflow lints every commit in a pull request
against `commitlint.config.cjs` and fails the check on a type or scope outside
these lists, a subject over 72 characters, an empty subject or a trailing full
stop. Check your branch before pushing:

```bash
make commitlint # lints commits on your branch that are not on origin/main
```

Fix a bad message with `git commit --amend` (last commit) or
`git rebase -i origin/main` (older ones), then force-push your branch. Merge
commits and `Revert "..."` commits are ignored.

**Examples:**

Expand Down
27 changes: 26 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: all build test fmt lint clean deploy-testnet help
.PHONY: all build test fmt lint clean deploy-testnet help unused-deps unused-deps-selftest sdk-lint commitlint hooks

# ─── Variables ────────────────────────────────────────────────────────────────
NETWORK ?= testnet
Expand Down Expand Up @@ -30,6 +30,14 @@ lint:
@echo "Running Clippy..."
$(CARGO) clippy --all-targets --all-features -- -D warnings

# Needs cargo-machete: cargo install cargo-machete --locked --version 0.7.0
unused-deps:
@echo "Checking for unused Rust dependencies..."
bash scripts/check-unused-deps.sh

unused-deps-selftest:
bash scripts/check-unused-deps.sh --self-test

clean:
$(CARGO) clean
rm -rf sdk/dist sdk/node_modules
Expand All @@ -44,6 +52,19 @@ sdk-build: sdk-install
sdk-test: sdk-install
cd sdk && npm test

sdk-lint: sdk-install
cd sdk && npm run lint

# ─── Git hooks and commit style ───────────────────────────────────────────────
# Installs the pre-commit hooks in .pre-commit-config.yaml (cargo fmt, eslint).
# Needs the pre-commit tool: pipx install pre-commit
hooks: sdk-install
pre-commit install

# Lints the commits on this branch that are not on origin/main, as CI does.
commitlint: sdk-install
cd sdk && npx --no-install commitlint --config ../commitlint.config.cjs --from origin/main --to HEAD --verbose

# ─── Deployment ───────────────────────────────────────────────────────────────
# scripts/deploy.sh builds, deploys all four contracts with their constructor
# arguments, and writes deployed-contracts.json. Deploying one wasm here with no
Expand All @@ -62,8 +83,12 @@ help:
@echo " make test Run all Rust contract tests"
@echo " make fmt Format Rust source code"
@echo " make lint Run Clippy linter"
@echo " make unused-deps Fail on unused Rust dependencies (cargo-machete)"
@echo " make clean Remove build artifacts"
@echo " make sdk-build Build the TypeScript SDK"
@echo " make sdk-test Run SDK unit tests"
@echo " make sdk-lint Run eslint on the SDK"
@echo " make hooks Install the pre-commit hooks (cargo fmt, eslint)"
@echo " make commitlint Lint this branch's commit messages against origin/main"
@echo " make deploy-testnet Deploy all four contracts to Stellar testnet"
@echo ""
Loading