@layerzerolabs/framework-solana-anchor-trait 0.2.63 → 0.2.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,316 @@
1
- # anchor-trait
1
+ # Anchor Trait
2
2
 
3
- Compile-time instruction interface system for Anchor programs — trait-based definitions with default implementations and override levels.
3
+ ## Overview
4
4
 
5
- For the full specification, design decisions, and override resolution rules, see [SPEC.md](./SPEC.md).
5
+ The Anchor framework provides no mechanism for defining or enforcing instruction-level interfaces across programs. When a standard requires a set of instructions — e.g., every OApp program must expose `set_peer` and `lz_receive` with specific signatures — there is no compile-time guarantee that a conforming program provides them all, uses the correct parameter types, or returns the expected types. Violations surface only at runtime, typically as CPI failures or deserialization errors.
6
+
7
+ `anchor-trait` introduces **instruction traits** to Anchor: compile-time enforceable instruction interfaces with optional default implementations. A trait author declares an instruction set — each instruction's parameter type, return type, and whether a default implementation is provided — and the framework ensures, during proc-macro expansion, that every implementing program satisfies the trait contract. Missing required instructions produce compile errors. View instructions additionally enforce **compile-time immutability**: handlers receive `&Context<T>` (not `&mut`), so read-only semantics are upheld by the type system rather than relying solely on Solana runtime protections.
8
+
9
+ The framework is domain-agnostic. It operates purely at the instruction-signature and accounts-struct level, with no knowledge of any specific application domain. Macro crates such as `oapp-macros` or `rbac-macros` integrate with `anchor-trait` to define their own domain-specific instruction traits.
10
+
11
+ ### Correspondence to Rust traits
12
+
13
+ | Rust trait | `anchor-trait` |
14
+ | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
15
+ | Required method (no default body) | Instruction with `default_impl: None` — implementor must provide a handler |
16
+ | Default method (has body) | Instruction with `default_impl: Some(...)` — framework provides a complete implementation |
17
+ | `impl Trait for Type` | Applying the domain macro to a module (e.g., `#[oapp(...)] mod my_program { ... }`) |
18
+ | Overriding a default method | Annotating a handler with `#[oapp_instruction]` — the handler's `Context<T>` determines the accounts type |
19
+ | Compile error on unimplemented required method | Compile error on missing required instruction handler |
20
+
21
+ ## Instruction Kinds
22
+
23
+ Each instruction in a trait is classified as one of two kinds:
24
+
25
+ **Required** — The trait provides no default implementation. The implementor must supply a handler annotated with the override attribute; omitting it produces a compile error. This is analogous to a required method in a Rust trait.
26
+
27
+ ```rust
28
+ InstructionSpec {
29
+ params_type: Some(quote!(LzReceiveParams)),
30
+ return_type: quote!(()),
31
+ is_view: false,
32
+ default_impl: None,
33
+ }
34
+ ```
35
+
36
+ **Provided** — The trait supplies a complete default implementation, including an accounts struct and a handler function. The default accounts struct is always emitted, so it remains available even when the implementor overrides the handler. This is analogous to a default method in a Rust trait.
37
+
38
+ ```rust
39
+ InstructionSpec {
40
+ params_type: Some(quote!(SetPeerParams)),
41
+ return_type: quote!(()),
42
+ is_view: false,
43
+ default_impl: Some(DefaultImpl {
44
+ handler_ident: format_ident!("apply"),
45
+ generator: generate_default_impl,
46
+ }),
47
+ }
48
+ ```
49
+
50
+ ## Overriding Instructions
51
+
52
+ For provided instructions, the implementor may accept the default or supply a custom handler annotated with the override attribute (e.g., `#[oapp_instruction]`). The handler's `Context<T>` type determines the entrypoint's accounts type:
53
+
54
+ - To **reuse** the default accounts struct, declare `Context<StdSetPeer>`.
55
+ - To use **custom** accounts, declare `Context<MyCustomAccounts>`.
56
+
57
+ The framework does not distinguish between these cases — the default struct is always emitted regardless.
58
+
59
+ For required instructions, no default exists. The implementor must provide a handler.
60
+
61
+ ### Resolution algorithm
62
+
63
+ 1. **Handler present** → dispatch to the handler (entrypoint uses the handler's declared context type)
64
+ 2. **No handler, default exists** → dispatch to the default handler
65
+ 3. **No handler, no default** → compile error
66
+
67
+ Unannotated functions whose identifiers collide with managed instruction names are rejected at compile time. Such functions must be annotated with the override attribute (e.g. `#[oapp_instruction]`) to disambiguate intent.
68
+
69
+ ## Defining a Trait
70
+
71
+ The following steps describe how to create a new instruction trait using the framework.
72
+
73
+ ### 1. Define a codegen context
74
+
75
+ The codegen context is a struct that carries domain-specific resolved types and is threaded through every instruction generator:
76
+
77
+ ```rust
78
+ pub(crate) struct CodegenContext<'a> {
79
+ pub role_type: &'a TokenStream,
80
+ pub state_type: &'a TokenStream,
81
+ // additional domain-specific fields
82
+ }
83
+ ```
84
+
85
+ ### 2. Declare the instruction set
86
+
87
+ The `declare_instructions!` macro enumerates all instructions in the trait. It generates:
88
+
89
+ - Sub-module declarations (`mod set_peer;`, `mod get_peer;`)
90
+ - A named enum implementing `InstructionSet` with `all()`, `from_ident()`, `canonical_name()`, and `spec()`
91
+
92
+ ```rust
93
+ use anchor_trait::declare_instructions;
94
+
95
+ declare_instructions! {
96
+ name = OAppInstructionSet;
97
+ context = CodegenContext;
98
+
99
+ SetPeer => set_peer,
100
+ GetPeer => get_peer,
101
+ }
102
+ ```
103
+
104
+ ### 3. Define per-instruction specs
105
+
106
+ Each sub-module exports a `spec()` function returning an `InstructionSpec<CodegenContext>` that describes the instruction's signature and default implementation status:
107
+
108
+ ```rust
109
+ // Provided instruction — implementor may accept the default or override:
110
+ pub(super) fn spec<'a>() -> InstructionSpec<CodegenContext<'a>> {
111
+ InstructionSpec {
112
+ params_type: Some(quote!(::my_crate::params::SetPeerParams)),
113
+ return_type: quote!(()),
114
+ is_view: false,
115
+ default_impl: Some(DefaultImpl {
116
+ handler_ident: format_ident!("apply"),
117
+ generator: generate_default_impl,
118
+ }),
119
+ }
120
+ }
121
+
122
+ fn generate_default_impl(
123
+ ctx: &CodegenContext<'_>,
124
+ spec: &InstructionSpec<CodegenContext<'_>>,
125
+ ) -> DefaultAccounts {
126
+ let params_type = spec.params_type.as_ref().unwrap();
127
+ let handler_ident = &spec.default_impl.as_ref().unwrap().handler_ident;
128
+
129
+ // Return complete struct + impl — the framework emits as-is and decomposes for the extend arm
130
+ DefaultAccounts {
131
+ struct_type: parse_quote! {
132
+ #[event_cpi]
133
+ #[derive(Accounts)]
134
+ #[instruction(params: #params_type)]
135
+ pub struct StdSetPeer<'info> {
136
+ pub authority: Signer<'info>,
137
+ // ... additional fields ...
138
+ }
139
+ },
140
+ impl_block: parse_quote! {
141
+ impl StdSetPeer<'_> {
142
+ pub fn #handler_ident(ctx: &mut Context<Self>, params: &#params_type) -> Result<()> {
143
+ // ... default handler logic ...
144
+ Ok(())
145
+ }
146
+ }
147
+ },
148
+ }
149
+ }
150
+
151
+ // Required instruction — implementor must provide a handler:
152
+ pub(super) fn spec<'a>() -> InstructionSpec<CodegenContext<'a>> {
153
+ InstructionSpec {
154
+ params_type: Some(quote!(::my_crate::params::LzReceiveParams)),
155
+ return_type: quote!(()),
156
+ is_view: false,
157
+ default_impl: None,
158
+ }
159
+ }
160
+ ```
161
+
162
+ ### 4. Configure the override attribute
163
+
164
+ ```rust
165
+ const OVERRIDE_CONFIG: OverrideConfig = OverrideConfig {
166
+ attr_name: "oapp_instruction", // attribute written by implementors
167
+ domain: "OApp", // label used in diagnostics
168
+ };
169
+ ```
170
+
171
+ ### 5. Implement the macro entry point
172
+
173
+ The entry point follows a four-phase pipeline: **parse → collect → generate → assemble**.
174
+
175
+ ```rust
176
+ pub fn my_macro_impl(attr: TokenStream, item: TokenStream) -> syn::Result<TokenStream> {
177
+ // Phase 0: Parse inputs (domain-specific; outside the framework's scope)
178
+ let attr_args = NestedMeta::parse_meta_list(attr)?;
179
+ let attrs = MyDomainAttr::from_list(&attr_args)?;
180
+ let module = syn::parse2::<ItemMod>(item)?;
181
+ let codegen_ctx = /* resolve domain types from attrs */;
182
+
183
+ // Phase 1: Collect — scan for override handlers
184
+ let overrides = InstructionOverrides::collect(&module, OVERRIDE_CONFIG)?;
185
+
186
+ // Phase 2: Generate — emit instruction code
187
+ let instructions = generate_instructions::<MyInstructionSet>(&codegen_ctx, &overrides)?;
188
+
189
+ // Phase 3: Assemble — combine all generated fragments into the final output
190
+ Ok(Assembly {
191
+ prelude: /* domain-specific definitions */,
192
+ overrides,
193
+ instructions,
194
+ extra_mod_attrs: /* e.g. quote!(#[program]) */,
195
+ source_mod: module,
196
+ extend_macro_name: Some(format_ident!("my_domain_extend_accounts")),
197
+ }
198
+ .assemble())
199
+ }
200
+ ```
201
+
202
+ ### Assembly output structure
203
+
204
+ ```text
205
+ <prelude> — domain-specific definitions (emitted by the macro crate)
206
+ <wrapper_defs> — relocated override handler modules
207
+ <default_impls> — generated default accounts structs + handler impls
208
+ <extend_macro> — macro_rules! for extending default accounts (if configured)
209
+ <original_mod_attrs> — preserved module attributes
210
+ <extra_mod_attrs> — additional attributes (e.g., #[program])
211
+ [vis] mod <name> {
212
+ use super::*;
213
+ <filtered_items> — original module items, excluding relocated handlers
214
+ <entrypoints> — generated instruction entrypoints
215
+ }
216
+ ```
217
+
218
+ ## Extending Default Accounts
219
+
220
+ For provided instructions, the framework generates a `macro_rules!` (named via `Assembly::extend_macro_name`) that lets users create extended accounts structs — the default fields plus extra accounts — without duplicating the base implementation.
221
+
222
+ ### Usage
223
+
224
+ The extend macro must be invoked **after** the domain macro module in the source file:
225
+
226
+ ```rust
227
+ #[my_domain(state = MyState)]
228
+ pub mod my_program {
229
+ #[my_domain_instruction]
230
+ pub fn set_peer(ctx: &mut Context<ExtSetPeer>, params: &SetPeerParams) -> Result<()> {
231
+ ExtSetPeer::apply(ctx, params)?; // default handler, copied to extended struct
232
+ ctx.accounts.my_extra.do_something()?; // extra account
233
+ Ok(())
234
+ }
235
+ }
236
+
237
+ // Must appear after the domain macro module (macro_rules! is textually scoped).
238
+ // Rust item name resolution is NOT order-dependent, so ExtSetPeer is visible above.
239
+ my_domain_extend_accounts!(set_peer, ExtSetPeer, {
240
+ pub my_extra: Account<'info, MyExtraState>,
241
+ });
242
+ ```
243
+
244
+ ### How it works
245
+
246
+ Each provided instruction's `DefaultAccounts` produces two outputs: the concrete `Std*` struct (via `to_definition()`, which emits the `ItemStruct` and `ItemImpl` as-is) and a `macro_rules!` arm (via `to_extend_rule()`, which decomposes the struct into attrs/fields, replaces the name with `$name`, and appends `$($extra)*`). `Assembly` collects all arms into one `macro_rules!` named by `extend_macro_name`.
247
+
248
+ ## Reference
249
+
250
+ ### Naming conventions
251
+
252
+ | Item | Convention | Example |
253
+ | ----------------------- | ---------------------------- | ------------------------------ |
254
+ | Default accounts struct | `Std<PascalCaseInstruction>` | `StdSetPeer` |
255
+ | Override attribute | `<domain>_instruction` | `oapp_instruction` |
256
+ | Params type | Fully qualified path | `::oapp::types::SetPeerParams` |
257
+
258
+ ### InstructionSpec fields
259
+
260
+ | Field | Type | Description |
261
+ | -------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
262
+ | `params_type` | `Option<TokenStream>` | Parameter type for the generated entrypoint. Must be a single struct type (e.g., `SetPeerParams`); multiple parameters are not supported. `None` indicates the instruction accepts no parameters. |
263
+ | `return_type` | `TokenStream` | Return type of the generated entrypoint (the `T` in `Result<T>`). |
264
+ | `is_view` | `bool` | When `true`, the entrypoint receives `ctx: Context<T>` (immutable) and passes `&ctx` to the handler. When `false`, it receives `mut ctx: Context<T>` and passes `&mut ctx`. |
265
+ | `default_impl` | `Option<DefaultImpl<Ctx>>` | `Some(...)` designates a provided instruction with a default implementation. `None` designates a required instruction; the implementor must supply a handler. |
266
+
267
+ ### DefaultImpl fields
268
+
269
+ | Field | Type | Description |
270
+ | --------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
271
+ | `handler_ident` | `Ident` | Identifier of the default handler method (e.g., `apply`). When no override is present, the entrypoint invokes `<accounts_ident>::<handler_ident>(...)`. |
272
+ | `generator` | `fn(&Ctx, &InstructionSpec<Ctx>) -> DefaultAccounts` | Returns a `DefaultAccounts` containing a complete `ItemStruct` and `ItemImpl`. The framework emits them directly for the concrete definition and decomposes them for the extend `macro_rules!` arm. |
273
+
274
+ ### DefaultAccounts fields
275
+
276
+ | Field | Type | Description |
277
+ | ------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
278
+ | `struct_type` | `ItemStruct` | A complete accounts struct definition including attributes, name, generics, and fields. Written with `parse_quote!` so generators read as plain Rust. The struct name becomes the `accounts_ident` (e.g. `StdSetPeer`). |
279
+ | `impl_block` | `ItemImpl` | A complete `impl` block for the struct, containing handler methods. Written with `parse_quote!` so generators read as plain Rust. Handler methods must use `Context<Self>` so they work for both the concrete struct and any extended variant. |
280
+
281
+ ### Default accounts struct
282
+
283
+ Each `DefaultAccounts` returned by a generator must satisfy the following:
284
+
285
+ - `struct_type` must be a named-field struct with a `<'info>` lifetime parameter (the standard Anchor accounts lifetime)
286
+ - `struct_type` must carry the standard Anchor attributes (e.g. `#[derive(Accounts)]`) — these are preserved as-is in `to_definition()` and re-emitted by `to_extend_rule()`
287
+ - `impl_block` must define a handler method matching `handler_ident` with one of the following signatures:
288
+ - Mutable: `fn apply(ctx: &mut Context<Self>, params: &ParamsType) -> Result<ReturnType>`
289
+ - View: `fn apply(ctx: &Context<Self>, params: &ParamsType) -> Result<ReturnType>`
290
+ - Omit the `params` parameter when `params_type` is `None`
291
+ - Handler methods must use `Context<Self>` (not a concrete struct name) so they work for both the concrete `Std*` struct and any extended variant
292
+ - The struct is emitted **outside** the `#[program]` module
293
+
294
+ ### Entrypoint signatures
295
+
296
+ ```rust
297
+ // Mutable instruction with parameters:
298
+ pub fn set_peer(mut ctx: Context<CtxType>, params: ParamsType) -> Result<ReturnType> { ... }
299
+
300
+ // View instruction with parameters:
301
+ pub fn get_peer(ctx: Context<CtxType>, params: ParamsType) -> Result<ReturnType> { ... }
302
+
303
+ // Instruction without parameters:
304
+ pub fn set_delegate(mut ctx: Context<CtxType>) -> Result<()> { ... }
305
+ ```
306
+
307
+ ### Dispatch
308
+
309
+ | Scenario | `CtxType` | Dispatch (with params) | Dispatch (no params) |
310
+ | ---------------- | ---------------------- | ----------------------------------------- | -------------------------------- |
311
+ | No handler | `StdSetPeer` | `StdSetPeer::apply(&mut ctx, &params)` | `StdSetPeer::apply(&mut ctx)` |
312
+ | Handler provided | handler's declared `T` | `__wrapper__::handler(&mut ctx, &params)` | `__wrapper__::handler(&mut ctx)` |
313
+
314
+ - All handlers receive `ctx` by reference (`&mut` for mutable, `&` for views) and `params` by reference. `__wrapper__` denotes the relocated wrapper module (e.g., `__oapp_instruction__set_peer_handler`).
315
+ - For view instructions, `&mut` becomes `&` in the dispatch expressions above.
316
+ - Override handlers that pass `ctx` by value (`Context<T>` instead of `&Context<T>` or `&mut Context<T>`) are rejected at compile time.
@@ -1 +1 @@
1
- [232,43,121,215,8,177,189,163,23,27,105,187,80,185,26,170,127,141,171,29,218,50,59,155,79,80,166,83,126,227,1,85,153,141,180,16,156,70,10,137,245,226,101,58,98,97,44,114,199,91,200,214,245,85,125,191,104,55,85,74,230,170,42,185]
1
+ [143,237,208,137,148,97,72,197,129,0,160,178,75,9,2,44,196,175,204,40,70,117,61,37,237,233,142,249,249,238,28,61,109,101,145,186,7,51,68,36,42,149,5,235,136,87,24,144,57,197,82,169,204,215,249,105,58,110,10,53,187,25,153,198]
@@ -1 +1 @@
1
- [171,106,218,79,73,64,48,157,88,222,80,186,245,21,186,211,43,43,29,126,64,251,220,188,214,138,238,147,225,6,82,248,161,52,175,31,58,252,235,113,30,132,102,160,165,206,186,218,152,120,184,18,49,232,189,143,242,177,202,110,105,232,255,216]
1
+ [244,248,2,78,59,247,194,68,247,45,37,98,43,8,253,138,212,87,43,247,99,192,196,197,47,42,160,42,176,138,196,243,228,63,253,20,179,133,24,94,71,230,27,114,191,144,199,250,248,153,140,126,39,189,51,5,161,92,192,178,37,165,229,47]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@layerzerolabs/framework-solana-anchor-trait",
3
- "version": "0.2.63",
3
+ "version": "0.2.65",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -11,19 +11,23 @@
11
11
  "reference",
12
12
  "Cargo.toml",
13
13
  "rust-toolchain.toml",
14
- "rustfmt.toml",
15
- "SPEC.md"
14
+ "rustfmt.toml"
16
15
  ],
