@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.
- package/Cargo.toml +3 -0
- package/README.md +316 -0
- package/examples/shapes/Cargo.lock +4126 -0
- package/examples/shapes/Cargo.toml +16 -0
- package/examples/shapes/macros/src/audit.rs +15 -0
- package/examples/shapes/macros/src/instructions/clear.rs +19 -0
- package/examples/shapes/macros/src/instructions/compute_area.rs +18 -20
- package/examples/shapes/macros/src/instructions/compute_perimeter.rs +18 -20
- package/examples/shapes/macros/src/instructions/get_dimension.rs +19 -0
- package/examples/shapes/macros/src/instructions/get_dimension_count.rs +19 -17
- package/examples/shapes/macros/src/instructions/mod.rs +8 -0
- package/examples/shapes/macros/src/instructions/reset.rs +50 -0
- package/examples/shapes/macros/src/instructions/scale.rs +52 -0
- package/examples/shapes/macros/src/lib.rs +14 -11
- package/examples/shapes/macros/src/shapes.rs +21 -26
- package/examples/shapes/macros/src/state_types.rs +14 -0
- package/examples/shapes/programs/ellipse/Cargo.toml +7 -0
- package/examples/shapes/programs/ellipse/src/lib.rs +69 -17
- package/examples/shapes/programs/ellipse/tests/ellipse/helpers.rs +110 -0
- package/examples/shapes/programs/ellipse/tests/ellipse/negative.rs +49 -0
- package/examples/shapes/programs/ellipse/tests/ellipse/positive.rs +231 -0
- package/examples/shapes/programs/ellipse/tests/ellipse.rs +23 -0
- package/examples/shapes/programs/triangle/Cargo.toml +7 -0
- package/examples/shapes/programs/triangle/src/lib.rs +69 -16
- package/examples/shapes/programs/triangle/tests/triangle/helpers.rs +123 -0
- package/examples/shapes/programs/triangle/tests/triangle/negative.rs +49 -0
- package/examples/shapes/programs/triangle/tests/triangle/positive.rs +225 -0
- package/examples/shapes/programs/triangle/tests/triangle.rs +23 -0
- package/examples/shapes/target/deploy/shapes_ellipse-keypair.json +1 -0
- package/examples/shapes/target/deploy/shapes_ellipse.so +0 -0
- package/examples/shapes/target/deploy/shapes_triangle-keypair.json +1 -0
- package/examples/shapes/target/deploy/shapes_triangle.so +0 -0
- package/package.json +20 -9
- package/src/assembly.rs +41 -6
- package/src/generation.rs +108 -131
- package/src/lib.rs +6 -4
- package/src/overrides.rs +56 -81
- package/tests/assembly.rs +154 -23
- package/tests/common/mod.rs +68 -14
- package/tests/generation.rs +137 -65
- package/tests/override.rs +93 -55
- package/tests/snapshots/snapshots__tests__snapshot_all_defaults.snap +32 -5
- package/tests/snapshots/snapshots__tests__snapshot_full_override.snap +35 -7
- package/tests/snapshots/snapshots__tests__snapshot_handler_override.snap +37 -7
- package/tests/snapshots/snapshots__tests__snapshot_mixed_overrides.snap +40 -9
- package/tests/snapshots/snapshots__tests__snapshot_with_extend_macro.snap +82 -0
- package/tests/snapshots.rs +51 -18
- package/examples/shapes/Anchor.toml +0 -19
- package/examples/shapes/programs/ellipse/src/state.rs +0 -9
- 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
|
-
///
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
119
|
+
// OVERRIDE — custom EllipseAccounts, ellipse perimeter approximation.
|
|
80
120
|
#[shapes_instruction]
|
|
81
|
-
pub fn compute_perimeter(
|
|
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
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
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
|
|
69
|
+
#[shapes_macros::shapes]
|
|
48
70
|
pub mod shapes_triangle {
|
|
49
71
|
|
|
50
72
|
#[shapes_instruction]
|
|
51
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
+
});
|