@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
@@ -0,0 +1,123 @@
1
+ //! Shared test helpers: PDA derivation, account builders, fixtures, and instruction builders.
2
+
3
+ use anchor_lang::{AnchorSerialize, Discriminator};
4
+ use mollusk_svm::{program::keyed_account_for_system_program, Mollusk};
5
+ use shapes_triangle::{instruction as ix, DimensionParams, InitParams, ShapeState};
6
+ use solana_account::Account;
7
+ use solana_instruction::{AccountMeta, Instruction};
8
+ use solana_program_error::ProgramError;
9
+ use solana_pubkey::Pubkey;
10
+
11
+ // ── Program + PDA derivation ──────────────────────────────────────────────────
12
+
13
+ /// Bridge anchor-lang Pubkey (solana-program 2.x) → solana-pubkey 4.x via bytes.
14
+ pub fn program_id() -> Pubkey {
15
+ let mut bytes = [0u8; 32];
16
+ bytes.copy_from_slice(shapes_triangle::ID.as_ref());
17
+ Pubkey::new_from_array(bytes)
18
+ }
19
+
20
+ /// Shape PDA: seeds = [b"shape"]
21
+ pub fn shape_pda() -> (Pubkey, u8) {
22
+ Pubkey::find_program_address(&[b"shape"], &program_id())
23
+ }
24
+
25
+ // @todo shankar extract shared mollusk helpers (mollusk(), ix_data(), payer_key/account(),
26
+ // sys_keyed(), to_program_pubkey(), decode_event()) into a common test-utils crate
27
+ // at contracts/common/test-utils/solana/ to eliminate duplication across examples.
28
+ pub fn mollusk() -> Mollusk {
29
+ // `anchor build` outputs .so relative to the Cargo workspace root.
30
+ // Walk up from the crate to find the nearest Cargo.lock (workspace root)
31
+ // which is where target/deploy/ lives.
32
+ let binary = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
33
+ .ancestors()
34
+ .find(|p| p.join("Cargo.lock").exists())
35
+ .expect("Cargo.lock not found — workspace root missing")
36
+ .join("target/deploy/shapes_triangle");
37
+ Mollusk::new(&program_id(), binary.to_str().unwrap())
38
+ }
39
+
40
+ // ── Fixtures ──────────────────────────────────────────────────────────────────
41
+
42
+ pub fn payer_key() -> Pubkey {
43
+ Pubkey::new_from_array([0xAA; 32])
44
+ }
45
+
46
+ pub fn payer_account() -> Account {
47
+ Account::new(10_000_000, 0, &Pubkey::new_from_array([0u8; 32]))
48
+ }
49
+
50
+ /// Extra signer injected by `ExtComputeArea` (triangle-specific extension).
51
+ pub fn verifier_key() -> Pubkey {
52
+ Pubkey::new_from_array([0xBB; 32])
53
+ }
54
+
55
+ pub fn verifier_account() -> Account {
56
+ Account::new(1_000_000, 0, &Pubkey::new_from_array([0u8; 32]))
57
+ }
58
+
59
+ pub fn sys_keyed() -> (Pubkey, Account) {
60
+ keyed_account_for_system_program()
61
+ }
62
+
63
+ // ── Serialization helpers ─────────────────────────────────────────────────────
64
+
65
+ pub fn ix_data<T: AnchorSerialize>(discriminator: &[u8], params: T) -> Vec<u8> {
66
+ let mut d = discriminator.to_vec();
67
+ d.extend(params.try_to_vec().unwrap());
68
+ d
69
+ }
70
+
71
+ // ── Account data builders ─────────────────────────────────────────────────────
72
+
73
+ pub fn shape_data(width: u64, height: u64, bump: u8) -> Vec<u8> {
74
+ let mut d = ShapeState::DISCRIMINATOR.to_vec();
75
+ d.extend_from_slice(&width.to_le_bytes());
76
+ d.extend_from_slice(&height.to_le_bytes());
77
+ d.push(bump);
78
+ d
79
+ }
80
+
81
+ pub fn shape_account(width: u64, height: u64, bump: u8) -> Account {
82
+ Account {
83
+ lamports: 1_000_000,
84
+ data: shape_data(width, height, bump),
85
+ owner: program_id(),
86
+ executable: false,
87
+ rent_epoch: u64::MAX,
88
+ }
89
+ }
90
+
91
+ // ── Error helpers ─────────────────────────────────────────────────────────────
92
+
93
+ pub fn program_err(e: impl Into<u32>) -> ProgramError {
94
+ ProgramError::Custom(e.into())
95
+ }
96
+
97
+ // ── Instruction builders ──────────────────────────────────────────────────────
98
+
99
+ /// `initialize` (Anchor `init` on shape PDA):
100
+ /// 0: payer, 1: shape, 2: system_program
101
+ pub fn initialize_ix(payer: &Pubkey, shape: &Pubkey, width: u64, height: u64) -> Instruction {
102
+ let (sys, _) = sys_keyed();
103
+ Instruction {
104
+ program_id: program_id(),
105
+ accounts: vec![
106
+ AccountMeta::new(*payer, true),
107
+ AccountMeta::new(*shape, false),
108
+ AccountMeta::new_readonly(sys, false),
109
+ ],
110
+ data: ix_data(ix::Initialize::DISCRIMINATOR, InitParams { width, height }),
111
+ }
112
+ }
113
+
114
+ /// `get_dimension` (view):
115
+ /// 0: shape
116
+ pub fn get_dimension_ix(shape: &Pubkey, index: u8) -> Instruction {
117
+ Instruction {
118
+ program_id: program_id(),
119
+ accounts: vec![AccountMeta::new_readonly(*shape, false)],
120
+ data: ix_data(ix::GetDimension::DISCRIMINATOR, DimensionParams { index }),
121
+ }
122
+ }
123
+
@@ -0,0 +1,49 @@
1
+ use crate::helpers::*;
2
+ use mollusk_svm::result::types::ProgramResult;
3
+ use solana_program_error::ProgramError;
4
+
5
+ // ── initialize ────────────────────────────────────────────────────────────────
6
+
7
+ /// double-init: shape account already exists → `init` constraint rejects.
8
+ #[test]
9
+ fn test_initialize_fails_on_double_init() {
10
+ let mollusk = mollusk();
11
+ let payer = payer_key();
12
+ let (shape_key, shape_bump) = shape_pda();
13
+
14
+ let result = mollusk.process_instruction(
15
+ &initialize_ix(&payer, &shape_key, 6, 4),
16
+ &[
17
+ (payer, payer_account()),
18
+ // Supply pre-existing shape account → `init` must fail.
19
+ (shape_key, shape_account(6, 4, shape_bump)),
20
+ sys_keyed(),
21
+ ],
22
+ );
23
+
24
+ assert_eq!(
25
+ result.program_result,
26
+ ProgramResult::Failure(ProgramError::Custom(0)),
27
+ "double-init should fail with system AccountAlreadyInUse"
28
+ );
29
+ }
30
+
31
+ // ── get_dimension ─────────────────────────────────────────────────────────────
32
+
33
+ /// index out of range (index = 2) → `InvalidProgramId`.
34
+ #[test]
35
+ fn test_get_dimension_fails_with_out_of_bounds_index() {
36
+ let mollusk = mollusk();
37
+ let (shape_key, shape_bump) = shape_pda();
38
+
39
+ let result = mollusk.process_instruction(
40
+ &get_dimension_ix(&shape_key, 2),
41
+ &[(shape_key, shape_account(6, 4, shape_bump))],
42
+ );
43
+
44
+ assert_eq!(
45
+ result.program_result,
46
+ ProgramResult::Failure(program_err(anchor_lang::error::ErrorCode::InvalidProgramId)),
47
+ "get_dimension should fail with InvalidProgramId for out-of-bounds index"
48
+ );
49
+ }
@@ -0,0 +1,225 @@
1
+ use crate::helpers::*;
2
+ use anchor_lang::Discriminator;
3
+ use mollusk_svm::result::Check;
4
+ use shapes_triangle::{instruction as ix, ComputeParams, ScaleParams};
5
+ use solana_account::Account;
6
+ use solana_instruction::{AccountMeta, Instruction};
7
+
8
+ // ── initialize ────────────────────────────────────────────────────────────────
9
+
10
+ #[test]
11
+ fn test_initialize_creates_shape_with_correct_dimensions() {
12
+ let mollusk = mollusk();
13
+ let payer = payer_key();
14
+ let (shape_key, shape_bump) = shape_pda();
15
+
16
+ mollusk.process_and_validate_instruction(
17
+ &initialize_ix(&payer, &shape_key, 6, 4),
18
+ &[
19
+ (payer, payer_account()),
20
+ (shape_key, Account::default()),
21
+ sys_keyed(),
22
+ ],
23
+ &[
24
+ Check::success(),
25
+ Check::account(&shape_key)
26
+ .data(&shape_data(6, 4, shape_bump))
27
+ .build(),
28
+ ],
29
+ );
30
+ }
31
+
32
+ // ── get_name ──────────────────────────────────────────────────────────────────
33
+
34
+ #[test]
35
+ fn test_get_name_returns_triangle() {
36
+ let mollusk = mollusk();
37
+ let (shape_key, shape_bump) = shape_pda();
38
+
39
+ let result = mollusk.process_and_validate_instruction(
40
+ &Instruction {
41
+ program_id: program_id(),
42
+ accounts: vec![AccountMeta::new_readonly(shape_key, false)],
43
+ data: ix::GetName::DISCRIMINATOR.to_vec(),
44
+ },
45
+ &[(shape_key, shape_account(6, 4, shape_bump))],
46
+ &[Check::success()],
47
+ );
48
+
49
+ // Borsh String: 4-byte LE length prefix + UTF-8 bytes
50
+ let ret = &result.return_data;
51
+ let len = u32::from_le_bytes(ret[..4].try_into().unwrap()) as usize;
52
+ assert_eq!(std::str::from_utf8(&ret[4..4 + len]).unwrap(), "Triangle");
53
+ }
54
+
55
+ // ── get_dimension ─────────────────────────────────────────────────────────────
56
+
57
+ #[test]
58
+ fn test_get_dimension_returns_width_at_index_0() {
59
+ let mollusk = mollusk();
60
+ let (shape_key, shape_bump) = shape_pda();
61
+
62
+ let result = mollusk.process_and_validate_instruction(
63
+ &get_dimension_ix(&shape_key, 0),
64
+ &[(shape_key, shape_account(6, 4, shape_bump))],
65
+ &[Check::success()],
66
+ );
67
+
68
+ assert_eq!(u64::from_le_bytes(result.return_data[..8].try_into().unwrap()), 6);
69
+ }
70
+
71
+ #[test]
72
+ fn test_get_dimension_returns_height_at_index_1() {
73
+ let mollusk = mollusk();
74
+ let (shape_key, shape_bump) = shape_pda();
75
+
76
+ let result = mollusk.process_and_validate_instruction(
77
+ &get_dimension_ix(&shape_key, 1),
78
+ &[(shape_key, shape_account(6, 4, shape_bump))],
79
+ &[Check::success()],
80
+ );
81
+
82
+ assert_eq!(u64::from_le_bytes(result.return_data[..8].try_into().unwrap()), 4);
83
+ }
84
+
85
+ // ── clear ─────────────────────────────────────────────────────────────────────
86
+
87
+ #[test]
88
+ fn test_clear_zeros_dimensions() {
89
+ let mollusk = mollusk();
90
+ let (shape_key, shape_bump) = shape_pda();
91
+
92
+ mollusk.process_and_validate_instruction(
93
+ &Instruction {
94
+ program_id: program_id(),
95
+ accounts: vec![AccountMeta::new(shape_key, false)],
96
+ data: ix::Clear::DISCRIMINATOR.to_vec(),
97
+ },
98
+ &[(shape_key, shape_account(6, 4, shape_bump))],
99
+ &[
100
+ Check::success(),
101
+ Check::account(&shape_key)
102
+ .data(&shape_data(0, 0, shape_bump))
103
+ .build(),
104
+ ],
105
+ );
106
+ }
107
+
108
+ // ── compute_area ──────────────────────────────────────────────────────────────
109
+
110
+ /// Triangle override: w * h / 2; ExtComputeArea injects a verifier signer.
111
+ #[test]
112
+ fn test_compute_area_uses_triangle_formula() {
113
+ let mollusk = mollusk();
114
+ let verifier = verifier_key();
115
+ let (shape_key, shape_bump) = shape_pda();
116
+
117
+ let result = mollusk.process_and_validate_instruction(
118
+ &Instruction {
119
+ program_id: program_id(),
120
+ accounts: vec![
121
+ AccountMeta::new_readonly(shape_key, false),
122
+ AccountMeta::new(verifier, true),
123
+ ],
124
+ data: ix_data(ix::ComputeArea::DISCRIMINATOR, ComputeParams {}),
125
+ },
126
+ &[
127
+ (shape_key, shape_account(6, 4, shape_bump)),
128
+ (verifier, verifier_account()),
129
+ ],
130
+ &[Check::success()],
131
+ );
132
+
133
+ // 6 * 4 / 2 = 12
134
+ assert_eq!(u64::from_le_bytes(result.return_data[..8].try_into().unwrap()), 12);
135
+ }
136
+
137
+ // ── compute_perimeter ─────────────────────────────────────────────────────────
138
+
139
+ /// Isoceles triangle override: w + w + h = 6 + 6 + 4 = 16.
140
+ #[test]
141
+ fn test_compute_perimeter_uses_isoceles_formula() {
142
+ let mollusk = mollusk();
143
+ let (shape_key, shape_bump) = shape_pda();
144
+
145
+ let result = mollusk.process_and_validate_instruction(
146
+ &Instruction {
147
+ program_id: program_id(),
148
+ accounts: vec![AccountMeta::new_readonly(shape_key, false)],
149
+ data: ix_data(ix::ComputePerimeter::DISCRIMINATOR, ComputeParams {}),
150
+ },
151
+ &[(shape_key, shape_account(6, 4, shape_bump))],
152
+ &[Check::success()],
153
+ );
154
+
155
+ assert_eq!(u64::from_le_bytes(result.return_data[..8].try_into().unwrap()), 16);
156
+ }
157
+
158
+ // ── scale ─────────────────────────────────────────────────────────────────────
159
+
160
+ /// Isoceles override: only height scales, base stays fixed.
161
+ #[test]
162
+ fn test_scale_only_changes_height_for_isoceles_triangle() {
163
+ let mollusk = mollusk();
164
+ let (shape_key, shape_bump) = shape_pda();
165
+
166
+ mollusk.process_and_validate_instruction(
167
+ &Instruction {
168
+ program_id: program_id(),
169
+ accounts: vec![AccountMeta::new(shape_key, false)],
170
+ data: ix_data(ix::Scale::DISCRIMINATOR, ScaleParams { factor: 3 }),
171
+ },
172
+ &[(shape_key, shape_account(6, 4, shape_bump))],
173
+ &[
174
+ Check::success(),
175
+ Check::account(&shape_key)
176
+ .data(&shape_data(6, 12, shape_bump))
177
+ .build(),
178
+ ],
179
+ );
180
+ }
181
+
182
+ // ── reset ─────────────────────────────────────────────────────────────────────
183
+
184
+ /// Default reset: sets both dimensions to 1.
185
+ #[test]
186
+ fn test_reset_sets_both_dimensions_to_one() {
187
+ let mollusk = mollusk();
188
+ let (shape_key, shape_bump) = shape_pda();
189
+
190
+ mollusk.process_and_validate_instruction(
191
+ &Instruction {
192
+ program_id: program_id(),
193
+ accounts: vec![AccountMeta::new(shape_key, false)],
194
+ data: ix::Reset::DISCRIMINATOR.to_vec(),
195
+ },
196
+ &[(shape_key, shape_account(6, 4, shape_bump))],
197
+ &[
198
+ Check::success(),
199
+ Check::account(&shape_key)
200
+ .data(&shape_data(1, 1, shape_bump))
201
+ .build(),
202
+ ],
203
+ );
204
+ }
205
+
206
+ // ── get_dimension_count ───────────────────────────────────────────────────────
207
+
208
+ /// Default: always returns 2 for any 2D shape.
209
+ #[test]
210
+ fn test_get_dimension_count_returns_2() {
211
+ let mollusk = mollusk();
212
+ let (shape_key, shape_bump) = shape_pda();
213
+
214
+ let result = mollusk.process_and_validate_instruction(
215
+ &Instruction {
216
+ program_id: program_id(),
217
+ accounts: vec![AccountMeta::new_readonly(shape_key, false)],
218
+ data: ix::GetDimensionCount::DISCRIMINATOR.to_vec(),
219
+ },
220
+ &[(shape_key, shape_account(6, 4, shape_bump))],
221
+ &[Check::success()],
222
+ );
223
+
224
+ assert_eq!(result.return_data[0], 2u8);
225
+ }
@@ -0,0 +1,23 @@
1
+ //! Mollusk BPF tests for the Triangle shapes program.
2
+ //!
3
+ //! Positive tests (happy-path):
4
+ //! - `initialize` — creates ShapeState with correct dimensions
5
+ //! - `get_name` — view: returns "Triangle"
6
+ //! - `get_dimension` — view: returns width (index 0) and height (index 1)
7
+ //! - `clear` — zeros both dimensions
8
+ //! - `compute_area` — override: w * h / 2 with verifier signer (ExtComputeArea)
9
+ //! - `compute_perimeter` — override: w + w + h (isoceles triangle)
10
+ //! - `scale` — override: isoceles — only height scales, base stays fixed
11
+ //! - `reset` — default: sets both dimensions to 1
12
+ //! - `get_dimension_count` — default: always returns 2
13
+ //!
14
+ //! Negative tests (rejection):
15
+ //! - `initialize` — double-init rejected
16
+ //! - `get_dimension` — out-of-bounds index rejected
17
+
18
+ #[path = "triangle/helpers.rs"]
19
+ mod helpers;
20
+ #[path = "triangle/negative.rs"]
21
+ mod negative;
22
+ #[path = "triangle/positive.rs"]
23
+ mod positive;
@@ -0,0 +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]
@@ -0,0 +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]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@layerzerolabs/framework-solana-anchor-trait",
3
- "version": "0.2.61",
3
+ "version": "0.2.63",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -15,22 +15,29 @@
15
15
  "SPEC.md"
