Skip to content
Merged
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
34 changes: 34 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Normalize line endings across platforms.
#
# Windows contributors otherwise commit CRLF into shell scripts, which breaks
# scripts/deploy.sh when Linux CI executes it. Declaring the canonical form here
# makes the result independent of each contributor's core.autocrlf setting.

# Default: let Git decide what is text and normalize it to LF in the repository.
* text=auto eol=lf

# Source and documentation are always text, stored with LF.
*.sh text eol=lf
*.rs text eol=lf
*.ts text eol=lf
*.md text eol=lf

# Other text formats in the tree, kept LF for the same reason.
*.toml text eol=lf
*.json text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.cjs text eol=lf
*.mjs text eol=lf

# Lockfiles are tool-generated. Mark them so GitHub collapses them in diffs and
# reviewers are not asked to read machine output, while still storing LF.
Cargo.lock linguist-generated=true text eol=lf
**/package-lock.json linguist-generated=true text eol=lf

# Binary assets must never be line-ending converted.
*.wasm binary
*.png binary
*.jpg binary
*.gif binary
*.ico binary
36 changes: 36 additions & 0 deletions .github/workflows/commitlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Commitlint

# Enforces the Conventional Commits style documented in CONTRIBUTING.md.
# A pull request whose commits do not conform fails this check.
on:
pull_request:
branches: [main, develop]

permissions:
contents: read

jobs:
commitlint:
name: Lint PR commit messages
runs-on: ubuntu-latest
steps:
# fetch-depth 0 so the full PR commit range is available to lint.
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0

- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: "22"

# Pinned versions so the linter is identical on every run. These are dev
# tools only and are not added to any package manifest.
- name: Install commitlint
run: npm install --no-save @commitlint/cli@19.8.1 @commitlint/config-conventional@19.8.1

- name: Lint commits in range
run: |
npx commitlint \
--from "${{ github.event.pull_request.base.sha }}" \
--to "${{ github.event.pull_request.head.sha }}" \
--verbose
34 changes: 34 additions & 0 deletions .github/workflows/line-endings.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Line Endings

# Guards the LF normalization declared in .gitattributes. A file committed with
# CRLF (typically from a Windows checkout without the attributes applied) breaks
# scripts/deploy.sh on Linux CI, so it is rejected here.
on:
push:
branches: [main]
pull_request:
branches: [main, develop]

permissions:
contents: read

jobs:
check-crlf:
name: No CRLF in tracked text files
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

# `git ls-files --eol` reports the index end-of-line for every tracked
# file. "i/crlf" or "i/mixed" means CRLF reached the repository.
- name: Assert no CRLF in the index
run: |
offenders="$(git ls-files --eol | grep -E '^i/(crlf|mixed)' || true)"
if [ -n "$offenders" ]; then
echo "Files committed with CRLF line endings:"
echo "$offenders"
echo
echo "Fix with: git add --renormalize . && git commit"
exit 1
fi
echo "All tracked text files use LF."
4 changes: 4 additions & 0 deletions .github/workflows/sdk-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ jobs:
working-directory: sdk
run: npm ci

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

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

# Fails when a Cargo crate declares a dependency it does not use. Unused crates
# enlarge the compiled WASM and widen the audit surface, so they are caught
# before merge and on a schedule for crates that drift out of use over time.
on:
schedule:
# Weekly, Mondays 09:30 UTC, just after the security audit.
- cron: "30 9 * * 1"
push:
branches: [main]
paths: &cargo-paths
- "**/Cargo.toml"
- "Cargo.lock"
- ".github/workflows/unused-deps.yml"
pull_request:
branches: [main, develop]
paths: *cargo-paths
workflow_dispatch:

permissions:
contents: read

jobs:
machete:
name: cargo-machete (unused Cargo dependencies)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

# Pinned and --locked, so the checker is the same build on every run,
# mirroring how security.yml installs cargo-audit.
- name: Install cargo-machete
run: cargo install cargo-machete --locked --version 0.9.1

