diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 000000000..5c303692f --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,59 @@ +name: PR Title + +# PRs land on `dev` as squash merges, so the PR title becomes the commit message +# on the default branch. release-plz reads those messages to decide each crate's +# next version, which makes the title load-bearing: an unconventional title means +# the touched crates get no version bump and no changelog entry. +# +# Only the *type* is enforced. Scope is conventional but free-form on purpose — +# release-plz selects which crate to bump from the files a commit touches, not +# from the scope, so an allowlist would add friction without affecting releases. +# +# See the "Commit and PR titles" section of AGENTS.md for the full convention. + +# `pull_request`, not `pull_request_target`: the title is already in the event +# payload, so there is no reason to run in the base-repo context with secrets on +# a public repo. Nothing here checks out PR code. +on: + pull_request: + types: [opened, edited, reopened] + +permissions: + pull-requests: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + name: Validate conventional commit title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + # Keep in sync with the `commit_parsers` in `release-plz.toml`. + types: | + feat + fix + perf + refactor + docs + test + ci + build + chore + deploy + requireScope: false + # Guard the two mistakes that actually break version inference: + # a Linear ID used as the summary, or an empty/placeholder subject. + # (`ENG-504: Nest governance...` as a *title prefix* is not a valid + # type, so it is already rejected — the ID belongs in the PR body.) + subjectPattern: ^(?![A-Z]{2,}-\d+).+$ + subjectPatternError: | + The subject "{subject}" of PR title "{title}" is invalid. + Put the Linear issue ID in the PR description, not the title — + a title like "ENG-123: do the thing" breaks release version + inference. Write "feat(gateway): do the thing" instead. diff --git a/.github/workflows/publish-soroban-vault-cli.yml b/.github/workflows/publish-soroban-vault-cli.yml index 901135c49..8f6eec71a 100644 --- a/.github/workflows/publish-soroban-vault-cli.yml +++ b/.github/workflows/publish-soroban-vault-cli.yml @@ -1,6 +1,12 @@ name: Publish Soroban Vault CLI Image +# Fires automatically on the crate's release tag, so the published image version +# is the version release-plz actually released rather than free text someone +# typed. `workflow_dispatch` is kept for ad-hoc/dev images. on: + push: + tags: + - "templar-soroban-vault-cli-v*" workflow_dispatch: inputs: tag: @@ -14,7 +20,7 @@ on: default: false type: boolean -run-name: Publish Soroban Vault CLI image (${{ inputs.tag }}) +run-name: Publish Soroban Vault CLI image (${{ inputs.tag || github.ref_name }}) concurrency: group: publish-soroban-vault-cli-${{ github.ref }} @@ -41,6 +47,13 @@ jobs: run: | set -euo pipefail + # On a release tag, derive the image tag from the version + # (`templar-soroban-vault-cli-v1.2.0` -> `1.2.0`); otherwise use the + # dispatch input. + if [[ -z "${INPUT_TAG:-}" ]]; then + INPUT_TAG="${GITHUB_REF_NAME##*-v}" + fi + if [[ ! "$INPUT_TAG" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]]; then echo "Invalid Docker tag: $INPUT_TAG" >&2 exit 1 diff --git a/.github/workflows/release-artifacts.yml b/.github/workflows/release-artifacts.yml new file mode 100644 index 000000000..9fe531052 --- /dev/null +++ b/.github/workflows/release-artifacts.yml @@ -0,0 +1,137 @@ +name: Release Artifacts + +# Proves that a released contract blob is genuinely reproducible: rebuilds the +# contract in the pinned NEP-330 Docker image and fails unless the result is +# byte-for-byte identical to the blob checked in under +# `contract/artifacts/res/near///`. +# +# The rebuild happens at the release's recorded `source_commit`, NOT at the tag. +# `cargo near build reproducible-wasm` embeds the source commit into the WASM +# (NEP-330), so the same source built at two different commits produces +# different bytes — and a blob is necessarily committed *before* the tag that +# releases it exists. Verifying at the tag would therefore fail every time. +# +# This is what makes a checked-in blob *canonical* rather than "whatever some +# laptop produced", and it is what lets anyone verify the on-chain code against +# this source tree. The fast, build-free catalog checks run on every PR via +# `test.yml` → `artifact-drift-check`; this one is heavy (Docker + a full +# optimized WASM build), so it is reserved for release tags. +# +# Also uploads each verified blob plus a `checksums.txt` to the GitHub Release, +# so ops and auditors can fetch bytes without cloning the repo. + +on: + push: + tags: + # Broad glob: catalog membership is decided in `ids.rs`, not here. A + # tagged package the catalog doesn't know about fails fast at the + # `--print-metadata` lookup below. + - "*-contract-v*" + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + SQLX_OFFLINE: "true" + +jobs: + verify: + name: Verify reproducible build + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: write # attach assets to the GitHub Release + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + # `cargo near build reproducible-wasm` builds from committed git + # state and reads the repository, so the full checkout must be intact. + fetch-depth: 0 + persist-credentials: false + + # Not `./.github/actions/rust`: the WASM build runs inside the + # cargo-near Docker image, so a host rust-cache entry for this job would + # be cold on every release tag and just add eviction pressure. + - uses: dtolnay/rust-toolchain@dd44c20b1206a46e25fba8503d5d7c9a33bd355a + with: + toolchain: "1.86.0" + + - name: Resolve artifact from tag + id: resolve + env: + REF_NAME: ${{ github.ref_name }} + run: | + set -euo pipefail + # `templar-market-contract-v1.4.0` -> package `templar-market-contract`, version `1.4.0` + package="${REF_NAME%-v*}" + version="${REF_NAME##*-v}" + { + echo "package=${package}" + echo "version=${version}" + } >> "$GITHUB_OUTPUT" + + - name: Rebuild and compare against the checked-in blob + env: + PACKAGE: ${{ steps.resolve.outputs.package }} + VERSION: ${{ steps.resolve.outputs.version }} + run: | + set -euo pipefail + + # Ask the catalog for this package's source path and target name + # rather than re-deriving them here and drifting from ids.rs. + if ! metadata=$(cargo run --quiet -p templar-contract-artifacts \ + --features workspace-loader,clap --bin prebuild-test-contracts -- \ + --print-metadata --artifact "$PACKAGE" 2>/dev/null); then + echo "::notice::${PACKAGE} has no catalogued WASM artifact (e.g. a" \ + "Soroban contract); nothing to verify." + exit 0 + fi + read -r source_path target _current_version <<<"$metadata" + + committed="contract/artifacts/res/near/${target}/${VERSION}/${target}.wasm" + if [[ ! -f "$committed" ]]; then + echo "::error::${PACKAGE} ${VERSION} was tagged but no blob is checked in at ${committed}." >&2 + echo "Run 'just artifact-release' and commit the blob with its ids.rs entry." >&2 + exit 1 + fi + + # The blob-vs-pin check already runs on every PR via + # `check-artifact-drift.sh`; this job's unique job is the rebuild. + source_commit=$( + cargo run --quiet -p templar-contract-artifacts \ + --features workspace-loader,clap --bin prebuild-test-contracts -- \ + --print-release "$VERSION" --artifact "$PACKAGE" + ) + + if [[ -z "$source_commit" ]]; then + echo "::notice::${PACKAGE} ${VERSION} is a legacy blob with no recorded" \ + "source_commit (e.g. bytes recovered from chain), so it cannot be" \ + "reproducibly rebuilt. Skipping." + exit 0 + fi + + # Rebuild at the commit the blob was actually built from. The tag + # commit would embed a different NEP-330 snapshot and never match. + git worktree add ../verify "$source_commit" + (cd ../verify && cargo near build reproducible-wasm --manifest-path "${source_path}/Cargo.toml") + + rebuilt="../verify/target/near/${target}/${target}.wasm" + if ! cmp -s "$rebuilt" "$committed"; then + echo "::error::Reproducible build at ${source_commit} does not match the checked-in blob." >&2 + echo " committed: $(sha256sum "$committed" | cut -d' ' -f1)" >&2 + echo " rebuilt: $(sha256sum "$rebuilt" | cut -d' ' -f1)" >&2 + exit 1 + fi + echo "Reproducible: ${target} ${VERSION} matches byte-for-byte at ${source_commit}." + + mkdir -p dist + cp "$committed" "dist/${target}-${VERSION}.wasm" + (cd dist && sha256sum ./*.wasm > checksums.txt) + + - name: Attach artifacts to the release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REF_NAME: ${{ github.ref_name }} + run: gh release upload "$REF_NAME" dist/* --clobber diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml new file mode 100644 index 000000000..c3ac292d5 --- /dev/null +++ b/.github/workflows/release-plz.yml @@ -0,0 +1,97 @@ +name: Release-plz + +# Two jobs, both on every push to `dev`: +# +# release-plz-pr opens/updates one standing "chore: release" PR carrying the +# pending version bumps and CHANGELOG entries. +# release-plz runs after that PR is merged: tags each released crate and +# cuts its GitHub Release. Tier configuration lives in +# `release-plz.toml`. +# +# Merging the Release PR is the release trigger — `release_always = false` means +# an ordinary merge to `dev` ships nothing. +# +# Secrets: +# RELEASE_PLZ_TOKEN optional PAT. A PR opened with the default +# GITHUB_TOKEN does NOT trigger other workflows, so +# without this the Release PR never runs `test.yml` and +# you would be merging an unverified release. Falls back +# to GITHUB_TOKEN so the workflow still works unattended. +# CARGO_REGISTRY_TOKEN only consulted once crates leave `git_only` mode. +# Publishing is currently blocked upstream — see the +# header of `release-plz.toml`. + +on: + push: + branches: [dev] + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: "0" + # The workspace has sqlx macros; without this, any cargo invocation that + # builds `service/relayer` or `gateway/store` wants a live database. + SQLX_OFFLINE: "true" + +jobs: + release-plz-release: + name: Release + runs-on: ubuntu-latest + permissions: + contents: write # create tags and GitHub Releases + pull-requests: read + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + # release-plz derives each crate's current version from its git tags, + # so it needs the full history, not a shallow clone. + fetch-depth: 0 + persist-credentials: false + + # No wasm is compiled on the host in these jobs. + - uses: ./.github/actions/rust + with: + targets: "" + + - name: Run release-plz + uses: release-plz/action@2eb1d8bcb770b4c48ccfaad919734b38b51958c9 # v0.5.131 + with: + command: release + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN || secrets.GITHUB_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + + release-plz-pr: + name: Release PR + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + # Never let two runs race on the same release branch: one would force-push + # over the other's bumps. Queue instead of cancelling, so the last push to + # `dev` is always reflected in the PR. + concurrency: + group: release-plz-${{ github.ref }} + cancel-in-progress: false + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + fetch-depth: 0 + persist-credentials: false + + # No wasm is compiled on the host in these jobs. + - uses: ./.github/actions/rust + with: + targets: "" + + - name: Run release-plz + uses: release-plz/action@2eb1d8bcb770b4c48ccfaad919734b38b51958c9 # v0.5.131 + with: + command: release-pr + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN || secrets.GITHUB_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md index fcdb4d9c9..b3c233e07 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,6 +71,20 @@ Use this section as an execution checklist: read the local docs first, preserve - Preserve existing crate structure and naming patterns unless there is a strong reason to change them. - This codebase is security-sensitive. Review changes with an auditor mindset, especially in smart contracts and cross-contract flows. +## Commit And PR Titles + +PRs are squash-merged, so **the PR title becomes the commit message on `dev`**, and `release-plz` reads those messages to decide each crate's next version. A title that does not parse means the crates you touched get **no version bump and no changelog entry**. `.github/workflows/pr-title.yml` enforces this on every PR. + +Format: `type(scope): summary` + +- **Allowed types** — `feat`, `fix`, `perf`, `refactor`, `docs`, `test`, `ci`, `build`, `chore`, `deploy`. Keep this list in sync with `commit_parsers` in `release-plz.toml` and `types` in `.github/workflows/pr-title.yml`. +- **Version impact** — `fix` bumps patch, `feat` bumps minor, and a breaking change bumps major. Everything else is recorded but does not itself force a bump. +- **Breaking changes** — mark with `!` after the scope (`feat(gateway)!: drop the legacy read path`) or a `BREAKING CHANGE:` footer in the PR body. This is the only signal that produces a major bump; forgetting it ships a breaking change as a minor. +- **Scope is optional and free-form.** It is conventional to name the crate or area (`gateway`, `market`, `vault`, `relayer`, `proxy-oracle`, `manager`), but nothing enforces a fixed list: release-plz determines *which* crate to bump from the files a commit touches, not from the scope. +- **Linear IDs go in the PR description, never the title.** `ENG-504: Nest governance under proxy-oracle` is not a valid type and will be rejected — write `refactor(proxy-oracle): nest governance` and reference `ENG-504` in the body. + +Releases themselves are cut by merging the standing "chore: release" PR. See `RELEASING.md`. + ## Build And Test - Format: `cargo fmt` @@ -87,8 +101,10 @@ Notes: - The sandbox gate derives test selection and Cargo package narrowing from one package classification. See "Cross-Cutting Lists To Keep In Sync" below before adding a node-backed crate. If these tests fail because no neard is available, say that clearly instead of silently skipping them. - `cargo test -p templar-common --lib` is a good fast regression check for logic changes in `common`. - Node-backed tests need the contract Wasms prebuilt; rebuilding WASM inside each run is much slower. `just test-sandbox` sources `script/sandbox-up.sh`, which prebuilds them and exports `TEST_CONTRACTS_PREBUILT=1`. If you run a node test by hand outside that recipe (e.g. a plain `cargo test` in owned mode), do the same first. `--stale` works by setting that same variable on the way in: the script then only verifies the artifacts exist (`prebuild-test-contracts --check`) and fails before booting nodes if any are missing. -- Run `./script/check-artifact-drift.sh` when validating checked-in embedded WASM blobs; it is a pure hash/version check (no builds) that verifies each blob matches its pinned `expected_sha256` and catalog version. -- Embedded contract blobs under `contract/artifacts/res/near/` are pinned _release_ artifacts, NOT a mirror of source. **A contract source change does NOT refresh its blob, and no CI check will flag the blob as stale** (the drift check compares blob-vs-pin and version-vs-`Cargo.toml`, never blob-vs-source). When — and only when — you intend a contract source change to become what the gateway deploys, refresh its blob by following `contract/artifacts/README.md` ("⚠️ Refreshing a checked-in blob"): on a clean committed tree, `cargo near build reproducible-wasm --manifest-path /Cargo.toml`, copy the output into `res/near/`, update that entry's `expected_sha256` (+ `version`) in `contract/artifacts/src/ids.rs`, and commit them together. Unreleased work-in-progress is meant to lag the blob — do not refresh reflexively. +- Run `./script/check-artifact-drift.sh` when validating checked-in WASM blobs; it is a pure, build-free check (seconds) covering per-release hash pins, newest-release-vs-`Cargo.toml`, release-list well-formedness, and catalog-vs-disk agreement. +- Contract blobs under `contract/artifacts/res/near///` are **immutable released artifacts**, one directory per released version. Shipping new bytes means *adding* a release, never editing one — historical blobs back the migration and upgrade tests, so changing one silently invalidates them. +- Source is allowed to move ahead of the newest released blob; unreleased work-in-progress is *meant* to lag it. The tripwire is the crate version: bump a contract's `Cargo.toml` version and the drift check fails until you cut the matching blob with `just artifact-release ` (reproducible build from the committed tree → new `res/near/` directory → new entry in `contract/artifacts/src/ids.rs`). Commit the blob and its catalog entry together. +- Reproducibility (do the bytes match what the source actually compiles to?) is verified on release tags by `.github/workflows/release-artifacts.yml`, which rebuilds and requires a byte-for-byte match. ## Cross-Cutting Lists To Keep In Sync @@ -98,8 +114,9 @@ Several CI/test-infra files enumerate crates, contracts, or paths by hand. A fea - `justfile` — add the package to `sandbox_full_packages`; the sandbox filter and Cargo package boundary are both derived from that list. (`templar-gateway-service`'s node tests live in `src/` and are matched separately by module path.) - `.github/workflows/test.yml` — add the crate's `src/**` and `tests/**` under the `changes` job's `near_integration` paths filter, or the test job won't trigger on changes to it. - **Adding or removing a contract / mock WASM**: - - `contract/artifacts/src/ids.rs` — the `ArtifactId` enum and its embedded `res/near/...` blob (see `contract/artifacts/README.md`). This is the canonical list; `script/prebuild-test-contracts.sh` derives from it. + - `contract/artifacts/src/ids.rs` — the `ArtifactId` enum, the catalog entry's `releases` list, an `embedded_bytes_for_version` arm per release, and the `embedded_bytes` arm for the newest one (see `contract/artifacts/README.md`). This is the canonical list; `script/prebuild-test-contracts.sh` derives from it. - `gateway/testing/src/wasm.rs` — the `wasm_fns!` list, so the harness can load it. +- **Releasing a new version of an existing contract**: bump its `Cargo.toml` version, then `just artifact-release ` — the drift check stays red until the blob and its catalog entry land together. See `RELEASING.md`. - **Adding a new top-level source area / crate**: - `.github/workflows/test.yml` paths groups (`near_integration`, `soroban`, `feature_matrix`, `artifact_manifests`) and `.github/workflows/gas-report.yml` paths — add it to the right group so the relevant jobs fire. - Root `Cargo.toml` `[workspace] members` if an existing glob (`gateway/*`, `tools/*`, …) doesn't already cover the path. diff --git a/Cargo.lock b/Cargo.lock index ba3633cdf..426d45fde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13546,7 +13546,7 @@ dependencies = [ [[package]] name = "templar-proxy-oracle-near-contract" -version = "0.3.0" +version = "0.4.0" dependencies = [ "anyhow", "base64 0.22.1", @@ -13582,7 +13582,7 @@ dependencies = [ [[package]] name = "templar-proxy-oracle-near-governance-contract" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/Cargo.toml b/Cargo.toml index 379847656..1a83c620e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,8 @@ members = [ ] [workspace.package] -license = "MIT" +license = "GPL-3.0-only" +rust-version = "1.86" repository = "https://github.com/Templar-Protocol/contracts" edition = "2021" version = "1.2.1" @@ -130,42 +131,42 @@ solana-sdk = "3.0.0" sputnikdao2 = { git = "https://github.com/near-daos/sputnik-dao-contract", rev = "5434a59dec23ee0c49df8c8c4350c208a7e75b0b" } stellar-strkey = { version = "0.0.16", default-features = false } strum = { version = "0.27", features = ["derive"] } -templar-common = { path = "./common" } +templar-common = { path = "./common", version = "1.4.0" } templar-contract-artifacts = { path = "./contract/artifacts" } templar-gateway-artifacts-dispatch = { path = "./gateway/artifacts-dispatch" } templar-gateway-artifacts-spec = { path = "./gateway/artifacts-spec" } -templar-gateway-client = { path = "./gateway/client" } -templar-gateway-core = { path = "./gateway/core" } -templar-gateway-macros = { path = "./gateway/macros" } -templar-gateway-methods-dispatch = { path = "./gateway/methods-dispatch" } -templar-gateway-methods-spec = { path = "./gateway/methods-spec" } +templar-gateway-client = { path = "./gateway/client", version = "0.1.0" } +templar-gateway-core = { path = "./gateway/core", version = "0.1.0" } +templar-gateway-macros = { path = "./gateway/macros", version = "0.1.0" } +templar-gateway-methods-dispatch = { path = "./gateway/methods-dispatch", version = "0.1.0" } +templar-gateway-methods-spec = { path = "./gateway/methods-spec", version = "0.1.0" } templar-gateway-oracle-updates-dispatch = { path = "./gateway/oracle-updates-dispatch" } templar-gateway-oracle-updates-spec = { path = "./gateway/oracle-updates-spec" } templar-gateway-runtime = { path = "./gateway/runtime" } -templar-gateway-store = { path = "./gateway/store" } +templar-gateway-store = { path = "./gateway/store", version = "0.1.0" } templar-gateway-testing = { path = "./gateway/testing" } -templar-gateway-types = { path = "./gateway/types" } -templar-primitives = { path = "./primitives" } -templar-proxy-oracle-governance-kernel = { path = "./contract/proxy-oracle/governance-kernel" } -templar-proxy-oracle-kernel = { path = "./contract/proxy-oracle/kernel" } -templar-proxy-oracle-near-common = { path = "./contract/proxy-oracle/near/common" } +templar-gateway-types = { path = "./gateway/types", version = "0.1.0" } +templar-primitives = { path = "./primitives", version = "0.1.0" } +templar-proxy-oracle-governance-kernel = { path = "./contract/proxy-oracle/governance-kernel", version = "0.1.0" } +templar-proxy-oracle-kernel = { path = "./contract/proxy-oracle/kernel", version = "0.1.0" } +templar-proxy-oracle-near-common = { path = "./contract/proxy-oracle/near/common", version = "0.1.0" } templar-proxy-oracle-near-contract = { path = "./contract/proxy-oracle/near/contract" } -templar-proxy-oracle-near-governance-common = { path = "./contract/proxy-oracle/near/governance-common" } +templar-proxy-oracle-near-governance-common = { path = "./contract/proxy-oracle/near/governance-common", version = "0.1.0" } templar-proxy-oracle-near-governance-contract = { path = "./contract/proxy-oracle/near/governance-contract" } templar-pyth-lazer-adapter-contract = { path = "./contract/pyth-lazer/contract" } templar-pyth-lazer-verifier = { path = "./contract/pyth-lazer/verifier" } templar-redstone-bridge = { path = "./service/redstone-bridge" } templar-tools-common = { path = "./tools/tools-common" } -templar-universal-account = { path = "./universal-account" } +templar-universal-account = { path = "./universal-account", version = "1.2.1" } templar-vault-contract = { path = "./contract/vault/near" } -templar-vault-kernel = { path = "./contract/vault/kernel", features = [ +templar-vault-kernel = { path = "./contract/vault/kernel", version = "1.0.0", features = [ "near", "borsh-schema", "serde", "schemars", "all-actions", ] } -templar-vault-macros = { path = "./contract/vault/macros" } +templar-vault-macros = { path = "./contract/vault/macros", version = "0.1.0" } test-utils = { path = "./test-utils" } thiserror = "2.0.18" tokio = { version = "1.51.0", features = ["full"] } diff --git a/LICENSE b/LICENSE index 7930be326..94a9ed024 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,674 @@ -MIT License - -Copyright (c) 2025 Templar Foundation - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index 031261bd1..40295a229 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Test](https://github.com/Templar-Protocol/contracts/actions/workflows/test.yml/badge.svg)](https://github.com/Templar-Protocol/contracts/actions/workflows/test.yml) [![Kani](https://github.com/Templar-Protocol/contracts/actions/workflows/kani.yml/badge.svg)](https://github.com/Templar-Protocol/contracts/actions/workflows/kani.yml) [![Coverage](https://codecov.io/gh/Templar-Protocol/contracts/branch/dev/graph/badge.svg)](https://codecov.io/gh/Templar-Protocol/contracts) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) Templar Protocol is an overcollateralized lending protocol. This repository contains the core smart contracts, shared protocol logic, operator services, client libraries, test utilities, and supporting tooling used across the protocol. @@ -40,6 +40,10 @@ Templar Protocol is an overcollateralized lending protocol. This repository cont just test ``` +## Contributing + +PRs are squash-merged, so the PR title becomes the commit message and drives automated versioning. Titles must follow `type(scope): summary` — see [Commit And PR Titles](AGENTS.md#commit-and-pr-titles). Releases are cut by merging the standing "chore: release" PR; see [RELEASING.md](RELEASING.md). + ## Links - [Website](https://templarfi.org/) diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 000000000..320262174 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,103 @@ +# Releasing + +Every crate in this workspace versions itself independently, from its own commit +history. Releases are automated by [release-plz](https://release-plz.dev); +configuration lives in [`release-plz.toml`](release-plz.toml). + +## How to cut a release + +1. Land work on `dev` as normal. Each merge updates a standing pull request + titled **"chore: release"**, which accumulates the pending version bumps, + `Cargo.toml` edits, `Cargo.lock` updates, and `CHANGELOG.md` entries for every + affected crate. +2. When you want to ship, review that PR and **merge it**. That is the release + trigger — nothing is released by an ordinary merge to `dev`. +3. On merge, CI tags each released crate (`templar-gateway-client-v0.2.0`) and + cuts its GitHub Release. + +If you never merge the Release PR, nothing is ever released. + +### Choosing version numbers + +Bumps are proposed automatically from the commit messages since each crate's +last tag (`fix` → patch, `feat` → minor, `!`/`BREAKING CHANGE` → major; see +[Commit And PR Titles](AGENTS.md#commit-and-pr-titles)). Pre-1.0 crates follow +Cargo's semver rules, so a breaking change goes `0.1.0` → `0.2.0`. + +To override a proposed version, either comment on the Release PR: + +``` +release-plz set-version templar-gateway-core@2.0.0 +``` + +…or edit the version directly in the PR branch. + +### Editing the Release PR + +The Release PR is an ordinary PR: change versions, rewrite changelog prose, or +drop a crate from the batch. It runs the full `test.yml` gate. + +One behaviour to know: while the branch contains **only** bot commits, release-plz +force-pushes to keep it current. As soon as you push a **human** commit it stops +force-pushing — if more work lands on `dev`, it closes that PR and opens a fresh +one rather than overwrite your edits. So make hand-edits shortly before merging, +not days ahead. + +## Release tiers + +Which crates get what is set per-package in `release-plz.toml`. + +| Tier | Crates | Tag | CHANGELOG | GitHub Release | Registry | +|---|---|---|---|---|---| +| **A — published** | the 17-crate closure external consumers import | ✅ | ✅ | ✅ | ⏸ *deferred* | +| **B — tagged only** | contracts (NEAR and Soroban), `service/*`, `tools/*`, `client/vault` | ✅ | ✅ | ✅ | ❌ | +| **C — internal** | `mock/*`, `fuzz`, `test-utils`, `contract/artifacts`, soroban integration-tests | ❌ | ❌ | ❌ | ❌ | + +Tier B crates are real deliverables that ship somewhere other than a Rust build — +a deployed service, an on-chain WASM blob, a CLI image. They get a citable +version even though nobody `cargo add`s them. Tier C is build and test +scaffolding, where a version number would be noise. + +## ⚠️ Publishing to crates.io is currently blocked + +Tier A is configured but **not publishing yet**. `templar-common` depends on the +RedStone Rust SDK as a git dependency (`redstone`, tag `3.1.0-pre1`), which is not +on crates.io. Cargo refuses to publish any crate that has a git dependency — on +crates.io *or* a private registry — and every Tier A crate reaches +`templar-common`, so the whole closure is blocked. + +Feature-gating does not help: `gateway/core/src/client/redstone_oracle.rs` and +`gateway/methods-dispatch/src/oracle_impl.rs` use +`templar_common::oracle::redstone` types directly. + +**Until it is resolved, consumers pin per-crate git tags** — a real improvement +over pinning a raw commit: + +```toml +templar-gateway-client = { git = "https://github.com/Templar-Protocol/contracts", tag = "templar-gateway-client-v0.1.0" } +``` + +**To unblock**, once RedStone publishes the SDK (or we depend on a published +fork): set `redstone`'s workspace dependency to a registry version, then on each +Tier A block in `release-plz.toml` set `git_only = false` and `publish = true`, +and enable `semver_check` at the workspace level. No code changes are required. +Verify with `cargo publish --dry-run -p ` bottom-up through the closure. + +## Contract WASM artifacts + +Releasing a contract also means cutting its canonical WASM blob. Blobs are +immutable and versioned under `contract/artifacts/res/near///`; +a release *adds* a directory and never overwrites one. See +[`contract/artifacts/README.md`](contract/artifacts/README.md). + +## First-time setup + +- **Baseline tags.** Before the first release-plz run, tag current reality so it + has a starting point for each crate (`templar-common-v1.4.0`, + `templar-market-contract-v1.4.0`, …) from each crate's present `Cargo.toml` + version. Without this, release-plz treats every crate as brand new and replays + the entire history into the first changelog. +- **`RELEASE_PLZ_TOKEN`** (optional but recommended). A PR opened with the + default `GITHUB_TOKEN` does not trigger other workflows, so without a PAT the + Release PR never runs `test.yml` and you would merge an unverified release. +- **`CARGO_REGISTRY_TOKEN`.** Only consulted once Tier A leaves `git_only` mode. diff --git a/client/vault/Cargo.toml b/client/vault/Cargo.toml index 40839cf59..2fd919ad8 100644 --- a/client/vault/Cargo.toml +++ b/client/vault/Cargo.toml @@ -4,6 +4,7 @@ license.workspace = true repository.workspace = true edition.workspace = true version.workspace = true +publish = false [lib] crate-type = ["cdylib"] diff --git a/common/Cargo.toml b/common/Cargo.toml index 78ea1638a..7478efd89 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -4,6 +4,7 @@ license.workspace = true name = "templar-common" repository.workspace = true version = "1.4.0" +description = "Shared contract types, events, assets, and math for Templar Protocol" [dependencies] borsh = { workspace = true, features = ["rc"] } @@ -17,7 +18,7 @@ near-sdk = { workspace = true, default-features = false } primitive-types.workspace = true redstone = { workspace = true, default-features = false, features = [] } schemars.workspace = true -templar-curator-primitives = { path = "../contract/vault/curator-primitives", features = ["borsh", "serde", "std", "borsh-schema", "schemars"] } +templar-curator-primitives = { path = "../contract/vault/curator-primitives", version = "1.0.0", features = ["borsh", "serde", "std", "borsh-schema", "schemars"] } templar-primitives = { workspace = true, features = ["near", "redstone"] } templar-vault-kernel.workspace = true thiserror.workspace = true diff --git a/contract/artifacts/README.md b/contract/artifacts/README.md index b5f057bf3..006aba545 100644 --- a/contract/artifacts/README.md +++ b/contract/artifacts/README.md @@ -34,111 +34,107 @@ This crate deliberately does **not** compile contracts in a build script. Contract compilation is performed by `./script/prebuild-test-contracts.sh` or by `cargo near build`. The crate only *reads* the resulting artifacts. -## Embedded WASM and staleness +## Versioned release blobs -When the `embedded-wasm` feature is active, every `include_bytes!` call -reads from **checked-in files** under `contract/artifacts/res/near/`. -These blobs are pinned in version control and treated as versioned, immutable -release artifacts: source is free to move ahead of a shipped blob, and a blob is -only replaced when you deliberately cut new bytes (see below). +Contract bytes live under: + +``` +res/near///.wasm +``` + +One directory per **released** version. Releases are **immutable**: cutting a +new one *adds* a directory and a catalog entry, and never rewrites an existing +one. Historical blobs are what the migration and upgrade tests deploy — e.g. +`contract/universal-account/tests/migration.rs` upgrades from the real `0.2.0` +and `0.4.0` binaries — so rewriting one silently invalidates those tests. + +Each entry in `ArtifactId::metadata().releases` (oldest first) pins a version and +the SHA-256 of its blob. `ArtifactMetadata::current()` is the newest release — +what the gateway deploys, and what `version()` / `expected_sha256()` / +`version_key()` refer to. + +```rust +// Newest released bytes. +let bytes = ArtifactId::Market.embedded_bytes(); + +// A specific historical release, for upgrade tests. +let old = ArtifactId::UniversalAccount.embedded_bytes_for_version("0.2.0"); +``` ### The prebuild helper (test artifacts) `./script/prebuild-test-contracts.sh` builds contracts into Cargo's `target/near/` for the **test suite** (via `TEST_CONTRACTS_PREBUILT=1`). It uses -fast, non-reproducible `cargo near build` and never touches the checked-in -`res/near/` blobs. +fast, non-reproducible `cargo near build` and never touches `res/near/`. -Set `PREBUILD_TEST_CONTRACTS_JOBS=` to control how many contract builds run -concurrently. If unset, it uses a bounded default based on available CPU -parallelism. Set `PREBUILD_TEST_CONTRACTS_TIMEOUT_SECS=` or pass -`--timeout-secs ` to override the per-contract build timeout; the default is -30 minutes. Pass `--artifact ` to build a subset (repeatable or -comma-separated). Pass `--check` to report which artifacts are missing from -`target/near` and exit non-zero, without building anything. +Set `PREBUILD_TEST_CONTRACTS_JOBS=` to control build concurrency. Set +`PREBUILD_TEST_CONTRACTS_TIMEOUT_SECS=` or pass `--timeout-secs ` to +override the per-contract timeout (default 30 minutes). Pass `--artifact ` +to build a subset (repeatable or comma-separated). Pass `--check` to report which +artifacts are missing from `target/near` and exit non-zero without building. ```bash ./script/prebuild-test-contracts.sh --artifact market ./script/prebuild-test-contracts.sh --artifact market,mock-ft ``` -All catalogued artifacts (production and mock) have embedded bytes available. - -### ⚠️ Refreshing a checked-in blob — READ THIS - -**The embedded blobs do NOT track your source automatically.** They are pinned, -versioned *release* artifacts: the bytes the gateway will deploy on-chain. The -contract source is free to move ahead of them, and **CI will not tell you a blob -is stale**: - -- The **hash-pin check** only verifies `sha256(blob) == expected_sha256`. It - never looks at source. -- The **version-drift check** only verifies the catalog `version` matches the - contract's `Cargo.toml` version. It never looks at the blob's bytes. - -So a contract source change at the **same version** passes CI with a stale blob. -Keeping blobs fresh is a **deliberate, manual step** — this section is the only -thing standing between you and shipping outdated contract bytes. - -#### WHEN to refresh - -Refresh an artifact's blob **whenever you want that contract's current source to -become what the gateway deploys** — i.e. you are promoting a source change to a -shipped/deployed release. Do **not** refresh for every source edit; unreleased -work-in-progress is *meant* to lag the blob. - -Enforced tripwire: **bump the contract's `Cargo.toml` `version` whenever you make -a change you intend to ship.** That bump fails the version-drift check (catalog -`version` ≠ `Cargo.toml` version) and forces you back to this catalog — which is -exactly when you should do the full refresh below. Note the check only forces the -`version` *string* to line up; it does not verify you rebuilt the blob, so when -it fires, do the **whole** procedure, not just the version edit. - -#### HOW to refresh (exact steps, per affected artifact) - -1. **Commit your source change first — the git tree must be clean.** - `cargo near build reproducible-wasm` builds from the committed git state; on a - dirty tree it either hard-errors or embeds the wrong state and produces - non-reproducible bytes. (For a merge: commit the merge, *then* refresh.) -2. Build reproducibly (`` and `` are the entry's - `source_path` and `cargo_target_name` in `ids.rs`): - ```bash - cargo near build reproducible-wasm --manifest-path /Cargo.toml - ``` -3. Copy the output into `res/near/`: - ```bash - cp target/near//.wasm \ - contract/artifacts/res/near//.wasm - ``` -4. In `contract/artifacts/src/ids.rs`, set that entry's `expected_sha256` to the - new hash (printed by the build, or `sha256sum` the copied file) — and its - `version` if the crate version changed. -5. Verify: `./script/check-artifact-drift.sh` (must be green). -6. Commit the blob **and** the `ids.rs` change together, so the bytes and their - pinned hash always land in one reviewable diff. - -### Checking for stale bytes - -Run the drift check — pure, in-memory, no builds: +## Cutting a release + +Source is *allowed* to move ahead of the newest released blob — unreleased +work-in-progress is meant to lag it. The tripwire is the crate version: + +**Bump a contract's `Cargo.toml` version when you intend to ship it.** The drift +check then fails (newest catalogued release ≠ `Cargo.toml` version) until you cut +the blob: ```bash -./script/check-artifact-drift.sh +just artifact-release proxy-oracle ``` -It runs: +That script requires a **clean, committed tree** (`cargo near build +reproducible-wasm` builds from committed git state), builds the contract +reproducibly, installs it at `res/near///`, and prints the +catalog entry to add to `ids.rs`. Add the entry *and* a matching +`embedded_bytes_for_version` arm, then commit the blob together with the catalog +edit so bytes and pin always land in one reviewable diff. + +It refuses to overwrite an existing version — to ship new bytes, bump the +version. + +## Checking consistency ```bash -cargo test -p templar-contract-artifacts --features embedded-wasm,workspace-loader drift_check -- --include-ignored --nocapture +./script/check-artifact-drift.sh ``` -which covers both the **blob hash-pin check** (every embedded blob hashes to its -catalog `expected_sha256`) and the **version drift check** (catalog versions -match `Cargo.toml`), since `drift_check` is a substring filter matching -`embedded_drift_check` and `embedded_version_drift_check`. +Pure, in-memory, no builds, seconds. Four guarantees: + +| Check | Catches | +|---|---| +| `embedded_drift_check` | a release's bytes no longer hash to its pinned `sha256` (including historical releases, which must never change) | +| `embedded_version_drift_check` | the newest release does not match the crate's `Cargo.toml` version — i.e. a version bump whose blob was never cut | +| `catalog_releases_are_well_formed` | empty release lists, duplicate versions, malformed digests | +| `catalog_matches_disk` | a directory on disk nobody catalogued, or a catalog entry whose blob was never committed | + +What this does **not** check is whether the bytes match what the source actually +compiles to — that requires a reproducible rebuild, which runs on release tags +in `.github/workflows/release-artifacts.yml` and fails unless the rebuild is +byte-for-byte identical. + +### Why each release records a `source_commit` + +`cargo near build reproducible-wasm` embeds the source commit into the WASM +(NEP-330), so **the same source built at two different commits produces +different bytes**. Reproducibility is only meaningful *at a specific commit*. + +That is why `ArtifactRelease` carries `source_commit` and the verification +workflow rebuilds there rather than at the release tag: a blob is necessarily +committed *before* the tag that releases it exists, so verifying at the tag +could never match. -If either fails, update the offending catalog entry: for a blob change, refresh -the bytes and `expected_sha256` as above; for a version mismatch, update the -`version` field. +Legacy releases — bytes recovered from a deployed mainnet contract — carry +`source_commit: None`. Their hash pin is still enforced, but they cannot be +rebuilt, and the workflow says so rather than pretending to verify them. ## Usage examples @@ -193,19 +189,5 @@ artifact: templar_contract_artifacts::ArtifactId, ## Artifact list -| Artifact ID | Cargo package | `target/near` directory | -|---------------------|----------------------------------------|-------------------------------------| -| registry | templar-registry-contract | templar_registry_contract | -| market | templar-market-contract | templar_market_contract | -| vault | templar-vault-contract | templar_vault_contract | -| universal-account | templar-universal-account-contract | templar_universal_account_contract | -| proxy-oracle | templar-proxy-oracle-near-contract | templar_proxy_oracle_near_contract | -| proxy-governance | templar-proxy-oracle-near-governance-contract | templar_proxy_oracle_near_governance_contract | -| lst-oracle | templar-lst-oracle-contract | templar_lst_oracle_contract | -| redstone-adapter | templar-redstone-adapter-contract | templar_redstone_adapter_contract | -| pyth-lazer-adapter | templar-pyth-lazer-adapter-contract | templar_pyth_lazer_adapter_contract | -| mock-ft | mock-ft | mock_ft | -| mock-mt | mock-mt | mock_mt | -| mock-oracle | mock-oracle | mock_oracle | -| mock-ref-finance | mock-ref | mock_ref | -| mock-receiver | mock-receiver | mock_receiver | +See `ArtifactId::ALL` in `src/ids.rs` — the catalog is the single source of +truth, and a table here would have no drift check behind it. diff --git a/contract/artifacts/res/near/mock_ft/mock_ft.wasm b/contract/artifacts/res/near/mock_ft/0.0.0/mock_ft.wasm similarity index 100% rename from contract/artifacts/res/near/mock_ft/mock_ft.wasm rename to contract/artifacts/res/near/mock_ft/0.0.0/mock_ft.wasm diff --git a/contract/artifacts/res/near/mock_mt/mock_mt.wasm b/contract/artifacts/res/near/mock_mt/0.0.0/mock_mt.wasm similarity index 100% rename from contract/artifacts/res/near/mock_mt/mock_mt.wasm rename to contract/artifacts/res/near/mock_mt/0.0.0/mock_mt.wasm diff --git a/contract/artifacts/res/near/mock_oracle/mock_oracle.wasm b/contract/artifacts/res/near/mock_oracle/0.0.0/mock_oracle.wasm similarity index 100% rename from contract/artifacts/res/near/mock_oracle/mock_oracle.wasm rename to contract/artifacts/res/near/mock_oracle/0.0.0/mock_oracle.wasm diff --git a/contract/artifacts/res/near/mock_receiver/mock_receiver.wasm b/contract/artifacts/res/near/mock_receiver/1.2.1/mock_receiver.wasm similarity index 100% rename from contract/artifacts/res/near/mock_receiver/mock_receiver.wasm rename to contract/artifacts/res/near/mock_receiver/1.2.1/mock_receiver.wasm diff --git a/contract/artifacts/res/near/mock_ref/mock_ref.wasm b/contract/artifacts/res/near/mock_ref/1.2.1/mock_ref.wasm similarity index 100% rename from contract/artifacts/res/near/mock_ref/mock_ref.wasm rename to contract/artifacts/res/near/mock_ref/1.2.1/mock_ref.wasm diff --git a/contract/artifacts/res/near/templar_lst_oracle_contract/templar_lst_oracle_contract.wasm b/contract/artifacts/res/near/templar_lst_oracle_contract/1.2.1/templar_lst_oracle_contract.wasm similarity index 100% rename from contract/artifacts/res/near/templar_lst_oracle_contract/templar_lst_oracle_contract.wasm rename to contract/artifacts/res/near/templar_lst_oracle_contract/1.2.1/templar_lst_oracle_contract.wasm diff --git a/contract/artifacts/res/near/templar_market_contract/templar_market_contract.wasm b/contract/artifacts/res/near/templar_market_contract/1.4.0/templar_market_contract.wasm similarity index 100% rename from contract/artifacts/res/near/templar_market_contract/templar_market_contract.wasm rename to contract/artifacts/res/near/templar_market_contract/1.4.0/templar_market_contract.wasm diff --git a/gateway/testing/src/wasm/proxy_oracle_v0.wasm b/contract/artifacts/res/near/templar_proxy_oracle_near_contract/0.1.0/templar_proxy_oracle_near_contract.wasm similarity index 100% rename from gateway/testing/src/wasm/proxy_oracle_v0.wasm rename to contract/artifacts/res/near/templar_proxy_oracle_near_contract/0.1.0/templar_proxy_oracle_near_contract.wasm diff --git a/gateway/testing/src/wasm/proxy_oracle_0_3_0.wasm b/contract/artifacts/res/near/templar_proxy_oracle_near_contract/0.3.0/templar_proxy_oracle_near_contract.wasm similarity index 100% rename from gateway/testing/src/wasm/proxy_oracle_0_3_0.wasm rename to contract/artifacts/res/near/templar_proxy_oracle_near_contract/0.3.0/templar_proxy_oracle_near_contract.wasm diff --git a/contract/artifacts/res/near/templar_proxy_oracle_near_contract/0.4.0/templar_proxy_oracle_near_contract.wasm b/contract/artifacts/res/near/templar_proxy_oracle_near_contract/0.4.0/templar_proxy_oracle_near_contract.wasm new file mode 100644 index 000000000..1965c68b5 Binary files /dev/null and b/contract/artifacts/res/near/templar_proxy_oracle_near_contract/0.4.0/templar_proxy_oracle_near_contract.wasm differ diff --git a/contract/artifacts/res/near/templar_proxy_oracle_near_contract/templar_proxy_oracle_near_contract.wasm b/contract/artifacts/res/near/templar_proxy_oracle_near_contract/templar_proxy_oracle_near_contract.wasm deleted file mode 100644 index 9a618602f..000000000 Binary files a/contract/artifacts/res/near/templar_proxy_oracle_near_contract/templar_proxy_oracle_near_contract.wasm and /dev/null differ diff --git a/gateway/testing/src/wasm/proxy_governance_0_1_0.wasm b/contract/artifacts/res/near/templar_proxy_oracle_near_governance_contract/0.1.0/templar_proxy_oracle_near_governance_contract.wasm similarity index 100% rename from gateway/testing/src/wasm/proxy_governance_0_1_0.wasm rename to contract/artifacts/res/near/templar_proxy_oracle_near_governance_contract/0.1.0/templar_proxy_oracle_near_governance_contract.wasm diff --git a/contract/artifacts/res/near/templar_proxy_oracle_near_governance_contract/0.2.0/templar_proxy_oracle_near_governance_contract.wasm b/contract/artifacts/res/near/templar_proxy_oracle_near_governance_contract/0.2.0/templar_proxy_oracle_near_governance_contract.wasm new file mode 100644 index 000000000..7c67e466d Binary files /dev/null and b/contract/artifacts/res/near/templar_proxy_oracle_near_governance_contract/0.2.0/templar_proxy_oracle_near_governance_contract.wasm differ diff --git a/contract/artifacts/res/near/templar_proxy_oracle_near_governance_contract/templar_proxy_oracle_near_governance_contract.wasm b/contract/artifacts/res/near/templar_proxy_oracle_near_governance_contract/templar_proxy_oracle_near_governance_contract.wasm deleted file mode 100644 index 4f6545850..000000000 Binary files a/contract/artifacts/res/near/templar_proxy_oracle_near_governance_contract/templar_proxy_oracle_near_governance_contract.wasm and /dev/null differ diff --git a/contract/artifacts/res/near/templar_pyth_lazer_adapter_contract/templar_pyth_lazer_adapter_contract.wasm b/contract/artifacts/res/near/templar_pyth_lazer_adapter_contract/0.1.0/templar_pyth_lazer_adapter_contract.wasm similarity index 100% rename from contract/artifacts/res/near/templar_pyth_lazer_adapter_contract/templar_pyth_lazer_adapter_contract.wasm rename to contract/artifacts/res/near/templar_pyth_lazer_adapter_contract/0.1.0/templar_pyth_lazer_adapter_contract.wasm diff --git a/contract/artifacts/res/near/templar_redstone_adapter_contract/templar_redstone_adapter_contract.wasm b/contract/artifacts/res/near/templar_redstone_adapter_contract/0.2.0/templar_redstone_adapter_contract.wasm similarity index 100% rename from contract/artifacts/res/near/templar_redstone_adapter_contract/templar_redstone_adapter_contract.wasm rename to contract/artifacts/res/near/templar_redstone_adapter_contract/0.2.0/templar_redstone_adapter_contract.wasm diff --git a/contract/artifacts/res/near/templar_registry_contract/templar_registry_contract.wasm b/contract/artifacts/res/near/templar_registry_contract/1.2.1/templar_registry_contract.wasm similarity index 100% rename from contract/artifacts/res/near/templar_registry_contract/templar_registry_contract.wasm rename to contract/artifacts/res/near/templar_registry_contract/1.2.1/templar_registry_contract.wasm diff --git a/gateway/testing/src/wasm/uac_0_2_0.wasm b/contract/artifacts/res/near/templar_universal_account_contract/0.2.0/templar_universal_account_contract.wasm similarity index 100% rename from gateway/testing/src/wasm/uac_0_2_0.wasm rename to contract/artifacts/res/near/templar_universal_account_contract/0.2.0/templar_universal_account_contract.wasm diff --git a/gateway/testing/src/wasm/uac_0_4_0.wasm b/contract/artifacts/res/near/templar_universal_account_contract/0.4.0/templar_universal_account_contract.wasm similarity index 100% rename from gateway/testing/src/wasm/uac_0_4_0.wasm rename to contract/artifacts/res/near/templar_universal_account_contract/0.4.0/templar_universal_account_contract.wasm diff --git a/contract/artifacts/res/near/templar_universal_account_contract/templar_universal_account_contract.wasm b/contract/artifacts/res/near/templar_universal_account_contract/0.5.0/templar_universal_account_contract.wasm similarity index 100% rename from contract/artifacts/res/near/templar_universal_account_contract/templar_universal_account_contract.wasm rename to contract/artifacts/res/near/templar_universal_account_contract/0.5.0/templar_universal_account_contract.wasm diff --git a/contract/artifacts/res/near/templar_vault_contract/templar_vault_contract.wasm b/contract/artifacts/res/near/templar_vault_contract/1.2.1/templar_vault_contract.wasm similarity index 100% rename from contract/artifacts/res/near/templar_vault_contract/templar_vault_contract.wasm rename to contract/artifacts/res/near/templar_vault_contract/1.2.1/templar_vault_contract.wasm diff --git a/contract/artifacts/src/embedded.rs b/contract/artifacts/src/embedded.rs index 3247c9470..271e09e26 100644 --- a/contract/artifacts/src/embedded.rs +++ b/contract/artifacts/src/embedded.rs @@ -4,29 +4,37 @@ //! //! # Where the bytes live //! -//! Each catalog artifact has its WASM bytes checked in under -//! `contract/artifacts/res/near/{target_name}/{target_name}.wasm`. These files -//! are the **single source of truth** for the embedded bytes and are treated as -//! versioned, pinned release artifacts — source is free to move ahead of a -//! shipped blob. To deliberately ship new bytes for an artifact: +//! One directory per released version: +//! `contract/artifacts/res/near/{target_name}/{version}/{target_name}.wasm`. +//! These files are the **single source of truth** for the embedded bytes. +//! +//! Releases are **immutable**. Shipping new bytes means bumping the contract's +//! crate version and *adding* a release, never editing an existing one — +//! historical blobs are what migration and upgrade tests deploy, so rewriting +//! one silently invalidates those tests. Cut a release with: //! ```bash -//! cargo near build reproducible-wasm --manifest-path /Cargo.toml -//! cp target/near//.wasm \ -//! contract/artifacts/res/near//.wasm +//! just artifact-release //! ``` -//! then update that entry's `expected_sha256` (and `version`) in `ids.rs`. +//! which builds reproducibly from the committed tree, installs the blob, and +//! prints the catalog entry to add to `ids.rs`. //! -//! # Consistency guarantee +//! # Consistency guarantees //! -//! The [`embedded_drift_check`] test verifies every checked-in blob hashes to -//! the `expected_sha256` pinned in its catalog entry — a pure, in-memory check -//! with no rebuild. A blob change is therefore a reviewable edit: the binary and -//! its pinned hash must change together, or the check fails. +//! Source is allowed to move ahead of the newest released blob; unreleased +//! work-in-progress is *meant* to lag it. The checks below are pure and +//! in-memory (no rebuild), and run via `./script/check-artifact-drift.sh`: //! -//! Run the drift check (blob hash and catalog version): -//! ```bash -//! ./script/check-artifact-drift.sh -//! ``` +//! - [`embedded_drift_check`] — every release's blob hashes to its pinned +//! `sha256`, so bytes and pin must change together in one reviewable diff. +//! - [`embedded_version_drift_check`] — the newest release matches the crate's +//! `Cargo.toml` version. This is the tripwire: bumping the version fails the +//! check until the matching blob is cut. +//! - [`catalog_releases_have_embedded_bytes`] / [`catalog_releases_are_well_formed`] +//! / [`catalog_matches_disk`] — the catalog, the byte-loading arms, and the +//! files on disk all agree. +//! +//! Whether the bytes actually match what the source compiles to is verified +//! separately, on release tags, by `.github/workflows/release-artifacts.yml`. use crate::ArtifactId; @@ -101,31 +109,201 @@ mod tests { // Report every mismatch at once rather than panicking on the first, so a // batch refresh is a single edit pass instead of fix-one-then-rerun. + // Checks *every* release, not just the newest: historical blobs are + // immutable, so a change to one is always a mistake. let drifted = ArtifactId::ALL .iter() .map(|id| id.metadata()) - .filter_map(|artifact| { - let actual = sha256_hex(artifact.id.embedded_bytes()); - (actual != artifact.expected_sha256).then(|| { - format!( - " {} — embedded blob is {actual}, catalog pins {}", - artifact.package_name, artifact.expected_sha256, - ) + .flat_map(|artifact| { + artifact.releases.iter().filter_map(move |release| { + let bytes = artifact.id.embedded_bytes_for_version(release.version)?; + let actual = sha256_hex(bytes); + (actual != release.sha256).then(|| { + format!( + " {}@{} — blob is {actual}, catalog pins {}", + artifact.package_name, release.version, release.sha256, + ) + }) }) }) .collect::>(); assert!( drifted.is_empty(), - "Blob hash drift for {} artifact(s) — embedded bytes do not match the \ - `expected_sha256` pinned in ids.rs:\n{}\n\ - If this is an intended blob change, update each entry's \ - `expected_sha256` (and `version`) to match the new bytes.", + "Blob hash drift for {} release(s) — checked-in bytes do not match the \ + `sha256` pinned in ids.rs:\n{}\n\ + Released blobs are immutable. If you meant to ship new bytes, add a \ + NEW release entry (`just artifact-release `) rather than editing \ + an existing one.", drifted.len(), drifted.join("\n"), ); } + /// Every catalogued release must have an `include_bytes!` arm in + /// `embedded_bytes_for_version`, and the newest release must be loadable via + /// `embedded_bytes`. Catches a release added to the catalog table without a + /// matching byte-loading arm (or vice versa). + #[test] + fn catalog_releases_have_embedded_bytes() { + let missing = ArtifactId::ALL + .iter() + .map(|id| id.metadata()) + .flat_map(|artifact| { + artifact + .releases + .iter() + .filter(move |release| { + artifact + .id + .embedded_bytes_for_version(release.version) + .is_none() + }) + .map(move |release| format!(" {}@{}", artifact.package_name, release.version)) + }) + .collect::>(); + + assert!( + missing.is_empty(), + "{} catalogued release(s) have no embedded bytes — add an arm to \ + `ArtifactId::embedded_bytes_for_version`:\n{}", + missing.len(), + missing.join("\n"), + ); + + // An unknown version must be `None`, not a panic or the wrong blob. + assert!(ArtifactId::Market + .embedded_bytes_for_version("0.0.0-nonexistent") + .is_none()); + } + + /// `embedded_bytes` is a separate direct match (so the linker can drop + /// historical blobs from production binaries), which means its arms could + /// drift from the catalog's newest release. This is what stops that. + #[test] + fn embedded_bytes_matches_current_release() { + for artifact in ArtifactId::ALL.iter().map(|id| id.metadata()) { + let via_version = artifact + .id + .embedded_bytes_for_version(artifact.version()) + .unwrap_or_else(|| { + panic!( + "{} has no blob for its current version {}", + artifact.package_name, + artifact.version(), + ) + }); + assert!( + std::ptr::eq(artifact.id.embedded_bytes(), via_version), + "{}: `embedded_bytes` does not point at the current release ({}). \ + Update its arm in `ArtifactId::embedded_bytes`.", + artifact.package_name, + artifact.version(), + ); + } + } + + /// Structural invariants of each artifact's release list. + #[test] + fn catalog_releases_are_well_formed() { + for artifact in ArtifactId::ALL.iter().map(|id| id.metadata()) { + assert!( + !artifact.releases.is_empty(), + "{} has no releases; every artifact needs at least one set of \ + deployable bytes", + artifact.package_name, + ); + + let mut seen = std::collections::HashSet::new(); + for release in artifact.releases { + assert!( + seen.insert(release.version), + "{} lists version {} more than once — two different blobs \ + cannot both be that version", + artifact.package_name, + release.version, + ); + assert_eq!( + release.sha256.len(), + 64, + "{}@{} sha256 is not a 64-char hex digest", + artifact.package_name, + release.version, + ); + // A malformed commit would silently disable reproducible + // verification for this release, so hold it to the same bar. + if let Some(commit) = release.source_commit { + assert_eq!( + commit.len(), + 40, + "{}@{} source_commit is not a full 40-char git sha", + artifact.package_name, + release.version, + ); + } + } + + // `current()` is defined as "newest", and the rest of the crate + // relies on that being the last entry. + assert_eq!( + artifact.current().version, + artifact + .releases + .last() + .expect("non-empty checked above") + .version, + ); + } + } + + /// Every `res/near/**` blob on disk must be catalogued, and every catalogued + /// release must exist on disk. Catches an orphaned directory left behind by + /// a rename, and a catalog entry pointing at bytes nobody committed. + #[test] + fn catalog_matches_disk() { + let res = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("res/near"); + + let mut on_disk = std::collections::BTreeSet::new(); + for target in std::fs::read_dir(&res).expect("res/near is missing") { + let target = target.expect("unreadable res/near entry").path(); + let target_name = target + .file_name() + .and_then(|n| n.to_str()) + .expect("non-UTF8 artifact directory") + .to_owned(); + for version in std::fs::read_dir(&target).expect("unreadable artifact directory") { + let version = version.expect("unreadable version entry").path(); + let version_name = version + .file_name() + .and_then(|n| n.to_str()) + .expect("non-UTF8 version directory") + .to_owned(); + on_disk.insert((target_name.clone(), version_name)); + } + } + + let catalogued = ArtifactId::ALL + .iter() + .map(|id| id.metadata()) + .flat_map(|artifact| { + artifact + .releases + .iter() + .map(move |r| (artifact.cargo_target_name.to_owned(), r.version.to_owned())) + }) + .collect::>(); + + let orphaned = on_disk.difference(&catalogued).collect::>(); + let phantom = catalogued.difference(&on_disk).collect::>(); + + assert!( + orphaned.is_empty() && phantom.is_empty(), + "res/near and the catalog disagree.\n\ + On disk but not catalogued (delete, or add a release entry): {orphaned:?}\n\ + Catalogued but not on disk (commit the blob): {phantom:?}", + ); + } + /// **Version-key drift check** — verifies that every artifact's /// `metadata.version` matches the actual Cargo.toml version of its /// package. Requires the `workspace-loader` feature because version @@ -156,12 +334,18 @@ mod tests { let actual_version = package.version.to_string(); assert_eq!( - artifact.version, actual_version, - "Version drift for {} ({}) — catalog version '{}' does not \ - match Cargo.toml version '{}'.\n\ - Update the version field in contract/artifacts/src/ids.rs \ - for this artifact.", - artifact.package_name, artifact.package_name, artifact.version, actual_version, + artifact.version(), + actual_version, + "Version drift for {} — the newest catalogued release is '{}' but \ + Cargo.toml says '{}'.\n\ + If you bumped the crate version, that release's blob has not been \ + cut yet: run `just artifact-release {}` to build it reproducibly \ + and add the release entry. Do NOT simply edit the version string — \ + the bytes and the version must move together.", + artifact.package_name, + artifact.version(), + actual_version, + artifact.id, ); } } diff --git a/contract/artifacts/src/ids.rs b/contract/artifacts/src/ids.rs index 26c40df07..23223eb20 100644 --- a/contract/artifacts/src/ids.rs +++ b/contract/artifacts/src/ids.rs @@ -5,15 +5,18 @@ //! the single source of truth for artifact names, paths, and how they map //! to `target/near` directories. //! -//! ⚠️ The `expected_sha256` / `version` in each entry pin a *release* blob under -//! `res/near/`, NOT a mirror of current source. Changing a contract's source -//! does NOT refresh its blob, and CI will NOT catch a stale blob (the hash-pin -//! check compares blob vs pin, never blob vs source). When you want a source -//! change to become what the gateway deploys, follow the refresh procedure in -//! `contract/artifacts/README.md` ("Refreshing a checked-in blob") and update -//! the blob + `expected_sha256` (+ `version`) together. Bumping a contract's -//! `Cargo.toml` version fails the version-drift check until this catalog's -//! `version` is updated — treat that as your cue to do the full refresh. +//! ⚠️ Each entry's `releases` list pins *released* blobs under +//! `res/near///`, NOT a mirror of current source. Source is +//! allowed to move ahead of the newest release — unreleased work-in-progress is +//! meant to lag it. +//! +//! Releases are **immutable**: ship new bytes by bumping the contract's +//! `Cargo.toml` version and *adding* a release, never by editing one. Historical +//! blobs are what the migration and upgrade tests deploy, so rewriting one +//! silently invalidates them. +//! +//! Bumping the version fails the version-drift check until the matching blob is +//! cut with `just artifact-release `. See `contract/artifacts/README.md`. use std::{fmt, path::Path, str::FromStr}; @@ -125,45 +128,70 @@ impl ArtifactId { .find(|id| id.metadata().package_name == package_name) } + /// Bytes of the **newest** released version — what the gateway deploys. + /// + /// A direct `match self`, deliberately NOT routed through + /// [`Self::embedded_bytes_for_version`]: that takes a runtime `&str`, so + /// every historical blob would be reachable and the linker could not drop + /// them. Going through it put ~2 MB of test-only historical bytes into the + /// shipped gateway binary. `embedded_bytes_matches_current_release` keeps + /// these arms honest. #[cfg(feature = "embedded-wasm")] pub fn embedded_bytes(self) -> &'static [u8] { match self { - Self::Registry => include_bytes!( - "../res/near/templar_registry_contract/templar_registry_contract.wasm" - ), - Self::Market => include_bytes!( - "../res/near/templar_market_contract/templar_market_contract.wasm" - ), - Self::Vault => include_bytes!( - "../res/near/templar_vault_contract/templar_vault_contract.wasm" - ), - Self::UniversalAccount => include_bytes!( - "../res/near/templar_universal_account_contract/templar_universal_account_contract.wasm" - ), - Self::ProxyOracle => include_bytes!( - "../res/near/templar_proxy_oracle_near_contract/templar_proxy_oracle_near_contract.wasm" - ), - Self::ProxyGovernance => include_bytes!( - "../res/near/templar_proxy_oracle_near_governance_contract/templar_proxy_oracle_near_governance_contract.wasm" - ), - Self::LstOracle => include_bytes!( - "../res/near/templar_lst_oracle_contract/templar_lst_oracle_contract.wasm" - ), - Self::RedstoneAdapter => include_bytes!( - "../res/near/templar_redstone_adapter_contract/templar_redstone_adapter_contract.wasm" - ), - Self::PythLazerAdapter => include_bytes!( - "../res/near/templar_pyth_lazer_adapter_contract/templar_pyth_lazer_adapter_contract.wasm" - ), - Self::MockFt => include_bytes!("../res/near/mock_ft/mock_ft.wasm"), - Self::MockMt => include_bytes!("../res/near/mock_mt/mock_mt.wasm"), - Self::MockOracle => include_bytes!("../res/near/mock_oracle/mock_oracle.wasm"), - Self::MockRefFinance => include_bytes!("../res/near/mock_ref/mock_ref.wasm"), - Self::MockReceiver => include_bytes!("../res/near/mock_receiver/mock_receiver.wasm"), + Self::Registry => include_bytes!("../res/near/templar_registry_contract/1.2.1/templar_registry_contract.wasm"), + Self::Market => include_bytes!("../res/near/templar_market_contract/1.4.0/templar_market_contract.wasm"), + Self::Vault => include_bytes!("../res/near/templar_vault_contract/1.2.1/templar_vault_contract.wasm"), + Self::UniversalAccount => include_bytes!("../res/near/templar_universal_account_contract/0.5.0/templar_universal_account_contract.wasm"), + Self::ProxyOracle => include_bytes!("../res/near/templar_proxy_oracle_near_contract/0.4.0/templar_proxy_oracle_near_contract.wasm"), + Self::ProxyGovernance => include_bytes!("../res/near/templar_proxy_oracle_near_governance_contract/0.2.0/templar_proxy_oracle_near_governance_contract.wasm"), + Self::LstOracle => include_bytes!("../res/near/templar_lst_oracle_contract/1.2.1/templar_lst_oracle_contract.wasm"), + Self::RedstoneAdapter => include_bytes!("../res/near/templar_redstone_adapter_contract/0.2.0/templar_redstone_adapter_contract.wasm"), + Self::PythLazerAdapter => include_bytes!("../res/near/templar_pyth_lazer_adapter_contract/0.1.0/templar_pyth_lazer_adapter_contract.wasm"), + Self::MockFt => include_bytes!("../res/near/mock_ft/0.0.0/mock_ft.wasm"), + Self::MockMt => include_bytes!("../res/near/mock_mt/0.0.0/mock_mt.wasm"), + Self::MockOracle => include_bytes!("../res/near/mock_oracle/0.0.0/mock_oracle.wasm"), + Self::MockRefFinance => include_bytes!("../res/near/mock_ref/1.2.1/mock_ref.wasm"), + Self::MockReceiver => include_bytes!("../res/near/mock_receiver/1.2.1/mock_receiver.wasm"), } } -} + /// Bytes of a specific released version, or `None` if that version was never + /// released. Used by migration and upgrade tests to deploy the real + /// historical binary rather than an approximation of it. + #[cfg(feature = "embedded-wasm")] + // An exhaustive (artifact, version) → blob table, one arm per catalogued + // release. It grows by one arm per release and has no branching logic to + // factor out; splitting it would only scatter the mapping. + #[allow(clippy::too_many_lines)] + pub fn embedded_bytes_for_version(self, version: &str) -> Option<&'static [u8]> { + // Every arm mirrors one `ArtifactRelease` in the catalog below; + // `catalog_releases_have_embedded_bytes` fails if the two drift apart. + let bytes: &'static [u8] = match (self, version) { + (Self::Registry, "1.2.1") => include_bytes!("../res/near/templar_registry_contract/1.2.1/templar_registry_contract.wasm"), + (Self::Market, "1.4.0") => include_bytes!("../res/near/templar_market_contract/1.4.0/templar_market_contract.wasm"), + (Self::Vault, "1.2.1") => include_bytes!("../res/near/templar_vault_contract/1.2.1/templar_vault_contract.wasm"), + (Self::UniversalAccount, "0.2.0") => include_bytes!("../res/near/templar_universal_account_contract/0.2.0/templar_universal_account_contract.wasm"), + (Self::UniversalAccount, "0.4.0") => include_bytes!("../res/near/templar_universal_account_contract/0.4.0/templar_universal_account_contract.wasm"), + (Self::UniversalAccount, "0.5.0") => include_bytes!("../res/near/templar_universal_account_contract/0.5.0/templar_universal_account_contract.wasm"), + (Self::ProxyOracle, "0.1.0") => include_bytes!("../res/near/templar_proxy_oracle_near_contract/0.1.0/templar_proxy_oracle_near_contract.wasm"), + (Self::ProxyOracle, "0.3.0") => include_bytes!("../res/near/templar_proxy_oracle_near_contract/0.3.0/templar_proxy_oracle_near_contract.wasm"), + (Self::ProxyOracle, "0.4.0") => include_bytes!("../res/near/templar_proxy_oracle_near_contract/0.4.0/templar_proxy_oracle_near_contract.wasm"), + (Self::ProxyGovernance, "0.1.0") => include_bytes!("../res/near/templar_proxy_oracle_near_governance_contract/0.1.0/templar_proxy_oracle_near_governance_contract.wasm"), + (Self::ProxyGovernance, "0.2.0") => include_bytes!("../res/near/templar_proxy_oracle_near_governance_contract/0.2.0/templar_proxy_oracle_near_governance_contract.wasm"), + (Self::LstOracle, "1.2.1") => include_bytes!("../res/near/templar_lst_oracle_contract/1.2.1/templar_lst_oracle_contract.wasm"), + (Self::RedstoneAdapter, "0.2.0") => include_bytes!("../res/near/templar_redstone_adapter_contract/0.2.0/templar_redstone_adapter_contract.wasm"), + (Self::PythLazerAdapter, "0.1.0") => include_bytes!("../res/near/templar_pyth_lazer_adapter_contract/0.1.0/templar_pyth_lazer_adapter_contract.wasm"), + (Self::MockFt, "0.0.0") => include_bytes!("../res/near/mock_ft/0.0.0/mock_ft.wasm"), + (Self::MockMt, "0.0.0") => include_bytes!("../res/near/mock_mt/0.0.0/mock_mt.wasm"), + (Self::MockOracle, "0.0.0") => include_bytes!("../res/near/mock_oracle/0.0.0/mock_oracle.wasm"), + (Self::MockRefFinance, "1.2.1") => include_bytes!("../res/near/mock_ref/1.2.1/mock_ref.wasm"), + (Self::MockReceiver, "1.2.1") => include_bytes!("../res/near/mock_receiver/1.2.1/mock_receiver.wasm"), + _ => return None, + }; + Some(bytes) + } +} impl fmt::Display for ArtifactId { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(self.as_str()) @@ -184,6 +212,32 @@ impl FromStr for ArtifactId { } } +/// One released version of a contract, and the SHA-256 of the exact bytes that +/// were shipped for it. +/// +/// Releases are immutable. Cutting a new release *adds* an entry and a directory +/// under `res/near/{cargo_target_name}/{version}/`; it never rewrites an +/// existing one. That is what lets migration and upgrade tests deploy the real +/// historical bytes rather than an approximation of them. +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, schemars::JsonSchema)] +pub struct ArtifactRelease { + /// Crate version this blob was released as. + pub version: &'static str, + /// SHA-256 (lowercase hex) of the checked-in blob for this version. + /// + /// Pinned so a blob change is a reviewable, greppable edit rather than an + /// opaque binary diff: any change to the bytes must land with a matching + /// update here, or the drift check fails. + pub sha256: &'static str, + /// Git commit the blob was reproducibly built from; `None` for legacy blobs + /// recovered from a deployed contract, which cannot be rebuilt. + /// + /// `cargo near build reproducible-wasm` embeds the source commit per + /// NEP-330, so a rebuild is byte-identical **only at the same commit** — + /// which is why the release tag alone cannot verify a blob. + pub source_commit: Option<&'static str>, +} + /// Canonical metadata for a single contract artifact. /// /// Every field is a compile-time constant derived from the workspace layout @@ -198,17 +252,10 @@ pub struct ArtifactMetadata { pub cargo_target_name: &'static str, /// Source path relative to the workspace root (e.g. `contract/market`). pub source_path: &'static str, - /// Artifact version used in version keys (`{package}@{version}#{sha256}`). - /// - /// Set to the crate/workspace version at the time the embedded WASM blob was - /// checked in. Updated alongside the blob in `res/near/`. - pub version: &'static str, - /// SHA-256 (lowercase hex) of the checked-in `res/near/` blob. + /// Every released version of this contract, **oldest first**. /// - /// Pinned so a blob change is a reviewable, greppable edit rather than an - /// opaque binary diff: any change to the embedded bytes must land with a - /// matching update here, or the drift check fails. See `embedded_drift_check`. - pub expected_sha256: &'static str, + /// Never empty: an artifact with no release has no bytes to deploy. + pub releases: &'static [ArtifactRelease], } impl ArtifactMetadata { @@ -216,8 +263,38 @@ impl ArtifactMetadata { Path::new(self.source_path).join("Cargo.toml") } + /// The newest release — what the gateway deploys and what `version_key` + /// and `embedded_bytes` refer to by default. + /// + /// # Panics + /// Never in practice: `releases` is non-empty for every catalog entry, and + /// `catalog_releases_are_well_formed` enforces it. + // Infallible: every catalog entry ships at least one release, enforced by + // `catalog_releases_are_well_formed`. + #[allow(clippy::expect_used)] + pub fn current(&self) -> &'static ArtifactRelease { + self.releases + .last() + .expect("catalog entry has no releases; see catalog_releases_are_well_formed") + } + + /// Version of the newest release. + pub fn version(&self) -> &'static str { + self.current().version + } + + /// Pinned SHA-256 of the newest release's blob. + pub fn expected_sha256(&self) -> &'static str { + self.current().sha256 + } + + /// Look up a specific released version. + pub fn release(&self, version: &str) -> Option<&'static ArtifactRelease> { + self.releases.iter().find(|r| r.version == version) + } + pub fn version_key(&self, wasm_bytes: &[u8]) -> String { - crate::format_version_key(self.package_name, self.version, wasm_bytes) + crate::format_version_key(self.package_name, self.version(), wasm_bytes) } } @@ -233,14 +310,13 @@ pub fn artifact_catalog() -> impl ExactSizeIterator { + ($id:ident, $pkg:expr, $target:expr, $src:expr, [$(($ver:expr, $sha:expr, $commit:expr)),+ $(,)?]) => { ArtifactMetadata { id: ArtifactId::$id, package_name: $pkg, cargo_target_name: $target, source_path: $src, - version: $ver, - expected_sha256: $sha, + releases: &[$(ArtifactRelease { version: $ver, sha256: $sha, source_commit: $commit }),+], } }; } @@ -250,110 +326,183 @@ static REGISTRY_METADATA: ArtifactMetadata = entry!( "templar-registry-contract", "templar_registry_contract", "contract/registry", - "1.2.1", - "2512b842e31f8427fb0a47df4f1592de6babf6b13171af60069e3cf450423aa2" + [( + "1.2.1", + "2512b842e31f8427fb0a47df4f1592de6babf6b13171af60069e3cf450423aa2", + None + ),] ); static MARKET_METADATA: ArtifactMetadata = entry!( Market, "templar-market-contract", "templar_market_contract", "contract/market", - "1.4.0", - "8f2c487ebc873e3d6de7e8d2dc4d20b142ab22073e04ae89687746ebaca6ca52" + [( + "1.4.0", + "8f2c487ebc873e3d6de7e8d2dc4d20b142ab22073e04ae89687746ebaca6ca52", + None + ),] ); static VAULT_METADATA: ArtifactMetadata = entry!( Vault, "templar-vault-contract", "templar_vault_contract", "contract/vault/near", - "1.2.1", - "fc605cd4a3e09fdef3620ed9c6a4610bb639d7a5f625d648780a96b7d452ef18" + [( + "1.2.1", + "fc605cd4a3e09fdef3620ed9c6a4610bb639d7a5f625d648780a96b7d452ef18", + None + ),] ); static UNIVERSAL_ACCOUNT_METADATA: ArtifactMetadata = entry!( UniversalAccount, "templar-universal-account-contract", "templar_universal_account_contract", "contract/universal-account", - "0.5.0", - "7dae78aaf868844af5655d530c50f72a4a74baed92d1592c89c704989e4589c7" + [ + ( + "0.2.0", + "25ae83a0ee7d31542bd7b6039549f200cdd96f7bcaef56dd6763dd143ef00c2d", + None + ), + ( + "0.4.0", + "007d0a4643f63b3b2f543b0033f059ebc38b07365ff86aff1aa4476f6d73f9ae", + None + ), + ( + "0.5.0", + "7dae78aaf868844af5655d530c50f72a4a74baed92d1592c89c704989e4589c7", + None + ), + ] ); static PROXY_ORACLE_METADATA: ArtifactMetadata = entry!( ProxyOracle, "templar-proxy-oracle-near-contract", "templar_proxy_oracle_near_contract", "contract/proxy-oracle/near/contract", - "0.3.0", - "8f4da3363885b842e655c993a4247fe4fb08d6f4eb88db20846c4a0d0156ee0b" + [ + ( + "0.1.0", + "fb697b18f30cc19d4fc43768eae04ae94967663c4744ab052c7119c6d869d53b", + None + ), + ( + "0.3.0", + "d2e62c4566c98e55121a5aad32e0e5b8cfb911f82aca71dbaeaa83794fed9e8e", + None + ), + ( + "0.4.0", + "f03b159e9132a59ab929463866c672b734a5950fb41520ec33ad7a97030d5770", + Some("ea0a6357c89748e64b43036b776cd9459078af00") + ), + ] ); static PROXY_GOVERNANCE_METADATA: ArtifactMetadata = entry!( ProxyGovernance, "templar-proxy-oracle-near-governance-contract", "templar_proxy_oracle_near_governance_contract", "contract/proxy-oracle/near/governance-contract", - "0.1.0", - "084d6107838c7bd4d250237dafa12a618053dabadf20b70141db02fc501f7bdd" + [ + ( + "0.1.0", + "09ecfafa86bfdca5e05b9174590cd056d59bf3a9d8727e9d452cfb98701334b0", + None + ), + ( + "0.2.0", + "8de3b54494ef3601172543596aa91ec10a264da1093e6d413cbebddab4edb104", + Some("9d4477416f9b8d7bf1dc103ed5a9d788d13d9be2") + ), + ] ); static LST_ORACLE_METADATA: ArtifactMetadata = entry!( LstOracle, "templar-lst-oracle-contract", "templar_lst_oracle_contract", "contract/proxy-oracle/near/lst-contract", - "1.2.1", - "5ef7bedf78f3a3ecc9747b8aa3bb25ef6bd508d3c17ec166571f76f53ce8ee50" + [( + "1.2.1", + "5ef7bedf78f3a3ecc9747b8aa3bb25ef6bd508d3c17ec166571f76f53ce8ee50", + None + ),] ); static REDSTONE_ADAPTER_METADATA: ArtifactMetadata = entry!( RedstoneAdapter, "templar-redstone-adapter-contract", "templar_redstone_adapter_contract", "contract/redstone-adapter", - "0.2.0", - "b513b2e839ce1ea59ef4c57519ef9482b133f47c81db0d3a54517b2cd251511a" + [( + "0.2.0", + "b513b2e839ce1ea59ef4c57519ef9482b133f47c81db0d3a54517b2cd251511a", + None + ),] ); static PYTH_LAZER_ADAPTER_METADATA: ArtifactMetadata = entry!( PythLazerAdapter, "templar-pyth-lazer-adapter-contract", "templar_pyth_lazer_adapter_contract", "contract/pyth-lazer/contract", - "0.1.0", - "c993256a8b42313b2b0b024c783b4eb5a7be1c8b9f792789cb4f207f7007060b" + [( + "0.1.0", + "c993256a8b42313b2b0b024c783b4eb5a7be1c8b9f792789cb4f207f7007060b", + None + ),] ); static MOCK_FT_METADATA: ArtifactMetadata = entry!( MockFt, "mock-ft", "mock_ft", "mock/ft", - "0.0.0", - "c43561acd98e1a8d93ba85955f23847bcccd738f85703e914c3d4218471c262d" + [( + "0.0.0", + "c43561acd98e1a8d93ba85955f23847bcccd738f85703e914c3d4218471c262d", + None + ),] ); static MOCK_MT_METADATA: ArtifactMetadata = entry!( MockMt, "mock-mt", "mock_mt", "mock/mt", - "0.0.0", - "cd125c142722e48e5e1b6f350c751ed0691221d065c540dad02804dbaf55b453" + [( + "0.0.0", + "cd125c142722e48e5e1b6f350c751ed0691221d065c540dad02804dbaf55b453", + None + ),] ); static MOCK_ORACLE_METADATA: ArtifactMetadata = entry!( MockOracle, "mock-oracle", "mock_oracle", "mock/oracle", - "0.0.0", - "76f76816cf4d0ccaf4b4e181ee1104ec8e6cbd13084f311b87a86087099a749c" + [( + "0.0.0", + "76f76816cf4d0ccaf4b4e181ee1104ec8e6cbd13084f311b87a86087099a749c", + None + ),] ); static MOCK_REF_FINANCE_METADATA: ArtifactMetadata = entry!( MockRefFinance, "mock-ref", "mock_ref", "mock/ref", - "1.2.1", - "d2be82aa462f55baa2333bae898bee424560e7bf7342b606f6bd40c1aa8369c4" + [( + "1.2.1", + "d2be82aa462f55baa2333bae898bee424560e7bf7342b606f6bd40c1aa8369c4", + None + ),] ); static MOCK_RECEIVER_METADATA: ArtifactMetadata = entry!( MockReceiver, "mock-receiver", "mock_receiver", "mock/receiver", - "1.2.1", - "bf814164155b927b4e703d8e870137b7f8ab39cd2a69666a0b900bfe0c8a8ec5" + [( + "1.2.1", + "bf814164155b927b4e703d8e870137b7f8ab39cd2a69666a0b900bfe0c8a8ec5", + None + ),] ); diff --git a/contract/artifacts/src/lib.rs b/contract/artifacts/src/lib.rs index 31acce53f..23bd83c37 100644 --- a/contract/artifacts/src/lib.rs +++ b/contract/artifacts/src/lib.rs @@ -25,7 +25,9 @@ mod workspace_loader; #[cfg(feature = "embedded-wasm")] pub use embedded::embedded_sizes; -pub use ids::{artifact_catalog, ArtifactId, ArtifactMetadata, ArtifactParseError}; +pub use ids::{ + artifact_catalog, ArtifactId, ArtifactMetadata, ArtifactParseError, ArtifactRelease, +}; #[cfg(feature = "workspace-loader")] pub use workspace_loader::{ build_artifact, load_artifact, load_artifact_bytes, BuildContractError, LoadError, @@ -120,7 +122,7 @@ mod tests { let meta = ArtifactId::Vault.metadata(); assert_eq!(meta.id, ArtifactId::Vault); assert_eq!(meta.package_name, "templar-vault-contract"); - assert_eq!(meta.version, "1.2.1"); + assert_eq!(meta.version(), "1.2.1"); } #[test] @@ -255,6 +257,9 @@ mod tests { assert_eq!(parsed["package_name"], "templar-market-contract"); assert_eq!(parsed["cargo_target_name"], "templar_market_contract"); assert_eq!(parsed["source_path"], "contract/market"); - assert_eq!(parsed["version"], "1.4.0"); + // Versions live in the release list; `version` is a derived accessor, + // not a serialized field. + assert_eq!(parsed["releases"][0]["version"], "1.4.0"); + assert_eq!(meta.version(), "1.4.0"); } } diff --git a/contract/artifacts/src/prebuild.rs b/contract/artifacts/src/prebuild.rs index 723595fa5..cbeeef523 100644 --- a/contract/artifacts/src/prebuild.rs +++ b/contract/artifacts/src/prebuild.rs @@ -43,12 +43,75 @@ struct Args { /// Report whether the selected artifacts are already built, without building them. #[arg(long)] check: bool, + + /// Print ` ` for each selected + /// artifact and exit, without building. Lets shell tooling + /// (`script/artifact-release.sh`, `release-artifacts.yml`) read the catalog + /// instead of re-deriving paths and drifting from it. + #[arg(long)] + print_metadata: bool, + + /// Print the `source_commit` of the given released version of the single + /// selected artifact and exit, or an empty line for a legacy blob that + /// cannot be reproducibly rebuilt. + #[arg(long, value_name = "VERSION")] + print_release: Option, } pub fn main() -> ExitCode { let args = Args::parse(); let artifacts = selected_artifacts(&args.artifacts); + if args.print_metadata { + let metadata = match workspace_loader::get_metadata(&args.workspace_root) { + Ok(metadata) => metadata, + Err(error) => { + eprintln!("failed to read cargo metadata: {error}"); + return ExitCode::FAILURE; + } + }; + for artifact in &artifacts { + // Version comes from the crate's own Cargo.toml, so callers never + // re-derive it (and cannot disagree with the drift check). + let Some(package) = workspace_loader::find_package(&metadata, artifact.package_name) + else { + eprintln!( + "package {} not in workspace metadata", + artifact.package_name + ); + return ExitCode::FAILURE; + }; + println!( + "{} {} {}", + artifact.source_path, artifact.cargo_target_name, package.version, + ); + } + return ExitCode::SUCCESS; + } + + if let Some(version) = &args.print_release { + let [artifact] = artifacts[..] else { + eprintln!("--print-release needs exactly one --artifact"); + return ExitCode::FAILURE; + }; + let Some(release) = artifact.release(version) else { + eprintln!( + "{} has no release {version}; catalogued: {}", + artifact.package_name, + artifact + .releases + .iter() + .map(|r| r.version) + .collect::>() + .join(", "), + ); + return ExitCode::FAILURE; + }; + // Empty line = legacy blob that cannot be rebuilt. + println!("{}", release.source_commit.unwrap_or_default()); + return ExitCode::SUCCESS; + } + let result = if args.check { check_all(&args.workspace_root, &artifacts) } else { diff --git a/contract/market/Cargo.toml b/contract/market/Cargo.toml index 40300e993..2490557c5 100644 --- a/contract/market/Cargo.toml +++ b/contract/market/Cargo.toml @@ -4,6 +4,7 @@ license.workspace = true name = "templar-market-contract" repository.workspace = true version = "1.4.0" +publish = false [lib] crate-type = ["cdylib", "rlib"] diff --git a/contract/proxy-oracle/governance-kernel/Cargo.toml b/contract/proxy-oracle/governance-kernel/Cargo.toml index 061c685db..fae63df36 100644 --- a/contract/proxy-oracle/governance-kernel/Cargo.toml +++ b/contract/proxy-oracle/governance-kernel/Cargo.toml @@ -4,12 +4,13 @@ license.workspace = true repository.workspace = true edition.workspace = true version = "0.1.0" +description = "Storage-agnostic governance kernel for Templar proxy oracles" [dependencies] borsh = { version = "1.5", default-features = false, features = ["derive", "unstable__schema"], optional = true } schemars = { version = "0.8", default-features = false, features = ["derive"], optional = true } serde = { version = "1", default-features = false, features = ["alloc", "derive"], optional = true } -templar-primitives = { path = "../../../primitives", default-features = false, features = [] } +templar-primitives = { path = "../../../primitives", version = "0.1.0", default-features = false, features = [] } [dev-dependencies] rstest.workspace = true diff --git a/contract/proxy-oracle/kernel/Cargo.toml b/contract/proxy-oracle/kernel/Cargo.toml index 82034759b..6d540db5c 100644 --- a/contract/proxy-oracle/kernel/Cargo.toml +++ b/contract/proxy-oracle/kernel/Cargo.toml @@ -4,6 +4,7 @@ license.workspace = true repository.workspace = true edition.workspace = true version = "0.1.0" +description = "Storage-agnostic price-aggregation kernel for Templar proxy oracles" [dependencies] borsh = { version = "1.5", default-features = false, features = ["derive", "unstable__schema"], optional = true } @@ -11,7 +12,7 @@ getrandom = { version = "0.2", features = ["custom"], optional = true } hex = { version = "0.4", default-features = false, features = ["alloc"], optional = true } serde = { version = "1", default-features = false, features = ["alloc", "derive"], optional = true } schemars = { version = "0.8", default-features = false, features = ["derive"], optional = true } -templar-primitives = { path = "../../../primitives", default-features = false, features = [] } +templar-primitives = { path = "../../../primitives", version = "0.1.0", default-features = false, features = [] } [dev-dependencies] hex-literal.workspace = true diff --git a/contract/proxy-oracle/near/common/Cargo.toml b/contract/proxy-oracle/near/common/Cargo.toml index 0829e63ae..b23229376 100644 --- a/contract/proxy-oracle/near/common/Cargo.toml +++ b/contract/proxy-oracle/near/common/Cargo.toml @@ -4,6 +4,7 @@ license.workspace = true repository.workspace = true edition.workspace = true version = "0.1.0" +description = "Shared NEAR-facing types for the Templar proxy oracle contract" [dependencies] templar-common.workspace = true diff --git a/contract/proxy-oracle/near/contract/Cargo.toml b/contract/proxy-oracle/near/contract/Cargo.toml index 203ee94f9..056de0f48 100644 --- a/contract/proxy-oracle/near/contract/Cargo.toml +++ b/contract/proxy-oracle/near/contract/Cargo.toml @@ -3,7 +3,8 @@ edition.workspace = true license.workspace = true name = "templar-proxy-oracle-near-contract" repository.workspace = true -version = "0.3.0" +version = "0.4.0" +publish = false [lib] crate-type = ["cdylib", "rlib"] diff --git a/contract/proxy-oracle/near/contract/tests/common/mod.rs b/contract/proxy-oracle/near/contract/tests/common/mod.rs index f814f2ccc..a1854e621 100644 --- a/contract/proxy-oracle/near/contract/tests/common/mod.rs +++ b/contract/proxy-oracle/near/contract/tests/common/mod.rs @@ -184,7 +184,11 @@ pub async fn deploy_from_patch( deploy_code( &harness.network, &account_id, - templar_gateway_testing::wasm::PROXY_ORACLE_V0.to_vec(), + templar_gateway_testing::wasm::released( + templar_gateway_testing::ArtifactId::ProxyOracle, + "0.1.0", + ) + .to_vec(), ) .await?; diff --git a/contract/proxy-oracle/near/contract/tests/migrate_mainnet.rs b/contract/proxy-oracle/near/contract/tests/migrate_mainnet.rs index 4f965b41a..99a588da5 100644 --- a/contract/proxy-oracle/near/contract/tests/migrate_mainnet.rs +++ b/contract/proxy-oracle/near/contract/tests/migrate_mainnet.rs @@ -151,7 +151,11 @@ async fn failed_migration_reverts_contract_code() -> Result<()> { common::deploy_code( network, &account_id, - templar_gateway_testing::wasm::PROXY_ORACLE_V0.to_vec(), + templar_gateway_testing::wasm::released( + templar_gateway_testing::ArtifactId::ProxyOracle, + "0.1.0", + ) + .to_vec(), ) .await?; harness.patch_state(&account_id, patch()).await?; diff --git a/contract/proxy-oracle/near/governance-common/Cargo.toml b/contract/proxy-oracle/near/governance-common/Cargo.toml index 0247cc648..5f939bdbf 100644 --- a/contract/proxy-oracle/near/governance-common/Cargo.toml +++ b/contract/proxy-oracle/near/governance-common/Cargo.toml @@ -4,6 +4,7 @@ license.workspace = true repository.workspace = true edition.workspace = true version = "0.1.0" +description = "Shared NEAR-facing types for the Templar proxy oracle governance contract" [dependencies] templar-common.workspace = true diff --git a/contract/proxy-oracle/near/governance-contract/Cargo.toml b/contract/proxy-oracle/near/governance-contract/Cargo.toml index c25f40655..ec2989e38 100644 --- a/contract/proxy-oracle/near/governance-contract/Cargo.toml +++ b/contract/proxy-oracle/near/governance-contract/Cargo.toml @@ -3,7 +3,8 @@ edition.workspace = true license.workspace = true name = "templar-proxy-oracle-near-governance-contract" repository.workspace = true -version = "0.1.0" +version = "0.2.0" +publish = false [lib] crate-type = ["cdylib", "rlib"] diff --git a/contract/proxy-oracle/near/governance-contract/tests/upgrade_ordering.rs b/contract/proxy-oracle/near/governance-contract/tests/upgrade_ordering.rs index 4a5738d76..c0dca6307 100644 --- a/contract/proxy-oracle/near/governance-contract/tests/upgrade_ordering.rs +++ b/contract/proxy-oracle/near/governance-contract/tests/upgrade_ordering.rs @@ -13,8 +13,9 @@ //! empty map; each upgraded contract is then probed with a domain view to prove it still answers, //! not merely that its code hash changed. //! -//! Fixtures are the real on-chain blobs (`PROXY_ORACLE_0_3_0`, state v1; `PROXY_GOVERNANCE_0_1_0`, -//! pre-versioned-state), pinned from mainnet. +//! Fixtures are the real on-chain blobs (proxy-oracle `0.3.0`, state v1; +//! proxy-governance `0.1.0`, pre-versioned-state), pinned from mainnet and +//! catalogued as releases in `contract/artifacts/src/ids.rs`. #![allow(clippy::unwrap_used, clippy::expect_used)] mod common; @@ -28,7 +29,7 @@ use near_sdk::NearToken; use serde_json::{json, Value}; use templar_common::upgrade::UpgradeSource; use templar_common::Nanoseconds; -use templar_gateway_testing::{wasm, SandboxHarness, TEST_FINALITY_POLICY}; +use templar_gateway_testing::{wasm, ArtifactId, SandboxHarness, TEST_FINALITY_POLICY}; use templar_proxy_oracle_near_governance_common::{Operation, Proposal}; use common::{call, code_hash, deploy_code, signer, view}; @@ -112,14 +113,14 @@ async fn setup(harness: &SandboxHarness) -> Result<(AccountId, AccountId)> { deploy_with_init( network, &oracle, - wasm::PROXY_ORACLE_0_3_0.to_vec(), + wasm::released(ArtifactId::ProxyOracle, "0.3.0").to_vec(), "new", json!({ "owner_id": gov }), ), deploy_with_init( network, &gov, - wasm::PROXY_GOVERNANCE_0_1_0.to_vec(), + wasm::released(ArtifactId::ProxyGovernance, "0.1.0").to_vec(), "new", json!({ "proxy_oracle_id": oracle, "admin_id": admin, "ttls": old_ttls() }), ), diff --git a/contract/proxy-oracle/near/lst-contract/Cargo.toml b/contract/proxy-oracle/near/lst-contract/Cargo.toml index 364984a5c..fad99a785 100644 --- a/contract/proxy-oracle/near/lst-contract/Cargo.toml +++ b/contract/proxy-oracle/near/lst-contract/Cargo.toml @@ -4,6 +4,7 @@ license.workspace = true name = "templar-lst-oracle-contract" repository.workspace = true version = "1.2.1" +publish = false [lib] crate-type = ["cdylib", "rlib"] diff --git a/contract/proxy-oracle/soroban/common/Cargo.toml b/contract/proxy-oracle/soroban/common/Cargo.toml index 9e8d0b83e..059570f38 100644 --- a/contract/proxy-oracle/soroban/common/Cargo.toml +++ b/contract/proxy-oracle/soroban/common/Cargo.toml @@ -4,6 +4,7 @@ license.workspace = true repository.workspace = true edition.workspace = true version = "0.1.0" +publish = false [lib] crate-type = ["rlib"] diff --git a/contract/proxy-oracle/soroban/contract/Cargo.toml b/contract/proxy-oracle/soroban/contract/Cargo.toml index 42d19d3fe..021e31d0f 100644 --- a/contract/proxy-oracle/soroban/contract/Cargo.toml +++ b/contract/proxy-oracle/soroban/contract/Cargo.toml @@ -4,6 +4,7 @@ license.workspace = true repository.workspace = true edition.workspace = true version = "0.1.0" +publish = false [lib] crate-type = ["cdylib", "rlib"] diff --git a/contract/proxy-oracle/soroban/governance-common/Cargo.toml b/contract/proxy-oracle/soroban/governance-common/Cargo.toml index 01834b786..c7b36e689 100644 --- a/contract/proxy-oracle/soroban/governance-common/Cargo.toml +++ b/contract/proxy-oracle/soroban/governance-common/Cargo.toml @@ -4,6 +4,7 @@ license.workspace = true repository.workspace = true edition.workspace = true version = "0.1.0" +publish = false [lib] crate-type = ["rlib"] diff --git a/contract/proxy-oracle/soroban/governance-contract/Cargo.toml b/contract/proxy-oracle/soroban/governance-contract/Cargo.toml index fd061f040..3f823820f 100644 --- a/contract/proxy-oracle/soroban/governance-contract/Cargo.toml +++ b/contract/proxy-oracle/soroban/governance-contract/Cargo.toml @@ -4,6 +4,7 @@ license.workspace = true repository.workspace = true edition.workspace = true version = "0.1.0" +publish = false [lib] crate-type = ["cdylib", "rlib"] diff --git a/contract/proxy-oracle/soroban/sep40-adapter-contract/Cargo.toml b/contract/proxy-oracle/soroban/sep40-adapter-contract/Cargo.toml index 393e9768a..ab90972d4 100644 --- a/contract/proxy-oracle/soroban/sep40-adapter-contract/Cargo.toml +++ b/contract/proxy-oracle/soroban/sep40-adapter-contract/Cargo.toml @@ -4,6 +4,7 @@ license.workspace = true repository.workspace = true edition.workspace = true version = "0.1.0" +publish = false [lib] crate-type = ["cdylib", "rlib"] diff --git a/contract/pyth-lazer/contract/Cargo.toml b/contract/pyth-lazer/contract/Cargo.toml index 04c0a8e49..7339a7f1f 100644 --- a/contract/pyth-lazer/contract/Cargo.toml +++ b/contract/pyth-lazer/contract/Cargo.toml @@ -4,6 +4,7 @@ license.workspace = true name = "templar-pyth-lazer-adapter-contract" repository.workspace = true version = "0.1.0" +publish = false [lib] crate-type = ["cdylib", "rlib"] diff --git a/contract/pyth-lazer/verifier/Cargo.toml b/contract/pyth-lazer/verifier/Cargo.toml index c8df0f198..590d40b64 100644 --- a/contract/pyth-lazer/verifier/Cargo.toml +++ b/contract/pyth-lazer/verifier/Cargo.toml @@ -4,6 +4,7 @@ license.workspace = true name = "templar-pyth-lazer-verifier" repository.workspace = true version = "0.1.0" +publish = false [lib] path = "src/lib.rs" diff --git a/contract/redstone-adapter/Cargo.toml b/contract/redstone-adapter/Cargo.toml index ceb0ab2ce..607e511c5 100644 --- a/contract/redstone-adapter/Cargo.toml +++ b/contract/redstone-adapter/Cargo.toml @@ -4,6 +4,7 @@ license.workspace = true name = "templar-redstone-adapter-contract" repository.workspace = true version = "0.2.0" +publish = false [lib] crate-type = ["cdylib", "rlib"] diff --git a/contract/registry/Cargo.toml b/contract/registry/Cargo.toml index 12ea4f40d..823c960be 100644 --- a/contract/registry/Cargo.toml +++ b/contract/registry/Cargo.toml @@ -4,6 +4,7 @@ license.workspace = true name = "templar-registry-contract" repository.workspace = true version = "1.2.1" +publish = false [lib] crate-type = ["cdylib", "rlib"] diff --git a/contract/universal-account/Cargo.toml b/contract/universal-account/Cargo.toml index 025989305..62238dda4 100644 --- a/contract/universal-account/Cargo.toml +++ b/contract/universal-account/Cargo.toml @@ -4,6 +4,7 @@ license.workspace = true name = "templar-universal-account-contract" repository.workspace = true version = "0.5.0" +publish = false [lib] crate-type = ["cdylib", "rlib"] diff --git a/contract/universal-account/tests/migration.rs b/contract/universal-account/tests/migration.rs index ee7f9ec84..2cc164887 100644 --- a/contract/universal-account/tests/migration.rs +++ b/contract/universal-account/tests/migration.rs @@ -19,8 +19,8 @@ use near_sdk::{ }; use near_token::NearToken; use rstest::rstest; -use templar_gateway_testing::wasm::{UNIVERSAL_ACCOUNT_0_2_0, UNIVERSAL_ACCOUNT_0_4_0}; use templar_gateway_testing::SandboxHarness; +use templar_gateway_testing::{wasm, ArtifactId}; use templar_universal_account::{ authentication::{with_raw_string::WithRawString, Payload}, state, @@ -135,10 +135,20 @@ async fn deploy_for_sequence( deploy_current(harness, TestSigner::fixed_passkey([0x44_u8; 32]).id()).await } MigrationSequenceStart::From0_2_0 => { - deploy_patched(harness, UNIVERSAL_ACCOUNT_0_2_0, WASM_0_2_0_STATE_PATCH).await + deploy_patched( + harness, + wasm::released(ArtifactId::UniversalAccount, "0.2.0"), + WASM_0_2_0_STATE_PATCH, + ) + .await } MigrationSequenceStart::From0_4_0 => { - deploy_patched(harness, UNIVERSAL_ACCOUNT_0_4_0, WASM_0_4_0_STATE_PATCH).await + deploy_patched( + harness, + wasm::released(ArtifactId::UniversalAccount, "0.4.0"), + WASM_0_4_0_STATE_PATCH, + ) + .await } } } @@ -249,7 +259,12 @@ async fn migrate_accepts_legacy_direct_payload( ) -> Result<()> { // The legacy single-object (non-array) `migrate_args` shape must still be accepted. 0.4.0's // unbrick migration reaches the target version in one step, so a bare object completes it. - let ua = deploy_patched(&harness, UNIVERSAL_ACCOUNT_0_4_0, WASM_0_4_0_STATE_PATCH).await?; + let ua = deploy_patched( + &harness, + wasm::released(ArtifactId::UniversalAccount, "0.4.0"), + WASM_0_4_0_STATE_PATCH, + ) + .await?; let network = &harness.network; migrate( @@ -268,7 +283,12 @@ async fn migrate_accepts_legacy_direct_payload( #[tokio::test] async fn from_0_2_0(#[future(awt)] harness: SandboxHarness) -> Result<()> { let passkey = patch_keys().passkey; - let ua = deploy_patched(&harness, UNIVERSAL_ACCOUNT_0_2_0, WASM_0_2_0_STATE_PATCH).await?; + let ua = deploy_patched( + &harness, + wasm::released(ArtifactId::UniversalAccount, "0.2.0"), + WASM_0_2_0_STATE_PATCH, + ) + .await?; let network = &harness.network; assert_eq!(stored_state_version(network, &ua).await?, 0); @@ -307,7 +327,12 @@ async fn from_0_2_0(#[future(awt)] harness: SandboxHarness) -> Result<()> { #[tokio::test] async fn from_0_4_0_unbrick_v1(#[future(awt)] harness: SandboxHarness) -> Result<()> { let expected_keys = patch_keys(); - let ua = deploy_patched(&harness, UNIVERSAL_ACCOUNT_0_4_0, WASM_0_4_0_STATE_PATCH).await?; + let ua = deploy_patched( + &harness, + wasm::released(ArtifactId::UniversalAccount, "0.4.0"), + WASM_0_4_0_STATE_PATCH, + ) + .await?; let network = &harness.network; let ft = common::ft_id(&harness); @@ -372,7 +397,7 @@ async fn from_0_4_0_with_stored_v1_migrates_via_v1( ) -> Result<()> { let ua = deploy_patched_with_version( &harness, - UNIVERSAL_ACCOUNT_0_4_0, + wasm::released(ArtifactId::UniversalAccount, "0.4.0"), WASM_0_4_0_STATE_PATCH, Some(1), ) diff --git a/contract/vault/curator-primitives/Cargo.toml b/contract/vault/curator-primitives/Cargo.toml index adb64cdc3..e94bfcbd3 100644 --- a/contract/vault/curator-primitives/Cargo.toml +++ b/contract/vault/curator-primitives/Cargo.toml @@ -2,8 +2,9 @@ name = "templar-curator-primitives" version = "1.0.0" edition = "2021" -license = "GPL-3.0-only" +license.workspace = true description = "Chain-agnostic curator policy and recovery primitives for Templar Protocol vaults" +repository.workspace = true [lib] path = "src/lib.rs" @@ -23,7 +24,7 @@ recovery = [] [dependencies] # Core dependency - vault kernel types -templar-vault-kernel = { path = "../kernel" } +templar-vault-kernel = { path = "../kernel", version = "1.0.0" } templar-vault-macros.workspace = true derive_more.workspace = true typed-builder = "0.20" diff --git a/contract/vault/kernel/Cargo.toml b/contract/vault/kernel/Cargo.toml index fc1761789..39ba14db8 100644 --- a/contract/vault/kernel/Cargo.toml +++ b/contract/vault/kernel/Cargo.toml @@ -2,7 +2,9 @@ name = "templar-vault-kernel" version = "1.0.0" edition = "2021" -license = "GPL-3.0-only" +license.workspace = true +description = "Storage-agnostic vault kernel: actions, effects, and state transitions" +repository.workspace = true [lib] path = "src/lib.rs" diff --git a/contract/vault/macros/Cargo.toml b/contract/vault/macros/Cargo.toml index b52a04180..342d0cfb0 100644 --- a/contract/vault/macros/Cargo.toml +++ b/contract/vault/macros/Cargo.toml @@ -2,8 +2,9 @@ name = "templar-vault-macros" version = "0.1.0" edition = "2021" -license = "GPL-3.0-only" +license.workspace = true description = "Shared procedural macros for Templar vault crates" +repository.workspace = true [lib] proc-macro = true diff --git a/contract/vault/near/Cargo.toml b/contract/vault/near/Cargo.toml index 4166e0089..2eed3aade 100644 --- a/contract/vault/near/Cargo.toml +++ b/contract/vault/near/Cargo.toml @@ -1,9 +1,10 @@ [package] edition.workspace = true -license = "GPL-3.0-only" +license.workspace = true name = "templar-vault-contract" repository.workspace = true version.workspace = true +publish = false [lib] crate-type = ["cdylib", "rlib"] diff --git a/contract/vault/soroban/Cargo.toml b/contract/vault/soroban/Cargo.toml index 344636629..a2d834eab 100644 --- a/contract/vault/soroban/Cargo.toml +++ b/contract/vault/soroban/Cargo.toml @@ -2,8 +2,9 @@ name = "templar-soroban-runtime" version = "1.0.0" edition = "2021" -license = "GPL-3.0-only" +license.workspace = true description = "Soroban effect interpreter and runtime for Templar Protocol vaults" +publish = false [lib] crate-type = ["cdylib", "rlib"] diff --git a/contract/vault/soroban/blend-adapter/Cargo.toml b/contract/vault/soroban/blend-adapter/Cargo.toml index fa720d009..7cb2a54a2 100644 --- a/contract/vault/soroban/blend-adapter/Cargo.toml +++ b/contract/vault/soroban/blend-adapter/Cargo.toml @@ -2,8 +2,9 @@ name = "templar-soroban-blend-adapter" version = "1.0.0" edition = "2021" -license = "GPL-3.0-only" +license.workspace = true description = "Soroban adapter contract for Blend v2 pool integration" +publish = false [lib] crate-type = ["cdylib", "rlib"] diff --git a/contract/vault/soroban/curator-proxy/Cargo.toml b/contract/vault/soroban/curator-proxy/Cargo.toml index d9d2646cf..13b3685e8 100644 --- a/contract/vault/soroban/curator-proxy/Cargo.toml +++ b/contract/vault/soroban/curator-proxy/Cargo.toml @@ -2,8 +2,9 @@ name = "templar-curator-proxy-soroban" version = "1.0.0" edition = "2021" -license = "GPL-3.0-only" +license.workspace = true description = "Typed Soroban curator operations proxy for Templar Protocol" +publish = false [lib] crate-type = ["cdylib", "rlib"] diff --git a/contract/vault/soroban/custodial-adapter/Cargo.toml b/contract/vault/soroban/custodial-adapter/Cargo.toml index 1ba5d8b8b..3bf91a6db 100644 --- a/contract/vault/soroban/custodial-adapter/Cargo.toml +++ b/contract/vault/soroban/custodial-adapter/Cargo.toml @@ -2,8 +2,9 @@ name = "templar-soroban-custodial-adapter" version = "1.0.0" edition = "2021" -license = "GPL-3.0-only" +license.workspace = true description = "Soroban custodial market adapter for offchain-managed vault allocations" +publish = false [lib] crate-type = ["cdylib", "rlib"] diff --git a/contract/vault/soroban/governance/Cargo.toml b/contract/vault/soroban/governance/Cargo.toml index bbb4f5e24..2c56bf2ac 100644 --- a/contract/vault/soroban/governance/Cargo.toml +++ b/contract/vault/soroban/governance/Cargo.toml @@ -2,8 +2,9 @@ name = "templar-soroban-governance" version = "1.0.0" edition = "2021" -license = "GPL-3.0-only" +license.workspace = true description = "Timelocked governance executor for the Soroban vault" +publish = false [lib] crate-type = ["cdylib", "rlib"] diff --git a/contract/vault/soroban/proxy-4626/Cargo.toml b/contract/vault/soroban/proxy-4626/Cargo.toml index 5831e6cd8..0e069c5a3 100644 --- a/contract/vault/soroban/proxy-4626/Cargo.toml +++ b/contract/vault/soroban/proxy-4626/Cargo.toml @@ -2,8 +2,9 @@ name = "templar-4626-proxy-soroban" version = "1.0.0" edition = "2021" -license = "GPL-3.0-only" +license.workspace = true description = "Stub Soroban ERC-4626 proxy contract for Templar Protocol" +publish = false [lib] crate-type = ["cdylib", "rlib"] diff --git a/contract/vault/soroban/share-token/Cargo.toml b/contract/vault/soroban/share-token/Cargo.toml index cf96842a1..b7baf4c69 100644 --- a/contract/vault/soroban/share-token/Cargo.toml +++ b/contract/vault/soroban/share-token/Cargo.toml @@ -2,8 +2,9 @@ name = "templar-soroban-share-token" version = "1.0.0" edition = "2021" -license = "GPL-3.0-only" +license.workspace = true description = "Soroban vault share token with vault-controlled mint/burn and user-authorized transfers" +publish = false [lib] crate-type = ["cdylib", "rlib"] diff --git a/contract/vault/soroban/shared-types/Cargo.toml b/contract/vault/soroban/shared-types/Cargo.toml index cd0342c76..0c158d5af 100644 --- a/contract/vault/soroban/shared-types/Cargo.toml +++ b/contract/vault/soroban/shared-types/Cargo.toml @@ -2,8 +2,9 @@ name = "templar-soroban-shared-types" version = "1.0.0" edition = "2021" -license = "GPL-3.0-only" +license.workspace = true description = "Shared Soroban ABI selector types for Templar vault contracts" +publish = false [lib] crate-type = ["rlib"] diff --git a/gateway/artifacts-dispatch/Cargo.toml b/gateway/artifacts-dispatch/Cargo.toml index b53f21035..553b237f5 100644 --- a/gateway/artifacts-dispatch/Cargo.toml +++ b/gateway/artifacts-dispatch/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true +publish = false [dependencies] async-trait.workspace = true diff --git a/gateway/artifacts-dispatch/src/lib.rs b/gateway/artifacts-dispatch/src/lib.rs index 2f5739507..75d3bedf8 100644 --- a/gateway/artifacts-dispatch/src/lib.rs +++ b/gateway/artifacts-dispatch/src/lib.rs @@ -24,7 +24,7 @@ mod tests { // Then: metadata matches the catalog let meta = ArtifactId::Market.metadata(); assert_eq!(result.metadata.package_name, meta.package_name); - assert_eq!(result.metadata.version, meta.version); + assert_eq!(result.metadata.version, meta.version()); } #[tokio::test] @@ -39,7 +39,7 @@ mod tests { .unwrap(); let market_catalog = ArtifactId::Market.metadata(); assert_eq!(market.package_name, "templar-market-contract"); - assert_eq!(market.version, market_catalog.version); + assert_eq!(market.version, market_catalog.version()); let json = serde_json::to_value(&result).unwrap(); let first_artifact = json["artifacts"].as_array().unwrap().first().unwrap(); @@ -104,10 +104,12 @@ mod tests { .await .unwrap(); - assert_eq!(result.metadata.version, meta.version); - assert!(result - .version_key - .starts_with(&format!("{}@{}#", meta.package_name, meta.version))); + assert_eq!(result.metadata.version, meta.version()); + assert!(result.version_key.starts_with(&format!( + "{}@{}#", + meta.package_name, + meta.version() + ))); } } } diff --git a/gateway/artifacts-spec/Cargo.toml b/gateway/artifacts-spec/Cargo.toml index 0f4565fdc..b9d2ba98c 100644 --- a/gateway/artifacts-spec/Cargo.toml +++ b/gateway/artifacts-spec/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true +publish = false [dependencies] near-account-id.workspace = true diff --git a/gateway/artifacts-spec/src/artifact.rs b/gateway/artifacts-spec/src/artifact.rs index 0e8bed346..4fdf3fba7 100644 --- a/gateway/artifacts-spec/src/artifact.rs +++ b/gateway/artifacts-spec/src/artifact.rs @@ -48,7 +48,7 @@ impl From<&templar_contract_artifacts::ArtifactMetadata> for ArtifactMetadata { package_name: metadata.package_name.to_owned(), cargo_target_name: metadata.cargo_target_name.to_owned(), source_path: metadata.source_path.to_owned(), - version: metadata.version.to_owned(), + version: metadata.version().to_owned(), } } } diff --git a/gateway/catalog/Cargo.toml b/gateway/catalog/Cargo.toml index 2cf9d5bd5..ab620c458 100644 --- a/gateway/catalog/Cargo.toml +++ b/gateway/catalog/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true +publish = false [dependencies] templar-gateway-artifacts-spec.workspace = true diff --git a/gateway/client/Cargo.toml b/gateway/client/Cargo.toml index 4aae24e97..bd75db1b8 100644 --- a/gateway/client/Cargo.toml +++ b/gateway/client/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true +description = "Direct, in-process gateway client for Rust consumers of Templar Protocol" [dependencies] clap = { workspace = true, optional = true } diff --git a/gateway/core/Cargo.toml b/gateway/core/Cargo.toml index ec6b2e2bd..5effc6458 100644 --- a/gateway/core/Cargo.toml +++ b/gateway/core/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true +description = "Core actor, operation, and execution-outcome model for the Templar gateway" [dependencies] actix.workspace = true diff --git a/gateway/macros/Cargo.toml b/gateway/macros/Cargo.toml index bf7885544..a83db88b5 100644 --- a/gateway/macros/Cargo.toml +++ b/gateway/macros/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true +description = "Procedural macros for the Templar gateway spec and dispatch crates" [lib] proc-macro = true diff --git a/gateway/methods-dispatch/Cargo.toml b/gateway/methods-dispatch/Cargo.toml index 158c26a87..803fb7d39 100644 --- a/gateway/methods-dispatch/Cargo.toml +++ b/gateway/methods-dispatch/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true +description = "NEAR dispatch implementations for Templar gateway method specifications" [dependencies] async-trait.workspace = true diff --git a/gateway/methods-spec/Cargo.toml b/gateway/methods-spec/Cargo.toml index f53e5a57c..6d06fc19c 100644 --- a/gateway/methods-spec/Cargo.toml +++ b/gateway/methods-spec/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true +description = "Contract-method specifications for the Templar gateway" [dependencies] near-account-id = { workspace = true, features = ["serde", "schemars"] } diff --git a/gateway/oracle-updates-dispatch/Cargo.toml b/gateway/oracle-updates-dispatch/Cargo.toml index 3a1b51bb6..276f648c9 100644 --- a/gateway/oracle-updates-dispatch/Cargo.toml +++ b/gateway/oracle-updates-dispatch/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true +publish = false [dependencies] anyhow.workspace = true diff --git a/gateway/oracle-updates-spec/Cargo.toml b/gateway/oracle-updates-spec/Cargo.toml index aaf1e2b31..617ed917c 100644 --- a/gateway/oracle-updates-spec/Cargo.toml +++ b/gateway/oracle-updates-spec/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true +publish = false [dependencies] hex.workspace = true diff --git a/gateway/runtime/Cargo.toml b/gateway/runtime/Cargo.toml index 90dd89d4b..e4dfa6234 100644 --- a/gateway/runtime/Cargo.toml +++ b/gateway/runtime/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true +publish = false [dependencies] actix.workspace = true diff --git a/gateway/store/Cargo.toml b/gateway/store/Cargo.toml index c9b22a091..c8fb39e74 100644 --- a/gateway/store/Cargo.toml +++ b/gateway/store/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true +description = "Durable operation, idempotency, and persistence adapters for the Templar gateway" [dependencies] async-trait.workspace = true diff --git a/gateway/testing/Cargo.toml b/gateway/testing/Cargo.toml index 96def33aa..f68251ea1 100644 --- a/gateway/testing/Cargo.toml +++ b/gateway/testing/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true +publish = false [dependencies] anyhow.workspace = true @@ -20,7 +21,12 @@ rstest.workspace = true serde.workspace = true serde_json.workspace = true templar-common.workspace = true -templar-contract-artifacts = { workspace = true, features = ["workspace-loader"] } +# `embedded-wasm` backs `wasm::released()`: migration and upgrade tests deploy +# the real historical blobs, which live in the artifacts catalog. +templar-contract-artifacts = { workspace = true, features = [ + "workspace-loader", + "embedded-wasm", +] } templar-gateway-client.workspace = true templar-gateway-core.workspace = true templar-gateway-methods-dispatch.workspace = true diff --git a/gateway/testing/src/lib.rs b/gateway/testing/src/lib.rs index cf292502f..25fd5c4d2 100644 --- a/gateway/testing/src/lib.rs +++ b/gateway/testing/src/lib.rs @@ -9,6 +9,9 @@ pub mod wasm; pub use controller::TestController; pub use ops::{failed_receipts, DeployedMarket, DeployedVault}; pub use sandbox::{test_secret_key, test_signer, SandboxHarness}; +/// Re-exported so tests can name a historical release for [`wasm::released`] +/// without taking their own dependency on the artifacts catalog. +pub use templar_contract_artifacts::ArtifactId; pub use templar_gateway_types::ManagedAccountId; pub use test_utils::test_signer::TestSigner; diff --git a/gateway/testing/src/sandbox.rs b/gateway/testing/src/sandbox.rs index fd362883d..7312f1c0a 100644 --- a/gateway/testing/src/sandbox.rs +++ b/gateway/testing/src/sandbox.rs @@ -549,7 +549,7 @@ impl SandboxHarness { &self.network, account_id.clone(), signer, - crate::wasm::PROXY_ORACLE_V0.to_vec(), + crate::wasm::released(crate::ArtifactId::ProxyOracle, "0.1.0").to_vec(), "new", serde_json::json!({}), ) diff --git a/gateway/testing/src/wasm.rs b/gateway/testing/src/wasm.rs index 6d2d6b65e..685c2ffec 100644 --- a/gateway/testing/src/wasm.rs +++ b/gateway/testing/src/wasm.rs @@ -69,17 +69,26 @@ wasm_fns! { vault => Vault, } -/// Legacy `0.2.0` universal-account WASM (pinned blob), for migration tests. -pub const UNIVERSAL_ACCOUNT_0_2_0: &[u8] = include_bytes!("wasm/uac_0_2_0.wasm"); -/// Legacy `0.4.0` universal-account WASM (pinned blob), for migration tests. -pub const UNIVERSAL_ACCOUNT_0_4_0: &[u8] = include_bytes!("wasm/uac_0_4_0.wasm"); -/// Legacy (`0.1.0`, pre-kernelization) proxy-oracle WASM (pinned blob). -pub const PROXY_ORACLE_V0: &[u8] = include_bytes!("wasm/proxy_oracle_v0.wasm"); -/// Currently-deployed proxy-oracle WASM (`0.3.0`, on-chain state version 1), pinned from -/// `proxy-oracle-iethhemibtc-iethusdc.v1.tmplr.near`. The pre-standardized-upgrade blob, for -/// cross-version upgrade tests. -pub const PROXY_ORACLE_0_3_0: &[u8] = include_bytes!("wasm/proxy_oracle_0_3_0.wasm"); -/// Currently-deployed proxy-oracle-governance WASM (`0.1.0`, no versioned state / no `migrate`), -/// pinned from `proxy-gov-iethhemibtc-iethusdc.v1.tmplr.near`. The pre-standardized-upgrade blob, -/// for cross-version upgrade tests. -pub const PROXY_GOVERNANCE_0_1_0: &[u8] = include_bytes!("wasm/proxy_governance_0_1_0.wasm"); +/// Bytes of a specific *released* version of a contract, for migration and +/// upgrade tests that must deploy the real historical binary. +/// +/// Backed by the immutable release list in +/// [`templar_contract_artifacts`] — see `contract/artifacts/README.md`. These +/// blobs used to be hand-maintained `include_bytes!` consts in this module, +/// outside the catalog and outside the drift check; they are now catalogued +/// releases like any other, so a corrupted or silently-swapped historical blob +/// fails `embedded_drift_check`. +/// +/// # Panics +/// If `version` is not a catalogued release of `artifact`. That is a test bug: +/// the available versions are listed in `contract/artifacts/src/ids.rs`. +pub fn released(artifact: ArtifactId, version: &str) -> &'static [u8] { + artifact + .embedded_bytes_for_version(version) + .unwrap_or_else(|| { + panic!( + "{artifact}@{version} is not a catalogued release; \ + see contract/artifacts/src/ids.rs", + ) + }) +} diff --git a/gateway/types/Cargo.toml b/gateway/types/Cargo.toml index 116dbcb04..9a4fef4a2 100644 --- a/gateway/types/Cargo.toml +++ b/gateway/types/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true +description = "Shared wire, block, and error types for the Templar gateway" [dependencies] base64.workspace = true diff --git a/justfile b/justfile index e6f62406b..fb9f757c4 100644 --- a/justfile +++ b/justfile @@ -152,3 +152,17 @@ coverage-lcov: # Build the docs. docs: ./script/build-docs.sh + +# Cut a new release blob for a contract artifact. +# +# Builds reproducibly from the CURRENT COMMIT and installs the bytes +# as a new immutable release under contract/artifacts/res/near/. Requires a +# clean tree: `cargo near build reproducible-wasm` builds from committed git +# state, so uncommitted work would silently not be in the blob. +# +# Afterwards, add the printed release entry to contract/artifacts/src/ids.rs and +# commit the blob together with that entry. +# +# just artifact-release proxy-oracle +artifact-release artifact: + ./script/artifact-release.sh "$1" diff --git a/mock/receiver/Cargo.toml b/mock/receiver/Cargo.toml index 1f2661f4c..b2918389c 100644 --- a/mock/receiver/Cargo.toml +++ b/mock/receiver/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true license.workspace = true repository.workspace = true +publish = false [lib] crate-type = ["cdylib"] diff --git a/mock/ref/Cargo.toml b/mock/ref/Cargo.toml index 43c4db16d..4d2cc5b58 100644 --- a/mock/ref/Cargo.toml +++ b/mock/ref/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true license.workspace = true repository.workspace = true +publish = false [lib] crate-type = ["cdylib"] diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml index c430e0346..42730ae5e 100644 --- a/primitives/Cargo.toml +++ b/primitives/Cargo.toml @@ -4,6 +4,7 @@ license.workspace = true repository.workspace = true edition.workspace = true version = "0.1.0" +description = "Chain-agnostic numeric and decimal primitives for Templar Protocol" [dependencies] borsh = { version = "1.5", default-features = false, features = ["derive", "unstable__schema"], optional = true } diff --git a/release-plz.toml b/release-plz.toml new file mode 100644 index 000000000..f9a052b1d --- /dev/null +++ b/release-plz.toml @@ -0,0 +1,97 @@ +## Release automation. Policy, tiers, and why crates.io publishing is currently +## blocked are documented in RELEASING.md — this file only encodes the settings. + +[workspace] +# Tier B defaults. Versions come from git tags rather than a registry lookup, +# and `cargo publish` is skipped. Applies to every crate not overridden below. +git_only = true + +# Must be stated explicitly: release-plz validates its own `publish` setting +# against each `Cargo.toml`, and errors on any crate that has `publish = false` +# there while the config still defaults to `true` — `git_only` alone does not +# satisfy that check. +publish = false + +# Batch changes into a Release PR instead of releasing on every merge to `dev`. +release_always = false + +# Per-crate tags: `templar-gateway-client-v0.2.0`. Required for independent +# versioning — a bare `v0.2.0` cannot say which crate it refers to. +git_tag_name = "{{ package }}-v{{ version }}" +git_tag_enable = true +git_release_enable = true + +changelog_update = true + +# cargo-semver-checks needs a published baseline to diff against, and nothing is +# published yet. Turn this on for the Tier A crates at the same time as +# `publish` — it is the guard that stops a breaking change reaching consumers. +semver_check = false + +# `cargo update` on every release PR would churn the lockfile independently of +# the actual changes being released. +dependencies_update = false + +# Internal scaffolding: no tag, no changelog, no release. `release` defaults to +# true, so these blocks are load-bearing — without them, mocks and test helpers +# would get tags and changelog entries. + +[[package]] +name = "templar-contract-artifacts" +release = false + +[[package]] +name = "templar-fuzz" +release = false + +[[package]] +name = "test-utils" +release = false + +[[package]] +name = "templar-proxy-oracle-soroban-integration-tests" +release = false + +[[package]] +name = "mock-ft" +release = false + +[[package]] +name = "mock-mt" +release = false + +[[package]] +name = "mock-oracle" +release = false + +[[package]] +name = "mock-receiver" +release = false + +[[package]] +name = "mock-ref" +release = false + +# --------------------------------------------------------------------------- +# Changelog rendering. Groups mirror the commit types the PR-title lint accepts +# (.github/workflows/pr-title.yml) — keep the two in sync. +# --------------------------------------------------------------------------- + +[changelog] +protect_breaking_commits = true +sort_commits = "oldest" + +commit_parsers = [ + { message = "^feat", group = "Added" }, + { message = "^fix", group = "Fixed" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Changed" }, + { message = "^deploy", group = "Deployments" }, + { message = "^docs", group = "Documentation" }, + # Housekeeping that does not change shipped behaviour. Kept out of the + # changelog, but still counted for the version bump. + { message = "^test", skip = true }, + { message = "^ci", skip = true }, + { message = "^build", skip = true }, + { message = "^chore", skip = true }, +] diff --git a/script/artifact-release.sh b/script/artifact-release.sh new file mode 100755 index 000000000..f7747cefc --- /dev/null +++ b/script/artifact-release.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Cut a new immutable release blob for one contract artifact. +# +# Reproducibly builds the artifact from the CURRENT COMMIT and installs the +# bytes at contract/artifacts/res/near///.wasm, then +# prints the catalog entry to paste into contract/artifacts/src/ids.rs. +# +# Released blobs are immutable: this refuses to overwrite an existing version. +# To ship new bytes, bump the crate version first, then run this. +# +# ./script/artifact-release.sh proxy-oracle +set -euo pipefail + +ARTIFACT="${1:-}" +if [[ -z "$ARTIFACT" ]]; then + echo "usage: $0 (e.g. proxy-oracle; see ArtifactId::ALL)" >&2 + exit 2 +fi + +SCRIPT_DIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$ROOT_DIR" + +# `cargo near build reproducible-wasm` builds from committed git state. On a +# dirty tree it would either hard-error or embed a state that does not match +# what is checked in, producing bytes nobody can reproduce. +if [[ -n "$(git status --porcelain)" ]]; then + echo "error: working tree is dirty." >&2 + echo "Reproducible builds use committed git state — commit or stash first." >&2 + exit 1 +fi + +# Resolve catalog metadata for the artifact (source path, target name) and the +# crate's current version, straight from the single source of truth. +METADATA=$(cargo run --quiet -p templar-contract-artifacts \ + --features workspace-loader,clap --bin prebuild-test-contracts -- \ + --print-metadata --artifact "$ARTIFACT") || { + echo "error: unknown artifact '$ARTIFACT'" >&2 + exit 1 +} +read -r SOURCE_PATH TARGET VERSION <<<"$METADATA" +DEST_DIR="contract/artifacts/res/near/$TARGET/$VERSION" +if [[ -e "$DEST_DIR/$TARGET.wasm" ]]; then + echo "error: $TARGET $VERSION is already released ($DEST_DIR/$TARGET.wasm)." >&2 + echo "Released blobs are immutable — bump the crate version to ship new bytes." >&2 + exit 1 +fi + +COMMIT=$(git rev-parse HEAD) +echo ">> building $ARTIFACT ($TARGET) $VERSION reproducibly from $COMMIT" +cargo near build reproducible-wasm --manifest-path "$SOURCE_PATH/Cargo.toml" + +mkdir -p "$DEST_DIR" +cp "target/near/$TARGET/$TARGET.wasm" "$DEST_DIR/$TARGET.wasm" +SHA=$(sha256sum "$DEST_DIR/$TARGET.wasm" | cut -d' ' -f1) + +cat <> installed $DEST_DIR/$TARGET.wasm + +Add this release to the $ARTIFACT entry in contract/artifacts/src/ids.rs +(append to the release list — newest last — and add a matching +\`embedded_bytes_for_version\` arm): + + ("$VERSION", "$SHA", Some("$COMMIT")), + +…and a matching arm in \`embedded_bytes_for_version\`: + + (Self::, "$VERSION") => include_bytes!("../res/near/$TARGET/$VERSION/$TARGET.wasm"), + +Then verify and commit the blob together with the catalog edit: + + ./script/check-artifact-drift.sh +EOF diff --git a/script/check-artifact-drift.sh b/script/check-artifact-drift.sh index 42543e942..ddcb20cbb 100755 --- a/script/check-artifact-drift.sh +++ b/script/check-artifact-drift.sh @@ -1,13 +1,24 @@ #!/usr/bin/env bash -# Verify the checked-in embedded WASM catalog is self-consistent: -# - every blob in res/near/ hashes to the `expected_sha256` pinned in ids.rs -# - every catalog `version` matches its contract's Cargo.toml version +# Verify the checked-in contract artifact catalog is self-consistent: +# - every release blob in res/near/// hashes to the `sha256` +# pinned for that release in ids.rs (historical releases included — they are +# immutable, so any change to one is a mistake) +# - the NEWEST catalogued release of each artifact matches its Cargo.toml +# version (a bump with no cut blob fails here — run `just artifact-release`) +# - each artifact's release list is well-formed: non-empty, no duplicate +# versions, 64-char digests +# - res/near and the catalog agree: no orphaned directories, no phantom entries +# # These are pure, in-memory checks — no contract builds, no Docker, seconds. +# Reproducibility (do the bytes match what the source actually compiles to?) is +# verified separately, on release tags, by .github/workflows/release-artifacts.yml. set -ex SCRIPT_DIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$ROOT_DIR" +# The whole suite, not a name filter: the catalog invariants live in several +# tests and a filter silently skips any newly added one. cargo test -p templar-contract-artifacts --features embedded-wasm,workspace-loader \ - drift_check -- --include-ignored --nocapture + -- --include-ignored --nocapture diff --git a/service/accumulator/Cargo.toml b/service/accumulator/Cargo.toml index 7a2df4d3e..71c331854 100644 --- a/service/accumulator/Cargo.toml +++ b/service/accumulator/Cargo.toml @@ -4,6 +4,7 @@ edition.workspace = true license.workspace = true repository.workspace = true version = "0.1.0" +publish = false [[bin]] name = "accumulator" diff --git a/service/funding-bridge/Cargo.toml b/service/funding-bridge/Cargo.toml index 9ba4ed151..531d6f755 100644 --- a/service/funding-bridge/Cargo.toml +++ b/service/funding-bridge/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true repository.workspace = true license.workspace = true +publish = false [[bin]] name = "funding-bridge" diff --git a/service/gateway/Cargo.toml b/service/gateway/Cargo.toml index 74ab936b5..d0df0ab4f 100644 --- a/service/gateway/Cargo.toml +++ b/service/gateway/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true +publish = false [dependencies] actix.workspace = true diff --git a/service/gateway/src/rpc/tests/artifact_tests.rs b/service/gateway/src/rpc/tests/artifact_tests.rs index ba54f02d7..f18ff0491 100644 --- a/service/gateway/src/rpc/tests/artifact_tests.rs +++ b/service/gateway/src/rpc/tests/artifact_tests.rs @@ -79,7 +79,7 @@ async fn artifact_add_endpoint_works_against_sandbox() -> Result<()> { let stack = TestStack::start().await?; let registry_id = stack.harness.deploy_registry().await?; let mock_ft = templar_contract_artifacts::ArtifactId::MockFt.metadata(); - let expected_version_prefix = format!("{}@{}#", mock_ft.package_name, mock_ft.version); + let expected_version_prefix = format!("{}@{}#", mock_ft.package_name, mock_ft.version()); let write_result = stack .controller diff --git a/service/liquidator/Cargo.toml b/service/liquidator/Cargo.toml index 6ea0d1569..a62f17dd3 100644 --- a/service/liquidator/Cargo.toml +++ b/service/liquidator/Cargo.toml @@ -4,6 +4,7 @@ edition.workspace = true license.workspace = true repository.workspace = true version = "0.1.0" +publish = false [lib] path = "src/liquidator.rs" diff --git a/service/market-monitor/Cargo.toml b/service/market-monitor/Cargo.toml index b03b0192b..9d41af6a9 100644 --- a/service/market-monitor/Cargo.toml +++ b/service/market-monitor/Cargo.toml @@ -4,6 +4,7 @@ edition.workspace = true license.workspace = true repository.workspace = true version = "0.1.0" +publish = false [[bin]] name = "market-monitor" diff --git a/service/redstone-bridge/Cargo.toml b/service/redstone-bridge/Cargo.toml index 11d8f09a2..93c63c91c 100644 --- a/service/redstone-bridge/Cargo.toml +++ b/service/redstone-bridge/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true +publish = false [dependencies] hex.workspace = true diff --git a/service/relayer/Cargo.toml b/service/relayer/Cargo.toml index 75a2ec4bd..f6c5719be 100644 --- a/service/relayer/Cargo.toml +++ b/service/relayer/Cargo.toml @@ -3,6 +3,7 @@ name = "templar-relayer" version = "0.1.0" edition.workspace = true license.workspace = true +publish = false [dependencies] anyhow.workspace = true diff --git a/service/relayer/tests/relayer.rs b/service/relayer/tests/relayer.rs index 60f7527b5..c5e195a14 100644 --- a/service/relayer/tests/relayer.rs +++ b/service/relayer/tests/relayer.rs @@ -69,7 +69,7 @@ use templar_universal_account::{ ExecuteArgsMessage, KeyId, PayloadExecutionParameters, NEAR_TESTNET_CHAIN_ID, }; -use templar_gateway_testing::wasm::UNIVERSAL_ACCOUNT_0_2_0; +use templar_gateway_testing::{wasm, ArtifactId}; use test_utils::{market_configuration, DEFAULT_BORROW_PRICE_ID, DEFAULT_COLLATERAL_PRICE_ID}; mod common; @@ -979,9 +979,13 @@ pub async fn universal_account_regression_0_2_0(#[future(awt)] mut init_test: In // Deploy the historical `0.2.0` universal-account wasm to a fresh account. let ua = common::create_account(&harness, "ua-0-2-0").await.unwrap(); - common::deploy_code(&harness.network, &ua, UNIVERSAL_ACCOUNT_0_2_0.to_vec()) - .await - .unwrap(); + common::deploy_code( + &harness.network, + &ua, + wasm::released(ArtifactId::UniversalAccount, "0.2.0").to_vec(), + ) + .await + .unwrap(); common::call( &harness.network, &ua, diff --git a/tools/harvest-static-yield/Cargo.toml b/tools/harvest-static-yield/Cargo.toml index 3a91ffb4d..cde6c3e7d 100644 --- a/tools/harvest-static-yield/Cargo.toml +++ b/tools/harvest-static-yield/Cargo.toml @@ -4,6 +4,7 @@ license.workspace = true repository.workspace = true edition.workspace = true version = "0.1.0" +publish = false [dependencies] anyhow.workspace = true diff --git a/tools/manager/Cargo.toml b/tools/manager/Cargo.toml index 06c991501..35b0493c3 100644 --- a/tools/manager/Cargo.toml +++ b/tools/manager/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true repository.workspace = true license.workspace = true +publish = false [[bin]] name = "tmplrmgr" diff --git a/tools/market-config-cli/Cargo.toml b/tools/market-config-cli/Cargo.toml index 8b8625685..ea1933886 100644 --- a/tools/market-config-cli/Cargo.toml +++ b/tools/market-config-cli/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true repository.workspace = true license.workspace = true +publish = false [lib] name = "market_config_cli" diff --git a/tools/soroban-vault-cli/Cargo.toml b/tools/soroban-vault-cli/Cargo.toml index dbeb19480..a59c06292 100644 --- a/tools/soroban-vault-cli/Cargo.toml +++ b/tools/soroban-vault-cli/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true repository.workspace = true license.workspace = true +publish = false [[bin]] name = "tmplr-soroban-vault" diff --git a/tools/tools-common/Cargo.toml b/tools/tools-common/Cargo.toml index cb3a094dc..78995d204 100644 --- a/tools/tools-common/Cargo.toml +++ b/tools/tools-common/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true repository.workspace = true license.workspace = true +publish = false [dependencies] anyhow.workspace = true diff --git a/universal-account/Cargo.toml b/universal-account/Cargo.toml index 5fb665391..4a5b4d43b 100644 --- a/universal-account/Cargo.toml +++ b/universal-account/Cargo.toml @@ -4,6 +4,7 @@ license.workspace = true repository.workspace = true edition.workspace = true version = "1.2.1" +description = "Chain-abstraction account identifiers and EIP-712 signature payloads for Templar Protocol" [dependencies] alloy = { workspace = true, features = ["eip712", "signers", "sol-types"] }