@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
@@ -1,7 +1,4 @@
1
- pub mod state;
2
-
3
1
  use anchor_lang::prelude::*;
4
- use state::ShapeState;
5
2
 
6
3
  declare_id!("ELp1111111111111111111111111111111111111111");
7
4
 
@@ -28,12 +25,22 @@ pub struct GetName<'info> {
28
25
  pub shape: Account<'info, ShapeState>,
29
26
  }
30
27
 
31
- /// Full Override accounts — ellipse perimeter reads state differently.
28
+ /// Custom accounts — ellipse perimeter reads state differently.
32
29
  #[derive(Accounts)]
33
30
  pub struct EllipseAccounts<'info> {
34
31
  pub shape: Account<'info, ShapeState>,
35
32
  }
36
33
 
34
+ #[derive(Accounts)]
35
+ pub struct GetDimension<'info> {
36
+ pub shape: Account<'info, ShapeState>,
37
+ }
38
+
39
+ #[derive(Accounts)]
40
+ pub struct Clear<'info> {
41
+ #[account(mut)]
42
+ pub shape: Account<'info, ShapeState>,
43
+ }
37
44
  // --- Param types ---
38
45
 
39
46
  #[derive(Clone, AnchorSerialize, AnchorDeserialize)]
@@ -45,18 +52,33 @@ pub struct InitParams {
45
52
  #[derive(Clone, AnchorSerialize, AnchorDeserialize)]
46
53
  pub struct ComputeParams {}
47
54
 
48
- // --- Program module ---
49
- // Required: initialize, get_name
50
- // Handler Override: compute_area (π*w*h/4)
51
- // Full Override: compute_perimeter (custom EllipseAccounts)
52
- // Default: get_dimension_count
55
+ #[derive(Clone, AnchorSerialize, AnchorDeserialize)]
56
+ pub struct ScaleParams {
57
+ pub factor: u64,
58
+ }
59
+
60
+ #[derive(Clone, AnchorSerialize, AnchorDeserialize)]
61
+ pub struct DimensionParams {
62
+ pub index: u8,
63
+ }
53
64
 
54
- #[shapes_macros::shapes(state = ShapeState)]
65
+ // --- Program module ---
66
+ // ShapeState — generated in prelude by #[shapes]
67
+ //
68
+ // Required: initialize, get_name, get_dimension, clear
69
+ // Override: compute_area (reuses StdComputeArea, π*w*h/4)
70
+ // Override: compute_perimeter (custom EllipseAccounts)
71
+ // Override: reset (reuses StdReset, restores to unit circle: width=height=10)
72
+ // Default: scale, get_dimension_count
73
+
74
+ #[shapes_macros::shapes]
55
75
  pub mod shapes_ellipse {
56
76
 
57
- #[allow(unused_mut)]
58
77
  #[shapes_instruction]
59
- pub fn initialize(mut ctx: Context<Initialize>, params: InitParams) -> Result<()> {
78
+ pub fn initialize(ctx: &mut Context<Initialize>, params: &InitParams) -> Result<()> {
79
+ // This is a dummy check to ensure the audit macro is working.
80
+ let _ = SHAPES_AUDIT_ENABLED;
81
+
60
82
  ctx.accounts.shape.width = params.width;
61
83
  ctx.accounts.shape.height = params.height;
62
84
  ctx.accounts.shape.bump = ctx.bumps.shape;
@@ -64,25 +86,55 @@ pub mod shapes_ellipse {
64
86
  }
65
87
 
66
88
  #[shapes_instruction]
67
- pub fn get_name(_ctx: Context<GetName>) -> Result<String> {
89
+ pub fn get_name(_ctx: &Context<GetName>) -> Result<String> {
68
90
  Ok("Ellipse".to_string())
69
91
  }
70
92
 
71
- // HANDLER OVERRIDE — reuses StdComputeArea accounts, ellipse formula.
72
93
  #[shapes_instruction]
73
- pub fn compute_area(ctx: Context<StdComputeArea>, _params: ComputeParams) -> Result<u64> {
94
+ pub fn get_dimension(ctx: &Context<GetDimension>, params: &DimensionParams) -> Result<u64> {
95
+ let s = &ctx.accounts.shape;
96
+ match params.index {
97
+ 0 => Ok(s.width), // semi-axis a
98
+ 1 => Ok(s.height), // semi-axis b
99
+ _ => err!(anchor_lang::error::ErrorCode::InvalidProgramId),
100
+ }
101
+ }
102
+
103
+ #[shapes_instruction]
104
+ pub fn clear(ctx: &mut Context<Clear>) -> Result<()> {
105
+ let s = &mut ctx.accounts.shape;
106
+ s.width = 0;
107
+ s.height = 0;
108
+ Ok(())
109
+ }
110
+
111
+ // OVERRIDE — reuses StdComputeArea accounts, ellipse formula.
112
+ #[shapes_instruction]
113
+ pub fn compute_area(ctx: &Context<StdComputeArea>, _params: &ComputeParams) -> Result<u64> {
74
114
  let s = &ctx.accounts.shape;
75
115
  // Ellipse area = π * a * b where a=w/2, b=h/2 → π * w * h / 4
76
116
  Ok(314_159 * s.width * s.height / 400_000)
77
117
  }
78
118
 
79
- // FULL OVERRIDE — custom EllipseAccounts, ellipse perimeter approximation.
119
+ // OVERRIDE — custom EllipseAccounts, ellipse perimeter approximation.
80
120
  #[shapes_instruction]
81
- pub fn compute_perimeter(ctx: Context<EllipseAccounts>, _params: ComputeParams) -> Result<u64> {
121
+ pub fn compute_perimeter(
122
+ ctx: &Context<EllipseAccounts>,
123
+ _params: &ComputeParams,
124
+ ) -> Result<u64> {
82
125
  let s = &ctx.accounts.shape;
83
126
  // Simple approximation: π * (w + h) / 2
84
127
  Ok(314_159 * (s.width + s.height) / 200_000)
85
128
  }
86
129
 
130
+ // OVERRIDE — reuses StdReset, ellipse resets to unit circle (equal axes).
131
+ #[shapes_instruction]
132
+ pub fn reset(ctx: &mut Context<StdReset>) -> Result<()> {
133
+ let s = &mut ctx.accounts.shape;
134
+ s.width = 10;
135
+ s.height = 10;
136
+ Ok(())
137
+ }
138
+
87
139
  // DEFAULT — get_dimension_count returns 2, correct for any 2D shape.
88
140
  }
@@ -0,0 +1,110 @@
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_ellipse::{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_ellipse::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
+ pub fn mollusk() -> Mollusk {
26
+ // `anchor build` outputs .so relative to the Cargo workspace root.
27
+ // Walk up from the crate to find the nearest Cargo.lock (workspace root)
28
+ // which is where target/deploy/ lives.
29
+ let binary = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
30
+ .ancestors()
31
+ .find(|p| p.join("Cargo.lock").exists())
32
+ .expect("Cargo.lock not found — workspace root missing")
33
+ .join("target/deploy/shapes_ellipse");
34
+ Mollusk::new(&program_id(), binary.to_str().unwrap())
35
+ }
36
+
37
+ // ── Fixtures ──────────────────────────────────────────────────────────────────
38
+
39
+ pub fn payer_key() -> Pubkey {
40
+ Pubkey::new_from_array([0xAA; 32])
41
+ }
42
+
43
+ pub fn payer_account() -> Account {
44
+ Account::new(10_000_000, 0, &Pubkey::new_from_array([0u8; 32]))
45
+ }
46
+
47
+ pub fn sys_keyed() -> (Pubkey, Account) {
48
+ keyed_account_for_system_program()
49
+ }
50
+
51
+ // ── Serialization helpers ─────────────────────────────────────────────────────
52
+
53
+ pub fn ix_data<T: AnchorSerialize>(discriminator: &[u8], params: T) -> Vec<u8> {
54
+ let mut d = discriminator.to_vec();
55
+ d.extend(params.try_to_vec().unwrap());
56
+ d
57
+ }
58
+
59
+ // ── Account data builders ─────────────────────────────────────────────────────
60
+
61
+ pub fn shape_data(width: u64, height: u64, bump: u8) -> Vec<u8> {
62
+ let mut d = ShapeState::DISCRIMINATOR.to_vec();
63
+ d.extend_from_slice(&width.to_le_bytes());
64
+ d.extend_from_slice(&height.to_le_bytes());
65
+ d.push(bump);
66
+ d
67
+ }
68
+
69
+ pub fn shape_account(width: u64, height: u64, bump: u8) -> Account {
70
+ Account {
71
+ lamports: 1_000_000,
72
+ data: shape_data(width, height, bump),
73
+ owner: program_id(),
74
+ executable: false,
75
+ rent_epoch: u64::MAX,
76
+ }
77
+ }
78
+
79
+ // ── Error helpers ─────────────────────────────────────────────────────────────
80
+
81
+ pub fn program_err(e: impl Into<u32>) -> ProgramError {
82
+ ProgramError::Custom(e.into())
83
+ }
84
+
85
+ // ── Instruction builders ──────────────────────────────────────────────────────
86
+
87
+ /// `initialize` (Anchor `init` on shape PDA):
88
+ /// 0: payer, 1: shape, 2: system_program
89
+ pub fn initialize_ix(payer: &Pubkey, shape: &Pubkey, width: u64, height: u64) -> Instruction {
90
+ let (sys, _) = sys_keyed();
91
+ Instruction {
92
+ program_id: program_id(),
93
+ accounts: vec![
94
+ AccountMeta::new(*payer, true),
95
+ AccountMeta::new(*shape, false),
96
+ AccountMeta::new_readonly(sys, false),
97
+ ],
98
+ data: ix_data(ix::Initialize::DISCRIMINATOR, InitParams { width, height }),
99
+ }
100
+ }
101
+
102
+ /// `get_dimension` (view):
103
+ /// 0: shape
104
+ pub fn get_dimension_ix(shape: &Pubkey, index: u8) -> Instruction {
105
+ Instruction {
106
+ program_id: program_id(),
107
+ accounts: vec![AccountMeta::new_readonly(*shape, false)],
108
+ data: ix_data(ix::GetDimension::DISCRIMINATOR, DimensionParams { index }),
109
+ }
110
+ }
@@ -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, 10, 6),
16
+ &[
17
+ (payer, payer_account()),
18
+ // Supply pre-existing shape account → `init` must fail.
19
+ (shape_key, shape_account(10, 6, 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(10, 6, 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,231 @@
1
+ use crate::helpers::*;
2
+ use anchor_lang::Discriminator;
3
+ use mollusk_svm::result::Check;
4
+ use shapes_ellipse::{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, 10, 6),
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(10, 6, shape_bump))
27
+ .build(),
28
+ ],
29
+ );
30
+ }
31
+
32
+ // ── get_name ──────────────────────────────────────────────────────────────────
33
+
34
+ #[test]
35
+ fn test_get_name_returns_ellipse() {
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(10, 6, 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(), "Ellipse");
53
+ }
54
+
55
+ // ── get_dimension ─────────────────────────────────────────────────────────────
56
+
57
+ #[test]
58
+ fn test_get_dimension_returns_semi_axis_a_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(10, 6, shape_bump))],
65
+ &[Check::success()],
66
+ );
67
+
68
+ assert_eq!(
69
+ u64::from_le_bytes(result.return_data[..8].try_into().unwrap()),
70
+ 10
71
+ );
72
+ }
73
+
74
+ #[test]
75
+ fn test_get_dimension_returns_semi_axis_b_at_index_1() {
76
+ let mollusk = mollusk();
77
+ let (shape_key, shape_bump) = shape_pda();
78
+
79
+ let result = mollusk.process_and_validate_instruction(
80
+ &get_dimension_ix(&shape_key, 1),
81
+ &[(shape_key, shape_account(10, 6, shape_bump))],
82
+ &[Check::success()],
83
+ );
84
+
85
+ assert_eq!(
86
+ u64::from_le_bytes(result.return_data[..8].try_into().unwrap()),
87
+ 6
88
+ );
89
+ }
90
+
91
+ // ── clear ─────────────────────────────────────────────────────────────────────
92
+
93
+ #[test]
94
+ fn test_clear_zeros_dimensions() {
95
+ let mollusk = mollusk();
96
+ let (shape_key, shape_bump) = shape_pda();
97
+
98
+ mollusk.process_and_validate_instruction(
99
+ &Instruction {
100
+ program_id: program_id(),
101
+ accounts: vec![AccountMeta::new(shape_key, false)],
102
+ data: ix::Clear::DISCRIMINATOR.to_vec(),
103
+ },
104
+ &[(shape_key, shape_account(10, 6, shape_bump))],
105
+ &[
106
+ Check::success(),
107
+ Check::account(&shape_key)
108
+ .data(&shape_data(0, 0, shape_bump))
109
+ .build(),
110
+ ],
111
+ );
112
+ }
113
+
114
+ // ── compute_area ──────────────────────────────────────────────────────────────
115
+
116
+ /// Ellipse override: π * w * h / 4 (StdComputeArea accounts, readonly shape).
117
+ #[test]
118
+ fn test_compute_area_uses_ellipse_formula() {
119
+ let mollusk = mollusk();
120
+ let (shape_key, shape_bump) = shape_pda();
121
+
122
+ let result = mollusk.process_and_validate_instruction(
123
+ &Instruction {
124
+ program_id: program_id(),
125
+ accounts: vec![AccountMeta::new_readonly(shape_key, false)],
126
+ data: ix_data(ix::ComputeArea::DISCRIMINATOR, ComputeParams {}),
127
+ },
128
+ &[(shape_key, shape_account(10, 6, shape_bump))],
129
+ &[Check::success()],
130
+ );
131
+
132
+ // 314_159 * 10 * 6 / 400_000 = 47
133
+ assert_eq!(
134
+ u64::from_le_bytes(result.return_data[..8].try_into().unwrap()),
135
+ 47
136
+ );
137
+ }
138
+
139
+ // ── compute_perimeter ─────────────────────────────────────────────────────────
140
+
141
+ /// Ellipse override: π * (w + h) / 2 (custom EllipseAccounts).
142
+ #[test]
143
+ fn test_compute_perimeter_uses_ellipse_approximation() {
144
+ let mollusk = mollusk();
145
+ let (shape_key, shape_bump) = shape_pda();
146
+
147
+ let result = mollusk.process_and_validate_instruction(
148
+ &Instruction {
149
+ program_id: program_id(),
150
+ accounts: vec![AccountMeta::new_readonly(shape_key, false)],
151
+ data: ix_data(ix::ComputePerimeter::DISCRIMINATOR, ComputeParams {}),
152
+ },
153
+ &[(shape_key, shape_account(10, 6, shape_bump))],
154
+ &[Check::success()],
155
+ );
156
+
157
+ // 314_159 * (10 + 6) / 200_000 = 25
158
+ assert_eq!(
159
+ u64::from_le_bytes(result.return_data[..8].try_into().unwrap()),
160
+ 25
161
+ );
162
+ }
163
+
164
+ // ── scale ─────────────────────────────────────────────────────────────────────
165
+
166
+ /// Default scale: both axes scaled equally.
167
+ #[test]
168
+ fn test_scale_default_scales_both_axes() {
169
+ let mollusk = mollusk();
170
+ let (shape_key, shape_bump) = shape_pda();
171
+
172
+ mollusk.process_and_validate_instruction(
173
+ &Instruction {
174
+ program_id: program_id(),
175
+ accounts: vec![AccountMeta::new(shape_key, false)],
176
+ data: ix_data(ix::Scale::DISCRIMINATOR, ScaleParams { factor: 2 }),
177
+ },
178
+ &[(shape_key, shape_account(10, 6, shape_bump))],
179
+ &[
180
+ Check::success(),
181
+ Check::account(&shape_key)
182
+ .data(&shape_data(20, 12, shape_bump))
183
+ .build(),
184
+ ],
185
+ );
186
+ }
187
+
188
+ // ── reset ─────────────────────────────────────────────────────────────────────
189
+
190
+ /// Ellipse override: resets to unit circle (width = height = 10).
191
+ #[test]
192
+ fn test_reset_restores_unit_circle() {
193
+ let mollusk = mollusk();
194
+ let (shape_key, shape_bump) = shape_pda();
195
+
196
+ mollusk.process_and_validate_instruction(
197
+ &Instruction {
198
+ program_id: program_id(),
199
+ accounts: vec![AccountMeta::new(shape_key, false)],
200
+ data: ix::Reset::DISCRIMINATOR.to_vec(),
201
+ },
202
+ &[(shape_key, shape_account(30, 5, shape_bump))],
203
+ &[
204
+ Check::success(),
205
+ Check::account(&shape_key)
206
+ .data(&shape_data(10, 10, shape_bump))
207
+ .build(),
208
+ ],
209
+ );
210
+ }
211
+
212
+ // ── get_dimension_count ───────────────────────────────────────────────────────
213
+
214
+ /// Default: always returns 2 for any 2D shape.
215
+ #[test]
216
+ fn test_get_dimension_count_returns_2() {
217
+ let mollusk = mollusk();
218
+ let (shape_key, shape_bump) = shape_pda();
219
+
220
+ let result = mollusk.process_and_validate_instruction(
221
+ &Instruction {
222
+ program_id: program_id(),
223
+ accounts: vec![AccountMeta::new_readonly(shape_key, false)],
224
+ data: ix::GetDimensionCount::DISCRIMINATOR.to_vec(),
225
+ },
226
+ &[(shape_key, shape_account(10, 6, shape_bump))],
227
+ &[Check::success()],
228
+ );
229
+
230
+ assert_eq!(result.return_data[0], 2u8);
231
+ }
@@ -0,0 +1,23 @@
1
+ //! Mollusk BPF tests for the Ellipse shapes program.
2
+ //!
3
+ //! Positive tests (happy-path):
4
+ //! - `initialize` — creates ShapeState with correct dimensions
5
+ //! - `get_name` — view: returns "Ellipse"
6
+ //! - `get_dimension` — view: returns semi-axis a (index 0) and b (index 1)
7
+ //! - `clear` — zeros both dimensions
8
+ //! - `compute_area` — override: π * w * h / 4 (StdComputeArea accounts)
9
+ //! - `compute_perimeter` — override: π * (w + h) / 2 (custom EllipseAccounts)
10
+ //! - `scale` — default: scales both axes equally
11
+ //! - `reset` — override: restores to unit circle (width = height = 10)
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 = "ellipse/helpers.rs"]
19
+ mod helpers;
20
+ #[path = "ellipse/negative.rs"]
21
+ mod negative;
22
+ #[path = "ellipse/positive.rs"]
23
+ mod positive;
@@ -28,3 +28,10 @@ unexpected_cfgs = { level = "allow", check-cfg = [
28
28
  [dependencies]
29
29
  anchor-lang = { workspace = true }
30
30
  shapes-macros = { workspace = true }
31
+
32
+ [dev-dependencies]
33
+ mollusk-svm = { workspace = true }
34
+ solana-account = { workspace = true }
35
+ solana-instruction = { workspace = true }
36
+ solana-program-error = { workspace = true }
37
+ solana-pubkey = { workspace = true }
@@ -1,7 +1,4 @@
1
- pub mod state;
2
-
3
1
  use anchor_lang::prelude::*;
4
- use state::ShapeState;
5
2
 
6
3
  declare_id!("Tri1111111111111111111111111111111111111111");
7
4
 
@@ -28,6 +25,17 @@ pub struct GetName<'info> {
28
25
  pub shape: Account<'info, ShapeState>,
29
26
  }
30
27
 
28
+ #[derive(Accounts)]
29
+ pub struct GetDimension<'info> {
30
+ pub shape: Account<'info, ShapeState>,
31
+ }
32
+
33
+ #[derive(Accounts)]
34
+ pub struct Clear<'info> {
35
+ #[account(mut)]
36
+ pub shape: Account<'info, ShapeState>,
37
+ }
38
+
31
39
  // --- Param types ---
