Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ node_modules/

/target
deploy
.claude
4 changes: 2 additions & 2 deletions basics/account-data/quasar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ client = []
debug = []

[dependencies]
quasar-lang = "0.0"
quasar-lang = { git = "https://github.com/blueshift-gg/quasar" }
solana-instruction = { version = "3.2.0" }

[dev-dependencies]
# Not using the generated client: it depends on quasar_lang::client::DynBytes
# which isn't in the published crate yet. Tests build instruction data manually.
quasar-svm = { version = "0.1" }
quasar-svm = { git = "https://github.com/blueshift-gg/quasar-svm" }
solana-account = { version = "3.4.0" }
solana-address = { version = "2.2.0", features = ["decode"] }
solana-instruction = { version = "3.2.0", features = ["bincode"] }
Expand Down
28 changes: 13 additions & 15 deletions basics/account-data/quasar/src/instructions/create.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,31 @@
use {
crate::state::AddressInfo,
quasar_lang::prelude::*,
crate::state::{AddressInfo, AddressInfoInner},
quasar_lang::{prelude::*, sysvars::Sysvar},
};

/// Accounts for creating a new address info account.
/// Dynamic accounts use owned `Account<T>` rather than `&'info mut Account<T>` because
/// dynamic types carry cached byte offsets that cannot be represented as a pointer cast.
#[derive(Accounts)]
pub struct CreateAddressInfo<'info> {
pub struct CreateAddressInfo {
#[account(mut)]
pub payer: &'info mut Signer,
#[account(mut, init, payer = payer, seeds = [b"address_info", payer], bump)]
pub address_info: Account<AddressInfo<'info>>,
pub system_program: &'info Program<System>,
pub payer: Signer,
#[account(mut, init, payer = payer, seeds = AddressInfo::seeds(payer), bump)]
pub address_info: Account<AddressInfo>,
pub system_program: Program<System>,
}

#[inline(always)]
pub fn handle_create_address_info(
accounts: &mut CreateAddressInfo, name: &str,
accounts: &mut CreateAddressInfo,
name: &str,
house_number: u8,
street: &str,
city: &str,
) -> Result<(), ProgramError> {
let rent = Rent::get()?;
accounts.address_info.set_inner(
house_number,
name,
street,
city,
AddressInfoInner { house_number, name, street, city },
accounts.payer.to_account_view(),
None,
rent.lamports_per_byte(),
rent.exemption_threshold_raw(),
)
}
6 changes: 3 additions & 3 deletions basics/account-data/quasar/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ mod quasar_account_data {
#[instruction(discriminator = 0)]
pub fn create_address_info(
ctx: Ctx<CreateAddressInfo>,
name: String,
house_number: u8,
street: String,
city: String,
name: String<50>,
street: String<50>,
city: String<50>,
) -> Result<(), ProgramError> {
instructions::handle_create_address_info(&mut ctx.accounts, name, house_number, street, city)
}
Expand Down
14 changes: 6 additions & 8 deletions basics/account-data/quasar/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
use quasar_lang::prelude::*;

/// Onchain address info account with dynamic string fields.
/// Uses Quasar's `String<P, N>` marker type for variable-length string data.
/// The lifetime `'a` is required because the generated code produces `&'a str` accessors.
///
/// Note: Quasar requires all fixed-size fields to precede dynamic (String/Vec) fields.
#[account(discriminator = 1)]
pub struct AddressInfo<'a> {
#[account(discriminator = 1, set_inner)]
#[seeds(b"address_info", payer: Address)]
pub struct AddressInfo {
pub house_number: u8,
pub name: String<u8, 50>,
pub street: String<u8, 50>,
pub city: String<u8, 50>,
pub name: String<50>,
pub street: String<50>,
pub city: String<50>,
}
71 changes: 31 additions & 40 deletions basics/account-data/quasar/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,28 @@ fn empty(address: Pubkey) -> Account {
}
}

