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

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.
Files changed (51) hide show
  1. package/Cargo.toml +3 -0
  2. package/README.md +5 -0
  3. package/SPEC.md +318 -0
  4. package/examples/shapes/Cargo.lock +4126 -0
  5. package/examples/shapes/Cargo.toml +16 -0
  6. package/examples/shapes/macros/src/audit.rs +15 -0
  7. package/examples/shapes/macros/src/instructions/clear.rs +19 -0
  8. package/examples/shapes/macros/src/instructions/compute_area.rs +18 -20
  9. package/examples/shapes/macros/src/instructions/compute_perimeter.rs +18 -20
  10. package/examples/shapes/macros/src/instructions/get_dimension.rs +19 -0
  11. package/examples/shapes/macros/src/instructions/get_dimension_count.rs +19 -17
  12. package/examples/shapes/macros/src/instructions/mod.rs +8 -0
  13. package/examples/shapes/macros/src/instructions/reset.rs +50 -0
  14. package/examples/shapes/macros/src/instructions/scale.rs +52 -0
  15. package/examples/shapes/macros/src/lib.rs +14 -11
  16. package/examples/shapes/macros/src/shapes.rs +21 -26
  17. package/examples/shapes/macros/src/state_types.rs +14 -0
  18. package/examples/shapes/programs/ellipse/Cargo.toml +7 -0
  19. package/examples/shapes/programs/ellipse/src/lib.rs +69 -17
  20. package/examples/shapes/programs/ellipse/tests/ellipse/helpers.rs +110 -0
  21. package/examples/shapes/programs/ellipse/tests/ellipse/negative.rs +49 -0
  22. package/examples/shapes/programs/ellipse/tests/ellipse/positive.rs +231 -0
  23. package/examples/shapes/programs/ellipse/tests/ellipse.rs +23 -0
  24. package/examples/shapes/programs/triangle/Cargo.toml +7 -0
  25. package/examples/shapes/programs/triangle/src/lib.rs +69 -16
  26. package/examples/shapes/programs/triangle/tests/triangle/helpers.rs +123 -0
  27. package/examples/shapes/programs/triangle/tests/triangle/negative.rs +49 -0
  28. package/examples/shapes/programs/triangle/tests/triangle/positive.rs +225 -0
  29. package/examples/shapes/programs/triangle/tests/triangle.rs +23 -0
  30. package/examples/shapes/target/deploy/shapes_ellipse-keypair.json +1 -0
  31. package/examples/shapes/target/deploy/shapes_ellipse.so +0 -0
  32. package/examples/shapes/target/deploy/shapes_triangle-keypair.json +1 -0
  33. package/examples/shapes/target/deploy/shapes_triangle.so +0 -0
  34. package/package.json +14 -7
  35. package/src/assembly.rs +41 -6
  36. package/src/generation.rs +108 -131
  37. package/src/lib.rs +6 -4
  38. package/src/overrides.rs +56 -81
  39. package/tests/assembly.rs +154 -23
  40. package/tests/common/mod.rs +68 -14
  41. package/tests/generation.rs +137 -65
  42. package/tests/override.rs +93 -55
  43. package/tests/snapshots/snapshots__tests__snapshot_all_defaults.snap +32 -5
  44. package/tests/snapshots/snapshots__tests__snapshot_full_override.snap +35 -7
  45. package/tests/snapshots/snapshots__tests__snapshot_handler_override.snap +37 -7
  46. package/tests/snapshots/snapshots__tests__snapshot_mixed_overrides.snap +40 -9
  47. package/tests/snapshots/snapshots__tests__snapshot_with_extend_macro.snap +82 -0
  48. package/tests/snapshots.rs +51 -18
  49. package/examples/shapes/Anchor.toml +0 -19
  50. package/examples/shapes/programs/ellipse/src/state.rs +0 -9
  51. package/examples/shapes/programs/triangle/src/state.rs +0 -9
package/tests/assembly.rs CHANGED
@@ -3,9 +3,9 @@ mod common;
3
3
  #[cfg(test)]
