Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
1b41f69
Add state PDA
fedgiac Jun 4, 2026
e9ddb24
Add buffer PDAs
fedgiac Jun 4, 2026
22eac68
Move test tools to own feature
fedgiac Jun 5, 2026
a0bf3a3
Merge branch 'main' into expose-proptest-strategies-as-feature
kaze-cow Jun 8, 2026
c557c88
Add orders to BeginSettle
fedgiac Jun 8, 2026
c4db140
Fix name in comment
fedgiac Jun 9, 2026
af2862b
space -> size
fedgiac Jun 9, 2026
f788b8a
n_accounts -> fake_sequential_accounts::<n>
fedgiac Jun 9, 2026
3a8efc4
Merge remote-tracking branch 'authed/main' into state-pda
fedgiac Jun 9, 2026
5db57d2
Merge branch 'state-pda' into token-pdas
fedgiac Jun 9, 2026
c78d594
Fix formatting
fedgiac Jun 9, 2026
5d68fb6
super::super:: simplification
fedgiac Jun 9, 2026
54c10bf
Merge branch 'expose-proptest-strategies-as-feature' into include-ord…
fedgiac Jun 9, 2026
4b4f89d
Merge remote-tracking branch 'authed/main' into include-orders-in-set…
fedgiac Jun 9, 2026
0b161b4
Merge remote-tracking branch 'authed/main' into expose-proptest-strat…
fedgiac Jun 9, 2026
eb3521f
Merge branch 'expose-proptest-strategies-as-feature' into include-ord…
fedgiac Jun 9, 2026
e86bb43
[u8; 32] -> Hash
fedgiac Jun 9, 2026
1592674
Remove mention of zipping in doc comment
fedgiac Jun 9, 2026
b18e23c
decode_and_hash error bubbles up
fedgiac Jun 9, 2026
585fd18
Don't allocate vecs in `begin_settle`
fedgiac Jun 9, 2026
6d69063
Merge branch 'main' into include-orders-in-settlement
fedgiac Jun 10, 2026
d4fc5a4
Merge branch 'main' into state-pda
fedgiac Jun 10, 2026
eff2d70
Merge branch 'state-pda' into token-pdas
fedgiac Jun 10, 2026
d09903c
Comment on `process_initialize` checks
fedgiac Jun 11, 2026
7c8f236
Test different rent payer
fedgiac Jun 11, 2026
be64fd7
Fix phrasing
fedgiac Jun 11, 2026
0a97292
Fix phrasing, again
fedgiac Jun 11, 2026
3848ab6
Merge branch 'state-pda' into token-pdas
fedgiac Jun 11, 2026
63402fa
Merge branch 'main' into state-pda
fedgiac Jun 11, 2026
c060181
Merge branch 'state-pda' into token-pdas
fedgiac Jun 11, 2026
91cec54
Merge branch 'main' into include-orders-in-settlement
fedgiac Jun 11, 2026
febfcb2
Comment clarification
fedgiac Jun 12, 2026
4a2448f
Improve readability of canonical PDA creation
fedgiac Jun 12, 2026
9984f63
Test that buffers can receive funds
fedgiac Jun 12, 2026
5a21b36
Merge branch 'main' into token-pdas
fedgiac Jun 12, 2026
858d1d9
Merge branch 'token-pdas' into include-orders-in-settlement
fedgiac Jun 12, 2026
decf380
Use IntoIter instead of Iter
fedgiac Jun 12, 2026
94f24a3
Simplify expression
fedgiac Jun 12, 2026
76f9e46
Sort orders in interface
fedgiac Jun 12, 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
67 changes: 67 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ edition = "2021"
arrayref = "0.3"
derive_more = { version = "1", features = ["deref"] }
litesvm = "0.12.0"
litesvm-token = "0.12.0"
num_enum = "0.7"
pinocchio = "0.11.1"
pinocchio-system = "0.6"
pinocchio-token = "0.6"
proptest = "1"
settlement-client = { path = "client" }
settlement-interface = { path = "interface" }
Expand Down
34 changes: 33 additions & 1 deletion client/src/instructions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,39 @@ use settlement_interface::{

// Reexport the instruction builders that don't change from the interface.
// We want the client to provide all instruction builders.
pub use settlement_interface::settle::{begin_settle, finalize_settle};
pub use settlement_interface::settle::finalize_settle;

/// Build a `BeginSettle` instruction settling the specified orders.
pub fn begin_settle(
program_id: &Pubkey,
finalize_ix_index: u16,
intents: &[OrderIntent],
) -> Instruction {
let mut orders: Vec<(Pubkey, Pubkey, u8)> = intents
.iter()
.map(|intent| {
let (order_pda, bump) = find_order_pda(program_id, &intent.uid());
(order_pda, intent.sell_token_account, bump)
})
.collect();
orders.sort_by_key(|(order_pda, _, _)| *order_pda);

let mut order_pdas = Vec::with_capacity(orders.len());
let mut sell_token_accounts = Vec::with_capacity(orders.len());
let mut bumps = Vec::with_capacity(orders.len());
for (order_pda, sell_token_account, bump) in orders {
order_pdas.push(order_pda);
sell_token_accounts.push(sell_token_account);
bumps.push(bump);
}
settlement_interface::settle::begin_settle(
program_id,
finalize_ix_index,
&order_pdas,
&sell_token_accounts,
&bumps,
)
}

pub fn create_order(
program_id: &Pubkey,
Expand Down
7 changes: 7 additions & 0 deletions interface/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ edition.workspace = true
arrayref.workspace = true
derive_more.workspace = true
num_enum.workspace = true
proptest = { workspace = true, optional = true }
solana-instruction.workspace = true
solana-program-error.workspace = true
solana-pubkey = { workspace = true, features = ["curve25519"] }
Expand All @@ -17,5 +18,11 @@ solana-system-interface.workspace = true
[dev-dependencies]
proptest.workspace = true

[features]
# Exposes the test fixtures and proptest strategies under `data::*::fixtures` so
# other crates can build and fuzz against sample intents and order accounts.
# Enabled here by `cfg(test)` for this crate's own tests.
test-fixtures = ["dep:proptest"]

[lints]
workspace = true
140 changes: 71 additions & 69 deletions interface/src/data/intent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ impl EncodedOrderIntent {

/// Canonical hash of the bytes.
pub fn hash(&self) -> [u8; 32] {
solana_sha256_hasher::hashv(&[self.as_slice()]).to_bytes()
hash_bytes(&self.0)
}

/// Decode raw bytes to an [`OrderIntent`] and compute the UID in one shot.
Expand All @@ -134,11 +134,15 @@ impl EncodedOrderIntent {
// inputs that pass validation. Any normalization added to the `From`
// or `TryFrom` impls later would break this and the UID would silently
// diverge from `OrderIntent::uid()`.
let uid = solana_sha256_hasher::hashv(&[bytes.as_slice()]).to_bytes();
let uid = hash_bytes(bytes);
Ok((intent, uid))
}
}

pub fn hash_bytes(bytes: &[u8; EncodedOrderIntent::SIZE]) -> [u8; 32] {
Comment thread
fedgiac marked this conversation as resolved.
Outdated
solana_sha256_hasher::hashv(&[bytes.as_slice()]).to_bytes()
}

impl From<&EncodedOrderIntent> for [u8; EncodedOrderIntent::SIZE] {
fn from(encoded: &EncodedOrderIntent) -> Self {
encoded.0
Expand Down Expand Up @@ -258,24 +262,20 @@ impl OrderIntent {
}
}

#[cfg(test)]
pub(in crate::data) mod tests {
use super::*;
#[cfg(any(test, feature = "test-fixtures"))]
pub mod fixtures {
use proptest::{prelude::*, strategy::Union};

const ALL_ORDER_KINDS: [OrderKind; 2] = [OrderKind::Sell, OrderKind::Buy];
use super::{EncodedOrderIntent, OrderIntent, OrderKind, Pubkey};

// Full Cartesian product of `OrderKind × bool` for tests that need to
// exercise every shape an `OrderIntent` can take on these axes.
fn all_kind_and_fillable() -> impl Iterator<Item = (OrderKind, bool)> {
ALL_ORDER_KINDS
.into_iter()
.flat_map(|kind| core::iter::repeat(kind).zip([false, true]))
}
/// Every valid [`OrderKind`].
pub const ALL_ORDER_KINDS: [OrderKind; 2] = [OrderKind::Sell, OrderKind::Buy];

// Hardcoded but verified in a sanity-check test.
pub const KIND_OFFSET: usize = 116;
pub const PARTIALLY_FILLABLE_OFFSET: usize = KIND_OFFSET + EncodedOrderIntent::W_KIND;

// Hand-picked example used for both the roundtrip and the digest
// regression. Distinct pubkeys, non-zero amounts, `valid_to` with both
// halves set, recognizable `app_data` pattern.
pub(in crate::data) fn sample_intent(kind: OrderKind, partially_fillable: bool) -> OrderIntent {
pub fn sample_intent(kind: OrderKind, partially_fillable: bool) -> OrderIntent {
OrderIntent {
owner: Pubkey::new_from_array([0x11; 32]),
buy_token_account: Pubkey::new_from_array([0x22; 32]),
Expand All @@ -289,6 +289,55 @@ pub(in crate::data) mod tests {
}
}

/// Any valid [`OrderKind`].
pub fn arb_order_kind() -> impl Strategy<Value = OrderKind> {
Union::new(ALL_ORDER_KINDS.map(Just))
}

/// Any valid [`OrderIntent`].
pub fn arb_order_intent() -> impl Strategy<Value = OrderIntent> {
(
any::<[u8; 32]>(),
any::<[u8; 32]>(),
any::<[u8; 32]>(),
any::<u64>(),
any::<u64>(),
any::<u32>(),
arb_order_kind(),
any::<bool>(),
any::<[u8; 32]>(),
)
.prop_map(
|(owner, buy_tok, sell_tok, sell_amount, buy_amount, valid_to, kind, pf, app)| {
OrderIntent {
owner: Pubkey::new_from_array(owner),
buy_token_account: Pubkey::new_from_array(buy_tok),
sell_token_account: Pubkey::new_from_array(sell_tok),
sell_amount,
buy_amount,
valid_to,
kind,
partially_fillable: pf,
app_data: app,
}
},
)
}
}

#[cfg(test)]
mod tests {
use super::fixtures::{sample_intent, KIND_OFFSET, PARTIALLY_FILLABLE_OFFSET};
use super::*;

// Full Cartesian product of `OrderKind × bool` for tests that need to
// exercise every shape an `OrderIntent` can take on these axes.
fn all_kind_and_fillable() -> impl Iterator<Item = (OrderKind, bool)> {
fixtures::ALL_ORDER_KINDS
.into_iter()
.flat_map(|kind| core::iter::repeat(kind).zip([false, true]))
}

// Pin each width to the size of the `OrderIntent` field it encodes. The
// widths summing to `SIZE` is enforced separately, at compile time, by the
// `array_refs!` / `mut_array_refs!` invocations in the codec.
Expand Down Expand Up @@ -348,11 +397,6 @@ pub(in crate::data) mod tests {
}
}

// Hardcoded but verified in a sanity-check test.
pub(in crate::data) const KIND_OFFSET: usize = 116;
pub(in crate::data) const PARTIALLY_FILLABLE_OFFSET: usize =
KIND_OFFSET + EncodedOrderIntent::W_KIND;

#[test]
fn sanity_check_offsets() {
fn first_differing_byte(lhs: &[u8], rhs: &[u8]) -> Option<usize> {
Expand Down Expand Up @@ -450,16 +494,14 @@ pub(in crate::data) mod tests {
}

// Property-based tests, non-deterministic.
pub(in crate::data) mod proptest {
use ::proptest::{prelude::*, strategy::Union, test_runner::TestCaseError};
mod proptest {
use ::proptest::{prelude::*, test_runner::TestCaseError};

use super::super::fixtures::{
arb_order_intent, arb_order_kind, KIND_OFFSET, PARTIALLY_FILLABLE_OFFSET,
};
use super::*;

// Any valid `OrderKind`.
pub(in crate::data) fn arb_order_kind() -> impl Strategy<Value = OrderKind> {
Union::new(ALL_ORDER_KINDS.map(Just))
}

// Any byte not decoding to a valid order type.
fn arb_bad_order_kind_byte() -> impl Strategy<Value = u8> {
2u8..=255
Expand All @@ -470,46 +512,6 @@ pub(in crate::data) mod tests {
2u8..=255
}

// Any valid `OrderIntent`.
pub(in crate::data) fn arb_order_intent() -> impl Strategy<Value = OrderIntent> {
(
any::<[u8; 32]>(),
any::<[u8; 32]>(),
any::<[u8; 32]>(),
any::<u64>(),
any::<u64>(),
any::<u32>(),
arb_order_kind(),
any::<bool>(),
any::<[u8; 32]>(),
)
.prop_map(
|(
owner,
buy_tok,
sell_tok,
sell_amount,
buy_amount,
valid_to,
kind,
pf,
app,
)| {
OrderIntent {
owner: Pubkey::new_from_array(owner),
buy_token_account: Pubkey::new_from_array(buy_tok),
sell_token_account: Pubkey::new_from_array(sell_tok),
sell_amount,
buy_amount,
valid_to,
kind,
partially_fillable: pf,
app_data: app,
}
},
)
}

proptest! {
// For any `OrderIntent`, encoding an intent into an encoded
// intent and then decoding it with `decode_and_hash()` returns
Expand Down
Loading