16
16
  ],
17
17
  "devDependencies": {
18
- "@layerzerolabs/tsup-configuration": "0.2.61",
19
- "@layerzerolabs/build-utils-solana": "0.2.61",
20
- "@layerzerolabs/vm-tooling-solana": "0.2.61",
21
- "@layerzerolabs/typescript-configuration": "0.2.61"
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"
22
22
  },
23
23
  "publishConfig": {
24
24
  "access": "restricted",
25
25
  "registry": "https://registry.npmjs.org/"
26
26
  },
27
27
  "scripts": {
28
- "build": "pnpm exec lz-tool --script 'cargo build' anchor",
28
+ "allow-read-write": "[ -z \"$CI\" ] || sudo chmod -R a+rwx ./target ./examples/shapes/target",
29
+ "build": "pnpm build:lib && pnpm build:example && pnpm allow-read-write",
30
+ "build:example": "pnpm exec lz-tool --script 'anchor build --no-idl' anchor",
31
+ "build:lib": "pnpm exec lz-tool --script 'cargo build' anchor",
29
32
  "check": "pnpm exec lz-tool --script 'cargo check' anchor",
30
33
  "clean": "rm -rf target",
31
34
  "format": "pnpm exec lz-tool --script 'cargo +nightly-2025-06-01 fmt' anchor",
32
35
  "format:check": "pnpm exec lz-tool --script 'cargo +nightly-2025-06-01 fmt -- --check' anchor",
33
36
  "lint": "pnpm exec lz-tool --script 'cargo +nightly-2025-06-01 fmt -- --check && cargo clippy --all-targets' anchor",
34
- "test": "pnpm exec lz-tool --script 'cargo test --lib --tests' anchor"
37
+ "test": "pnpm test:lib && pnpm test:example",
38
+ "test:example": "pnpm test:example:ellipse && pnpm test:example:triangle",
39
+ "test:example:ellipse": "pnpm exec lz-tool --script 'cargo test-ellipse' anchor",
40
+ "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"
35
42
  }