- name: Run cargo-machete
run: cargo machete
49 changes: 49 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Local Git hooks for StellarForge, run by https://pre-commit.com.
#
# Install once per clone (see CONTRIBUTING.md "Development Setup"):
# pip install pre-commit # or: pipx install pre-commit / brew install pre-commit
# pre-commit install # formatting + lint hooks (pre-commit stage)
# pre-commit install --hook-type commit-msg # Conventional Commits check
#
# These mirror the CI gates so failures surface locally instead of after push.
minimum_pre_commit_version: "3.5.0"

repos:
# Line-ending hygiene, matching .gitattributes. Rewrites any CRLF in staged
# text files to LF so a bad checkout cannot reach the repository.
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: mixed-line-ending
args: ["--fix=lf"]

# Conventional Commits, matching .github/workflows/commitlint.yml and the
# commit-msg check below. Config is read from commitlint.config.cjs.
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
rev: v9.22.0
hooks:
- id: commitlint
stages: [commit-msg]
additional_dependencies: ["@commitlint/config-conventional@19.8.1"]

# Project-local hooks run against tools already required for development.
- repo: local
hooks:
# Rejects a commit with unformatted Rust. `cargo fmt --check` makes no
# changes; it exits non-zero when formatting is needed.
- id: cargo-fmt
name: cargo fmt (check)
entry: cargo fmt --all --
args: ["--check"]
language: system
types: [rust]
pass_filenames: false

# Runs ESLint over the SDK when a staged TypeScript file changes. Requires
# the SDK dependencies to be installed (`make sdk-install`).
- id: eslint-sdk
name: eslint (SDK)
entry: npm --prefix sdk run lint
language: system
files: '^sdk/.*\.ts$'
pass_filenames: false
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

