Thanks for your interest in contributing! This guide covers everything you need to go from zero to a merged PR.
Please read and follow our Code of Conduct. Reports of unacceptable behavior can be sent to conduct@trustlink.io.
TrustLink uses pre-commit to enforce formatting and linting before every commit.
Install the hooks once after cloning:
# Install pre-commit (choose one method):
pip install --user pre-commit # or: brew install pre-commit
# Ensure Rust tooling is available for the hooks:
rustup component add rustfmt clippy
# Install the git hooks (and fetch any hook dependencies):
pre-commit install --install-hooks
# Verify hooks run cleanly now:
pre-commit run --all-filesAfter that, every git commit automatically runs:
| Hook | What it checks |
|---|---|
cargo fmt --all -- --check |
Rust formatting (Rustfmt) |
cargo clippy --all-targets --all-features -- -D warnings |
Rust lints (Clippy) |
check-yaml |
Valid YAML syntax |
end-of-file-fixer |
Files end with a newline |
trailing-whitespace |
No trailing spaces |
If a hook fails the commit is blocked. Fix the reported issues and git commit again.
Run hooks manually at any time:
pre-commit run --all-files # check everything
pre-commit run cargo-fmt # check one hook by idBefore diving in, read docs/stellar-concepts.md for a beginner-friendly explanation of ledger timestamps, storage TTL, require_auth, and the WASM deployment model — concepts that come up throughout the codebase.
See docs/good-first-issues.md for concrete starter tasks across the contract, SDKs, indexer, examples, and docs.
| Tool | Version | Install |
|---|---|---|
| Rust | stable (see rust-toolchain.toml) |
https://rustup.rs |
| wasm32 target | — | rustup target add wasm32-unknown-unknown |
| Soroban CLI | latest | cargo install --locked soroban-cli |
Verify your setup:
rustc --version
cargo --version
soroban --version
rustup target list --installed | grep wasm32# 1. Fork and clone
git clone https://github.com/<your-username>/TrustLink.git
cd TrustLink
# 2. Install the wasm target (rust-toolchain.toml handles the Rust version)
rustup target add wasm32-unknown-unknown
# 3. Confirm the project compiles
cargo checkTrustLink includes a cargo-fuzz target for claim type validation edge cases.
cargo install --locked cargo-fuzz
cd fuzz
cargo fuzz run fuzz_validate_claim_typeThe fuzz target exercises Validation::validate_claim_type with arbitrary byte sequences, including null bytes, multi-byte UTF-8, and boundary-length inputs.
# Run all unit and integration tests
cargo test
# Or via make
make testAll tests must pass before submitting a PR.
This section covers everything specific to working on the TrustLink Soroban contract — environment setup, running tests, adding new functions, and keeping snapshot files in sync.
You need three things beyond a standard Rust install:
1. WASM compilation target
rustup target add wasm32-unknown-unknown2. Stellar CLI (includes the Soroban contract toolchain)
cargo install --locked stellar-cli --features optVerify:
stellar --version3. wasm-opt (required for make optimize and make check-size)
cargo install --locked wasm-opt
# or on Debian/Ubuntu: apt install binaryenConfirm everything is in place:
rustc --version
cargo --version
stellar --version
rustup target list --installed | grep wasm32TrustLink has three layers of tests. Run them all before opening a PR.
Unit and integration tests
cargo test
# or
make testSnapshot tests
Snapshot tests record the full Soroban auth and event trace for each test case as JSON files in test_snapshots/. They fail if the recorded trace no longer matches the contract output.
# Run snapshot tests alongside everything else — they are part of cargo test
cargo test
# Run only a specific snapshot test by name
cargo test test_initialize_and_get_adminIf a snapshot test fails with a diff, it means the contract's auth or event output changed. See Updating Snapshot Files below.
Lints and formatting (also checked by CI)
make fmt # auto-format
make clippy # zero-warning lint checkFollow this checklist when adding a public function to the contract:
-
Define the logic in the appropriate module under
src/(admin.rs,query.rs,attestation.rs, etc.) -
Expose it in
src/lib.rsinside the#[contractimpl]block. Add#[must_use]for read-only functions that return a value. -
Add any new storage keys to the
StorageKeyenum insrc/storage.rs. Add storage getter/setter methods to theStoragestruct in the same file. -
Emit an event via
src/events.rsif the function mutates state. Follow the existingtopics + datapattern. -
Add validation in
src/validation.rsif the function requires auth or input checks. -
Write tests in
tests/orsrc/test.rs. Cover the happy path, error cases, and auth boundaries. -
Add the SDK method in
sdk/typescript/src/client.ts. Read-only functions usethis.simulate(...), write functions usethis.invoke(...). -
Regenerate TypeScript bindings after any interface change:
make bindings
Commit the updated
bindings/typescript/alongside your contract changes. CI will fail if they are out of date. -
Update snapshot files if the new function changes any existing auth or event traces (see below).
Snapshot files in test_snapshots/ are committed to the repository and checked by CI. When you make an intentional change to a contract function's auth requirements or event output, the corresponding snapshots must be regenerated.
When do snapshots need updating?
- You changed which addresses
require_auth()is called on inside a function. - You added, removed, or changed an event emitted by a function.
- You changed the arguments or return type of an existing function in a way that affects the recorded trace.
How to regenerate
Set the SOROBAN_TEST_REGENERATE_SNAPSHOTS environment variable and re-run the tests:
# Regenerate all snapshots
SOROBAN_TEST_REGENERATE_SNAPSHOTS=1 cargo test
# Regenerate snapshots for a single test
SOROBAN_TEST_REGENERATE_SNAPSHOTS=1 cargo test test_register_and_remove_issuerThe test runner will overwrite the relevant JSON files in test_snapshots/ with the new output.
Review before committing
Always diff the regenerated snapshots before committing to confirm only the expected traces changed:
git diff test_snapshots/Commit the updated snapshot files in the same commit as the contract change:
git add test_snapshots/
git commit -m "test: update snapshots for <function name> change"Never regenerate all snapshots blindly after an unintended change. If a snapshot you did not expect to change is showing a diff, investigate the root cause before committing.
Use a local Stellar Quickstart node when iterating on deployment and invoke flows to avoid testnet rate limits.
docker compose up -d
# or: docker-compose up -dThis starts the stellar/quickstart standalone network from docker-compose.yml.
make local-deployWhat this does:
- Builds the contract WASM.
- Ensures local Soroban network + identity are configured.
- Funds the local identity via Friendbot.
- Deploys the contract.
- Invokes
initialize. - Writes the deployed contract ID to
.local.contract-id.
Use this RPC URL for local calls and scripts:
http://localhost:8000/soroban/rpc
Default local network values used by scripts/setup_local.sh:
- Network name:
local - Network passphrase:
Standalone Network ; February 2017
docker compose downThe TypeScript SDK ships with an end-to-end test suite (sdk/typescript/e2e/) that deploys the contract to a local Stellar node and exercises the SDK against it. These tests catch XDR encoding bugs and RPC parameter issues that unit tests with mocked RPC calls cannot detect.
- Docker (for the local Stellar Quickstart node)
- Node.js ≥ 16
- Stellar CLI (
cargo install --locked stellar-cli --features opt) - A compiled contract WASM (
make build)
docker compose up -dWait ~10 seconds for the node to be ready.
bash scripts/setup_local.shThis script:
- Configures the
localSoroban network pointing athttp://localhost:8000/soroban/rpc - Generates (or reuses) a
local-adminidentity - Funds the identity via Friendbot
- Deploys the contract WASM
- Calls
initializewith the admin address - Writes the deployed contract ID to
.local.contract-id
cd sdk/typescript
npm install
npm run test:e2eThe test runner reads the contract ID from .local.contract-id automatically. You can also pass it explicitly:
CONTRACT_ID=C... npm run test:e2eTo use a specific admin keypair (e.g. the one used during setup_local.sh):
ADMIN_SECRET=S... npm run test:e2e| Test | Description |
|---|---|
contract is already initialized |
Calls get_admin and asserts a valid address is returned |
admin can register a new issuer |
Calls register_issuer, then is_issuer to confirm |
registered issuer can create an attestation |
Calls create_attestation and retrieves the ID via get_subject_attestations |
has_valid_claim returns true |
Verifies the freshly created attestation is valid |
has_valid_claim returns false for unknown claim |
Confirms OR-logic does not leak across claim types |
issuer can revoke the attestation |
Calls revoke_attestation and checks revoked: true on the record |
has_valid_claim returns false after revocation |
Confirms the claim is no longer valid post-revocation |
| Variable | Default | Description |
|---|---|---|
CONTRACT_ID |
read from .local.contract-id |
Deployed contract address |
RPC_URL |
http://localhost:8000/soroban/rpc |
Soroban RPC endpoint |
NETWORK_PASSPHRASE |
Standalone Network ; February 2017 |
Network passphrase |
ADMIN_SECRET |
random (funded via Friendbot) | Admin keypair secret |
ISSUER_SECRET |
random (funded via Friendbot) | Issuer keypair secret |
docker compose down# Debug build
make build
# Optimized release build (requires soroban-cli)
make optimizeThis project enforces formatting and lint rules in CI.
# Format code (must be clean before committing)
make fmt # or: cargo fmt
# Run linter — zero warnings allowed
make clippy # or: cargo clippy --all-targets -- -D warningsRun both before every commit.
This project uses Conventional Commits to enable automated versioning and changelog generation. Every commit message must follow this format:
<type>(<scope>): <subject>
<body>
<footer>
Required. Must be one of:
| Type | Purpose | Semver Impact |
|---|---|---|
feat |
A new feature | Minor (0.x.0) |
fix |
A bug fix | Patch (0.0.x) |
docs |
Documentation only | None |
test |
Tests only | None |
refactor |
Code refactoring (no feature/fix) | None |
perf |
Performance improvement | Patch (0.0.x) |
chore |
Build, CI, dependencies | None |
Optional. Narrow the change to a specific area:
storage— storage layer changesvalidation— authorization/validation logicevents— event emissionindexer— off-chain indexersdk— TypeScript SDKci— CI/CD workflowsdocs— documentation
Examples: feat(storage), fix(validation), docs(indexer)
Required. Short description (50 chars max):
- Start with lowercase
- Use imperative mood ("add" not "adds" or "added")
- No period at the end
- Be specific: ✅ "add fee collection to attestation creation" vs ❌ "update code"
Optional. Explain why the change was made (not what — that's in the subject):
feat(storage): add dual indexing for subject and issuer lookups
The previous single index on subject made issuer-based queries O(n).
This adds a parallel index on issuer to enable fast lookups in both
directions. Queries now complete in O(log n) time.
Optional. Reference issues or breaking changes:
Closes #42
Closes #99
BREAKING CHANGE: removed the `get_all_attestations` function
Good commits:
feat(storage): add dual indexing for subject and issuer lookups
fix(validation): reject attestations with valid_from in the past
Previously, valid_from was only checked against the current time.
Now we also reject any valid_from that is before the current ledger
timestamp, preventing backdated attestations.
Closes #123
docs: update deployment guide with testnet contract IDs
test(events): add test for audit log append-only property
refactor: extract fee calculation into separate function
Bad commits:
❌ Updated stuff
❌ Fix bug
❌ feat: Add new feature.
❌ FEAT: ADD FEATURE
❌ feat(storage): added dual indexing
When you merge commits to main:
- Release Please reads your commit messages
- Determines the next version (major.minor.patch) based on commit types
- Creates a Release PR that:
- Updates
Cargo.tomlversion - Generates
CHANGELOG.mdfrom commits - Groups commits by type (Features, Bug Fixes, etc.)
- Updates
- When the Release PR is merged:
- A GitHub Release is created with the tag
- WASM artifacts are built and attached automatically
Example: If you merge feat: ... and fix: ... commits, the next release will be a minor version bump (0.1.0 → 0.2.0).
TrustLink uses a .github/CODEOWNERS file to automatically request reviews from the right team when a pull request touches sensitive paths:
| Path | Responsible team |
|---|---|
src/ |
@Haroldwonder/contract-team — core contract logic |
sdk/ |
@Haroldwonder/sdk-team — TypeScript/React SDKs |
docs/security*.md |
@Haroldwonder/security-team — security documentation |
docs/compliance.md |
@Haroldwonder/compliance-team — compliance documentation |
GitHub will add the matching team as a required reviewer automatically when you open a PR. You do not need to request them manually.
-
Branch off
mainwith a descriptive name:git checkout -b feat/your-feature # or git checkout -b fix/your-bugfix -
Commit with clear messages following Conventional Commits.
-
Before pushing, make sure:
-
cargo testpasses -
cargo fmt -- --checkis clean -
cargo clippy --all-targets -- -D warningsis clean - Commit messages follow Conventional Commits format
-
-
Open a PR against
mainusing the pull request template. GitHub loads it automatically when you open a PR. Fill in:- What the change does and why
- The type of change (bug fix / feature / docs / refactor)
- Testing done
- Any relevant issue numbers (
Closes #123) - Notes for reviewers if the change is non-obvious
-
Commit validation: The PR title must follow Conventional Commits format. This is checked automatically by CI.
-
Review: at least one approval is required before merging. Address all review comments; force-push to the same branch to update the PR.
-
Merge: Use "Squash and merge" or "Create a merge commit" (not "Rebase and merge") to preserve commit history for changelog generation.
TrustLink runs automated security audits on every push and weekly via scheduled scans. When vulnerabilities are detected:
- On every push:
cargo audit --deny warningsruns in CI and blocks merges if vulnerabilities are found - Weekly: Scheduled audit runs Monday at 00:00 UTC; failures create a GitHub issue with label
security
When a vulnerability is reported:
| Severity | Action | Timeline |
|---|---|---|
| Critical | Blocks all merges; must fix immediately | Same day |
| High | Blocks merges; fix within 48 hours | 2 days |
| Medium | Blocks merges; fix within 1 week | 7 days |
| Low | Can be accepted if justified; document in Cargo.audit |
Case-by-case |
Option A: Update the dependency
# Update to a patched version
cargo update <crate-name>
# Verify the fix
cargo audit
# Test thoroughly
cargo testOption B: Accept the vulnerability (Low severity only)
If the vulnerability does not affect TrustLink's usage pattern:
- Open
Cargo.auditand add an entry:
[[advisories]]
id = "RUSTSEC-YYYY-NNNNN"
reason = "Vulnerability does not affect our usage - we do not use feature X"
date = "2024-01-15"
reviewer = "your-github-username"- Run audit to verify it's accepted:
cargo audit- Commit with clear message:
git add Cargo.audit
git commit -m "security: accept RUSTSEC-YYYY-NNNNN - documented in Cargo.audit"- All vulnerability fixes require at least one approval
- Reviewer must verify:
- The fix doesn't introduce breaking changes
- Tests still pass
- No new vulnerabilities are introduced
- Document the decision in the PR description
For critical vulnerabilities affecting production:
- Create a private security advisory (GitHub Settings → Security → Advisories)
- Notify maintainers immediately
- Prepare a patch release
- Do not disclose publicly until patch is available
# Check for vulnerabilities
cargo audit
# Deny any warnings (same as CI)
cargo audit --deny warnings
# Generate a JSON report
cargo audit --json > audit-report.json
# Check specific advisory
cargo audit --advisory RUSTSEC-YYYY-NNNNN- Keep dependencies up-to-date with security patches
- Review changelogs before major version updates
- Test thoroughly after updates
- Document breaking changes in PR description
For changes large enough to benefit from discussion before any code is written — new storage layouts, public contract interface changes, new workflows, or anything with backward-compatibility implications — open an RFC instead of jumping straight to a PR. See docs/rfcs/README.md for the process and docs/rfcs/TEMPLATE.md for the template. RFCs capture pre-implementation discussion; once a proposal is accepted and built, it's typically followed by an ADR recording the final decision.
To set clear expectations for contributors, maintainers aim to respond within the following timeframes. "Respond" means an initial triage comment, label, or reaction — not necessarily a full resolution.
| Item | First response |
|---|---|
| New issue (bug, feature, question, docs) | Within 5 business days |
| New pull request | Within 5 business days |
| Follow-up comment on an open issue/PR | Within 5 business days |
| Security report (see Reporting Security Issues) | Follows the dedicated security disclosure process, not this table |
These are targets, not guarantees — response times may be longer around releases or during maintainer availability gaps. If you haven't heard back after the window above, a polite ping on the issue/PR is welcome.
Open a GitHub issue with:
- A clear description of the problem or feature request
- Steps to reproduce (for bugs)
- Expected vs actual behaviour
TypeScript bindings for the contract ABI live in bindings/typescript/ and are
generated from the compiled WASM using the Stellar CLI.
Prerequisites:
cargo install --locked stellar-cli --features opt
rustup target add wasm32-unknown-unknownRegenerate after any contract interface change:
make bindingsThis builds the WASM and runs:
stellar contract bindings typescript \
--wasm target/wasm32-unknown-unknown/release/trustlink.wasm \
--contract-id 0000000000000000000000000000000000000000000000000000000000000001 \
--network testnet \
--output-dir bindings/typescriptCommit the updated bindings/typescript/ directory alongside your contract
changes. CI runs make check-bindings and will fail if the committed bindings
do not match the current WASM.