17
16
  "devDependencies": {
18
- "@layerzerolabs/build-utils-solana": "0.2.63",
19
- "@layerzerolabs/tsup-configuration": "0.2.63",
20
- "@layerzerolabs/typescript-configuration": "0.2.63",
21
- "@layerzerolabs/vm-tooling-solana": "0.2.63"
17
+ "@layerzerolabs/build-utils-solana": "0.2.65",
18
+ "@layerzerolabs/typescript-configuration": "0.2.65",
19
+ "@layerzerolabs/vm-tooling-solana": "0.2.65",
20
+ "@layerzerolabs/tsup-configuration": "0.2.65"
22
21
  },
23
22
  "publishConfig": {
24
23
  "access": "restricted",
25
24
  "registry": "https://registry.npmjs.org/"
26
25
  },
26
+ "externalRepoConfig": {
27
+ "targets": [
28
+ "audit-external"
29
+ ]
30
+ },
27
31
  "scripts": {
28
32
  "allow-read-write": "[ -z \"$CI\" ] || sudo chmod -R a+rwx ./target ./examples/shapes/target",
29
33
  "build": "pnpm build:lib && pnpm build:example && pnpm allow-read-write",
@@ -38,6 +42,6 @@
38
42
  "test:example": "pnpm test:example:ellipse && pnpm test:example:triangle",
39
43
  "test:example:ellipse": "pnpm exec lz-tool --script 'cargo test-ellipse' anchor",
40
44
  "test:example:triangle": "pnpm exec lz-tool --script 'cargo test-triangle' anchor",
41
- "test:lib": "pnpm exec lz-tool --script 'cargo test --lib --tests' anchor"
45
+ "test:lib": "pnpm exec lz-tool --script 'cargo test -p anchor-trait --lib --tests' anchor"
42
46
  }