32
40
 
33
41
  #[derive(Clone, AnchorSerialize, AnchorDeserialize)]
@@ -39,17 +47,30 @@ pub struct InitParams {
39
47
  #[derive(Clone, AnchorSerialize, AnchorDeserialize)]
40
48
  pub struct ComputeParams {}
41
49
 
50
+ #[derive(Clone, AnchorSerialize, AnchorDeserialize)]
51
+ pub struct ScaleParams {
52
+ pub factor: u64,
53
+ }
54
+
55
+ #[derive(Clone, AnchorSerialize, AnchorDeserialize)]
56
+ pub struct DimensionParams {
57
+ pub index: u8,
58
+ }
59
+
42
60
  // --- Program module ---
43
- // Required: initialize, get_name
44
- // Handler Override: compute_area (triangle formula), compute_perimeter (isoceles)
45
- // Default: get_dimension_count
61
+ // ShapeState — generated in prelude by #[shapes]
62
+ //
63
+ // Required: initialize, get_name, get_dimension, clear
64
+ // Override: compute_area (EXTEND — adds verifier signer to default accounts)
65
+ // Override: compute_perimeter (reuses StdComputePerimeter, isoceles triangle)
66
+ // Override: scale (reuses StdScale, isoceles: only scale height)
67
+ // Default: reset, get_dimension_count
46
68
 
47
- #[shapes_macros::shapes(state = ShapeState)]
69
+ #[shapes_macros::shapes]
48
70
  pub mod shapes_triangle {
49
71
 
50
72
  #[shapes_instruction]
51
- #[allow(unused_mut)]
52
- pub fn initialize(mut ctx: Context<Initialize>, params: InitParams) -> Result<()> {
73
+ pub fn initialize(ctx: &mut Context<Initialize>, params: &InitParams) -> Result<()> {
53
74
  ctx.accounts.shape.width = params.width;
54
75
  ctx.accounts.shape.height = params.height;
55
76
  ctx.accounts.shape.bump = ctx.bumps.shape;
@@ -57,27 +78,59 @@ pub mod shapes_triangle {
57
78
  }
58
79
 
59
80
  #[shapes_instruction]
60
- pub fn get_name(_ctx: Context<GetName>) -> Result<String> {
81
+ pub fn get_name(_ctx: &Context<GetName>) -> Result<String> {
61
82
  Ok("Triangle".to_string())
62
83
  }
63
84
 
64
- // HANDLER OVERRIDE — reuses StdComputeArea accounts, triangle formula.
65
85
  #[shapes_instruction]
66
- pub fn compute_area(ctx: Context<StdComputeArea>, _params: ComputeParams) -> Result<u64> {
86
+ pub fn get_dimension(ctx: &Context<GetDimension>, params: &DimensionParams) -> Result<u64> {
87
+ let s = &ctx.accounts.shape;
88
+ match params.index {
89
+ 0 => Ok(s.width),
90
+ 1 => Ok(s.height),
91
+ _ => err!(anchor_lang::error::ErrorCode::InvalidProgramId),
92
+ }
93
+ }
94
+
95
+ #[shapes_instruction]
96
+ pub fn clear(ctx: &mut Context<Clear>) -> Result<()> {
97
+ let s = &mut ctx.accounts.shape;
98
+ s.width = 0;
99
+ s.height = 0;
100
+ Ok(())
101
+ }
102
+
103
+ #[shapes_instruction]
104
+ pub fn compute_area(ctx: &Context<ExtComputeArea>, _params: &ComputeParams) -> Result<u64> {
105
+ msg!("Verified by: {}", ctx.accounts.verifier.key());
67
106
  let s = &ctx.accounts.shape;
68
107
  Ok(s.width * s.height / 2)
69
108
  }
70
109
 
71
- // HANDLER OVERRIDE — reuses StdComputePerimeter accounts, isoceles triangle.
110
+ // OVERRIDE — reuses StdComputePerimeter accounts, isoceles triangle.
72
111
  #[shapes_instruction]
73
112
  pub fn compute_perimeter(
74
- ctx: Context<StdComputePerimeter>,
75
- _params: ComputeParams,
113
+ ctx: &Context<StdComputePerimeter>,
114
+ _params: &ComputeParams,
76
115
  ) -> Result<u64> {
77
116
  let s = &ctx.accounts.shape;
78
- // Isoceles: two equal sides (width) + base (height)
79
117
  Ok(s.width + s.width + s.height)
80
118
  }
81
119
 
120
+ // OVERRIDE — reuses StdScale accounts, isoceles: only scale height (base stays fixed).
121
+ #[shapes_instruction]
122
+ pub fn scale(ctx: &mut Context<StdScale>, params: &ScaleParams) -> Result<()> {
123
+ ctx.accounts.shape.height = ctx.accounts.shape.height.saturating_mul(params.factor);
124
+ Ok(())
125
+ }
126
+
82
127
  // DEFAULT — get_dimension_count returns 2, correct for any 2D shape.
128
+ // DEFAULT — reset sets width=1, height=1.
83
129
  }
130
+
131
+ // EXTEND — extend StdComputeArea with a verifier signer, inside the module
132
+ // so ExtComputeArea can reference ShapeState from the prelude.
133
+ shapes_extend_accounts!(compute_area, ExtComputeArea, {
134
+ #[account(mut)]
135
+ pub verifier: Signer<'info>,
136
+ });