Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
43a9724
Create the order struct with getters and setters
xbtmatt Dec 13, 2025
36026b4
Add orders dll head/tail to market header
xbtmatt Dec 15, 2025
296ebeb
Make the doubly linked list completely agnostic to the inner phantom …
xbtmatt Dec 15, 2025
626de85
Remove order data from header/account data to compare program sizes
xbtmatt Dec 15, 2025
95ad819
Add order data back in
xbtmatt Dec 15, 2025
37f819b
Add the orders fields to the client market view
xbtmatt Dec 15, 2025
7d37697
Add WI for order/seat operations changes
xbtmatt Dec 16, 2025
a81673b
Add order insertion initial pass
xbtmatt Dec 16, 2025
352eacd
Move around shared operations stuff so the market operations are sepa…
xbtmatt Dec 16, 2025
baf7064
Add variants and conversions from price OrderInfoError to DropsetError
xbtmatt Dec 16, 2025
85c2699
Add place order instruction initial pass
xbtmatt Dec 17, 2025
5190674
Add the `PlaceOrderEvent` and emission to the place order instruction
xbtmatt Dec 17, 2025
85d9ba9
Comment formatting
xbtmatt Dec 17, 2025
b830d17
Remove `iter_rev` as it's unused
xbtmatt Dec 17, 2025
6a08d2d
Add `place_ask` example
xbtmatt Dec 17, 2025
b4d9272
Fix mod level doc comment for processing the place order instruction
xbtmatt Dec 17, 2025
c6ce2fb
Add WIP for cancel order
xbtmatt Dec 17, 2025
d7413cf
Add `place_and_cancel` and `place_asks` examples with proper debug vi…
xbtmatt Dec 17, 2025
0ed6059
Fix the order insertion doc comment
xbtmatt Dec 17, 2025
4f5e629
Change all instances of `place` to `post` where appropriate
xbtmatt Dec 17, 2025
c741725
Rename files
xbtmatt Dec 17, 2025
7d90bee
Remove possibly soon outdated doc comments
xbtmatt Dec 18, 2025
58b9b9d
Add asks and bids to the market and not just a single orders linked l…
xbtmatt Dec 18, 2025
b1c1b93
Remove empty braces
xbtmatt Dec 18, 2025
0b6f53d
Update comment for canceling an order
xbtmatt Dec 18, 2025
e7b4c77
Remove LinkedListRevIter
xbtmatt Dec 18, 2025
a7af739
Add `static_assertions` to `client/Cargo.toml`
xbtmatt Jan 12, 2026
71e3802
Fix comments in post asks example
xbtmatt Jan 12, 2026
82e073e
Fix comment in post and cancel
xbtmatt Jan 12, 2026
aa00f1b
Singer => signer typo
xbtmatt Jan 12, 2026
4823391
Fix comments referencing bids/asks and ordinality incorrectly
xbtmatt Jan 12, 2026
aa90ad3
Fix `bids` to `asks` in market header count of asks
xbtmatt Jan 12, 2026
ec10e39
Rename `set_remaining` to `set_base_remaining`
xbtmatt Jan 12, 2026
1947abc
Fix typo in mod level doc comment for cancel_order_context.rs
xbtmatt Jan 12, 2026
db1b2bf
Make `orders` in `MarketViewAll` `bids` and `asks`
xbtmatt Jan 12, 2026
fe69cc6
Remove unused views
xbtmatt Jan 12, 2026
f900504
Remove `tree` in comment
xbtmatt Jan 12, 2026
3735d91
Add simple unit tests for `Order` struct
xbtmatt Jan 12, 2026
80d5ae2
Add comprehensive unit tests for inserting orders into bids and asks
xbtmatt Jan 13, 2026
c4a13ed
Initial plan
Copilot Jan 13, 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
4 changes: 4 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 client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dropset-interface = { path = "../interface", features = ["client", "solana-progr
itertools.workspace = true
futures.workspace = true
lazy-regex.workspace = true
price = { path = "../price" }
regex.workspace = true
solana-client.workspace = true
solana-commitment-config.workspace = true
Expand All @@ -28,6 +29,7 @@ solana-transaction-status-client-types.workspace = true
spl-associated-token-account-interface.workspace = true
spl-token-interface.workspace = true
spl-token-2022-interface.workspace = true
static_assertions.workspace = true
strum.workspace = true
strum_macros.workspace = true
tokio.workspace = true
Expand Down
115 changes: 115 additions & 0 deletions client/examples/post_and_cancel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use std::collections::HashSet;

use client::{
context::market::MarketContext,
transactions::{
CustomRpcClient,
SendTransactionConfig,
},
};
use dropset_interface::{
instructions::{
CancelOrderInstructionData,
PostOrderInstructionData,
},
state::sector::NIL,
};
use price::{
to_biased_exponent,
to_order_info,
};
use solana_sdk::signer::Signer;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let rpc = &CustomRpcClient::new(
None,
Some(SendTransactionConfig {
compute_budget: Some(2000000),
debug_logs: Some(true),
program_id_filter: HashSet::from([dropset_interface::program::ID.into()]),
}),
);
let payer = rpc.fund_new_account().await?;

let market_ctx = MarketContext::new_market(rpc).await?;
let register = market_ctx.register_market(payer.pubkey(), 10);

market_ctx.base.create_ata_for(rpc, &payer).await?;
market_ctx.quote.create_ata_for(rpc, &payer).await?;

market_ctx.base.mint_to(rpc, &payer, 10000).await?;
market_ctx.quote.mint_to(rpc, &payer, 10000).await?;

let deposit = market_ctx.deposit_base(payer.pubkey(), 1000, NIL);

rpc.send_and_confirm_txn(&payer, &[&payer], &[register.into(), deposit.into()])
.await?;

let market = market_ctx.view_market(rpc)?;
println!("Market after user deposit\n{:#?}", market);

let user_seat = market_ctx
.find_seat(rpc, &payer.pubkey())?
.expect("User should have been registered on deposit");

let (price_mantissa, base_scalar, base_exponent, quote_exponent) = (
10_000_000,
500,
to_biased_exponent!(0),
to_biased_exponent!(0),
);
let order_info = to_order_info(price_mantissa, base_scalar, base_exponent, quote_exponent)
.expect("Should be a valid order");

// Post an ask. The user provides base as collateral and receives quote when filled.
let is_bid = false;
let post_ask = market_ctx.post_order(
payer.pubkey(),
PostOrderInstructionData::new(
price_mantissa,
base_scalar,
base_exponent,
quote_exponent,
is_bid,
user_seat.index,
),
);

let res = rpc
.send_and_confirm_txn(&payer, &[&payer], &[post_ask.into()])
.await?;

println!(
"Post ask transaction signature: {}",
res.parsed_transaction.signature
);

let market = market_ctx.view_market(rpc)?;
println!("Market after posting user ask:\n{:#?}", market);

let user_seat = market_ctx.find_seat(rpc, &payer.pubkey())?.unwrap();
println!("User seat after posting ask: {user_seat:#?}");

let cancel_ask = market_ctx.cancel_order(
user_seat.user,
CancelOrderInstructionData::new(order_info.encoded_price.as_u32(), is_bid, user_seat.index),
);

let res = rpc
.send_and_confirm_txn(&payer, &[&payer], &[cancel_ask.into()])
.await?;

println!(
"Cancel ask transaction signature: {}",
res.parsed_transaction.signature
);

let user_seat = market_ctx.find_seat(rpc, &payer.pubkey())?.unwrap();
println!("User seat after canceling ask: {user_seat:#?}");

let market = market_ctx.view_market(rpc)?;
println!("Market after canceling user ask:\n{:#?}", market);

Ok(())
}
118 changes: 118 additions & 0 deletions client/examples/post_asks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use std::collections::HashSet;

use client::{
context::market::MarketContext,
transactions::{
CustomRpcClient,
SendTransactionConfig,
},
};
use dropset_interface::{
instructions::PostOrderInstructionData,
state::sector::NIL,
};
use itertools::Itertools;
use price::to_biased_exponent;
use solana_sdk::signer::Signer;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let rpc = &CustomRpcClient::new(
None,
Some(SendTransactionConfig {
compute_budget: Some(2000000),
debug_logs: Some(true),
program_id_filter: HashSet::from([dropset_interface::program::ID.into()]),
}),
);
let payer = rpc.fund_new_account().await?;

let market_ctx = MarketContext::new_market(rpc).await?;
let register = market_ctx.register_market(payer.pubkey(), 10);

market_ctx.base.create_ata_for(rpc, &payer).await?;
market_ctx.quote.create_ata_for(rpc, &payer).await?;

market_ctx.base.mint_to(rpc, &payer, 10000).await?;
market_ctx.quote.mint_to(rpc, &payer, 10000).await?;

let deposit = market_ctx.deposit_base(payer.pubkey(), 10000, NIL);

rpc.send_and_confirm_txn(&payer, &[&payer], &[register.into(), deposit.into()])
.await?;

let market = market_ctx.view_market(rpc)?;
println!("Market after user deposit\n{:#?}", market);

let user_seat = market_ctx
.find_seat(rpc, &payer.pubkey())?
.expect("User should have been registered on deposit");

let (price_mantissa, base_scalar, base_exponent, quote_exponent) = (
10_000_000,
500,
to_biased_exponent!(0),
to_biased_exponent!(0),
);

// Post an ask. The user provides base as collateral and receives quote when filled.
let is_bid = false;
let post_ask = market_ctx.post_order(
payer.pubkey(),
PostOrderInstructionData::new(
price_mantissa,
base_scalar,
base_exponent,
quote_exponent,
is_bid,
user_seat.index,
),
);

let res = rpc
.send_and_confirm_txn(&payer, &[&payer], &[post_ask.into()])
.await?;

println!(
"Post ask transaction signature: {}",
res.parsed_transaction.signature
);

let market = market_ctx.view_market(rpc)?;
println!("Market after posting user ask:\n{:#?}", market);

let user_seat = market_ctx.find_seat(rpc, &payer.pubkey())?.unwrap();
println!("User seat after posting ask: {user_seat:#?}");

// Post an ask. The user provides base as collateral and receives quote when filled.
let is_bid = false;

let ask_instructions = (1..5)
.map(|i| {
market_ctx
.post_order(
payer.pubkey(),
PostOrderInstructionData::new(
price_mantissa + i,
base_scalar,
base_exponent,
quote_exponent,
is_bid,
user_seat.index,
),
)
.into()
})
.collect_vec();

rpc.send_and_confirm_txn(&payer, &[&payer], &ask_instructions)
.await?;

let market = market_ctx.view_market(rpc)?;
println!("Market after posting many user asks:\n{:#?}", market);

let user_seat = market_ctx.find_seat(rpc, &payer.pubkey())?.unwrap();
println!("User seat after posting many asks: {user_seat:#?}");

Ok(())
}
34 changes: 34 additions & 0 deletions client/src/context/market.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
use dropset_interface::{
instructions::{
generated_client::*,
CancelOrderInstructionData,
CloseSeatInstructionData,
DepositInstructionData,
PostOrderInstructionData,
RegisterMarketInstructionData,
WithdrawInstructionData,
},
Expand Down Expand Up @@ -167,6 +169,38 @@ impl MarketContext {
self.withdraw(user, data, false)
}

pub fn post_order(
&self,
user: Pubkey,
data: PostOrderInstructionData,
) -> SingleSignerInstruction {
PostOrder {
event_authority: event_authority::ID.into(),
user,
market_account: self.market,
dropset_program: dropset::ID.into(),
}
.create_instruction(data)
.try_into()
.expect("Should be a single signer instruction")
}

pub fn cancel_order(
&self,
user: Pubkey,
data: CancelOrderInstructionData,
) -> SingleSignerInstruction {
CancelOrder {
event_authority: event_authority::ID.into(),
user,
market_account: self.market,
dropset_program: dropset::ID.into(),
}
.create_instruction(data)
.try_into()
.expect("Should be a single signer instruction")
}

fn deposit(
&self,
user: Pubkey,
Expand Down
26 changes: 26 additions & 0 deletions interface/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Common error types and conversion helpers to represent them as error message strings.

use pinocchio::program_error::ProgramError;
use price::OrderInfoError;

#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "client", derive(strum_macros::FromRepr))]
Expand Down Expand Up @@ -37,6 +38,12 @@ pub enum DropsetError {
OrderWithPriceAlreadyExists,
UserHasMaxOrders,
OrderNotFound,
ExponentUnderflow,
ArithmeticOverflow,
InvalidPriceMantissa,
InvalidBiasedExponent,
InfinityIsNotAFloat,
PostOnlyWouldImmediatelyFill,
}

impl From<DropsetError> for ProgramError {
Expand All @@ -46,6 +53,19 @@ impl From<DropsetError> for ProgramError {
}
}

impl From<OrderInfoError> for DropsetError {
#[inline(always)]
fn from(order_error: OrderInfoError) -> Self {
match order_error {
OrderInfoError::ExponentUnderflow => DropsetError::ExponentUnderflow,
OrderInfoError::ArithmeticOverflow => DropsetError::ArithmeticOverflow,
OrderInfoError::InvalidPriceMantissa => DropsetError::InvalidPriceMantissa,
OrderInfoError::InvalidBiasedExponent => DropsetError::InvalidBiasedExponent,
OrderInfoError::InfinityIsNotAFloat => DropsetError::InfinityIsNotAFloat,
}
}
}

impl From<DropsetError> for &'static str {
fn from(value: DropsetError) -> Self {
match value {
Expand Down Expand Up @@ -80,6 +100,12 @@ impl From<DropsetError> for &'static str {
DropsetError::OrderWithPriceAlreadyExists => "An order with this price already exists",
DropsetError::UserHasMaxOrders => "User already has the max number of open orders",
DropsetError::OrderNotFound => "Order not found",
DropsetError::ExponentUnderflow => "Order exponent underflow",
DropsetError::ArithmeticOverflow => "Order arithmetic overflow",
DropsetError::InvalidPriceMantissa => "Invalid price mantissa in price calculation",
DropsetError::InvalidBiasedExponent => "Invalid biased exponent in price calculation",
DropsetError::InfinityIsNotAFloat => "Can't convert infinity to a float value",
DropsetError::PostOnlyWouldImmediatelyFill => "Post only order would immediately fill",
}
}
}
Expand Down
Loading