Skip to content

Importable registry #[contracttrait]s + StatelessDeployable (registry-traits crate) - #33

Draft
willemneal wants to merge 3 commits into
mainfrom
feat/registry-importable-stateless-deploy
Draft

Importable registry #[contracttrait]s + StatelessDeployable (registry-traits crate)#33
willemneal wants to merge 3 commits into
mainfrom
feat/registry-importable-stateless-deploy

Conversation

@willemneal

@willemneal willemneal commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Implements stellar-registry/perch#38 — part of stellar-registry/perch#37; supersedes #10.

What this does

Makes the registry's #[contracttrait]s importable by a downstream contract on a different soroban-sdk, and adds a content-addressed StatelessDeployable deploy.

1. Importable traits (admin-sep style)

The public #[contracttrait] default bodies no longer hard-reference the concrete crate::Contract/crate::storage. Helpers moved onto a neutral zero-sized RegistryHelpers namespace; everything is env/storage-key based, so the traits type-check against a foreign Self. Admin auth routes through a fixed ADMIN storage-key convention (no admin trait required downstream). The registry crate gained an rlib target.

2. New registry-traits crate (the thing perch depends on)

The importable traits + storage/name/version/events/error were extracted into contracts/registry-traits (rlib). It depends only on soroban-sdk — never admin-sep or soroban-sdk-tools, both of which pin sdk ^25 and would block a sdk-26 consumer. To get there:

  • error.rs: soroban-sdk-tools::scerr → hand-written #[contracterror] (identical discriminants 1..=21, same exported spec → unchanged wire ABI).
  • storage.rs: soroban-sdk-tools::InstanceItem → plain instance storage at the same ROOT_REG key.
  • admin.rs: tiny in-crate admin (reads/require-auths the ADMIN instance key, byte-identical to admin-sep's) replaces the AdministratableExtension bound on Batchable.

The registry cdylib now depends on registry-traits and re-exports its modules; it keeps admin-sep only for its own Administratable/Upgradable entry points (sdk 25).

soroban-sdk pin is >=25.3.1, <27. The source compiles clean under 25, 26 and 27 (all verified), but cargo won't unify a range spanning two majors — a <28 range makes a sdk-26 consumer resolve two soroban-sdk versions. So the ceiling sits at the consumer's major + 1. Perch is on soroban-sdk 26 → <27. When perch moves to 27, bump the pin to <28.

3. StatelessDeployable::deploy_stateless (folds in #10)

salt = wasm_hash, init = () always → the deployed address is a pure function of (deployer, wasm_hash); a wasm whose __constructor needs args just traps (self-enforcing deployer-independence, no ABI introspection). Idempotent: if the derived address already holds an executable it returns it instead of trapping with AlreadyDeployed. Wired with one line in registry/src/lib.rs. A caller-supplied deployer must authorize at the root of the call; defaulting to the registry needs no external auth.

Proof / tests

  • External importability: contracts/test/registry-consumer (compile-only) implements the full trait set on a foreign contract type, depending only on registry-traits (exactly as perch will).
  • sdk 26: a standalone probe pinning soroban-sdk = "26.0.1" resolves to a single sdk 26 and compiles impl Deployable/StatelessDeployable/… for Probe {}.
  • deploy_stateless (registry/src/test/stateless.rs, new zero-arg-ctor fixture immutable_hello): salt == wasm hash; returned id equals the offline-derived with_address(registry, hash).deployed_address(); a second call is a no-op success (also via try_); an arg-requiring constructor traps (proving no init args are ever passed); explicit deployer honored + idempotent.

Build/test status

  • just build (stellar scaffold build) — OK; contracts workspace stays on soroban-sdk 25.3.1.
  • cargo test --workspace48 pass, 0 fail (46 registry + 1 registry-traits + 1 tansu-manager).
  • just clippy and just clippy-test (pedantic, -Dwarnings) — clean. cargo fmt --all --check — clean.

For the perch side

Depend on this crate at this PR's git rev:

registry-traits = { git = "https://github.com/stellar-registry/contracts", rev = "<merge-commit>" }

and in the stateless registry contract:

use registry_traits::registry::contract::{Deployable, StatelessDeployable /* … */};
use registry_traits::registry::wasm::Publishable;
use registry_traits::Error;                 // needed in scope for the #[contracttrait] glue
// admin lives at the `ADMIN` instance key (registry_traits::admin) — store your admin there

Done vs TODO

Done

  • Registry #[contracttrait]s importable from an external crate (proven by registry-consumer); registry contract still builds + passes its suite.
  • Importable-traits crate (registry-traits) compiles under soroban-sdk 26 (perch) — and 25/27.
  • StatelessDeployable::deploy_stateless + tests (salt = wasm hash; offline-derived address match; idempotent no-op; no init args).

TODO / follow-ups

  • build: use stellar.expert verified build workflow #10 (perch, "constructorless-immutable deploy entry point") is already closed; its behavior is folded into StatelessDeployable here.
  • Open the perch-side "stateless managed registry contract" PR consuming registry-traits at this rev.
  • If/when perch bumps to soroban-sdk 27, change registry-traits' soroban-sdk pin (and dev-dep) from <27 to <28 (source already builds under 27).
  • Product decision: deploy_stateless is intentionally permissionless (any published wasm, content-addressed, no manager gate). If a managed subregistry wants to gate it, gate at the perch layer.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AVMqraqa1ERu7HATfbNiEj

willemneal and others added 3 commits August 20, 2026 13:47
Decouple the registry's public #[contracttrait] default bodies from the
concrete `crate::Contract` so a downstream contract can
`impl Deployable for MyContract {}` and reuse the exact logic
(admin-sep style). The former inherent `impl Contract` helpers move onto
a neutral `RegistryHelpers` zero-sized type — contract-agnostic, all
state behind the fixed storage-key convention; admin routing now goes
through `Self::require_admin` (`Batchable: AdministratableExtension`).
Expose an `rlib` target so the traits are importable at all.

Add `StatelessDeployable::deploy_stateless`: content-addressed deploy
(salt = wasm_hash, init = () always), idempotent — returns the existing
instance if the derived address already holds an executable rather than
trapping with AlreadyDeployed. Folds in the old #10 `deploy_immutable`.
Wired with one line in `registry/src/lib.rs`.

Prove external importability with a compile-only `registry-consumer`
crate that implements the whole trait set on a foreign contract type.

Tests (new `immutable_hello` zero-arg-ctor fixture): salt = wasm hash,
returned id equals the offline-derived address, a second call is a
no-op success, and no init args are ever passed (an arg-requiring
constructor traps).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVMqraqa1ERu7HATfbNiEj
Move the importable #[contracttrait]s + storage/name/version/events/error
into a new `registry-traits` rlib that depends ONLY on soroban-sdk — never
admin-sep or soroban-sdk-tools, both of which pin sdk ^25 and would block a
soroban-sdk 26 consumer (perch). This is what perch will `use`.

- error.rs: `scerr` -> hand-written `#[contracterror]`, same discriminants
  (1..=21) and same exported spec, so the wire ABI is unchanged.
- storage.rs: soroban-sdk-tools `InstanceItem` -> plain instance storage at
  the identical `ROOT_REG` key.
- admin: `Batchable`'s auth no longer needs admin-sep's
  `AdministratableExtension`; it requires the admin stored at the same
  `ADMIN` instance key via a tiny in-crate `admin` module (storage-key
  convention), so downstream contracts reuse the traits with no admin trait.

soroban-sdk pin is `>=25.3.1, <27`: the source compiles under 25/26/27, but
cargo won't unify a range spanning two majors, so the ceiling sits at the
consumer's major + 1 (perch = 26 -> `<27`). In this workspace admin-sep's
^25 keeps everything on 25.3.x.

The `registry` cdylib now depends on `registry-traits` and re-exports its
modules; it keeps admin-sep only for its own Administratable/Upgradable entry
points. `registry-consumer` now depends solely on registry-traits, mirroring
perch. Verified: contracts workspace stays on sdk 25.3.1 with all tests
green; a standalone consumer pinning soroban-sdk 26 resolves to a single sdk
26 and compiles the whole trait set (27 too).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVMqraqa1ERu7HATfbNiEj
…on 27.0.2)

Perch — the downstream consumer — pins soroban-sdk 27.0.2, so a <27 ceiling
forced a two-major split (perch 27 + registry-traits 26) that cargo won't
unify, blocking perch#39. <28 still admits the contracts workspace's 25.3.x
(admin-sep-pinned), so both consumers resolve to a single soroban-sdk.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVMqraqa1ERu7HATfbNiEj
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant