Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9ce4ad9
setup reclaim order
kaze-cow Jul 1, 2026
555239a
fix lints and errors
kaze-cow Jul 1, 2026
013557a
simplify reclaim order test even more
kaze-cow Jul 1, 2026
b892ae7
Merge branch 'main' into kaze/sc-150-close-order-account
kaze-cow Jul 3, 2026
9199880
stub timestamp function only runs when explicit feature is enabled
kaze-cow Jul 3, 2026
83e3556
Merge branch 'kaze/sc-150-close-order-account' of https://github.com/…
kaze-cow Jul 3, 2026
ce3280c
Merge branch 'main' into kaze/sc-150-close-order-account
kaze-cow Jul 8, 2026
68a3bea
Update interface/src/instruction/reclaim_order.rs
kaze-cow Jul 8, 2026
8919f68
Update programs/settlement/src/reclaim_order.rs
kaze-cow Jul 8, 2026
76e8434
Update programs/settlement/src/reclaim_order.rs
kaze-cow Jul 8, 2026
af95669
Update programs/settlement/tests/reclaim_order.rs
kaze-cow Jul 8, 2026
3d25f1f
remove settlement-test-clock and related unit test, will move to inte…
kaze-cow Jul 8, 2026
908681b
fix test error
kaze-cow Jul 8, 2026
c6627d7
fix lint failures
kaze-cow Jul 8, 2026
8a4a989
use signed_tx helper
kaze-cow Jul 8, 2026
9ed99a6
remove unnecessary test
kaze-cow Jul 8, 2026
ffdefe4
add a helper function to load an order account with canonical safety …
kaze-cow Jul 8, 2026
a7afebd
use suggested code from pinnochio to construct an account with fake data
kaze-cow Jul 8, 2026
4c045d4
just fmt
kaze-cow Jul 8, 2026
fee918a
Merge branch 'main' into kaze/sc-150-close-order-account
kaze-cow Jul 16, 2026
e0c8467
Update interface/src/instruction/mod.rs
kaze-cow Jul 17, 2026
ba0fd79
Update interface/src/instruction/reclaim_order.rs
kaze-cow Jul 17, 2026
bfb85ae
Update programs/settlement/src/reclaim_order.rs
kaze-cow Jul 17, 2026
09b2d87
Update programs/settlement/tests/reclaim_order.rs
kaze-cow Jul 17, 2026
ed4b7e1
Update interface/src/data/order.rs
kaze-cow Jul 17, 2026
a2d2d99
Merge branch 'main' into kaze/sc-150-close-order-account
kaze-cow Jul 17, 2026
a227027
fix lints and remove AFTER_EXPIRY
kaze-cow Jul 17, 2026
b3134f2
update comments related to bump and whether its canonical orn ot
kaze-cow Jul 17, 2026
d279b62
rename to `AccountNotDerivable` because its not strictly correct to s…
kaze-cow Jul 17, 2026
ab25eff
Update programs/settlement/src/reclaim_order.rs
kaze-cow Jul 17, 2026
496d7d6
Update interface/src/data/order.rs
kaze-cow Jul 17, 2026
bd7e61d
remove reference to byte count of order in test
kaze-cow Jul 17, 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
5 changes: 3 additions & 2 deletions interface/src/data/intent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ use solana_program_error::ProgramError;
use solana_pubkey::Pubkey;

/// Direction of the trade.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
#[repr(u8)]
pub enum OrderKind {
#[default]
Sell = 0,
Buy = 1,
}

