@layerzerolabs/framework-solana-anchor-trait 0.2.62 → 0.2.64

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 (50) hide show
  1. package/Cargo.toml +3 -0
  2. package/README.md +316 -0
  3. package/examples/shapes/Cargo.lock +4126 -0
  4. package/examples/shapes/Cargo.toml +16 -0
  5. package/examples/shapes/macros/src/audit.rs +15 -0
  6. package/examples/shapes/macros/src/instructions/clear.rs +19 -0
  7. package/examples/shapes/macros/src/instructions/compute_area.rs +18 -20
  8. package/examples/shapes/macros/src/instructions/compute_perimeter.rs +18 -20
  9. package/examples/shapes/macros/src/instructions/get_dimension.rs +19 -0
  10. package/examples/shapes/macros/src/instructions/get_dimension_count.rs +19 -17
  11. package/examples/shapes/macros/src/instructions/mod.rs +8 -0
  12. package/examples/shapes/macros/src/instructions/reset.rs +50 -0
  13. package/examples/shapes/macros/src/instructions/scale.rs +52 -0
  14. package/examples/shapes/macros/src/lib.rs +14 -11
  15. package/examples/shapes/macros/src/shapes.rs +21 -26
  16. package/examples/shapes/macros/src/state_types.rs +14 -0
  17. package/examples/shapes/programs/ellipse/Cargo.toml +7 -0
  18. package/examples/shapes/programs/ellipse/src/lib.rs +69 -17
  19. package/examples/shapes/programs/ellipse/tests/ellipse/helpers.rs +110 -0
  20. package/examples/shapes/programs/ellipse/tests/ellipse/negative.rs +49 -0
  21. package/examples/shapes/programs/ellipse/tests/ellipse/positive.rs +231 -0
  22. package/examples/shapes/programs/ellipse/tests/ellipse.rs +23 -0
  23. package/examples/shapes/programs/triangle/Cargo.toml +7 -0
  24. package/examples/shapes/programs/triangle/src/lib.rs +69 -16
  25. package/examples/shapes/programs/triangle/tests/triangle/helpers.rs +123 -0
  26. package/examples/shapes/programs/triangle/tests/triangle/negative.rs +49 -0
  27. package/examples/shapes/programs/triangle/tests/triangle/positive.rs +225 -0
  28. package/examples/shapes/programs/triangle/tests/triangle.rs +23 -0
  29. package/examples/shapes/target/deploy/shapes_ellipse-keypair.json +1 -0
  30. package/examples/shapes/target/deploy/shapes_ellipse.so +0 -0
  31. package/examples/shapes/target/deploy/shapes_triangle-keypair.json +1 -0
  32. package/examples/shapes/target/deploy/shapes_triangle.so +0 -0
  33. package/package.json +20 -9
  34. package/src/assembly.rs +41 -6
  35. package/src/generation.rs +108 -131
  36. package/src/lib.rs +6 -4
  37. package/src/overrides.rs +56 -81
  38. package/tests/assembly.rs +154 -23
  39. package/tests/common/mod.rs +68 -14
  40. package/tests/generation.rs +137 -65
  41. package/tests/override.rs +93 -55
  42. package/tests/snapshots/snapshots__tests__snapshot_all_defaults.snap +32 -5
  43. package/tests/snapshots/snapshots__tests__snapshot_full_override.snap +35 -7
  44. package/tests/snapshots/snapshots__tests__snapshot_handler_override.snap +37 -7
  45. package/tests/snapshots/snapshots__tests__snapshot_mixed_overrides.snap +40 -9
  46. package/tests/snapshots/snapshots__tests__snapshot_with_extend_macro.snap +82 -0
  47. package/tests/snapshots.rs +51 -18
  48. package/examples/shapes/Anchor.toml +0 -19
  49. package/examples/shapes/programs/ellipse/src/state.rs +0 -9
  50. package/examples/shapes/programs/triangle/src/state.rs +0 -9
@@ -3,7 +3,7 @@ mod common;
3
3
  #[cfg(test)]
