Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
450e1e3
initial commit for testing cli
kaze-cow Jun 3, 2026
086c197
fix edge case where placing another order with the account still dele…
kaze-cow Jun 3, 2026
27a0829
fixes from my personal review
kaze-cow Jun 3, 2026
18c63a0
fix lints and fmt
kaze-cow Jun 3, 2026
1bccfba
format one more time
kaze-cow Jun 3, 2026
5ffb7c0
Merge branch 'main' into test-cli
kaze-cow Jun 10, 2026
cba3bc5
fix lockfile conflict
kaze-cow Jun 10, 2026
d59e1d5
remove all token 2022 references
kaze-cow Jun 11, 2026
35c3653
Merge remote-tracking branch 'origin/main' into test-cli
kaze-cow Jun 11, 2026
03251db
update command flow
kaze-cow Jun 11, 2026
91dd6eb
verify ata ownership if manually specified
kaze-cow Jun 11, 2026
39d0cb4
minor refactor, simplification of some of the code, and structure update
kaze-cow Jun 11, 2026
b6233cc
fmt
kaze-cow Jun 11, 2026
b7d25d7
Merge branch 'main' into test-cli
kaze-cow Jun 11, 2026
11934e5
Merge branch 'main' into test-cli
kaze-cow Jun 30, 2026
341852e
fix deps and one compile error
kaze-cow Jun 30, 2026
76f40cb
update to be named test-cli for help
kaze-cow Jun 30, 2026
12f08b8
Update test-cli/src/token.rs
kaze-cow Jul 1, 2026
f3040a4
Merge branch 'main' into test-cli
kaze-cow Jul 1, 2026
2f1605f
Update test-cli/src/token.rs
kaze-cow Jul 1, 2026
91071b6
Update test-cli/src/cmd/mod.rs
kaze-cow Jul 1, 2026
d47b4dc
remove close authority handling; improve decimal handling to not use …
kaze-cow Jul 1, 2026
a0f815c
fix compilation errors, program id, and genesis hash matching for reg…
kaze-cow Jul 1, 2026
b2a5867
update comment to clarify
kaze-cow Jul 1, 2026
b42debe
refactor into branching match structure
kaze-cow Jul 1, 2026
157eb76
should error if not a SPL token account
kaze-cow Jul 1, 2026
7bd9323
use more ergonomic constructor for Context
kaze-cow Jul 1, 2026
4cebdd5
lint fix & remove tilde handling/expansion from the cli
kaze-cow Jul 1, 2026
11cf098
cli: build a single RpcClient in Context instead of one per call
kaze-cow Jul 1, 2026
6353c98
cli: load the payer keypair once in Context instead of per call
kaze-cow Jul 1, 2026
8105724
cli: derive the settlement PDA via find_state_pda helper
kaze-cow Jul 1, 2026
2fe8786
cli: make sell/buy token account default docs consistent
kaze-cow Jul 1, 2026
d240de9
cli: clarify that sell/buy only creates the order, not the swap
kaze-cow Jul 1, 2026
7d933fc
cli: rename tokens field to terms and drop dangling doc reference
kaze-cow Jul 1, 2026
b469ef7
cli: fix long_about not rendering for sell/buy --help
kaze-cow Jul 1, 2026
91a05e6
cli: replace ParsedSyntax tuple with a named struct
kaze-cow Jul 1, 2026
666490d
restructure create_order for clarity
kaze-cow Jul 3, 2026
011f13c
fix unusual use of all_ixs when ixs vec already exists
kaze-cow Jul 3, 2026
f49e0fe
Encode account and instruction integers as little-endian
kaze-cow Jul 8, 2026
ce23f0c
Add single-byte account discriminators via a SettlementAccount enum
kaze-cow Jul 8, 2026
0e74418
fmt
kaze-cow Jul 8, 2026
c181269
Move the IDL to its own PR
kaze-cow Jul 8, 2026
f99f7c7
Add hand-written Anchor-compatible IDL
kaze-cow Jul 8, 2026
bec0e15
Merge branch 'test-cli' into idl
kaze-cow Jul 9, 2026
2265a60
fixes from testing of idl generation
kaze-cow Jul 14, 2026
a530b4a
update idl
kaze-cow Jul 16, 2026
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
2,447 changes: 2,153 additions & 294 deletions Cargo.lock

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["client", "interface", "programs/settlement", "programs/test/cpi-caller"]
members = ["test-cli", "client", "interface", "programs/settlement", "programs/test/cpi-caller"]