36
43
  }
package/src/assembly.rs CHANGED
@@ -1,6 +1,6 @@
1
1
  //! Final assembly of generated code into the macro output.
2
2
 
3
- use proc_macro2::TokenStream;
3
+ use proc_macro2::{Ident, TokenStream};
4
4
  use quote::quote;
5
5
  use syn::ItemMod;
6
6
 
@@ -16,43 +16,78 @@ pub struct Assembly<Ix: InstructionSet> {
16
16
  pub overrides: InstructionOverrides<Ix>,
17
17
  /// The generated instruction code.
18
18
  pub instructions: GeneratedInstructions,
19
- /// Extra attributes placed on the module (e.g. `#[program]`, `#[rbac(...)]`).
19
+ /// Extra attributes placed on the module (e.g. `#[rbac(...)]`).
20
20
  pub extra_mod_attrs: TokenStream,
21
21
  /// The original module being transformed.
22
22
  pub source_mod: ItemMod,
23
+ /// Name of the `macro_rules!` for extending default accounts structs.
24
+ /// `None` if the domain has no provided instructions.
25
+ pub extend_macro_name: Option<Ident>,
23
26
  }
24
27
 
25
28
  impl<Ix: InstructionSet> Assembly<Ix> {
26
29
  /// Assembles the final macro output from all generated parts.
27
30
  ///
31
+ /// `#[program]` is always emitted as the innermost attribute on the module so
32
+ /// that Anchor expands last — after all instruction macros have injected their
33
+ /// code. If `#[program]` already appears on `source_mod` (placed by a
34
+ /// previously-expanded macro), it is filtered out and re-emitted at the correct
35
+ /// position. This makes stacking order irrelevant for any combination of
36
+ /// domain macros (e.g. `#[oapp]` + `#[composer]`).
37
+ ///
28
38
  /// ```text
29
39
  /// prelude // domain-specific definitions (state types, events, etc.)
30
40
  /// wrapper_defs // relocated override handler modules
31
41
  /// default_impls // generated default accounts structs + handler impls
32
- /// #[extra_mod_attrs] // e.g. #[program]
42
+ /// extend_macro // macro_rules! for extending default accounts (if configured)
33
43
  /// #[original_mod_attrs]
44
+ /// #[extra_mod_attrs] // e.g. #[rbac(...)]
45
+ /// #[program] // always innermost — auto-managed by Assembly
34
46
  /// mod name {
35
47
  /// filtered_items // original items minus relocated handlers
36
48
  /// entrypoints // generated instruction entrypoints
37
49
  /// }
38
50
  /// ```
39
51
  pub fn assemble(self) -> TokenStream {
40
- let Self { prelude, overrides, instructions, extra_mod_attrs, source_mod } = self;
52
+ let Self {
53
+ prelude,
54
+ overrides,
55
+ instructions,
56
+ extra_mod_attrs,
57
+ source_mod,
58
+ extend_macro_name,
59
+ } = self;
41
60
 
42
61
  let wrapper_defs = &overrides.wrapper_defs;
43
62
  let filtered_items = overrides.filtered_items(&source_mod);
44
- let GeneratedInstructions { default_impls, entrypoints } = &instructions;
63
+ let GeneratedInstructions { default_impls, entrypoints, extend_rules } = &instructions;
64
+
65
+ // If there are extend rules, emit a macro_rules! with the given name.
66
+ let extend_macro = match extend_macro_name {
67
+ Some(name) if !extend_rules.is_empty() => quote! {
68
+ #[allow(unused_macros)]
69
+ macro_rules! #name {
70
+ #extend_rules
71
+ }
72
+ },
73
+ _ => TokenStream::new(),
74
+ };
45
75
 
46
76
  let ItemMod { attrs: mod_attrs, vis: mod_vis, ident: mod_ident, .. } = &source_mod;
47
77
 
78
+ // Filter out #[program] attribute if it is already present.
79
+ let mod_attrs: Vec<_> =
80
+ mod_attrs.iter().filter(|a| !a.path().is_ident("program")).collect();
81
+
48
82
  quote! {
49
83
  #prelude
50
-
51
84
  #wrapper_defs
52
85
  #default_impls
86
+ #extend_macro
53
87
 
54
88
  #(#mod_attrs)*
55
89
  #extra_mod_attrs
90
+ #[program]
56
91
  #mod_vis mod #mod_ident {
57
92
  use super::*;
58
93
  #filtered_items