#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq, Default)]
pub struct OrderIntent {
/// Account authorized to create and invalidate this order and whose
/// signature authenticates it. For off-chain orders this is the Ed25519
Expand Down
99 changes: 98 additions & 1 deletion interface/src/data/order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,18 @@ use core::mem::size_of;

use arrayref::{array_refs, mut_array_refs};
use derive_more::Deref;
use solana_account_view::AccountView;
use solana_address::Address;
use solana_hash::Hash;
use solana_program_error::ProgramError;
use solana_pubkey::Pubkey;

use crate::data::intent::{self, EncodedOrderIntent, OrderIntent};
use crate::pda::order::order_pda_signer_seeds;
use crate::SettlementError;

/// Idiomatic representation of an order PDA's body.
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq, Default)]
pub struct OrderAccount {
/// `false` = the order is still active and can be filled; `true` = the
/// order has been cancelled by the owner and must not be filled.
Expand All @@ -51,6 +55,33 @@ pub struct OrderAccount {
pub intent: OrderIntent,
}

impl OrderAccount {
/// Load and decode the order at the given PDA, and confirm
/// the PDA is reached by its own data and the provided bump
Comment thread
kaze-cow marked this conversation as resolved.
Outdated
pub fn load_from_pda(
order_pda: &AccountView,
program_id: &Address,
bump: u8,
) -> Result<Self, ProgramError> {
let (account, uid) = {
let data = order_pda.try_borrow()?;
let bytes: &[u8; EncodedOrderAccount::SIZE] = (&*data)
.try_into()
.map_err(|_| ProgramError::InvalidAccountData)?;
EncodedOrderAccount::decode_and_hash(bytes)?
};

let expected =
Address::create_program_address(&order_pda_signer_seeds(&uid, &[bump]), program_id)
.map_err(|_| SettlementError::AccountNotDerivable)?;
if &expected != order_pda.address() {
return Err(SettlementError::AccountNotDerivable.into());
}

Ok(account)
}
}
Comment on lines +58 to +83

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to have traits for account deserialization just like we do for Instruction parsing?

(ref)

@kaze-cow kaze-cow Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would but as of right now this is the only load_from_pda function we have so I didn't want to go too crazy for this PR.

I didn't see any great stdlib traits that would have made sense here either so if we do make a trait it would literally just be defining this function and thats it; and as you mention there are the bump consideration issues.


/// Canonical 200-byte representation of an [`OrderAccount`]. The bytes
Comment thread
kaze-cow marked this conversation as resolved.
/// written to/read from the order PDA's data area.
///
Expand Down Expand Up @@ -388,6 +419,72 @@ mod tests {
assert_eq!(err, ProgramError::InvalidAccountData);
}

mod load_from_pda {
use super::*;
use crate::instruction::fixtures::fake_account_with_data;
use crate::pda::order::find_order_pda;

const PROGRAM_ID: Address = Address::new_from_array([9; 32]);

#[test]
fn accepts_the_canonical_pda() {
let account = sample_account(false);
let (pda_address, bump) = find_order_pda(&PROGRAM_ID, &account.intent.uid());
let order_pda = fake_account_with_data(
pda_address,
&EncodedOrderAccount::from(account.clone())[..],
);

let loaded = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID, bump)
.expect("canonical PDA must load");
assert_eq!(loaded, account);
}

#[test]
fn rejects_a_non_canonical_address() {
let account = sample_account(false);
let (_, bump) = find_order_pda(&PROGRAM_ID, &account.intent.uid());
// An address unrelated to the intent's canonical seeds.
let wrong_address = Pubkey::new_from_array([0x42; 32]);
let order_pda =
fake_account_with_data(wrong_address, &EncodedOrderAccount::from(account)[..]);

let err = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID, bump)
.expect_err("a non-canonical address must be rejected");
assert_eq!(err, SettlementError::AccountNotDerivable.into());
}

#[test]
fn rejects_a_wrong_bump() {
let account = sample_account(false);
let (pda_address, bump) = find_order_pda(&PROGRAM_ID, &account.intent.uid());
let order_pda =
fake_account_with_data(pda_address, &EncodedOrderAccount::from(account)[..]);

// Any bump other than the canonical one either derives a
// different address or fails to derive one at all (falling on
// curve); either way, the PDA can no longer be proven canonical.
Comment thread
fedgiac marked this conversation as resolved.
let wrong_bump = bump.wrapping_sub(1);
let err = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID, wrong_bump)
.expect_err("a non-canonical bump must be rejected");
assert_eq!(err, SettlementError::AccountNotDerivable.into());
}

