Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 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
671478e
starting point
kaze-cow Jul 6, 2026
128d870
i think we are good to go
kaze-cow Jul 6, 2026
a5238a9
remove unnecessary associated token dependency that is dead in most p…
kaze-cow Jul 6, 2026
4699831
lint and fmt
kaze-cow Jul 6, 2026
ce3280c
Merge branch 'main' into kaze/sc-150-close-order-account
kaze-cow Jul 8, 2026
34793d1
Merge branch 'kaze/sc-150-close-order-account' into kaze/sc-151-close…
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
1221424
Merge branch 'kaze/sc-150-close-order-account' into kaze/sc-151-close…
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ 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-associated-token-account-interface = "2"
spl-token = "9"
spl-token-interface = "2"

Expand Down
36 changes: 36 additions & 0 deletions client/src/instructions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ impl From<CreateBuffers<'_>> for Instruction {
pub struct Initialize {
pub program_id: Pubkey,
pub payer: Pubkey,
pub receiver: Pubkey,
}

impl From<Initialize> for Instruction {
Expand All @@ -178,6 +179,41 @@ impl From<Initialize> for Instruction {
program_id: builder.program_id,
payer: builder.payer,
state_pda,
receiver: builder.receiver,
}
.into()
}
}

/// Builder for a `ReclaimBuffer` instruction closing the buffer for each of
/// `mints`.
///
/// **Warning:** any token balance still held by a buffer is burned, not
/// recovered, before the buffer is closed. Only reclaim buffers expected to
/// be empty, or to write off dust/dead balances — never one that might still
/// hold funds of useful value.
pub struct ReclaimBuffer<'a> {
pub program_id: Pubkey,
pub receiver: Pubkey,
pub mints: &'a [Pubkey],
}

impl From<ReclaimBuffer<'_>> for Instruction {
fn from(builder: ReclaimBuffer<'_>) -> Self {
let (state_pda, _bump) = find_state_pda(&builder.program_id);
let buffers: Vec<(Pubkey, Pubkey)> = builder
.mints
.iter()
.map(|mint| {
let (buffer_pda, _bump) = find_buffer_pda(&builder.program_id, mint);
(buffer_pda, *mint)
})
.collect();
settlement_interface::instruction::reclaim_buffer::ReclaimBuffer {
program_id: builder.program_id,
state_pda,
receiver: builder.receiver,
buffers: &buffers,
}
.into()
}
Expand Down
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
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;
97 changes: 96 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,31 @@ pub struct OrderAccount {
pub intent: OrderIntent,
}

impl OrderAccount {
/// Load and decode the order at the given PDA, and confirm its canonical
pub fn load_from_pda(
order_pda: &AccountView,
program_id: &Address,
bump: u8,
) -> Result<Self, ProgramError> {
let data = order_pda.try_borrow()?;
let bytes: &[u8; EncodedOrderAccount::SIZE] = (&*data)
.try_into()
.map_err(|_| ProgramError::InvalidAccountData)?;
let (account, uid) = EncodedOrderAccount::decode_and_hash(bytes)?;
drop(data);

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

Ok(account)
}
}

/// Canonical 199-byte representation of an [`OrderAccount`]. The bytes
/// written to/read from the order PDA's data area.
///
Expand Down Expand Up @@ -359,6 +388,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::OrderNotCanonical.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.
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::OrderNotCanonical.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
93 changes: 93 additions & 0 deletions interface/src/data/state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//! Settlement state PDA body and its canonical byte representation.
//!
//! The state PDA (see [`crate::pda::state`]) stores a single piece of
//! protocol configuration: the `receiver` account that collects reclaimed
//! buffer funds (see `ReclaimBuffer`). Unlike [`crate::data::order`], both
//! directions of the encoding are infallible: every 32-byte sequence is a
//! valid [`Pubkey`].

use derive_more::Deref;
use solana_pubkey::Pubkey;

/// Idiomatic representation of the state PDA's body.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct StateAccount {
/// Account configured at `Initialize` time that collects reclaimed
/// buffer funds: it must sign `ReclaimBuffer`, receives each closed
/// buffer's rent lamports directly, and receives any leftover token
/// balance via its associated token account for that mint.
pub receiver: Pubkey,
}

/// Canonical 32-byte representation of a [`StateAccount`]: exactly
/// `receiver`'s bytes, the whole of the state PDA's data area.
#[derive(Clone, Copy, Debug, Deref, Eq, PartialEq)]
pub struct EncodedStateAccount([u8; Self::SIZE]);

impl EncodedStateAccount {
pub const SIZE: usize = 32;
}

impl From<EncodedStateAccount> for [u8; EncodedStateAccount::SIZE] {
fn from(encoded: EncodedStateAccount) -> Self {
encoded.0
}
}

impl From<StateAccount> for EncodedStateAccount {
fn from(account: StateAccount) -> Self {
Self(account.receiver.to_bytes())
}
}

impl From<[u8; EncodedStateAccount::SIZE]> for StateAccount {
fn from(bytes: [u8; EncodedStateAccount::SIZE]) -> Self {
StateAccount {
receiver: Pubkey::new_from_array(bytes),
}
}
}

impl From<EncodedStateAccount> for StateAccount {
fn from(encoded: EncodedStateAccount) -> Self {
StateAccount::from(encoded.0)
}
}

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

#[test]
fn roundtrip() {
let account = StateAccount {
receiver: Pubkey::new_from_array([0x42; 32]),
};
let encoded = EncodedStateAccount::from(account);
let decoded = StateAccount::from(encoded);
assert_eq!(decoded, account);
}

#[test]
fn encoding_is_exactly_the_receiver_bytes() {
let receiver = Pubkey::new_from_array([0x7; 32]);
let encoded = EncodedStateAccount::from(StateAccount { receiver });
assert_eq!(*encoded, receiver.to_bytes());
}

mod proptest {
use ::proptest::prelude::*;

use super::*;

proptest! {
#[test]
fn account_roundtrip(bytes in any::<[u8; 32]>()) {
let account = StateAccount::from(bytes);
let encoded = EncodedStateAccount::from(account);
let decoded = StateAccount::from(encoded);
prop_assert_eq!(decoded, account);
}
}
}
}
Loading
Loading