4
4
  mod tests {
5
5
  use super::common::*;
6
- use quote::ToTokens;
6
+ use quote::{quote, ToTokens};
7
7
 
8
8
  // -----------------------------------------------------------------------
9
9
  // Tests — override level resolution
@@ -11,24 +11,21 @@ mod tests {
11
11
 
12
12
  #[test]
13
13
  fn default_level_emits_context_and_entrypoint() {
14
- // All defaults — no overrides except required lz_receive
15
14
  let module: syn::ItemMod = syn::parse_quote! {
16
15
  mod my_program {
17
16
  #[test_instruction]
18
- pub fn lz_receive(ctx: Context<MyReceive>, params: LzReceiveParams) -> Result<()> {
17
+ pub fn lz_receive(ctx: &mut Context<MyReceive>, params: &LzReceiveParams) -> Result<()> {
19
18
  Ok(())
20
19
  }
21
20
  }
22
21
  };
23
22
  let gen = generate_ok(&module);
24
23
 
25
- // Default impls should be generated for each provided instruction
26
24
  let structs = parse_structs(&gen.default_impls);
27
25
  assert!(find_struct(&structs, "StdSetPeer").is_some(), "should emit StdSetPeer");
28
26
  assert!(find_struct(&structs, "StdGetPeer").is_some(), "should emit StdGetPeer");
29
27
  assert!(find_struct(&structs, "StdToken").is_some(), "should emit StdToken");
30
28
 
31
- // Entrypoints should include all instructions
32
29
  let fns = parse_fns(&gen.entrypoints);
33
30
  assert!(find_fn(&fns, "set_peer").is_some(), "should have set_peer entrypoint");
34
31
  assert!(find_fn(&fns, "get_peer").is_some(), "should have get_peer entrypoint");
@@ -41,7 +38,7 @@ mod tests {
41
38
  let module: syn::ItemMod = syn::parse_quote! {
42
39
  mod my_program {
43
40
  #[test_instruction]
44
- pub fn lz_receive(ctx: Context<MyReceive>, params: LzReceiveParams) -> Result<()> {
41
+ pub fn lz_receive(ctx: &mut Context<MyReceive>, params: &LzReceiveParams) -> Result<()> {
45
42
  Ok(())
46
43
  }
47
44
  }
@@ -55,11 +52,11 @@ mod tests {
55
52
  assert!(is_mut, "set_peer should have `mut ctx`");
56
53
  assert_eq!(ctx_type, "StdSetPeer", "should use default context type");
57
54
 
58
- // Default handler called by reference: StdSetPeer::apply(&mut ctx, ...)
55
+ // Default handler called by reference: StdSetPeer::apply(&mut ctx, &params)
59
56
  let body = set_peer.block.to_token_stream().to_string();
60
57
  assert!(
61
- body.contains("StdSetPeer :: apply (& mut ctx"),
62
- "mutable default should pass &mut ctx to handler: {body}"
58
+ body.contains("StdSetPeer :: apply (& mut ctx , & params)"),
59
+ "mutable default should pass &mut ctx and &params to handler: {body}"
63
60
  );
64
61
  }
65
62
 
@@ -68,7 +65,7 @@ mod tests {
68
65
  let module: syn::ItemMod = syn::parse_quote! {
69
66
  mod my_program {
70
67
  #[test_instruction]
71
- pub fn lz_receive(ctx: Context<MyReceive>, params: LzReceiveParams) -> Result<()> {
68
+ pub fn lz_receive(ctx: &mut Context<MyReceive>, params: &LzReceiveParams) -> Result<()> {
72
69
  Ok(())
73
70
  }
74
71
  }
@@ -82,40 +79,38 @@ mod tests {
82
79
  assert!(!is_mut, "view default should use `ctx` (no mut)");
83
80
  assert_eq!(ctx_type, "StdGetPeer", "should use default context type");
84
81
 
85
- // Default handler called by immutable reference: StdGetPeer::apply(&ctx, ...)
82
+ // Default handler called by immutable reference: StdGetPeer::apply(&ctx, &params)
86
83
  let body = get_peer.block.to_token_stream().to_string();
87
84
  assert!(
88
- body.contains("StdGetPeer :: apply (& ctx"),
89
- "view default should pass &ctx to handler: {body}"
85
+ body.contains("StdGetPeer :: apply (& ctx , & params)"),
86
+ "view default should pass &ctx and &params to handler: {body}"
90
87
  );
91
88
  }
92
89
 
93
90
  #[test]
94
- fn handler_override_emits_default_context() {
95
- // Override set_peer but reuse StdSetPeer context
91
+ fn override_with_default_context_emits_default() {
92
+ // Override set_peer but reuse StdSetPeer context.
96
93
  let module: syn::ItemMod = syn::parse_quote! {
97
94
  mod my_program {
98
95
  #[test_instruction]
99
- pub fn set_peer(ctx: Context<StdSetPeer>, params: SetPeerParams) -> Result<()> {
96
+ pub fn set_peer(ctx: &mut Context<StdSetPeer>, params: &SetPeerParams) -> Result<()> {
100
97
  Ok(())
101
98
  }
102
99
 
103
100
  #[test_instruction]
104
- pub fn lz_receive(ctx: Context<MyReceive>, params: LzReceiveParams) -> Result<()> {
101
+ pub fn lz_receive(ctx: &mut Context<MyReceive>, params: &LzReceiveParams) -> Result<()> {
105
102
  Ok(())
106
103
  }
107
104
  }
108
105
  };
109
106
  let gen = generate_ok(&module);
110
107
 
111
- // Default impl should still be generated (handler reuses it)
112
108
  let structs = parse_structs(&gen.default_impls);
113
109
  assert!(
114
110
  find_struct(&structs, "StdSetPeer").is_some(),
115
- "handler override should still emit StdSetPeer context"
111
+ "override should still emit StdSetPeer context"
116
112
  );
117
113
 
118
- // Entrypoint should dispatch to wrapper
119
114
  let fns = parse_fns(&gen.entrypoints);
120
115
  let set_peer = find_fn(&fns, "set_peer").expect("set_peer entrypoint should exist");
121
116
  let body = set_peer.block.to_token_stream().to_string();
@@ -126,29 +121,29 @@ mod tests {
126
121
  }
127
122
 
128
123
  #[test]
129
- fn full_override_skips_default_context() {
130
- // Override set_peer with a custom context type (not StdSetPeer)
124
+ fn override_with_custom_context_still_emits_default() {
125
+ // Override set_peer with a custom context type (not StdSetPeer).
126
+ // Default struct is always emitted for provided instructions, even when
127
+ // the override handler uses a different context type.
131
128
  let module: syn::ItemMod = syn::parse_quote! {
132
129
  mod my_program {
133
130
  #[test_instruction]
134
- pub fn set_peer(ctx: Context<MyCustomAccounts>, params: SetPeerParams) -> Result<()> {
131
+ pub fn set_peer(ctx: &mut Context<MyCustomAccounts>, params: &SetPeerParams) -> Result<()> {
135
132
  Ok(())
136
133
  }
137
134
 
138
135
  #[test_instruction]
139
- pub fn lz_receive(ctx: Context<MyReceive>, params: LzReceiveParams) -> Result<()> {
136
+ pub fn lz_receive(ctx: &mut Context<MyReceive>, params: &LzReceiveParams) -> Result<()> {
140
137
  Ok(())
141
138
  }
142
139
  }
143
140
  };
144
141
  let gen = generate_ok(&module);
145
142
 
146
- // Full override should NOT emit default impl for set_peer.
147
- // Other provided instructions (get_peer, token) still get their defaults.
148
143
  let structs = parse_structs(&gen.default_impls);
149
144
  assert!(
150
- find_struct(&structs, "StdSetPeer").is_none(),
151
- "full override should skip default impl for set_peer"
145
+ find_struct(&structs, "StdSetPeer").is_some(),
146
+ "override should still emit StdSetPeer (always available)"
152
147
  );
153
148
  assert!(
154
149
  find_struct(&structs, "StdGetPeer").is_some(),
@@ -156,14 +151,12 @@ mod tests {
156
151
  );
157
152
  assert!(find_struct(&structs, "StdToken").is_some(), "token should still have default");
158
153
 
159
- // Entrypoint should use the custom context type
160
154
  let fns = parse_fns(&gen.entrypoints);
161
155
  let set_peer = find_fn(&fns, "set_peer").expect("set_peer entrypoint should exist");
162
156
  let (is_mut, ctx_type) = first_param_context_type(set_peer);
163
157
  assert!(is_mut, "set_peer should have `mut ctx`");
164
- assert_eq!(ctx_type, "MyCustomAccounts", "full override should use custom context type");
158
+ assert_eq!(ctx_type, "MyCustomAccounts", "override should use custom context type");
165
159
 
166
- // Should dispatch to wrapper
167
160
  let body = set_peer.block.to_token_stream().to_string();
168
161
  assert!(
169
162
  body.contains("__test_instruction__set_peer_handler :: set_peer"),
@@ -172,16 +165,16 @@ mod tests {
172
165
  }
173
166
 
174
167
  #[test]
175
- fn override_receives_params_by_value() {
168
+ fn override_receives_params_by_ref() {
176
169
  let module: syn::ItemMod = syn::parse_quote! {
177
170
  mod my_program {
178
171
  #[test_instruction]
179
- pub fn set_peer(ctx: Context<MyAccounts>, params: SetPeerParams) -> Result<()> {
172
+ pub fn set_peer(ctx: &mut Context<MyAccounts>, params: &SetPeerParams) -> Result<()> {
180
173
  Ok(())
181
174
  }
182
175
 
183
176
  #[test_instruction]
184
- pub fn lz_receive(ctx: Context<MyReceive>, params: LzReceiveParams) -> Result<()> {
177
+ pub fn lz_receive(ctx: &mut Context<MyReceive>, params: &LzReceiveParams) -> Result<()> {
185
178
  Ok(())
186
179
  }
187
180
  }
@@ -190,11 +183,10 @@ mod tests {
190
183
  let fns = parse_fns(&gen.entrypoints);
191
184
  let set_peer = find_fn(&fns, "set_peer").expect("set_peer entrypoint should exist");
192
185
 
193
- // Override handlers get (ctx, params) by value — no & or &mut on the call
194
186
  let body = set_peer.block.to_token_stream().to_string();
195
187
  assert!(
196
- body.contains("set_peer (ctx , params)"),
197
- "override should pass ctx and params by value: {body}"
188
+ body.contains("set_peer (& mut ctx , & params)"),
189
+ "override should pass ctx by &mut and params by &: {body}"
198
190
  );
199
191
  }
200
192
 
@@ -203,7 +195,7 @@ mod tests {
203
195
  let module: syn::ItemMod = syn::parse_quote! {
204
196
  mod my_program {
205
197
  #[test_instruction]
206
- pub fn lz_receive(ctx: Context<MyReceive>, params: LzReceiveParams) -> Result<()> {
198
+ pub fn lz_receive(ctx: &mut Context<MyReceive>, params: &LzReceiveParams) -> Result<()> {
207
199
  Ok(())
208
200
  }
209
201
  }
@@ -212,7 +204,6 @@ mod tests {
212
204
  let fns = parse_fns(&gen.entrypoints);
213
205
  let token = find_fn(&fns, "token").expect("token entrypoint should exist");
214
206
 
215
- // Token instruction has no params — entrypoint should only have ctx
216
207
  assert_eq!(token.sig.inputs.len(), 1, "no-params instruction should have only ctx");
217
208
  let (is_mut, ctx_type) = first_param_context_type(token);
218
209
  assert!(!is_mut, "token is a view — should not be mut");
@@ -224,18 +215,18 @@ mod tests {
224
215
  // -----------------------------------------------------------------------
225
216
 
226
217
  #[test]
227
- fn view_override_with_params_gets_immutable_ctx() {
228
- // Override get_peer (view, has params) — entrypoint should have
229
- // immutable ctx and dispatch to wrapper with (ctx, params) by value.
218
+ fn view_override_with_params_gets_immutable_ref() {
219
+ // Override get_peer (view, has params) — entrypoint must have immutable ctx
220
+ // and dispatch to wrapper with (&ctx, &params).
230
221
  let module: syn::ItemMod = syn::parse_quote! {
231
222
  mod my_program {
232
223
  #[test_instruction]
233
- pub fn get_peer(ctx: Context<MyGetPeerAccounts>, params: GetPeerParams) -> Result<[u8; 32]> {
224
+ pub fn get_peer(ctx: &Context<MyGetPeerAccounts>, params: &GetPeerParams) -> Result<[u8; 32]> {
234
225
  Ok([0u8; 32])
235
226
  }
236
227
 
237
228
  #[test_instruction]
238
- pub fn lz_receive(ctx: Context<MyReceive>, params: LzReceiveParams) -> Result<()> {
229
+ pub fn lz_receive(ctx: &mut Context<MyReceive>, params: &LzReceiveParams) -> Result<()> {
239
230
  Ok(())
240
231
  }
241
232
  }
@@ -244,32 +235,29 @@ mod tests {
244
235
  let fns = parse_fns(&gen.entrypoints);
245
236
  let get_peer = find_fn(&fns, "get_peer").expect("get_peer entrypoint should exist");
246
237
 
247
- // View override: ctx should NOT be mut
248
238
  let (is_mut, ctx_type) = first_param_context_type(get_peer);
249
239
  assert!(!is_mut, "view override should have immutable ctx");
250
240
  assert_eq!(ctx_type, "MyGetPeerAccounts", "should use override's context type");
251
241
 
252
- // Dispatches to wrapper by value
253
242
  let body = get_peer.block.to_token_stream().to_string();
254
243
  assert!(
255
- body.contains("get_peer (ctx , params)"),
256
- "view override should pass ctx and params by value: {body}"
244
+ body.contains("get_peer (& ctx , & params)"),
245
+ "view override should pass ctx by & and params by &: {body}"
257
246
  );
258
247
  }
259
248
 
260
249
  #[test]
261
- fn override_no_params_receives_ctx_by_value() {
262
- // Override the token instruction (no params) — entrypoint should
263
- // dispatch to wrapper with just `ctx` by value.
250
+ fn override_no_params_receives_ctx_by_ref() {
251
+ // Override token (no params, view) — entrypoint must dispatch with just `&ctx`.
264
252
  let module: syn::ItemMod = syn::parse_quote! {
265
253
  mod my_program {
266
254
  #[test_instruction]
267
- pub fn token(ctx: Context<MyTokenAccounts>) -> Result<Pubkey> {
255
+ pub fn token(ctx: &Context<MyTokenAccounts>) -> Result<Pubkey> {
268
256
  Ok(Pubkey::default())
269
257
  }
270
258
 
271
259
  #[test_instruction]
272
- pub fn lz_receive(ctx: Context<MyReceive>, params: LzReceiveParams) -> Result<()> {
260
+ pub fn lz_receive(ctx: &mut Context<MyReceive>, params: &LzReceiveParams) -> Result<()> {
273
261
  Ok(())
274
262
  }
275
263
  }
@@ -278,13 +266,9 @@ mod tests {
278
266
  let fns = parse_fns(&gen.entrypoints);
279
267
  let token = find_fn(&fns, "token").expect("token entrypoint should exist");
280
268
 
281
- // No-params override: entrypoint has only ctx, dispatches wrapper::handler(ctx)
282
269
  assert_eq!(token.sig.inputs.len(), 1, "no-params override should have only ctx");
283
270
  let body = token.block.to_token_stream().to_string();
284
- assert!(
285
- body.contains("token (ctx)"),
286
- "no-params override should pass ctx by value: {body}"
287
- );
271
+ assert!(body.contains("token (& ctx)"), "no-params override should pass ctx by &: {body}");
288
272
  }
289
273
 
290
274
  #[test]
@@ -292,7 +276,7 @@ mod tests {
292
276
  let module: syn::ItemMod = syn::parse_quote! {
293
277
  mod my_program {
294
278
  #[test_instruction]
295
- pub fn lz_receive(ctx: Context<MyReceive>, params: LzReceiveParams) -> Result<()> {
279
+ pub fn lz_receive(ctx: &mut Context<MyReceive>, params: &LzReceiveParams) -> Result<()> {
296
280
  Ok(())
297
281
  }
298
282
  }
@@ -300,17 +284,15 @@ mod tests {
300
284
  let gen = generate_ok(&module);
301
285
  let fns = parse_fns(&gen.entrypoints);
302
286
 
303
- // get_peer returns [u8; 32] per spec
287
+ // Return types come from the spec, not the override handler.
304
288
  let get_peer = find_fn(&fns, "get_peer").expect("get_peer should exist");
305
289
  let ret = get_peer.sig.output.to_token_stream().to_string();
306
290
  assert!(ret.contains("[u8 ; 32]"), "get_peer return type should be [u8; 32]: {ret}");
307
291
 
308
- // token returns Pubkey per spec
309
292
  let token = find_fn(&fns, "token").expect("token should exist");
310
293
  let ret = token.sig.output.to_token_stream().to_string();
311
294
  assert!(ret.contains("Pubkey"), "token return type should be Pubkey: {ret}");
312
295
 
313
- // set_peer returns () per spec
314
296
  let set_peer = find_fn(&fns, "set_peer").expect("set_peer should exist");
315
297
  let ret = set_peer.sig.output.to_token_stream().to_string();
316
298
  assert!(ret.contains("()"), "set_peer return type should be (): {ret}");
@@ -323,8 +305,8 @@ mod tests {
323
305
  mod my_program {
324
306
  #[test_instruction]
325
307
  pub fn lz_receive<'info>(
326
- ctx: Context<'_, '_, '_, 'info, MyReceive<'info>>,
327
- params: LzReceiveParams,
308
+ ctx: &mut Context<'_, '_, '_, 'info, MyReceive<'info>>,
309
+ params: &LzReceiveParams,
328
310
  ) -> Result<()> {
329
311
  Ok(())
330
312
  }
@@ -341,13 +323,103 @@ mod tests {
341
323
  );
342
324
  }
343
325
 
326
+ // -----------------------------------------------------------------------
327
+ // Tests — extend rules
328
+ // -----------------------------------------------------------------------
329
+
330
+ #[test]
331
+ fn extend_rules_generated_for_provided_instructions() {
332
+ let module: syn::ItemMod = syn::parse_quote! {
333
+ mod my_program {
334
+ #[test_instruction]
335
+ pub fn lz_receive(ctx: &mut Context<MyReceive>, params: &LzReceiveParams) -> Result<()> {
336
+ Ok(())
337
+ }
338
+ }
339
+ };
340
+ let gen = generate_ok(&module);
341
+
342
+ let rules = gen.extend_rules.to_string();
343
+ assert!(
344
+ !rules.is_empty(),
345
+ "extend_rules should be non-empty when provided instructions exist"
346
+ );
347
+ assert!(rules.contains("set_peer"), "should contain a rule for set_peer");
348
+ assert!(rules.contains("get_peer"), "should contain a rule for get_peer");
349
+ assert!(rules.contains("token"), "should contain a rule for token");
350
+ assert!(
351
+ !rules.contains("lz_receive"),
352
+ "required instruction should NOT have an extend rule"
353
+ );
354
+ }
355
+
356
+ #[test]
357
+ fn extend_rule_contains_base_fields_and_extra_placeholder() {
358
+ let module: syn::ItemMod = syn::parse_quote! {
359
+ mod my_program {
360
+ #[test_instruction]
361
+ pub fn lz_receive(ctx: &mut Context<MyReceive>, params: &LzReceiveParams) -> Result<()> {
362
+ Ok(())
363
+ }
364
+ }
365
+ };
366
+ let gen = generate_ok(&module);
367
+
368
+ let rules = gen.extend_rules.to_string();
369
+ assert!(rules.contains("placeholder"), "extend rules should include base fields");
370
+ assert!(
371
+ rules.contains("extra"),
372
+ "extend rules should include $($extra)* for appended accounts"
373
+ );
374
+ }
375
+
376
+ #[test]
377
+ fn extend_rule_preserves_struct_attributes() {
378
+ let module: syn::ItemMod = syn::parse_quote! {
379
+ mod my_program {
380
+ #[test_instruction]
381
+ pub fn lz_receive(ctx: &mut Context<MyReceive>, params: &LzReceiveParams) -> Result<()> {
382
+ Ok(())
383
+ }
384
+ }
385
+ };
386
+ let gen = generate_ok(&module);
387
+
388
+ let rules = gen.extend_rules.to_string();
389
+ // Compare against the tokenized form rather than a raw string to be precise.
390
+ let expected = quote! { #[derive(Accounts)] }.to_string();
391
+ assert!(
392
+ rules.contains(&expected),
393
+ "extend rules should preserve struct attributes `{expected}`: {rules}"
394
+ );
395
+ }
396
+
397
+ #[test]
398
+ fn extend_rule_handler_uses_context_self() {
399
+ let module: syn::ItemMod = syn::parse_quote! {
400
+ mod my_program {
401
+ #[test_instruction]
402
+ pub fn lz_receive(ctx: &mut Context<MyReceive>, params: &LzReceiveParams) -> Result<()> {
403
+ Ok(())
404
+ }
405
+ }
406
+ };
407
+ let gen = generate_ok(&module);
408
+
409
+ let rules = gen.extend_rules.to_string();
410
+ assert!(
411
+ rules.contains("Context < Self >"),
412
+ "extend rule handlers must use Context<Self> so they work for any extended struct name: {rules}"
413
+ );
414
+ }
415
+
344
416
  // -----------------------------------------------------------------------
345
417
  // Tests — signature validation errors
346
418
  // -----------------------------------------------------------------------
347
419
 
348
420
  #[test]
349
421
  fn error_required_instruction_missing_handler() {
350
- // lz_receive is required (no default) but not overridden
422
+ // lz_receive is required (no default) but not overridden.
351
423
  let module: syn::ItemMod = syn::parse_quote! {
352
424
  mod my_program {}
353
425
  };