#[test]
fn propagates_decode_errors() {
let account = sample_account(false);
let (pda_address, bump) = find_order_pda(&PROGRAM_ID, &account.intent.uid());
let mut bytes: [u8; EncodedOrderAccount::SIZE] =
EncodedOrderAccount::from(account).into();
bytes[CANCELLED_OFFSET] = 0xff;
let order_pda = fake_account_with_data(pda_address, &bytes);

let err = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID, bump)
.expect_err("a corrupt account must fail to decode");
assert_eq!(err, ProgramError::InvalidAccountData);
}
}

#[test]
fn direct_write_account_matches_order_account_decoding() {
let cancelled = true;
Expand Down
74 changes: 70 additions & 4 deletions interface/src/instruction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::{recover_discriminator, SettlementInstruction};
pub mod create_buffer;
pub mod create_order;
pub mod initialize;
pub mod reclaim_order;
pub mod settle;

/// Shared components for parsing generic instruction input.
Expand Down Expand Up @@ -51,6 +52,7 @@ pub trait InstructionInputParsing<'a>: Sized {
pub mod fixtures {
use solana_account_view::{AccountView, RuntimeAccount};
use solana_address::Address;
use std::{mem, ptr};

/// Build an `AccountView` based on the input `RuntimeAccount` and whose
/// data region is empty.
Expand Down Expand Up @@ -85,11 +87,75 @@ pub mod fixtures {
unsafe { AccountView::new_unchecked(backing as *mut RuntimeAccount) }
}

/// Adapted from pinocchio's crate-private test helper; kept structurally
/// close for comparison:
/// https://docs.rs/crate/pinocchio/0.11.1/source/src/sysvars/slot_hashes/test_utils.rs#120-160
///
/// Allocate a heap-backed `AccountView` whose data region is initialized with
/// `data`, whose address is `address`, and whose borrow flag is `borrow_state`.
///
/// The function also returns the backing `Vec<u64>` so the caller can keep it
/// alive for the duration of the test (otherwise the memory would be freed and
/// the raw pointer inside `AccountView` would dangle).
///
/// # Safety
/// The caller must ensure the returned `AccountView` is used only for reading
/// or according to borrow rules because the Solana runtime invariants are not
/// fully enforced in this hand-rolled representation.
#[allow(
clippy::arithmetic_side_effects,
reason = "the function is mostly vendored and don't want to introduce unnecessary changes"
)]
pub unsafe fn make_account_view(
address: Address,
data: &[u8],
borrow_state: u8,
) -> (AccountView, Vec<u64>) {
// pinocchio writes a hand-rolled `AccountLayout` mirror and casts the
// pointer to `*mut RuntimeAccount`, because inside that crate the
// account struct's fields are private and its tests cannot set them by
// name. Here we instead write a real `RuntimeAccount`, since
// `solana_account_view::RuntimeAccount` has public fields and a
// `Default` impl.
let hdr_size = mem::size_of::<RuntimeAccount>();
let total = hdr_size + data.len();
let words = total.div_ceil(8);
let mut backing: Vec<u64> = vec![0u64; words];
assert!(
mem::align_of::<u64>() >= mem::align_of::<RuntimeAccount>(),
"`backing` should be properly aligned to store a `RuntimeAccount` instance"
);

let hdr_ptr = backing.as_mut_ptr() as *mut RuntimeAccount;
ptr::write(
hdr_ptr,
RuntimeAccount {
address,
borrow_state,
data_len: data.len() as u64,
..Default::default()
},
);

ptr::copy_nonoverlapping(
data.as_ptr(),
(hdr_ptr as *mut u8).add(hdr_size),
data.len(),
);

(AccountView::new_unchecked(hdr_ptr), backing)
}

/// Create an account view storing the input data.
pub fn fake_account_with_data(address: Address, data: &[u8]) -> AccountView {
let (account, backing) =
unsafe { make_account_view(address, data, solana_account_view::NOT_BORROWED) };
core::mem::forget(backing);
account
}

pub fn fake_account(address: Address) -> AccountView {
fake_account_from(RuntimeAccount {
address,
..Default::default()
})
fake_account_with_data(address, &[])
}

Comment thread
kaze-cow marked this conversation as resolved.
pub fn fake_account_from_array(address_array: [u8; 32]) -> AccountView {
Expand Down
Loading