43
47
  }
package/SPEC.md DELETED
@@ -1,318 +0,0 @@
1
- # Anchor Trait Specification
2
-
3
- > **Status: Draft**
4
-
5
- ## Overview
6
-
7
- The Anchor framework provides no mechanism for defining or enforcing instruction-level interfaces across programs. When a standard requires a set of instructions — e.g., every OApp program must expose `set_peer` and `lz_receive` with specific signatures — there is no compile-time guarantee that a conforming program provides them all, uses the correct parameter types, or returns the expected types. Violations surface only at runtime, typically as CPI failures or deserialization errors.
8
-
9
- `anchor-trait` introduces **instruction traits** to Anchor: compile-time enforceable instruction interfaces with optional default implementations. A trait author declares an instruction set — each instruction's parameter type, return type, and whether a default implementation is provided — and the framework ensures, during proc-macro expansion, that every implementing program satisfies the trait contract. Missing required instructions produce compile errors. View instructions additionally enforce **compile-time immutability**: handlers receive `&Context<T>` (not `&mut`), so read-only semantics are upheld by the type system rather than relying solely on Solana runtime protections.
10
-
11
- The framework is domain-agnostic. It operates purely at the instruction-signature and accounts-struct level, with no knowledge of any specific application domain. Macro crates such as `lz-oapp-macros` or `rbac-macros` integrate with `anchor-trait` to define their own domain-specific instruction traits.
12
-
13
- ### Correspondence to Rust traits
14
-
15
- | Rust trait | `anchor-trait` |
16
- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
17
- | Required method (no default body) | Instruction with `default_impl: None` — implementor must provide a handler |
18
- | Default method (has body) | Instruction with `default_impl: Some(...)` — framework provides a complete implementation |
19
- | `impl Trait for Type` | Applying the domain macro to a module (e.g., `#[oapp(...)] mod my_program { ... }`) |
20
- | Overriding a default method | Annotating a handler with `#[oapp_instruction]` — the handler's `Context<T>` determines the accounts type |
21
- | Compile error on unimplemented required method | Compile error on missing required instruction handler |
22
-
23
- ## Instruction Kinds
24
-
25
- Each instruction in a trait is classified as one of two kinds:
26
-
27
- **Required** — The trait provides no default implementation. The implementor must supply a handler annotated with the override attribute; omitting it produces a compile error. This is analogous to a required method in a Rust trait.
28
-
29
- ```rust
30
- InstructionSpec {
31
- params_type: Some(quote!(LzReceiveParams)),
32
- return_type: quote!(()),
33
- is_view: false,
34
- default_impl: None,
35
- }
36
- ```
37
-
38
- **Provided** — The trait supplies a complete default implementation, including an accounts struct and a handler function. The default accounts struct is always emitted, so it remains available even when the implementor overrides the handler. This is analogous to a default method in a Rust trait.
39
-
40
- ```rust
41
- InstructionSpec {
42
- params_type: Some(quote!(SetPeerParams)),
43
- return_type: quote!(()),
44
- is_view: false,
45
- default_impl: Some(DefaultImpl {
46
- handler_ident: format_ident!("apply"),
47
- generator: generate_default_impl,
48
- }),
49
- }
50
- ```
51
-
52
- ## Overriding Instructions
53
-
54
- For provided instructions, the implementor may accept the default or supply a custom handler annotated with the override attribute (e.g., `#[oapp_instruction]`). The handler's `Context<T>` type determines the entrypoint's accounts type:
55
-
56
- - To **reuse** the default accounts struct, declare `Context<StdSetPeer>`.
57
- - To use **custom** accounts, declare `Context<MyCustomAccounts>`.
58
-
59
- The framework does not distinguish between these cases — the default struct is always emitted regardless.
60
-
61
- For required instructions, no default exists. The implementor must provide a handler.
62
-
63
- ### Resolution algorithm
64
-
65
- 1. **Handler present** → dispatch to the handler (entrypoint uses the handler's declared context type)
66
- 2. **No handler, default exists** → dispatch to the default handler
67
- 3. **No handler, no default** → compile error
68
-
69
- Unannotated functions whose identifiers collide with managed instruction names are rejected at compile time. Such functions must be annotated with the override attribute (e.g. `#[oapp_instruction]`) to disambiguate intent.
70
-
71
- ## Defining a Trait
72
-
73
- The following steps describe how to create a new instruction trait using the framework.
74
-
75
- ### 1. Define a codegen context
76
-
77
- The codegen context is a struct that carries domain-specific resolved types and is threaded through every instruction generator:
78
-
79
- ```rust
80
- pub(crate) struct CodegenContext<'a> {
81
- pub role_type: &'a TokenStream,
82
- pub state_type: &'a TokenStream,
83
- // additional domain-specific fields
84
- }
85
- ```
86
-
87
- ### 2. Declare the instruction set
88
-
89
- The `declare_instructions!` macro enumerates all instructions in the trait. It generates:
90
-
91
- - Sub-module declarations (`mod set_peer;`, `mod get_peer;`)
92
- - A named enum implementing `InstructionSet` with `all()`, `from_ident()`, `canonical_name()`, and `spec()`
93
-
94
- ```rust
95
- use anchor_trait::declare_instructions;
96
-
97
- declare_instructions! {
98
- name = OAppInstructionSet;
99
- context = CodegenContext;
100
-
101
- SetPeer => set_peer,
102
- GetPeer => get_peer,
103
- }
104
- ```
105
-
106
- ### 3. Define per-instruction specs
107
-
108
- Each sub-module exports a `spec()` function returning an `InstructionSpec<CodegenContext>` that describes the instruction's signature and default implementation status:
109
-
110
- ```rust
111
- // Provided instruction — implementor may accept the default or override:
112
- pub(super) fn spec<'a>() -> InstructionSpec<CodegenContext<'a>> {
113
- InstructionSpec {
114
- params_type: Some(quote!(::my_crate::params::SetPeerParams)),
115
- return_type: quote!(()),
116
- is_view: false,
117
- default_impl: Some(DefaultImpl {
118
- handler_ident: format_ident!("apply"),
119
- generator: generate_default_impl,
120
- }),
121
- }
122
- }
123
-
124
- fn generate_default_impl(
125
- ctx: &CodegenContext<'_>,
126
- spec: &InstructionSpec<CodegenContext<'_>>,
127
- ) -> DefaultAccounts {
128
- let params_type = spec.params_type.as_ref().unwrap();
129
- let handler_ident = &spec.default_impl.as_ref().unwrap().handler_ident;
130
-
131
- // Return complete struct + impl — the framework emits as-is and decomposes for the extend arm
132
- DefaultAccounts {
133
- struct_type: parse_quote! {
134
- #[event_cpi]
135
- #[derive(Accounts)]
136
- #[instruction(params: #params_type)]
137
- pub struct StdSetPeer<'info> {
138
- pub authority: Signer<'info>,
139
- // ... additional fields ...
140
- }
141
- },
142
- impl_block: parse_quote! {
143
- impl StdSetPeer<'_> {
144
- pub fn #handler_ident(ctx: &mut Context<Self>, params: &#params_type) -> Result<()> {
145
- // ... default handler logic ...
146
- Ok(())
147
- }
148
- }
149
- },
150
- }
151
- }
152
-
153
- // Required instruction — implementor must provide a handler:
154
- pub(super) fn spec<'a>() -> InstructionSpec<CodegenContext<'a>> {
155
- InstructionSpec {
156
- params_type: Some(quote!(::my_crate::params::LzReceiveParams)),
157
- return_type: quote!(()),
158
- is_view: false,
159
- default_impl: None,
160
- }
161
- }
162
- ```
163
-
164
- ### 4. Configure the override attribute
165
-
166
- ```rust
167
- const OVERRIDE_CONFIG: OverrideConfig = OverrideConfig {
168
- attr_name: "oapp_instruction", // attribute written by implementors
169
- domain: "OApp", // label used in diagnostics
170
- };
171
- ```
172
-
173
- ### 5. Implement the macro entry point
174
-
175
- The entry point follows a four-phase pipeline: **parse → collect → generate → assemble**.
176
-
177
- ```rust
178
- pub fn my_macro_impl(attr: TokenStream, item: TokenStream) -> syn::Result<TokenStream> {
179
- // Phase 0: Parse inputs (domain-specific; outside the framework's scope)
180
- let attr_args = NestedMeta::parse_meta_list(attr)?;
181
- let attrs = MyDomainAttr::from_list(&attr_args)?;
182
- let module = syn::parse2::<ItemMod>(item)?;
183
- let codegen_ctx = /* resolve domain types from attrs */;
184
-
185
- // Phase 1: Collect — scan for override handlers
186
- let overrides = InstructionOverrides::collect(&module, OVERRIDE_CONFIG)?;
187
-
188
- // Phase 2: Generate — emit instruction code
189
- let instructions = generate_instructions::<MyInstructionSet>(&codegen_ctx, &overrides)?;
190
-
191
- // Phase 3: Assemble — combine all generated fragments into the final output
192
- Ok(Assembly {
193
- prelude: /* domain-specific definitions */,
194
- overrides,
195
- instructions,
196
- extra_mod_attrs: /* e.g. quote!(#[program]) */,
197
- source_mod: module,
198
- extend_macro_name: Some(format_ident!("my_domain_extend_accounts")),
199
- }
200
- .assemble())
201
- }
202
- ```
203
-
204
- ### Assembly output structure
205
-
206
- ```text
207
- <prelude> — domain-specific definitions (emitted by the macro crate)
208
- <wrapper_defs> — relocated override handler modules
209
- <default_impls> — generated default accounts structs + handler impls
210
- <extend_macro> — macro_rules! for extending default accounts (if configured)
211
- <original_mod_attrs> — preserved module attributes
212
- <extra_mod_attrs> — additional attributes (e.g., #[program])
213
- [vis] mod <name> {
214
- use super::*;
215
- <filtered_items> — original module items, excluding relocated handlers
216
- <entrypoints> — generated instruction entrypoints
217
- }
218
- ```
219
-
220
- ## Extending Default Accounts
221
-
222
- For provided instructions, the framework generates a `macro_rules!` (named via `Assembly::extend_macro_name`) that lets users create extended accounts structs — the default fields plus extra accounts — without duplicating the base implementation.
223
-
224
- ### Usage
225
-
226
- The extend macro must be invoked **after** the domain macro module in the source file:
227
-
228
- ```rust
229
- #[my_domain(state = MyState)]
230
- pub mod my_program {
231
- #[my_domain_instruction]
232
- pub fn set_peer(ctx: &mut Context<ExtSetPeer>, params: &SetPeerParams) -> Result<()> {
233
- ExtSetPeer::apply(ctx, params)?; // default handler, copied to extended struct
234
- ctx.accounts.my_extra.do_something()?; // extra account
235
- Ok(())
236
- }
237
- }
238
-
239
- // Must appear after the domain macro module (macro_rules! is textually scoped).
240
- // Rust item name resolution is NOT order-dependent, so ExtSetPeer is visible above.
241
- my_domain_extend_accounts!(set_peer, ExtSetPeer, {
242
- pub my_extra: Account<'info, MyExtraState>,
243
- });
244
- ```
245
-
246
- ### How it works
247
-
248
- Each provided instruction's `DefaultAccounts` produces two outputs: the concrete `Std*` struct (via `to_definition()`, which emits the `ItemStruct` and `ItemImpl` as-is) and a `macro_rules!` arm (via `to_extend_rule()`, which decomposes the struct into attrs/fields, replaces the name with `$name`, and appends `$($extra)*`). `Assembly` collects all arms into one `macro_rules!` named by `extend_macro_name`.
249
-
250
- ## Reference
251
-
252
- ### Naming conventions
253
-
254
- | Item | Convention | Example |
255
- | ----------------------- | ---------------------------- | --------------------------------- |
256
- | Default accounts struct | `Std<PascalCaseInstruction>` | `StdSetPeer` |
257
- | Override attribute | `<domain>_instruction` | `oapp_instruction` |
258
- | Params type | Fully qualified path | `::lz_oapp::types::SetPeerParams` |
259
-
260
- ### InstructionSpec fields
261
-
262
- | Field | Type | Description |
263
- | -------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
264
- | `params_type` | `Option<TokenStream>` | Parameter type for the generated entrypoint. Must be a single struct type (e.g., `SetPeerParams`); multiple parameters are not supported. `None` indicates the instruction accepts no parameters. |
265
- | `return_type` | `TokenStream` | Return type of the generated entrypoint (the `T` in `Result<T>`). |
266
- | `is_view` | `bool` | When `true`, the entrypoint receives `ctx: Context<T>` (immutable) and passes `&ctx` to the handler. When `false`, it receives `mut ctx: Context<T>` and passes `&mut ctx`. |
267
- | `default_impl` | `Option<DefaultImpl<Ctx>>` | `Some(...)` designates a provided instruction with a default implementation. `None` designates a required instruction; the implementor must supply a handler. |
268
-
269
- ### DefaultImpl fields
270
-
271
- | Field | Type | Description |
272
- | --------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
273
- | `handler_ident` | `Ident` | Identifier of the default handler method (e.g., `apply`). When no override is present, the entrypoint invokes `<accounts_ident>::<handler_ident>(...)`. |
274
- | `generator` | `fn(&Ctx, &InstructionSpec<Ctx>) -> DefaultAccounts` | Returns a `DefaultAccounts` containing a complete `ItemStruct` and `ItemImpl`. The framework emits them directly for the concrete definition and decomposes them for the extend `macro_rules!` arm. |
275
-
276
- ### DefaultAccounts fields
277
-
278
- | Field | Type | Description |
279
- | ------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
280
- | `struct_type` | `ItemStruct` | A complete accounts struct definition including attributes, name, generics, and fields. Written with `parse_quote!` so generators read as plain Rust. The struct name becomes the `accounts_ident` (e.g. `StdSetPeer`). |
281
- | `impl_block` | `ItemImpl` | A complete `impl` block for the struct, containing handler methods. Written with `parse_quote!` so generators read as plain Rust. Handler methods must use `Context<Self>` so they work for both the concrete struct and any extended variant. |
282
-
283
- ### Default accounts struct
284
-
285
- Each `DefaultAccounts` returned by a generator must satisfy the following:
286
-
287
- - `struct_type` must be a named-field struct with a `<'info>` lifetime parameter (the standard Anchor accounts lifetime)
288
- - `struct_type` must carry the standard Anchor attributes (e.g. `#[derive(Accounts)]`) — these are preserved as-is in `to_definition()` and re-emitted by `to_extend_rule()`
289
- - `impl_block` must define a handler method matching `handler_ident` with one of the following signatures:
290
- - Mutable: `fn apply(ctx: &mut Context<Self>, params: &ParamsType) -> Result<ReturnType>`
291
- - View: `fn apply(ctx: &Context<Self>, params: &ParamsType) -> Result<ReturnType>`
292
- - Omit the `params` parameter when `params_type` is `None`
293
- - Handler methods must use `Context<Self>` (not a concrete struct name) so they work for both the concrete `Std*` struct and any extended variant
294
- - The struct is emitted **outside** the `#[program]` module
295
-
296
- ### Entrypoint signatures
297
-
298
- ```rust
299
- // Mutable instruction with parameters:
300
- pub fn set_peer(mut ctx: Context<CtxType>, params: ParamsType) -> Result<ReturnType> { ... }
301
-
302
- // View instruction with parameters:
303
- pub fn get_peer(ctx: Context<CtxType>, params: ParamsType) -> Result<ReturnType> { ... }
304
-
305
- // Instruction without parameters:
306
- pub fn set_delegate(mut ctx: Context<CtxType>) -> Result<()> { ... }
307
- ```
308
-
309
- ### Dispatch
310
-
311
- | Scenario | `CtxType` | Dispatch (with params) | Dispatch (no params) |
312
- | ---------------- | ---------------------- | ----------------------------------------- | -------------------------------- |
313
- | No handler | `StdSetPeer` | `StdSetPeer::apply(&mut ctx, &params)` | `StdSetPeer::apply(&mut ctx)` |
314
- | Handler provided | handler's declared `T` | `__wrapper__::handler(&mut ctx, &params)` | `__wrapper__::handler(&mut ctx)` |
315
-
316
- - All handlers receive `ctx` by reference (`&mut` for mutable, `&` for views) and `params` by reference. `__wrapper__` denotes the relocated wrapper module (e.g., `__oapp_instruction__set_peer_handler`).
317
- - For view instructions, `&mut` becomes `&` in the dispatch expressions above.
318
- - Override handlers that pass `ctx` by value (`Context<T>` instead of `&Context<T>` or `&mut Context<T>`) are rejected at compile time.