[workspace.package]
version = "0.1.0"
Expand All @@ -11,7 +11,12 @@ edition = "2021"
license = "MIT OR Apache-2.0"

[workspace.dependencies]
anyhow = "1"
arrayref = "0.3"
clap = { version = "4", features = ["derive", "env"] }
solana-commitment-config = "3"
solana-program-pack = "3"
solana-rpc-client = "3"
derive_more = { version = "1", features = ["deref"] }
hex-literal = "1"
litesvm = "0.12.0"
Expand All @@ -34,6 +39,8 @@ solana-sdk = "3"
solana-sdk-ids = "3"
solana-sha256-hasher = { version = "3", features = ["sha2"] }
solana-system-interface = "3"
spl-associated-token-account-interface = { version = "2" }
spl-token = "9"
spl-token-interface = "2"

[workspace.metadata.cli]
Expand Down
28 changes: 14 additions & 14 deletions interface/src/data/intent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ pub struct OrderIntent {
///
/// Layout: one character per byte, cell widths proportional to field size,
/// each divider belongs to the cell on its right. The byte range is
/// annotated below. Amounts and `valid_to` are big-endian encoded.
/// annotated below. Amounts and `valid_to` are little-endian encoded.
///
/// ```text
/// partially_fillable ─────┐
Expand Down Expand Up @@ -178,9 +178,9 @@ impl From<&OrderIntent> for EncodedOrderIntent {
*owner = intent.owner.to_bytes();
*buy_token = intent.buy_token_account.to_bytes();
*sell_token = intent.sell_token_account.to_bytes();
*sell_amount = intent.sell_amount.to_be_bytes();
*buy_amount = intent.buy_amount.to_be_bytes();
*valid_to = intent.valid_to.to_be_bytes();
*sell_amount = intent.sell_amount.to_le_bytes();
*buy_amount = intent.buy_amount.to_le_bytes();
*valid_to = intent.valid_to.to_le_bytes();
*kind = [intent.kind as u8];
*partially_fillable = [intent.partially_fillable as u8];
*app_data = intent.app_data;
Expand Down Expand Up @@ -226,9 +226,9 @@ impl TryFrom<&[u8; EncodedOrderIntent::SIZE]> for OrderIntent {
owner: Pubkey::new_from_array(*owner),
buy_token_account: Pubkey::new_from_array(*buy_token),
sell_token_account: Pubkey::new_from_array(*sell_token),
sell_amount: u64::from_be_bytes(*sell_amount),
buy_amount: u64::from_be_bytes(*buy_amount),
valid_to: u32::from_be_bytes(*valid_to),
sell_amount: u64::from_le_bytes(*sell_amount),
buy_amount: u64::from_le_bytes(*buy_amount),
valid_to: u32::from_le_bytes(*valid_to),
kind: match kind {
[0] => OrderKind::Sell,
[1] => OrderKind::Buy,
Expand Down Expand Up @@ -457,7 +457,7 @@ mod tests {
#[test]
fn uid_digest_regression() {
let intent = sample_intent(OrderKind::Buy, true);
let expected = hex!("091d7e1959ac6f7a400a91f1dcd9ce436f8f53e2b7a1d968acb08f79d3c1231d");
let expected = hex!("7ce7c6a74671090771fa33851387444064aca759ce55b80708723076722f5e00");
assert_eq!(intent.uid(), Hash::from(expected));
}

Expand All @@ -482,12 +482,12 @@ mod tests {
0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
// sell_amount (0x0123_4567_89ab_cdef, BE u64)
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
// buy_amount (0xfedc_ba98_7654_3210, BE u64)
0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10,
// valid_to (0xdead_beef, BE u32)
0xde, 0xad, 0xbe, 0xef,
// sell_amount (0x0123_4567_89ab_cdef, LE u64)
0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01,
// buy_amount (0xfedc_ba98_7654_3210, LE u64)
0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe,
// valid_to (0xdead_beef, LE u32)
0xef, 0xbe, 0xad, 0xde,
// kind (Buy = 1)
0x01,
// partially_fillable (true = 1)
Expand Down
1 change: 1 addition & 0 deletions interface/src/data/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@

pub mod intent;
pub mod order;
pub mod state;
68 changes: 51 additions & 17 deletions interface/src/data/order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,40 +51,49 @@ pub struct OrderAccount {
pub intent: OrderIntent,
}

/// Canonical 199-byte representation of an [`OrderAccount`]. The bytes
/// Canonical 200-byte representation of an [`OrderAccount`]. The bytes
/// written to/read from the order PDA's data area.
///
/// Layout: one character per byte, cell widths proportional to field size,
/// each divider belongs to the cell on its right. Integers are big-endian.
/// The intent slot holds a verbatim [`EncodedOrderIntent`]; see that
/// type's docs for its inner layout.
/// each divider belongs to the cell on its right. Integers are little-endian
/// (Anchor/Borsh convention). The intent slot holds a verbatim
/// [`EncodedOrderIntent`]; see that type's docs for its inner layout.
///
/// ```text
/// ┌──── cancelled
/// ┌──── discriminator
/// │┌─── cancelled
/// ┌┬───────┬───────┬───────────────────────────────┬─────────────────...─────────────────┐
/// ││amount_│amount_│ │ │
/// ││with- │re- │ created_by │ intent (EncodedOrderIntent) │
/// ││drawn │ceived │ │ │
/// └┴───────┴───────┴───────────────────────────────┴─────────────────...─────────────────┘
/// 0 1 9 17 49 ... 199
/// 0 1 2 10 18 ... 200
/// ```
///
/// The first byte is an IDL-style account discriminator (see
/// [`EncodedOrderAccount::DISCRIMINATOR`]), letting IDL-driven tooling (e.g.
/// Solscan) identify the account type before decoding the rest.
#[derive(Clone, Debug, Deref, Eq, PartialEq)]
pub struct EncodedOrderAccount([u8; Self::SIZE]);

impl EncodedOrderAccount {
// Per-field widths, derived from the `OrderAccount` field types.
const W_DISCRIMINATOR: usize = 1;
const W_CANCELLED: usize = size_of::<bool>();
const W_AMOUNT_WITHDRAWN: usize = size_of::<u64>();
const W_AMOUNT_RECEIVED: usize = size_of::<u64>();
const W_CREATED_BY: usize = size_of::<Pubkey>();
const W_INTENT: usize = EncodedOrderIntent::SIZE;

pub const SIZE: usize = 199;
pub const SIZE: usize = 200;

/// Single-byte account discriminator. See [`crate::SettlementAccount`].
pub const DISCRIMINATOR: [u8; 1] = [crate::SettlementAccount::OrderAccount.discriminator()];

/// Decode the account body and compute the embedded intent's UID in one
/// shot, mirroring [`EncodedOrderIntent::decode_and_hash`]. Decoding
/// validates the intent; returns [`ProgramError::InvalidAccountData`] on a
/// decode error.
/// validates the discriminator and the intent; returns
/// [`ProgramError::InvalidAccountData`] on a decode error.
pub fn decode_and_hash(bytes: &[u8; Self::SIZE]) -> Result<(OrderAccount, Hash), ProgramError> {
let order_account = OrderAccount::try_from(*bytes)?;
// The order UID is the hash of the intent's canonical bytes. Decoding
Expand All @@ -111,17 +120,26 @@ pub fn write_account(
created_by: &Pubkey,
encoded_intent: &[u8; EncodedOrderIntent::SIZE],
) {
let (cancelled_slot, amount_withdrawn_slot, amount_received_slot, created_by_slot, intent_slot) = mut_array_refs![
let (
discriminator_slot,
cancelled_slot,
amount_withdrawn_slot,
amount_received_slot,
created_by_slot,
intent_slot,
) = mut_array_refs![
buffer,
EncodedOrderAccount::W_DISCRIMINATOR,
EncodedOrderAccount::W_CANCELLED,
EncodedOrderAccount::W_AMOUNT_WITHDRAWN,
EncodedOrderAccount::W_AMOUNT_RECEIVED,
EncodedOrderAccount::W_CREATED_BY,
EncodedOrderAccount::W_INTENT
];
*discriminator_slot = EncodedOrderAccount::DISCRIMINATOR;
*cancelled_slot = [cancelled as u8];
*amount_withdrawn_slot = amount_withdrawn.to_be_bytes();
*amount_received_slot = amount_received.to_be_bytes();
*amount_withdrawn_slot = amount_withdrawn.to_le_bytes();
*amount_received_slot = amount_received.to_le_bytes();
*created_by_slot = created_by.to_bytes();
*intent_slot = *encoded_intent;
}
Expand Down Expand Up @@ -151,23 +169,28 @@ impl TryFrom<[u8; EncodedOrderAccount::SIZE]> for OrderAccount {
type Error = ProgramError;

fn try_from(bytes: [u8; EncodedOrderAccount::SIZE]) -> Result<Self, Self::Error> {
let (cancelled, amount_withdrawn, amount_received, created_by, intent) = array_refs![
let (discriminator, cancelled, amount_withdrawn, amount_received, created_by, intent) = array_refs![
&bytes,
EncodedOrderAccount::W_DISCRIMINATOR,
EncodedOrderAccount::W_CANCELLED,
EncodedOrderAccount::W_AMOUNT_WITHDRAWN,
EncodedOrderAccount::W_AMOUNT_RECEIVED,
EncodedOrderAccount::W_CREATED_BY,
EncodedOrderAccount::W_INTENT
];

if *discriminator != EncodedOrderAccount::DISCRIMINATOR {
return Err(ProgramError::InvalidAccountData);
}

Ok(OrderAccount {
cancelled: match cancelled {
[0] => false,
[1] => true,
_ => return Err(ProgramError::InvalidAccountData),
},
amount_withdrawn: u64::from_be_bytes(*amount_withdrawn),
amount_received: u64::from_be_bytes(*amount_received),
amount_withdrawn: u64::from_le_bytes(*amount_withdrawn),
amount_received: u64::from_le_bytes(*amount_received),
created_by: Pubkey::new_from_array(*created_by),
intent: OrderIntent::try_from(intent).map_err(|_| ProgramError::InvalidAccountData)?,
})
Expand All @@ -193,8 +216,8 @@ pub mod fixtures {
};

// Hardcoded but verified in a sanity-check test.
pub const CANCELLED_OFFSET: usize = 0;
pub const INTENT_OFFSET: usize = 49;
pub const CANCELLED_OFFSET: usize = 1;
pub const INTENT_OFFSET: usize = 50;

/// Hand-picked example order account wrapping [`sample_intent`].
pub fn sample_account(cancelled: bool) -> OrderAccount {
Expand Down Expand Up @@ -321,6 +344,15 @@ mod tests {
);
}

#[test]
fn decode_rejects_wrong_discriminator() {
let mut bytes: [u8; EncodedOrderAccount::SIZE] =
EncodedOrderAccount::from(sample_account(false)).into();
bytes[0] ^= 0xff;
let err = OrderAccount::try_from(bytes).expect_err("wrong discriminator must be rejected");
assert_eq!(err, ProgramError::InvalidAccountData);
}

#[test]
fn decode_rejects_non_boolean_cancelled() {
let mut bytes: [u8; EncodedOrderAccount::SIZE] =
Expand Down Expand Up @@ -416,6 +448,8 @@ mod tests {
kind in arb_order_kind(),
partially_fillable in any::<bool>(),
) {
bytes[..EncodedOrderAccount::DISCRIMINATOR.len()]
.copy_from_slice(&EncodedOrderAccount::DISCRIMINATOR);
bytes[CANCELLED_OFFSET] = cancelled as u8;
bytes[INTENT_OFFSET + KIND_OFFSET] = kind as u8;
bytes[INTENT_OFFSET + PARTIALLY_FILLABLE_OFFSET] = partially_fillable as u8;
Expand Down
40 changes: 40 additions & 0 deletions interface/src/data/state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//! Settlement state PDA body.
//!
//! The state PDA carries no fields of its own (see [`crate::pda::state`]); its
//! only content is the account discriminator, so that IDL-driven tooling can
//! identify the account type.

use solana_program_error::ProgramError;

use crate::SettlementAccount;

/// Canonical size of the settlement state PDA's body: just the discriminator.
pub const SIZE: usize = 1;

/// Single-byte account discriminator. See [`crate::SettlementAccount`].
pub const DISCRIMINATOR: [u8; SIZE] = [SettlementAccount::SettlementState.discriminator()];

/// Validate that `bytes` carries the expected discriminator.
pub fn decode(bytes: &[u8; SIZE]) -> Result<(), ProgramError> {
if *bytes != DISCRIMINATOR {
return Err(ProgramError::InvalidAccountData);
}
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn decode_accepts_discriminator() {
assert_eq!(decode(&DISCRIMINATOR), Ok(()));
}

#[test]
fn decode_rejects_wrong_discriminator() {
let mut bytes = DISCRIMINATOR;
bytes[0] ^= 0xff;
assert_eq!(decode(&bytes), Err(ProgramError::InvalidAccountData));
}
}
2 changes: 1 addition & 1 deletion interface/src/instruction/initialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ impl From<Initialize> for Instruction {
/// Parsed inputs of an `Initialize` instruction.
pub struct InitializeInput<'a> {
pub payer: &'a AccountView,
pub state_pda: &'a AccountView,
pub state_pda: &'a mut AccountView,
}

impl<'a> InstructionInputParsing<'a> for InitializeInput<'a> {
Expand Down
18 changes: 9 additions & 9 deletions interface/src/instruction/settle/begin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub struct Pull {
/// the builder.
///
/// Wire format (grouped, with `n` orders and `T` total transfers):
/// `[discriminator=0][finalize_ix_index: u16 BE][n: u8][bump×n][transfer_count×n]
/// `[discriminator=0][finalize_ix_index: u16 LE][n: u8][bump×n][transfer_count×n]
/// [amount: u64 BE ×T]`.
/// Required accounts: `[instructions_sysvar (R), state_pda (R), token_program
/// (R)]` followed, per order, by `[order_pda (R), sell_token_account (W),
Expand Down Expand Up @@ -79,7 +79,7 @@ impl From<BeginSettle<'_>> for Instruction {
.collect();
let data = [
&[SettlementInstruction::BeginSettle.discriminator()][..],
&finalize_ix_index.to_be_bytes()[..],
&finalize_ix_index.to_le_bytes()[..],
&[order_pdas.len() as u8][..],
&order
.iter()
Expand Down Expand Up @@ -309,7 +309,7 @@ mod tests {
data,
[
&[SettlementInstruction::BeginSettle.discriminator()][..],
&hex!("1337")[..], // counterpart index
&hex!("3713")[..], // counterpart index, little-endian
&[0][..], // order count
]
.concat(),
Expand Down Expand Up @@ -354,7 +354,7 @@ mod tests {
data,
[
&[SettlementInstruction::BeginSettle.discriminator()][..],
&hex!("1337")[..], // counterpart index
&hex!("3713")[..], // counterpart index, little-endian
&[2][..], // order count
&[low_bump, high_bump][..], // bumps
&[0, 0][..], // transfer counts (both zero)
Expand Down Expand Up @@ -430,7 +430,7 @@ mod tests {
data,
[
&[SettlementInstruction::BeginSettle.discriminator()][..],
&hex!("1337")[..], // counterpart index
&hex!("3713")[..], // counterpart index, little-endian
&[2][..], // order count
&[0xa1, 0xb1][..], // bumps
&[2, 1][..], // counts
Expand Down Expand Up @@ -480,7 +480,7 @@ mod tests {
];
let data = ix_data![
[SettlementInstruction::BeginSettle.discriminator()],
[0x13, 0x37], // finalize index
[0x37, 0x13], // finalize index, little-endian
[0x00], // order count
];
let BeginSettleInput {
Expand Down Expand Up @@ -539,7 +539,7 @@ mod tests {
];
let data = ix_data![
[SettlementInstruction::BeginSettle.discriminator()],
[0x13, 0x37], // finalize index
[0x37, 0x13], // finalize index, little-endian
[0x01], // order count
[0xab], // one order's bump
[0x00], // that order's transfer count
Expand Down Expand Up @@ -585,7 +585,7 @@ mod tests {
];
let data = ix_data![
[SettlementInstruction::BeginSettle.discriminator()],
[0x13, 0x37], // finalize index
[0x37, 0x13], // finalize index, little-endian
[0x01], // order count
[0xab], // bump
[0x02], // transfer count
Expand Down Expand Up @@ -640,7 +640,7 @@ mod tests {
// then all transfer counts (every order has zero transfers).
let data = ix_data![
[SettlementInstruction::BeginSettle.discriminator()],
[0x13, 0x37], // finalize index
[0x37, 0x13], // finalize index, little-endian
[ORDER_COUNT as u8], // order count
bumps,
[0u8; ORDER_COUNT],
Expand Down
Loading