Skip to main content

thruput_macros/
lib.rs

1//! Generate interfacing materials from a trait or impl that has
2//! been annotated with the `contract` macro.  This crate produces:
3//!
4//! - Client-stub implementations for making contract calls
5//! - Guest method stubs and external symbols for calling into the contract
6//! - Test-registration code
7
8use darling::FromMeta;
9use proc_macro::TokenStream;
10use quote::quote;
11
12mod common;
13mod contract;
14mod contract_exception;
15mod contract_type;
16mod derive_contract_type_spec;
17mod derive_event;
18mod derive_exn;
19mod static_hash;
20
21/// Early check that all enum variants have explicit discriminants.
22/// Called by attribute macros before emitting derives, so the user
23/// sees one error instead of cascading failures from multiple derives.
24fn check_enum_discriminants(
25    variants: &syn::punctuated::Punctuated<syn::Variant, syn::Token![,]>,
26) -> syn::Result<()> {
27    for variant in variants {
28        if variant.discriminant.is_none() {
29            return Err(syn::Error::new_spanned(
30                variant,
31                "enum variants must use explicit discriminants for contract specs",
32            ));
33        }
34    }
35    Ok(())
36}
37
38/// Shared helper: generate `impl ContractTypeName for #ident`.
39/// Used by both `#[contract_type]` and `#[contract_exception]`.
40///
41/// `name = "..."` replaces the final segment of the id, substituted verbatim.
42/// Both callers first reject a value carrying `::`
43/// ([`common::util::validate_spec_name`]), so a name cannot spell segments of
44/// its own: the module path is always the declaration's. The crate name and
45/// major version still prefix every id and come from the declaring crate, so a
46/// name cannot reach another crate's id — the most it can do is collide with
47/// another type in its own, which is the author's conflict to resolve. The id
48/// itself comes from [`common::spec_id::spec_id_tokens`], which every
49/// spec-named item shares.
50///
51/// `version` is the item's declared identity version, or `None` for the
52/// crate's. `#[contract_exception(version = "...")]` pins one to hold its
53/// selector still across releases, and the id has to be pinned with it.
54fn contract_type_name_impl(
55    krate: &syn::Path,
56    ident: &syn::Ident,
57    name: Option<&str>,
58    version: Option<&str>,
59) -> proc_macro2::TokenStream {
60    let declared = name.map_or_else(|| ident.to_string(), str::to_string);
61    let id = common::spec_id::spec_id_tokens(krate, &declared, version);
62    quote! {
63        impl #krate::__contract_spec::ContractTypeName for #ident {
64            const NAME: #krate::__contract_spec::ContractSpecIdView<'static> = #id;
65        }
66    }
67}
68
69mod deletable;
70mod storage;
71
72/// Compute a `StorageKey` at compile time: sha256 of the string literal's
73/// **value** (quotes stripped, escapes resolved).
74///
75/// `static_key!("counter")` hashes the 7 bytes `counter`, so the key can be
76/// reproduced off-chain (indexer, client, another language) as
77/// `sha256("counter")`. Source spelling does not matter:
78/// `static_key!(r"a")` == `static_key!("a")`.
79#[proc_macro]
80pub fn static_key(item: TokenStream) -> TokenStream {
81    let input = syn::parse_macro_input!(item as syn::LitStr);
82
83    static_hash::storage_key_static(input)
84}
85
86/// Compute a `SHA256Hash` at compile time: sha256 of the string literal's
87/// **value** (quotes stripped, escapes resolved).
88///
89/// `sha256_static!("foo")` hashes the 3 bytes `foo` and agrees with
90/// `thruput_sdk::crypto::sha256(b"foo")` at runtime and with
91/// [`hash_to_array!`](macro@hash_to_array).
92#[proc_macro]
93pub fn sha256_static(item: TokenStream) -> TokenStream {
94    let input = syn::parse_macro_input!(item as syn::LitStr);
95
96    static_hash::sha256_static(input)
97}
98
99/// Anchor a `#[contract]` trait's spec for inclusion. Spec shaking drops a
100/// trait's (candidate) interface spec unless something anchors it — normally a
101/// `#[contract] impl`. Use this in an interface crate's spec-carrier `main.rs`
102/// so building that crate on its own emits the interface's full spec:
103///
104/// ```ignore
105/// thruput_sdk::export_interface_spec!(thruput_erc20_if::ERC20);
106/// ```
107#[proc_macro]
108pub fn export_interface_spec(item: TokenStream) -> TokenStream {
109    let path = syn::parse_macro_input!(item as syn::Path);
110    common::spec_reg::generate_interface_anchor_for_trait(&path).into()
111}
112
113/// Compute a `SHA256Hash` at compile time: sha256 of the bare identifier's
114/// text (no quotes — `sha256_ident!(foo)` hashes the 3 bytes `foo`).
115#[proc_macro]
116pub fn sha256_ident(item: TokenStream) -> TokenStream {
117    let input = syn::parse_macro_input!(item as syn::Ident);
118
119    static_hash::sha256_ident(input)
120}
121
122/// Like [`sha256_ident!`](macro@sha256_ident) (sha256 of the bare
123/// identifier's text), but expands to a `[u8; 32]` array instead of a
124/// `SHA256Hash`.
125#[proc_macro]
126pub fn sha256_array_ident(item: TokenStream) -> TokenStream {
127    let input = syn::parse_macro_input!(item as syn::Ident);
128
129    static_hash::sha256_array_ident(input)
130}
131
132/// Compute a `[u8; 32]` at compile time: sha256 of the literal's **value**
133/// (quotes stripped, escapes resolved). Accepts string and byte-string
134/// literals: `hash_to_array!("foo")` == sha256 of the 3 bytes `foo` and
135/// agrees with `thruput_sdk::crypto::sha256(b"foo")` at runtime and with
136/// [`sha256_static!`](macro@sha256_static)/[`static_key!`](macro@static_key)
137/// for the same string.
138#[proc_macro]
139pub fn hash_to_array(item: TokenStream) -> TokenStream {
140    let input = syn::parse_macro_input!(item as syn::Lit);
141
142    static_hash::sha256_array(input)
143}
144
145/// Turn a trait, impl block, or free function into contract entrypoints,
146/// generating exports, a typed client, and spec registration.
147///
148/// # State
149///
150/// Methods taking `&self`/`&mut self` load the contract's state from
151/// storage: the self type must be a storage struct (annotate it with
152/// `#[storage]`; a unit struct like `struct Token;` works for contracts
153/// that keep no state in the struct). Contracts with only associated
154/// functions need no storage annotation.
155///
156/// # Method visibility (bare impl blocks)
157///
158/// In a bare (non-trait) impl block, `pub` methods become entrypoints
159/// and private methods are skipped (use them for helpers). Restricted
160/// visibility (`pub(crate)`, `pub(super)`, `pub(in ...)`) is a compile
161/// error because the intent is ambiguous. Trait impls export every
162/// trait method regardless of visibility.
163///
164/// # Arguments
165///
166/// `version = "x.y.z"`, `interface = "Name"`, `exports = "mod_name"`,
167/// `client = ClientIdent`, `throws = Error` / `throws(E1, E2)`,
168/// `emits(Event1, Event2)`.
169///
170/// Trait implementations inherit the trait's interface name, version, and
171/// entrypoint IDs, including when the trait comes from another crate or is
172/// imported under an alias. A plain `#[contract] impl Trait for Contract`
173/// is sufficient. `version` is not allowed on trait impls. An explicit
174/// `interface` argument is an optional assertion that the name matches the trait.
175/// For trait definitions, bare impls, and free functions, the version still
176/// defaults to the declaring crate's `CARGO_PKG_VERSION`.
177///
178/// No `throws(...)` type may contain a `thruput_sdk::signer::Signed`: abort
179/// frames carry no auth-token handles, so such a payload could only decode
180/// to an uninitialized sentinel. Spec canonicalization rejects it when the
181/// contract is built. See `#[contract_exception]`.
182///
183/// # Declared method effects
184///
185/// On trait methods and public methods of bare impls, use
186/// `#[contract(effect = "pure")]`, `#[contract(effect = "readOnly")]`, or
187/// `#[contract(effect = "stateChanging")]`. Omission leaves the effect absent, never
188/// read-only. Trait implementations inherit declarations from the trait;
189/// annotating an implementation method is rejected.
190///
191/// Effects cover the whole invocation, including nested calls. `pure` uses
192/// only arguments and constants, without state or execution-context reads.
193/// `readOnly` permits those reads but no application effects. `stateChanging`
194/// permits storage deltas, events, transfers and deployment; it need not
195/// change state on every call. Event emission is an application effect.
196///
197/// These are author declarations. The macro checks syntax and duplicates,
198/// not method behavior; neither the compiler nor runtime enforces the
199/// declared effects. Metadata does not change entrypoint IDs, execution
200/// paths, authorization, gas accounting, or whether a call can fail.
201///
202/// # Limitations
203///
204/// Generic parameters — including explicit lifetime parameters — are not
205/// supported on `#[contract]` items or their methods: generated clients,
206/// entrypoint wrappers, and spec consts are non-generic, so a spliced
207/// `T` or `'a` cannot resolve there. Elided borrow lifetimes (`&self`,
208/// `&[u8]`) are fine, and `#[derive(Event)]` types may carry lifetime
209/// parameters (folded to `'static` in the spec). Associated types are not
210/// supported in `#[contract]` traits.
211/// A struct must carry only one `#[contract]` impl block: depending on
212/// configuration a second block either collides on generated items
213/// (E0428 on names you never wrote) or compiles while silently merging
214/// the entrypoint surface — neither is supported.
215///
216/// # Interface id
217///
218/// A `#[contract]` trait gains an associated
219/// `INTERFACE_ID: thruput_sdk::InterfaceId`, derived from the effective
220/// interface name and major version (the pair that also namespaces its
221/// entrypoint IDs), and the generated `<Iface>Entrypoints` wrapper carries
222/// the same constant. The name is reserved on contract traits. Every
223/// `#[contract]` impl block also records its interface's id in the
224/// contract's interface table (`thruput_sdk::interface_table`), which is what
225/// `thruput-erc165-if` answers `supports_interface` from.
226///
227/// # Storage-domain collision detection
228///
229/// When a contract uses multiple storage structs, `#[contract]` generates a
230/// lib test that verifies their domain strings are all distinct. This check
231/// runs as a `#[cfg(test)]` lib test; a crate that sets `[lib] test = false`
232/// in its `Cargo.toml`, or that only runs an external test harness without
233/// compiling lib tests, will silently skip this collision check at build time.
234#[proc_macro_attribute]
235pub fn contract(
236    attr_args: TokenStream,
237    item_tokens: TokenStream,
238) -> TokenStream {
239    // Parse the item tokens into syntax defined in `syn`
240    let item = syn::parse_macro_input!(item_tokens as syn::Item);
241
242    // Apply `#[contract]` to its target item
243    contract::apply(attr_args, item)
244        .unwrap_or_else(syn::Error::into_compile_error)
245        .into()
246}
247
248#[proc_macro_derive(Storage, attributes(slot, storage_domain))]
249pub fn derive_storage(input: TokenStream) -> TokenStream {
250    let input = syn::parse_macro_input!(input as syn::DeriveInput);
251
252    storage::generate_impls(&input)
253        .unwrap_or_else(syn::Error::into_compile_error)
254        .into()
255}
256
257/// Mark a struct as contract storage: generates `From<Keypath>` so each
258/// field is bound to a storage slot derived from the struct's keypath.
259///
260/// Works on named-field structs (fields must themselves be storage
261/// types like `NNInt`, `Map`, `Value`, or nested storage structs) and
262/// on unit structs (`struct Token;`) for contracts that keep all state
263/// behind raw storage keys.
264///
265/// Accepts an optional `domain = "..."` argument to override the default
266/// domain (the struct's bare identifier name).
267#[proc_macro_attribute]
268pub fn storage(
269    attr_args: TokenStream,
270    item_tokens: TokenStream,
271) -> TokenStream {
272    let attr_args: proc_macro2::TokenStream = attr_args.into();
273    let item = syn::parse_macro_input!(item_tokens as syn::Item);
274
275    // Accept only `domain = "..."`; reject anything else.
276    let mut domain: Option<syn::LitStr> = None;
277    if !attr_args.is_empty() {
278        let parser = syn::meta::parser(|meta| {
279            if meta.path.is_ident("domain") {
280                domain = Some(meta.value()?.parse()?);
281                Ok(())
282            } else {
283                Err(meta.error("#[storage] only accepts `domain = \"...\"`"))
284            }
285        });
286        if let Err(e) = syn::parse::Parser::parse2(parser, attr_args) {
287            return e.into_compile_error().into();
288        }
289    }
290
291    let domain_attr = match domain {
292        Some(lit) => quote::quote! { #[storage_domain = #lit] },
293        None => quote::quote! {},
294    };
295
296    quote::quote! {
297        #[derive(::thruput_sdk::Storage)]
298        #domain_attr
299        #item
300    }
301    .into()
302}
303
304#[proc_macro_derive(Deletable)]
305pub fn derive_deletable(input: TokenStream) -> TokenStream {
306    let input = syn::parse_macro_input!(input as syn::DeriveInput);
307
308    deletable::generate_impls(&input)
309        .unwrap_or_else(syn::Error::into_compile_error)
310        .into()
311}
312
313/// `#[exn]` or `#[exn(version = "1.0.0")]` — emits `impl Exn` with a
314/// computed selector. Use this when you handle your own derives.
315/// For the full convenience macro, use `#[contract_exception]` instead.
316///
317/// `wire_crate = "..."` pins the package label used in the selector hash, as
318/// it does for `#[contract_exception]`.
319///
320/// Handling your own derives means owning the rule that
321/// `#[contract_exception]` enforces structurally: **an exception payload
322/// may not contain a `thruput_sdk::signer::Signed`**, because abort
323/// frames carry no auth-token handles and such a field could only decode to
324/// an uninitialized sentinel. See `#[contract_exception]` for the full
325/// reasoning. Nothing here can check that for you, but the contract's spec
326/// is validated at build time and will reject it.
327#[proc_macro_attribute]
328pub fn exn(attr_args: TokenStream, item_tokens: TokenStream) -> TokenStream {
329    let item = syn::parse_macro_input!(item_tokens as syn::Item);
330    match derive_exn::parse_args(attr_args) {
331        Ok(args) => derive_exn::expand(args, item),
332        Err(err) => err,
333    }
334}
335
336/// `#[contract_exception]` or `#[contract_exception(version = "1.0.0", name = "...")]` —
337/// full convenience macro: derives + `#[repr(i32)]` + `impl Exn` +
338/// `impl ContractTypeName`.
339///
340/// `name = "..."` renames the type in the spec as it does for
341/// `#[contract_type]`, and may not contain `::` either.
342///
343/// `wire_crate = "..."` pins the package label used in the selector hash.
344/// Use it for a protocol-defined exception whose wire identity must survive
345/// a package rename. It does not change the type's spec name or provenance.
346///
347/// # Exception payloads cannot carry auth tokens
348///
349/// No field may be — or contain — a `thruput_sdk::signer::Signed`.
350///
351/// A `Signed` occupies zero wire bytes; its real handle arrives out of band
352/// in the auth-token list the host returns beside a *successful* return.
353/// Abort frames carry no such list (the host refuses to return auth tokens
354/// on a revert frame), so a `Signed` reached through an exception decodes
355/// to the uninitialized `u32::MAX` sentinel — a value that looks like an
356/// auth token, is not one, and faults when consumed or returned.
357///
358/// There is no way to write this correctly, so hand the token back through
359/// a successful return instead, or identify it by something inert (the
360/// minter's address, an id) and let the caller re-obtain it.
361///
362/// Two things catch it. This macro's `PartialEq` derive (required by `Exn`)
363/// blocks the direct route — `Signed` does not implement `PartialEq` —
364/// so reaching the situation at all takes hand-written impls (or `#[exn]`).
365/// Beyond that, spec canonicalization walks each `throws(...)` payload when
366/// the contract is built and rejects any that reaches a signed token,
367/// however deeply nested, so hand-rolled types do not slip through.
368#[proc_macro_attribute]
369pub fn contract_exception(
370    attr_args: TokenStream,
371    item_tokens: TokenStream,
372) -> TokenStream {
373    let item = syn::parse_macro_input!(item_tokens as syn::Item);
374    let args = if attr_args.is_empty() {
375        contract_exception::ContractExceptionArgs::default()
376    } else {
377        let meta_list = match darling::ast::NestedMeta::parse_meta_list(
378            attr_args.into(),
379        ) {
380            Ok(v) => v,
381            Err(e) => {
382                return TokenStream::from(
383                    darling::Error::from(e).write_errors(),
384                );
385            }
386        };
387        match contract_exception::ContractExceptionArgs::from_list(&meta_list)
388        {
389            Ok(v) => v,
390            Err(e) => return TokenStream::from(e.write_errors()),
391        }
392    };
393    contract_exception::expand(args, item).into()
394}
395
396/// Derive event encoding, signature and spec metadata.
397///
398/// `#[event_spec(wire_crate = "...")]` pins the package label used in the
399/// signature hash, for protocol-defined events whose wire identity must survive
400/// a package rename. Spec names still use the declaring package. Combine with
401/// `export` to retain the event's spec without an interface `emits(...)` entry.
402#[proc_macro_derive(Event, attributes(topic, event_spec))]
403pub fn derive_event(input: TokenStream) -> TokenStream {
404    let input = syn::parse_macro_input!(input as syn::DeriveInput);
405
406    derive_event::expand(&input)
407        .unwrap_or_else(syn::Error::into_compile_error)
408        .into()
409}
410
411/// Make a struct or enum usable as a contract argument, return value, or
412/// auth-token payload: derives XDR codecs, spec registration,
413/// auth-handle walkers, and the `Mintable` preimage machinery.
414///
415/// Both structs and enums recurse when minting auth tokens: a
416/// `Signed<T>` field — including one inside an enum variant — becomes a
417/// child auth-token node, and its preimage type in the generated
418/// `Preimage` mirror is `AuthTokenPreimage<T::Preimage>`.
419///
420/// Enum variants must carry explicit discriminants.
421///
422/// `name = "..."` renames the type in the spec: it replaces the final segment
423/// of the type's spec id, and may not contain `::` (the module path is always
424/// the one the type is declared in).
425#[proc_macro_attribute]
426pub fn contract_type(
427    attr_args: TokenStream,
428    item_tokens: TokenStream,
429) -> TokenStream {
430    let item = syn::parse_macro_input!(item_tokens as syn::Item);
431    let args = if attr_args.is_empty() {
432        contract_type::ContractTypeArgs::default()
433    } else {
434        let meta_list = match darling::ast::NestedMeta::parse_meta_list(
435            attr_args.into(),
436        ) {
437            Ok(v) => v,
438            Err(e) => {
439                return TokenStream::from(
440                    darling::Error::from(e).write_errors(),
441                );
442            }
443        };
444        match contract_type::ContractTypeArgs::from_list(&meta_list) {
445            Ok(v) => v,
446            Err(e) => return TokenStream::from(e.write_errors()),
447        }
448    };
449    contract_type::expand(args, item).into()
450}
451
452/// Derive `ContractTypeSpec` (plus `ContractTypeRefSpec`, a linker anchor, and
453/// the raw typedef link-section fragment) for a struct or enum.
454///
455/// # Options
456///
457/// - `#[contract_type_spec(export)]` — emit the fragment as an *exported*
458///   typedef, so it survives spec shaking without an inbound reference.
459/// - `#[contract_type_spec(crate = "...")]` — resolve the emitted paths through
460///   the given crate path instead of `::thruput_sdk`.
461///
462/// # `crate = "..."`
463///
464/// The derive normally emits literal `::thruput_sdk::…` paths, so a crate
465/// that `thruput-sdk` itself depends on cannot use it — naming
466/// `::thruput_sdk` from below `thruput-sdk` is a dependency cycle. But
467/// everything the derive reaches for is a `thruput-sdk` *re-export* of a
468/// crate below `thruput-sdk`: `__contract_spec` is `thruput-contract-spec`
469/// and `__xdrs` is `xdrs`. Nothing here is owned by `thruput-sdk`.
470///
471/// So such a crate can supply a shim matching those two names and point the
472/// derive at it:
473///
474/// ```ignore
475/// #[doc(hidden)]
476/// pub mod __sdk_paths {
477///     pub use thruput_contract_spec as __contract_spec;
478///     pub use xdrs as __xdrs;
479/// }
480///
481/// #[derive(ContractTypeSpec)]
482/// #[contract_type_spec(crate = "crate::__sdk_paths")]
483/// pub struct U256([u32; 8]);
484/// ```
485///
486/// Note the `crate::` prefix. The derive is applied *inside* the crate that
487/// owns the shim, and a crate cannot name itself as `::its_own_name` without a
488/// separate `extern crate self as its_own_name;` — spelling it
489/// `::thruput_u256::__sdk_paths` there fails with E0433. An absolute path is
490/// right only when some *other* crate owns the shim.
491///
492/// The consumer still supplies its own `impl ContractTypeName`, which the
493/// derive references but never emits.
494///
495/// Note this covers *spec* impls only. `#[contract_type]` additionally emits
496/// impls of traits genuinely owned by `thruput-sdk` (`CollectAuthHandles`,
497/// `ReceiveAuthHandles`, `MintableType`), which a lower crate cannot implement
498/// — that is a dependency problem, not a path problem, and has no override.
499#[proc_macro_derive(ContractTypeSpec, attributes(contract_type_spec))]
500pub fn derive_contract_type_spec(input: TokenStream) -> TokenStream {
501    let input = syn::parse_macro_input!(input as syn::DeriveInput);
502
503    derive_contract_type_spec::expand(&input)
504        .unwrap_or_else(syn::Error::into_compile_error)
505        .into()
506}