- Stage 1 developer-experience tooling:
- `.gitattributes` normalizing all tracked text to LF (`*.sh`, `*.rs`, `*.ts`, `*.md`, and the other text formats in the tree) and marking `Cargo.lock` and `package-lock.json` as generated. The tree was re-normalized once, and a new **Line Endings** workflow fails any pull request that reintroduces CRLF into the index (#85).
- A **Commitlint** workflow and `commitlint.config.cjs` enforcing the Conventional Commits style documented in `CONTRIBUTING.md`; a pull request with a non-conforming commit now fails the check. Allowed scopes are documented and now include the `vault` and `common` crates (#90).
- A `.pre-commit-config.yaml` with local hooks for LF line endings, `cargo fmt --check`, SDK ESLint, and commitlint, installed via `make hooks`. Failures now surface locally instead of in CI (#89).
- An **Unused Dependencies** workflow running `cargo-machete` on pull requests and weekly, plus a `make udeps` target, to catch Cargo dependencies that are declared but unused (#92).
- A working ESLint setup for the SDK (`sdk/eslint.config.mjs`, flat config with `typescript-eslint`), wired into `npm run lint`, the SDK CI job, and the pre-commit hook. Removed one unused `rpc` type import in `sdk/src/vault.ts` that the new lint surfaced.
- `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
43 changes: 42 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,43 @@ cargo install --locked stellar-cli

# Optional but recommended
cargo install cargo-watch # auto-rerun tests on save

# Developer tooling checked in CI (see "Local Git hooks" below)
pip install pre-commit # or pipx/brew
cargo install cargo-machete --locked --version 0.9.1 # unused-dependency finder
```

### Local Git hooks

StellarForge ships a [pre-commit](https://pre-commit.com) configuration
(`.pre-commit-config.yaml`) that mirrors the CI gates, so formatting, lint, and
commit-message failures surface before you push. Install it once per clone:

```bash
make sdk-install # the ESLint hook needs the SDK dependencies
make hooks # installs the pre-commit and commit-msg hooks
```

`make hooks` runs `pre-commit install` and `pre-commit install --hook-type
commit-msg`. From then on, every commit runs, on staged files:

- **LF line endings** — rewrites any CRLF to LF, matching `.gitattributes`.
- **`cargo fmt --check`** — rejects unformatted Rust.
- **ESLint** — lints the SDK when a staged `.ts` file changes.
- **commitlint** — rejects a commit message that is not a valid Conventional Commit.

Run everything against the whole tree at any time with `pre-commit run
--all-files`.

### Finding unused dependencies

```bash
make udeps # runs cargo-machete across the Cargo workspace
```

The **Unused Dependencies** workflow runs the same check on pull requests and on
a weekly schedule.

### Initial Setup

```bash
Expand Down Expand Up @@ -232,7 +267,13 @@ 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`, `vault`, `common`, `sdk`, `ci`, `docs`, `deps`, `release`

This style is enforced. The **Commitlint** CI job runs `commitlint.config.cjs`
against every commit in a pull request, and the local `commit-msg` hook runs the
same check (see [Local Git hooks](#local-git-hooks)). A non-conforming commit
fails the check. The scope is optional, but when present it must be one of the
list above.

**Examples:**

Expand Down
12 changes: 11 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 sdk-install sdk-build sdk-test sdk-lint sdk-typecheck deploy-testnet help
.PHONY: all build test fmt lint clean sdk-install sdk-build sdk-test sdk-lint sdk-typecheck deploy-testnet udeps hooks help

# ─── Variables ────────────────────────────────────────────────────────────────
NETWORK ?= testnet
Expand Down Expand Up @@ -51,6 +51,16 @@ sdk-lint: sdk-install ## Run ESLint on the SDK
sdk-typecheck: sdk-install ## Run TypeScript typecheck on the SDK
cd sdk && npm run typecheck

##@ Tooling
udeps: ## Find unused Cargo dependencies (install: cargo install cargo-machete --locked)
@echo "Scanning for unused Cargo dependencies..."
cargo machete

hooks: ## Install the pre-commit Git hooks (install: pip install pre-commit)
pre-commit install
pre-commit install --hook-type commit-msg
@echo "Pre-commit hooks installed. Run 'pre-commit run --all-files' to check the tree."

##@ Release
deploy-testnet: ## Deploy all four contracts to Stellar testnet
@echo "Deploying all four contracts to Stellar testnet..."
Expand Down
41 changes: 41 additions & 0 deletions commitlint.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Commitlint configuration enforcing the Conventional Commits style that
// CONTRIBUTING.md documents ("Commit Style" section). The CI job in
// .github/workflows/commitlint.yml runs this against every commit in a pull
// request, so a non-conforming subject line fails the check.
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: {
// Types, verbatim from CONTRIBUTING.md.
"type-enum": [
2,
"always",
["feat", "fix", "docs", "test", "refactor", "chore", "perf"],
],
// Scopes documented in CONTRIBUTING.md, plus the contract crates that have
// since been added to the workspace (common, vault) and the conventional
// "deps"/"release" scopes used by automation. A scope is optional, but when
// present it must be one of these.
"scope-enum": [
2,
"always",
[
"rwa-asset",
"registry",
"compliance",
"governance",
"oracle-adapter",
"vault",
"common",
"sdk",
"ci",
"docs",
"deps",
"release",
],
],
// CONTRIBUTING.md: "Keep the subject line under 72 characters."
"header-max-length": [2, "always", 72],
},
// Ignore bot-generated merge commits, which do not follow the convention.
ignores: [(message) => /^Merge /.test(message)],
};
29 changes: 29 additions & 0 deletions sdk/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Flat ESLint config for the StellarForge SDK.
//
// CONTRIBUTING.md requires strict TypeScript with no `any` types and JSDoc on
// every exported symbol. The rules below encode the parts of that policy ESLint
// can check mechanically; `npm run typecheck` covers the type system itself.
import js from "@eslint/js";
import tseslint from "typescript-eslint";

export default tseslint.config(
{
// Build output and vendored dependencies are never linted.
ignores: ["dist/**", "node_modules/**"],
},
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ["src/**/*.ts"],
rules: {
// CONTRIBUTING.md: "Strict TypeScript - no `any` types."
"@typescript-eslint/no-explicit-any": "error",
// Unused code is a review smell; allow a leading underscore as the
// conventional "intentionally unused" marker for arguments.
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
},
},
);
Loading