4
4
  mod tests {
5
5
  use super::common::*;
6
- use anchor_trait::{generate_instructions, Assembly};
6
+ use anchor_trait::{generate_instructions, Assembly, GeneratedInstructions};
7
7
  use proc_macro2::TokenStream;
8
- use quote::{quote, ToTokens};
8
+ use quote::{format_ident, quote, ToTokens};
9
9
 
10
10
  // -----------------------------------------------------------------------
11
11
  // Helper
@@ -26,6 +26,7 @@ mod tests {
26
26
  instructions,
27
27
  extra_mod_attrs,
28
28
  source_mod: module.clone(),
29
+ extend_macro_name: None,
29
30
  };
30
31
  syn::parse2(assembly.assemble()).expect("assembly output should parse")
31
32
  }
@@ -48,7 +49,7 @@ mod tests {
48
49
  syn::parse_quote! {
49
50
  mod my_program {
50
51
  #[test_instruction]
51
- pub fn lz_receive(ctx: Context<MyReceive>, params: LzReceiveParams) -> Result<()> {
52
+ pub fn lz_receive(ctx: &mut Context<MyReceive>, params: &LzReceiveParams) -> Result<()> {
52
53
  Ok(())
53
54
  }
54
55
  }
@@ -60,12 +61,12 @@ mod tests {
60
61
  syn::parse_quote! {
61
62
  mod my_program {
62
63
  #[test_instruction]
63
- pub fn set_peer(ctx: Context<MyCustomAccounts>, params: SetPeerParams) -> Result<()> {
64
+ pub fn set_peer(ctx: &mut Context<MyCustomAccounts>, params: &SetPeerParams) -> Result<()> {
64
65
  Ok(())
65
66
  }
66
67
 
67
68
  #[test_instruction]
68
- pub fn lz_receive(ctx: Context<MyReceive>, params: LzReceiveParams) -> Result<()> {
69
+ pub fn lz_receive(ctx: &mut Context<MyReceive>, params: &LzReceiveParams) -> Result<()> {
69
70
  Ok(())
70
71
  }
71
72
 
@@ -108,7 +109,6 @@ mod tests {
108
109
  let file = assemble_ok(&module, quote! {}, quote! {});
109
110
  let (mod_pos, _) = find_program_mod(&file).expect("module should exist");
110
111
 
111
- // Wrapper module for the override should be outside and before the program module
112
112
  let wrapper_pos = file
113
113
  .items
114
114
  .iter()
@@ -121,10 +121,9 @@ mod tests {
121
121
  "wrapper defs (pos {wrapper_pos}) should come before module (pos {mod_pos})"
122
122
  );
123
123
 
124
- // Default impls (e.g. StdGetPeer, StdToken) should be outside the module
125
- // set_peer is fully overridden so no StdSetPeer
124
+ // Default structs are always emitted for provided instructions, even when overridden.
126
125
  let has_default_impl_outside = file.items[..mod_pos].iter().any(|item| {
127
- matches!(item, syn::Item::Struct(s) if s.ident == "StdGetPeer" || s.ident == "StdToken")
126
+ matches!(item, syn::Item::Struct(s) if s.ident == "StdGetPeer" || s.ident == "StdToken" || s.ident == "StdSetPeer")
128
127
  });
129
128
  assert!(has_default_impl_outside, "default impls should be outside the module");
130
129
  }
@@ -132,7 +131,7 @@ mod tests {
132
131
  #[test]
133
132
  fn extra_mod_attrs_applied_to_module() {
134
133
  let module = module_with_required_only();
135
- let file = assemble_ok(&module, quote! {}, quote! { #[program] });
134
+ let file = assemble_ok(&module, quote! {}, quote! {});
136
135
  let (_, m) = find_program_mod(&file).expect("module should exist");
137
136
 
138
137
  let has_program_attr = m
@@ -148,12 +147,12 @@ mod tests {
148
147
  #[some_custom_attr]
149
148
  mod my_program {
150
149
  #[test_instruction]
151
- pub fn lz_receive(ctx: Context<MyReceive>, params: LzReceiveParams) -> Result<()> {
150
+ pub fn lz_receive(ctx: &mut Context<MyReceive>, params: &LzReceiveParams) -> Result<()> {
152
151
  Ok(())
153
152
  }
154
153
  }
155
154
  };
156
- let file = assemble_ok(&module, quote! {}, quote! { #[program] });
155
+ let file = assemble_ok(&module, quote! {}, quote! {});
157
156
  let (_, m) = find_program_mod(&file).expect("module should exist");
158
157
 
159
158
  let has_custom_attr = m.attrs.iter().any(|attr| {
@@ -169,9 +168,9 @@ mod tests {
169
168
  let (_, m) = find_program_mod(&file).expect("module should exist");
170
169
  let items = mod_items(m);
171
170
 
172
- // Override handler should NOT be a function definition inside the module
173
- // (it was relocated to wrapper). Note: the name still appears in dispatch
174
- // calls inside entrypoints, so we check for function definitions specifically.
171
+ // The override handler is relocated to a wrapper module, so it should not appear
172
+ // as a function definition inside the program module. The name still appears in
173
+ // dispatch calls inside entrypoints, so we match on function definitions specifically.
175
174
  let has_override_fn = items.iter().any(|item| {
176
175
  matches!(item, syn::Item::Fn(f) if f.sig.ident == "set_peer"
177
176
  && f.block.to_token_stream().to_string().contains("MyCustomAccounts"))
@@ -181,7 +180,6 @@ mod tests {
181
180
  "overridden handler with custom body should be filtered out of module body"
182
181
  );
183
182
 
184
- // Non-handler items should be preserved inside the module
185
183
  let has_helper = items
186
184
  .iter()
187
185
  .any(|item| matches!(item, syn::Item::Fn(f) if f.sig.ident == "helper_function"));
@@ -228,7 +226,6 @@ mod tests {
228
226
  let module = module_with_override_and_extras();
229
227
  let file = assemble_ok(&module, quote! {}, quote! {});
230
228
 
231
- // Find the wrapper module for set_peer in the assembled output
232
229
  let wrapper = file
233
230
  .items
234
231
  .iter()
@@ -242,7 +239,6 @@ mod tests {
242
239
  })
243
240
  .expect("wrapper module for set_peer should exist in assembled output");
244
241
 
245
- // The relocated handler should be inside the wrapper
246
242
  let wrapper_items = mod_items(wrapper);
247
243
  let has_handler = wrapper_items
248
244
  .iter()
@@ -255,7 +251,7 @@ mod tests {
255
251
  let module: syn::ItemMod = syn::parse_quote! {
256
252
  pub mod my_program {
257
253
  #[test_instruction]
258
- pub fn lz_receive(ctx: Context<MyReceive>, params: LzReceiveParams) -> Result<()> {
254
+ pub fn lz_receive(ctx: &mut Context<MyReceive>, params: &LzReceiveParams) -> Result<()> {
259
255
  Ok(())
260
256
  }
261
257
  }
@@ -282,10 +278,148 @@ mod tests {
282
278
  assert!(has_use_super, "module should contain `use super::*`");
283
279
  }
284
280
 
281
+ // -----------------------------------------------------------------------
282
+ // Tests — extend macro emission
283
+ // -----------------------------------------------------------------------
284
+
285
+ #[test]
286
+ fn extend_macro_emitted_when_configured() {
287
+ let module = module_with_required_only();
288
+ let overrides = collect_overrides(&module);
289
+ let instructions =
290
+ generate_instructions(&TestCtx, &overrides).expect("generate should succeed");
291
+ let assembly = Assembly {
292
+ prelude: quote! {},
293
+ overrides,
294
+ instructions,
295
+ extra_mod_attrs: quote! {},
296
+ source_mod: module.clone(),
297
+ extend_macro_name: Some(format_ident!("test_extend_accounts")),
298
+ };
299
+ let file: syn::File =
300
+ syn::parse2(assembly.assemble()).expect("assembly output should parse");
301
+
302
+ let has_macro = file.items.iter().any(|item| {
303
+ matches!(item, syn::Item::Macro(m) if m.ident.as_ref().is_some_and(|id| id == "test_extend_accounts"))
304
+ });
305
+ assert!(has_macro, "should emit macro_rules! test_extend_accounts");
306
+ }
307
+
308
+ #[test]
309
+ fn program_attr_already_present_is_not_duplicated() {
310
+ // If the source module already carries `#[program]` (from a prior macro pass),
311
+ // assembly must strip it before re-emitting it as the innermost attribute.
312
+ let module: syn::ItemMod = syn::parse_quote! {
313
+ #[program]
314
+ mod my_program {
315
+ #[test_instruction]
316
+ pub fn lz_receive(ctx: &mut Context<MyReceive>, params: &LzReceiveParams) -> Result<()> {
317
+ Ok(())
318
+ }
319
+ }
320
+ };
321
+ let file = assemble_ok(&module, quote! {}, quote! {});
322
+ let (_, m) = find_program_mod(&file).expect("module should exist");
323
+
324
+ let program_count = m.attrs.iter().filter(|a| a.path().is_ident("program")).count();
325
+ assert_eq!(
326
+ program_count, 1,
327
+ "#[program] should appear exactly once, not duplicated: {program_count}"
328
+ );
329
+ }
330
+
331
+ #[test]
332
+ fn extend_macro_omitted_when_extend_rules_empty() {
333
+ // `extend_macro_name = Some(name)` but no extend rules → no macro_rules! emitted.
334
+ let module = module_with_required_only();
335
+ let overrides = collect_overrides(&module);
336
+ let assembly = Assembly {
337
+ prelude: quote! {},
338
+ overrides,
339
+ instructions: GeneratedInstructions::default(),
340
+ extra_mod_attrs: quote! {},
341
+ source_mod: module.clone(),
342
+ extend_macro_name: Some(format_ident!("test_extend_accounts")),
343
+ };
344
+ let file: syn::File = syn::parse2(assembly.assemble()).expect("should parse");
345
+
346
+ let has_any_macro = file.items.iter().any(|item| matches!(item, syn::Item::Macro(_)));
347
+ assert!(
348
+ !has_any_macro,
349
+ "no macro_rules! should be emitted when extend_rules is empty, even when extend_macro_name is Some"
350
+ );
351
+ }
352
+
353
+ #[test]
354
+ fn extend_macro_omitted_when_none() {
355
+ let module = module_with_required_only();
356
+ let file = assemble_ok(&module, quote! {}, quote! {});
357
+
358
+ let has_any_macro = file.items.iter().any(|item| matches!(item, syn::Item::Macro(_)));
359
+ assert!(!has_any_macro, "no macro_rules! should be emitted when extend_macro_name is None");
360
+ }
361
+
362
+ #[test]
363
+ fn extend_macro_positioned_before_program_module() {
364
+ let module = module_with_required_only();
365
+ let overrides = collect_overrides(&module);
366
+ let instructions =
367
+ generate_instructions(&TestCtx, &overrides).expect("generate should succeed");
368
+ let assembly = Assembly {
369
+ prelude: quote! {},
370
+ overrides,
371
+ instructions,
372
+ extra_mod_attrs: quote! {},
373
+ source_mod: module.clone(),
374
+ extend_macro_name: Some(format_ident!("test_extend_accounts")),
375
+ };
376
+ let file: syn::File =
377
+ syn::parse2(assembly.assemble()).expect("assembly output should parse");
378
+
379
+ let macro_pos = file
380
+ .items
381
+ .iter()
382
+ .position(|item| matches!(item, syn::Item::Macro(_)))
383
+ .expect("extend macro should exist");
384
+ let (mod_pos, _) = find_program_mod(&file).expect("module should exist");
385
+ assert!(
386
+ macro_pos < mod_pos,
387
+ "extend macro (pos {macro_pos}) should come before program module (pos {mod_pos})"
388
+ );
389
+ }
390
+
391
+ #[test]
392
+ fn extend_macro_contains_rules_for_each_provided_instruction() {
393
+ let module = module_with_required_only();
394
+ let overrides = collect_overrides(&module);
395
+ let instructions =
396
+ generate_instructions(&TestCtx, &overrides).expect("generate should succeed");
397
+ let assembly = Assembly {
398
+ prelude: quote! {},
399
+ overrides,
400
+ instructions,
401
+ extra_mod_attrs: quote! {},
402
+ source_mod: module.clone(),
403
+ extend_macro_name: Some(format_ident!("test_extend_accounts")),
404
+ };
405
+ let output = assembly.assemble().to_string();
406
+
407
+ // Check for the rule-arm matcher pattern `(set_peer ,` rather than just the bare name.
408
+ // The bare name also appears in entrypoints and wrapper modules, making it a trivially
409
+ // weak check. The `(name ,` prefix is unique to the macro_rules! arm head.
410
+ assert!(output.contains("(set_peer ,"), "macro should contain set_peer rule arm: {output}");
411
+ assert!(output.contains("(get_peer ,"), "macro should contain get_peer rule arm: {output}");
412
+ assert!(output.contains("(token ,"), "macro should contain token rule arm: {output}");
413
+ }
414
+
415
+ // -----------------------------------------------------------------------
416
+ // Tests — full integration
417
+ // -----------------------------------------------------------------------
418
+
285
419
  #[test]
286
420
  fn full_assembly_integration() {
287
421
  let module = module_with_override_and_extras();
288
- let file = assemble_ok(&module, quote! { struct PreludeMarker; }, quote! { #[program] });
422
+ let file = assemble_ok(&module, quote! { struct PreludeMarker; }, quote! {});
289
423
 
290
424
  // Verify complete ordering: prelude → wrapper → default impls → module
291
425
  let prelude_pos = file
@@ -312,19 +446,16 @@ mod tests {
312
446
 
313
447
  let (mod_pos, m) = find_program_mod(&file).expect("module should exist");
314
448
 
315
- // Ordering: prelude < wrapper < default impls < module
316
449
  assert!(prelude_pos < wrapper_pos, "prelude before wrapper");
317
450
  assert!(wrapper_pos < context_pos, "wrapper before default impls");
318
451
  assert!(context_pos < mod_pos, "default impls before module");
319
452
 
320
- // Module has #[program]
321
453
  let has_program = m
322
454
  .attrs
323
455
  .iter()
324
456
  .any(|attr| attr.path().segments.last().is_some_and(|seg| seg.ident == "program"));
325
457
  assert!(has_program, "module should have #[program]");
326
458
 
327
- // Entrypoints inside module
328
459
  let items = mod_items(m);
329
460
  let fn_names: Vec<_> = items
330
461
  .iter()
@@ -3,12 +3,14 @@
3
3
  #![allow(dead_code)]
4
4
 
5
5
  use anchor_trait::{
6
- generate_instructions, DefaultImpl, GeneratedInstructions, InstructionOverrides,
7
- InstructionSet, InstructionSpec, OverrideConfig,
6
+ generate_instructions, DefaultAccounts, DefaultImpl, GeneratedInstructions,
7
+ InstructionOverrides, InstructionSet, InstructionSpec, OverrideConfig,
8
8
  };
9
9
  use proc_macro2::TokenStream;
10
10
  use quote::{format_ident, quote, ToTokens};
11
- use syn::{FnArg, GenericArgument, ItemFn, ItemStruct, Pat, PatIdent, PathArguments, Type};
11
+ use syn::{
12
+ parse_quote, FnArg, GenericArgument, ItemFn, ItemStruct, Pat, PatIdent, PathArguments, Type,
13
+ };
12
14
 
13
15
  // ---------------------------------------------------------------------------
14
16
  // Test instruction set
@@ -31,11 +33,66 @@ pub enum TestIx {
31
33
  /// Dummy codegen context.
32
34
  pub struct TestCtx;
33
35
 
34
- /// Generates a placeholder default impl (struct only) named after the instruction's
35
- /// default accounts type, mirroring real codegen which emits instruction-specific structs.
36
- pub fn gen_default_impl(_ctx: &TestCtx, spec: &InstructionSpec<TestCtx>) -> TokenStream {
37
- let ident = &spec.default_impl.as_ref().unwrap().accounts_ident;
38
- quote! { struct #ident; }
36
+ fn gen_set_peer(_ctx: &TestCtx, spec: &InstructionSpec<TestCtx>) -> DefaultAccounts {
37
+ let handler_ident = &spec.default_impl.as_ref().unwrap().handler_ident;
38
+ let params_type = spec.params_type.as_ref().unwrap();
39
+ let return_type = &spec.return_type;
40
+ DefaultAccounts {
41
+ struct_type: parse_quote! {
42
+ #[derive(Accounts)]
43
+ pub struct StdSetPeer<'info> {
44
+ pub placeholder: UncheckedAccount<'info>,
45
+ }
46
+ },
47
+ impl_block: parse_quote! {
48
+ impl StdSetPeer<'_> {
49
+ pub fn #handler_ident(_ctx: &mut Context<Self>, _params: &#params_type) -> Result<#return_type> {
50
+ unimplemented!()
51
+ }
52
+ }
53
+ },
54
+ }
55
+ }
56
+
57
+ fn gen_get_peer(_ctx: &TestCtx, spec: &InstructionSpec<TestCtx>) -> DefaultAccounts {
58
+ let handler_ident = &spec.default_impl.as_ref().unwrap().handler_ident;
59
+ let params_type = spec.params_type.as_ref().unwrap();
60
+ let return_type = &spec.return_type;
61
+ DefaultAccounts {
62
+ struct_type: parse_quote! {
63
+ #[derive(Accounts)]
64
+ pub struct StdGetPeer<'info> {
65
+ pub placeholder: UncheckedAccount<'info>,
66
+ }
67
+ },
68
+ impl_block: parse_quote! {
69
+ impl StdGetPeer<'_> {
70
+ pub fn #handler_ident(_ctx: &Context<Self>, _params: &#params_type) -> Result<#return_type> {
71
+ unimplemented!()
72
+ }
73
+ }
74
+ },
75
+ }
76
+ }
77
+
78
+ fn gen_token(_ctx: &TestCtx, spec: &InstructionSpec<TestCtx>) -> DefaultAccounts {
79
+ let handler_ident = &spec.default_impl.as_ref().unwrap().handler_ident;
80
+ let return_type = &spec.return_type;
81
+ DefaultAccounts {
82
+ struct_type: parse_quote! {
83
+ #[derive(Accounts)]
84
+ pub struct StdToken<'info> {
85
+ pub placeholder: UncheckedAccount<'info>,
86
+ }
87
+ },
88
+ impl_block: parse_quote! {
89
+ impl StdToken<'_> {
90
+ pub fn #handler_ident(_ctx: &Context<Self>) -> Result<#return_type> {
91
+ unimplemented!()
92
+ }
93
+ }
94
+ },
95
+ }
39
96
  }
40
97
 
41
98
  impl InstructionSet for TestIx {
@@ -71,9 +128,8 @@ impl InstructionSet for TestIx {
71
128
  return_type: quote!(()),
72
129
  is_view: false,
73
130
  default_impl: Some(DefaultImpl {
74
- accounts_ident: format_ident!("StdSetPeer"),
75
131
  handler_ident: format_ident!("apply"),
76
- generator: gen_default_impl,
132
+ generator: gen_set_peer,
77
133
  }),
78
134
  },
79
135
  Self::GetPeer => InstructionSpec {
@@ -81,9 +137,8 @@ impl InstructionSet for TestIx {
81
137
  return_type: quote!([u8; 32]),
82
138
  is_view: true,
83
139
  default_impl: Some(DefaultImpl {
84
- accounts_ident: format_ident!("StdGetPeer"),
85
140
  handler_ident: format_ident!("apply"),
86
- generator: gen_default_impl,
141
+ generator: gen_get_peer,
87
142
  }),
88
143
  },
89
144
  Self::LzReceive => InstructionSpec {
@@ -97,9 +152,8 @@ impl InstructionSet for TestIx {
97
152
  return_type: quote!(Pubkey),
98
153
  is_view: true,
99
154
  default_impl: Some(DefaultImpl {
100
- accounts_ident: format_ident!("StdToken"),
101
155
  handler_ident: format_ident!("apply"),
102
- generator: gen_default_impl,
156
+ generator: gen_token,
103
157
  }),
104
158
  },
105
159
  }