/// Build create_address_info instruction data manually.
/// Build the create_address_info instruction data using Quasar's compact
/// wire format: a header containing all fixed fields and length prefixes,
/// followed by a tail with all dynamic byte payloads grouped together.
///
/// Wire format (from reading the #[instruction] codegen):
/// [disc: 1 byte]
/// [ZC struct: house_number u8]
/// [name: u32 LE length prefix + bytes] (String → DynKind::Str with U32 prefix)
/// [street: u32 LE length prefix + bytes]
/// [city: u32 LE length prefix + bytes]
/// Layout:
/// header: [disc: u8 = 0][house_number: u8][name_len: u8][street_len: u8][city_len: u8]
/// tail: [name bytes][street bytes][city bytes]
///
/// `String<50>` defaults to a u8 length prefix because MAX (50) fits in a byte.
fn build_create_instruction_data(name: &str, house_number: u8, street: &str, city: &str) -> Vec<u8> {
let mut data = vec![0u8]; // discriminator = 0
let mut data = Vec::with_capacity(5 + name.len() + street.len() + city.len());

// Fixed ZC struct: house_number
// Header
data.push(0u8); // instruction discriminator
data.push(house_number);
data.push(name.len() as u8);
data.push(street.len() as u8);
data.push(city.len() as u8);

// Dynamic String args with u32 length prefix
data.extend_from_slice(&(name.len() as u32).to_le_bytes());
// Tail
data.extend_from_slice(name.as_bytes());

data.extend_from_slice(&(street.len() as u32).to_le_bytes());
data.extend_from_slice(street.as_bytes());

data.extend_from_slice(&(city.len() as u32).to_le_bytes());
data.extend_from_slice(city.as_bytes());

data
Expand Down Expand Up @@ -81,34 +81,25 @@ fn test_create_address_info() {
// Verify the account data.
let account = result.account(&address_info).unwrap();

// Onchain layout (from #[account] dynamic codegen):
// [disc: 1 byte = 1]
// [ZC header: house_number u8]
// [name: u8 prefix + bytes] (String<u8, 50> uses u8 prefix)
// [street: u8 prefix + bytes]
// [city: u8 prefix + bytes]
// Onchain layout for a Quasar `#[account]` with dynamic fields uses the
// compact "header then tail" format. Length prefixes are grouped in the
// header, the actual bytes follow in the tail.
// header: [disc: 1][house_number: u8][name_len: u8][street_len: u8][city_len: u8]
// tail: [name bytes][street bytes][city bytes]
// String<50> defaults to a u8 length prefix because MAX (50) fits in a byte.
assert_eq!(account.data[0], 1, "discriminator");
assert_eq!(account.data[1], 42, "house_number");

let mut offset = 2;

// name: u8 prefix + "Alice"
let name_len = account.data[offset] as usize;
offset += 1;
let name_len = account.data[2] as usize;
let street_len = account.data[3] as usize;
let city_len = account.data[4] as usize;
assert_eq!(name_len, 5);
assert_eq!(&account.data[offset..offset + name_len], b"Alice");
offset += name_len;

// street: u8 prefix + "Main Street"
let street_len = account.data[offset] as usize;
offset += 1;
assert_eq!(street_len, 11);
assert_eq!(&account.data[offset..offset + street_len], b"Main Street");
offset += street_len;

// city: u8 prefix + "New York"
let city_len = account.data[offset] as usize;
offset += 1;
assert_eq!(city_len, 8);
assert_eq!(&account.data[offset..offset + city_len], b"New York");

let header_end = 5;
assert_eq!(&account.data[header_end..header_end + name_len], b"Alice");
let street_start = header_end + name_len;
assert_eq!(&account.data[street_start..street_start + street_len], b"Main Street");
let city_start = street_start + street_len;
assert_eq!(&account.data[city_start..city_start + city_len], b"New York");
}
4 changes: 2 additions & 2 deletions basics/checking-accounts/quasar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ client = []
debug = []

[dependencies]
quasar-lang = "0.0"
quasar-lang = { git = "https://github.com/blueshift-gg/quasar" }
solana-instruction = { version = "3.2.0" }

[dev-dependencies]
quasar-checking-accounts-client = { path = "target/client/rust/quasar-checking-accounts-client" }
quasar-svm = { version = "0.1" }
quasar-svm = { git = "https://github.com/blueshift-gg/quasar-svm" }
solana-account = { version = "3.4.0" }
solana-address = { version = "2.2.0", features = ["decode"] }
solana-instruction = { version = "3.2.0", features = ["bincode"] }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,21 @@ use quasar_lang::prelude::*;
/// Note: Anchor's `#[account(owner = id())]` owner constraint is not directly available
/// in Quasar. Owner checks can be done manually in the instruction body if needed.
#[derive(Accounts)]
pub struct CheckAccounts<'info> {
pub struct CheckAccounts {
/// Checks that this account signed the transaction.
pub payer: &'info Signer,
pub payer: Signer,
/// No checks performed — the caller is responsible for validation.
#[account(mut)]
pub account_to_create: &'info mut UncheckedAccount,
pub account_to_create: UncheckedAccount,
/// No automatic owner check in Quasar; see note above.
#[account(mut)]
pub account_to_change: &'info mut UncheckedAccount,
pub account_to_change: UncheckedAccount,
/// Checks the account is executable and matches the system program address.
pub system_program: &'info Program<System>,
pub system_program: Program<System>,
}

#[inline(always)]
pub fn handle_check_accounts(accounts: &CheckAccounts) -> Result<(), ProgramError> {
pub fn handle_check_accounts(_accounts: &mut CheckAccounts) -> Result<(), ProgramError> {
// All validation happens declaratively via the account types above.
// If any check fails, the runtime rejects the transaction before this runs.
Ok(())
Expand Down
4 changes: 2 additions & 2 deletions basics/close-account/quasar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ client = []
debug = []

[dependencies]
quasar-lang = "0.0"
quasar-lang = { git = "https://github.com/blueshift-gg/quasar" }
solana-instruction = { version = "3.2.0" }

[dev-dependencies]
quasar-svm = { version = "0.1" }
quasar-svm = { git = "https://github.com/blueshift-gg/quasar-svm" }
solana-account = { version = "3.4.0" }
solana-address = { version = "2.2.0", features = ["decode"] }
solana-instruction = { version = "3.2.0", features = ["bincode"] }
Expand Down
11 changes: 4 additions & 7 deletions basics/close-account/quasar/src/instructions/close_user.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
use {
crate::state::UserState,
quasar_lang::prelude::*,
};
use {crate::state::UserState, quasar_lang::prelude::*};

/// Accounts for closing a user account.
/// The `close = user` attribute in the Anchor version triggers an automatic epilogue.
/// In Quasar, we call `close()` explicitly — it zeros the discriminator, drains lamports
/// to the destination, reassigns the owner to the system program, and resizes to 0.
#[derive(Accounts)]
pub struct CloseUser<'info> {
pub struct CloseUser {
#[account(mut)]
pub user: &'info mut Signer,
pub user: Signer,
#[account(mut)]
pub user_account: Account<UserState<'info>>,
pub user_account: Account<UserState>,
}

#[inline(always)]
Expand Down
28 changes: 16 additions & 12 deletions basics/close-account/quasar/src/instructions/create_user.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
use {
crate::state::UserState,
quasar_lang::prelude::*,
crate::state::{UserState, UserStateInner},
quasar_lang::{prelude::*, sysvars::Sysvar},
};

/// Accounts for creating a new user.
#[derive(Accounts)]
pub struct CreateUser<'info> {
pub struct CreateUser {
#[account(mut)]
pub user: &'info mut Signer,
#[account(mut, init, payer = user, seeds = [b"USER", user], bump)]
pub user_account: Account<UserState<'info>>,
pub system_program: &'info Program<System>,
pub user: Signer,
#[account(mut, init, payer = user, seeds = UserState::seeds(user), bump)]
pub user_account: Account<UserState>,
pub system_program: Program<System>,
}

#[inline(always)]
pub fn handle_create_user(accounts: &mut CreateUser, name: &str, bump: u8) -> Result<(), ProgramError> {
pub fn handle_create_user(
accounts: &mut CreateUser,
name: &str,
bump: u8,
) -> Result<(), ProgramError> {
let user_address = *accounts.user.to_account_view().address();
let rent = Rent::get()?;
accounts.user_account.set_inner(
bump,
user_address,
name,
UserStateInner { bump, user: user_address, name },
accounts.user.to_account_view(),
None,
rent.lamports_per_byte(),
rent.exemption_threshold_raw(),
)
}
2 changes: 1 addition & 1 deletion basics/close-account/quasar/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ mod quasar_close_account {

/// Create a user account with a name.
#[instruction(discriminator = 0)]
pub fn create_user(ctx: Ctx<CreateUser>, name: String) -> Result<(), ProgramError> {
pub fn create_user(ctx: Ctx<CreateUser>, name: String<50>) -> Result<(), ProgramError> {
let bump = ctx.bumps.user_account;
instructions::handle_create_user(&mut ctx.accounts, name, bump)
}
Expand Down
7 changes: 4 additions & 3 deletions basics/close-account/quasar/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ use quasar_lang::prelude::*;

/// User account with a dynamic name field.
/// Fixed fields (bump, user) must precede dynamic fields (name).
#[account(discriminator = 1)]
pub struct UserState<'a> {
#[account(discriminator = 1, set_inner)]
#[seeds(b"USER", user: Address)]
pub struct UserState {
pub bump: u8,
pub user: Address,
pub name: String<u8, 50>,
pub name: String<50>,
}
12 changes: 8 additions & 4 deletions basics/close-account/quasar/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@ fn empty(address: Pubkey) -> Account {
}
}

/// Build create_user instruction data.
/// Wire format: [disc=0] [name: u32 prefix + bytes]
/// Build create_user instruction data using Quasar's compact wire format
/// (header then tail). `String<50>` defaults to a u8 length prefix.
///
/// header: [disc: u8 = 0][name_len: u8]
/// tail: [name bytes]
fn build_create_instruction(name: &str) -> Vec<u8> {
let mut data = vec![0u8]; // discriminator = 0
data.extend_from_slice(&(name.len() as u32).to_le_bytes());
let mut data = Vec::with_capacity(2 + name.len());
data.push(0u8); // discriminator
data.push(name.len() as u8);
data.extend_from_slice(name.as_bytes());
data
}
Expand Down
4 changes: 2 additions & 2 deletions basics/counter/quasar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ client = []
debug = []

[dependencies]
quasar-lang = "0.0"
quasar-lang = { git = "https://github.com/blueshift-gg/quasar" }
solana-instruction = { version = "3.2.0" }

[dev-dependencies]
quasar-counter-client = { path = "target/client/rust/quasar-counter-client" }
quasar-svm = { version = "0.1" }
quasar-svm = { git = "https://github.com/blueshift-gg/quasar-svm" }
solana-account = { version = "3.4.0" }
solana-address = { version = "2.2.0", features = ["decode"] }
solana-instruction = { version = "3.2.0", features = ["bincode"] }
Expand Down
9 changes: 3 additions & 6 deletions basics/counter/quasar/src/instructions/increment.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
use {
crate::state::Counter,
quasar_lang::prelude::*,
};
use {crate::state::Counter, quasar_lang::prelude::*};

/// Accounts for incrementing a counter.
#[derive(Accounts)]
pub struct Increment<'info> {
pub struct Increment {
#[account(mut)]
pub counter: &'info mut Account<Counter>,
pub counter: Account<Counter>,
}

#[inline(always)]
Expand Down
Loading
Loading