Skip to content

feat: import_contract! macro + consolidate registry macros (stellar-scaffold/cli#419)#17

Open
willemneal wants to merge 12 commits into
mainfrom
feat/import-contract-macro
Open

feat: import_contract! macro + consolidate registry macros (stellar-scaffold/cli#419)#17
willemneal wants to merge 12 commits into
mainfrom
feat/import-contract-macro

Conversation

@willemneal

@willemneal willemneal commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Implements import_contract! (stellar-scaffold/cli#419) and consolidates all registry macros into this repo: import_contract!, import_contract_client!, and import_asset! now live in crates/stellar-registry-macro and are re-exported from stellar-registry. The stellar-scaffold-macro dependency is gone.

What

stellar_registry::import_contract!(env, name) returns a type-safe soroban Client already bound to the named contract's deployed on-chain address, resolved at build time:

let dao = stellar_registry::import_contract!(env, our_dao);
dao.create_proposal(/* ... */);

Unlike import_contract_client! (which imports a published wasm, optionally @version), import_contract! imports a deployed contract: it has no version, and its client types are generated from the deployed instance's own wasm (stellar contract fetch --id …), so contracts whose wasm was never published to the registry still work.

Parse, don't validate

Names are now typed in stellar-registry-build::name and parsing is the only way to construct them:

  • Prefixedname or channel/name. Rejects empty names, @ (deployed contracts have no version), multiple slashes, and invalid characters. Private fields; name() / channel() / mod_name() / canonical_name() accessors.
  • VersionedPrefixed + optional @version (leading v tolerated). A malformed version is an error, never silently "latest" (the old code dropped @v1.0.0 on the floor).

import_contract! parses Prefixed, import_contract_client! parses Versioned — so "contracts have no version" is enforced by the type, not a string check. The same types back the CLI's clap args (14 commands), so bad names fail at arg parsing with real messages.

Address resolution & caching

  • Online: stellar registry fetch-contract-id every build — a cached id is deliberately ignored so a contract flagged after the first build can't slip through. Result cached at target/stellar/<network>/deployed/[<channel>__]<mod_name>.id, wasm beside it. The wasm is refetched when the resolved address changes (redeploy under the same name).
  • The deployed/ namespace + channel-qualified stems mean the cache can never collide with registry-downloaded wasms, workspace-built wasms, or the same name on a different channel.
  • STELLAR_NO_REGISTRY=1 forbids network calls and requires the cached id + wasm.
  • The STELLAR_CONTRACT_ID_<NAME> override is removed (per review).

Flagged contracts (breaking)

  • Registry::fetch_contract_id (build crate) now rejects flagged contracts by default; fetch_contract_id_unchecked is the explicit opt-out.
  • stellar registry fetch-contract-id errors on flagged contracts unless --force; create-alias honors its existing --force the same way.
  • import_contract! fails compilation on a flagged contract (with an up-to-date plugin).

Errors now say which failure occurred (CLI missing vs plugin missing vs plugin too old vs flagged vs not found, with the exact stderr) and link the registry explorer on testnet/mainnet.

Dependency footprint

stellar-registry-macro is re-exported from stellar-registry, which user contracts depend on — so it must stay light. stellar-registry-build now gates everything network-facing behind the default cli feature; with default-features = false (what the macro uses) its tree is semver + thiserror, not the stellar-cli stack.

Removed

  • docs/superpowers/ (per review)
  • named_registry.rs (superseded by name::Prefixed)
  • stellar-scaffold-macro dependency

🤖 Generated with Claude Code

@chadoh chadoh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stop slopping me bro

Comment thread crates/stellar-registry-macro/src/lib.rs Outdated
Comment thread crates/stellar-registry-macro/src/lib.rs Outdated

@chadoh chadoh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also needs to fail compilation if the requested contract is flagged

willemneal added a commit that referenced this pull request Jul 14, 2026
Addresses review on #17 ("stop slopping me bro"): the macro was built by
analogy to import_contract_client! and conflated a contract with its wasm.

- Drop @Version. A deployed contract has no version (only a wasm does);
  reject `@` with a clear compile_error!.
- Stop delegating codegen to import_contract_client!(name), which resolves a
  wasm by name and wrongly assumes contract-name == wasm-name. Instead fetch
  the deployed contract's own on-chain wasm by address (`stellar contract
  fetch --id`) and inline `soroban_sdk::contractimport!` — so a registered
  contract whose wasm was never published still works.
- Fail compilation if the contract is flagged as compromised (#38, #52). No
  on-chain getter exists, so read the registry's ContractEntry persistent
  ledger entry directly via RPC (key (Symbol("CR"), <canonical name>); a
  3-element vec == flagged), behind a new `fetch-contract-id --reject-flagged`.
- Online builds no longer trust the cached .id, so a contract flagged after
  the first build can't slip through; the .id/.wasm caches are the offline
  (STELLAR_NO_REGISTRY=1) fallback, and env override / offline are the only
  explicit opt-outs of the flag check.
- Self-contained rustdoc (no import_contract_client! reference).

New: stellar_registry_build::Registry::is_contract_flagged (raw ledger read),
fetch_contract_id --reject-flagged. Design/plan docs get a post-review
revision note.

Verified end to end on testnet: fetch-contract-id --reject-flagged returns the
address when unflagged, errors "contract `oz` is flagged as compromised" once
flag_contract sets it, and the plain lookup is unchanged. Unit tests + pedantic
clippy pass.

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

@chadoh chadoh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better but still a lot to improve here

Comment thread crates/stellar-registry-cli/src/commands/fetch_contract_id.rs Outdated
Comment thread crates/stellar-registry-macro/src/lib.rs Outdated
Comment thread crates/stellar-registry-macro/src/lib.rs Outdated
Comment thread crates/stellar-registry/src/lib.rs Outdated
@@ -0,0 +1,213 @@
# `import_contract!` macro — Design

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I... kind of hate this docs/superpowers pattern. It seems definitionally like a bunch of bloat for the sake of AI. Convince me that it's not. Convince me that this will ever be useful to a human, and should be checked into source control.

My thoughts on why we're better off without it:

  • It's a LOT to read through (no human's going to do that, c'mon)
  • It's going to drift from the implementation VERY quickly
  • The decisions about why we built things the way we did should already in the git log, and at least in GitHub issues/PRs

willemneal and others added 12 commits July 15, 2026 12:48
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5b9Nj9oNVLjeRetxCeyxP
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5b9Nj9oNVLjeRetxCeyxP
Implement the import_contract! macro helper functions in TDD style:
- Create crate skeleton with proc-macro lib type
- Wire workspace dependencies (proc-macro2, quote, syn)
- Add 6 pure helper functions: mod_name_from, split_version, env_var_name,
  validate_contract_id, cache_id_path, manifest
- Add comprehensive unit tests for all helpers (5 tests, all passing)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5b9Nj9oNVLjeRetxCeyxP
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5b9Nj9oNVLjeRetxCeyxP
Add resolve_address (fully injectable for testability) and fetch_contract_id
(shell-out to stellar CLI) to implement address resolution precedence:
env var > cache > registry lookup. Includes 4 unit tests verifying the
resolution order (env override, cache fallback, no-registry error, fetch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5b9Nj9oNVLjeRetxCeyxP
Implement struct Input with syn::parse::Parse trait to parse the macro's
input (env expression and contract name as bare ident or string literal).
Implement fn expand() to emit a block expression that delegates wasm import
to import_contract_client! macro and constructs the client bound to the
resolved contract address.

Add codegen test module with 3 tests covering parsing and code generation.

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

Adds the #[proc_macro] entry point that ties together the pure helpers,
build-time address resolution, and codegen from the prior tasks, and
re-exports it as stellar_registry::import_contract for consumers. This
wires every previously-dead helper into the entry point, so no
dead_code warnings remain and no #[allow(dead_code)] was needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5b9Nj9oNVLjeRetxCeyxP
Prevents a compiler panic on names like "foo/" or "@v1.0.0" (empty module
identifier) and stops a malformed fetch-contract-id response from poisoning
the .id cache. Findings from the final whole-branch review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5b9Nj9oNVLjeRetxCeyxP
Addresses review on #17 ("stop slopping me bro"): the macro was built by
analogy to import_contract_client! and conflated a contract with its wasm.

- Drop @Version. A deployed contract has no version (only a wasm does);
  reject `@` with a clear compile_error!.
- Stop delegating codegen to import_contract_client!(name), which resolves a
  wasm by name and wrongly assumes contract-name == wasm-name. Instead fetch
  the deployed contract's own on-chain wasm by address (`stellar contract
  fetch --id`) and inline `soroban_sdk::contractimport!` — so a registered
  contract whose wasm was never published still works.
- Fail compilation if the contract is flagged as compromised (#38, #52). No
  on-chain getter exists, so read the registry's ContractEntry persistent
  ledger entry directly via RPC (key (Symbol("CR"), <canonical name>); a
  3-element vec == flagged), behind a new `fetch-contract-id --reject-flagged`.
- Online builds no longer trust the cached .id, so a contract flagged after
  the first build can't slip through; the .id/.wasm caches are the offline
  (STELLAR_NO_REGISTRY=1) fallback, and env override / offline are the only
  explicit opt-outs of the flag check.
- Self-contained rustdoc (no import_contract_client! reference).

New: stellar_registry_build::Registry::is_contract_flagged (raw ledger read),
fetch_contract_id --reject-flagged. Design/plan docs get a post-review
revision note.

Verified end to end on testnet: fetch-contract-id --reject-flagged returns the
address when unflagged, errors "contract `oz` is flagged as compromised" once
flag_contract sets it, and the plain lookup is unchanged. Unit tests + pedantic
clippy pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5b9Nj9oNVLjeRetxCeyxP
This builds on the initial PrefixedName type but adds a Versioned type
which makes checking for versions on wasm parsable.

Also addressed other issues in PR review.
@willemneal
willemneal force-pushed the feat/import-contract-macro branch from 29d0cf1 to 0acb8ae Compare July 16, 2026 13:01
@willemneal
willemneal requested a review from chadoh July 16, 2026 13:54
@willemneal willemneal changed the title feat: import_contract! macro (stellar-scaffold/cli#419) feat: import_contract! macro + consolidate registry macros (stellar-scaffold/cli#419) Jul 16, 2026

@chadoh chadoh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't have time to give this a full deep-dive today, but from what I saw it's looking great! One comment so far. Happy to have you push back on this idea, but I think it could be nice. See below.

Comment on lines +3 to +6
//! The `name` module (typed registry names) is always available and
//! dependency-light so proc-macro crates can use it. Everything that talks to
//! the network — `contract`, `registry`, `error` — sits behind the default
//! `cli` feature, which pulls in the full stellar-cli stack.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is cool and clever but I do wonder if creating a separate crates/stellar-registry-name would be a more straightforward way to handle it

@willemneal willemneal Jul 17, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused; this section is about how the modules in this crate are exposed. I do like that idea though since it removes the download at build time part but wouldn't allow for stopping flagged contracts.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm saying the clever "cli features" stuff, and corresponding "skip default features" thing done by the proc-macro, wouldn't be necessary if we had a stripped-down stellar-registry-name crate that only did the proc-macro-compatible stuff. Then that crate can be used in both places, and would be easier for external parties to consume and use too, if they care to, without the "embeds stellar cli by default" footgun.

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.

2 participants