Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e075a77
solver interaction
tilacog Aug 17, 2026
2955218
remove low value tests and the `solve-all` fn
tilacog Aug 17, 2026
1be45fa
move timeout to Auction level
tilacog Aug 17, 2026
827e641
source timeline from env (optionally)
tilacog Aug 17, 2026
0b7f4e6
simplify: remove serde traits for OrderUid
tilacog Aug 17, 2026
a4f0ef9
simplify base64 serialization
tilacog Aug 17, 2026
3a02112
improve dull test for ATA derivation
tilacog Aug 17, 2026
34ff1f3
doc: clarify clarify into_domain comment on unknown order UIDs
tilacog Aug 18, 2026
f95991d
fix: compute request timeout after acquiring a permit
tilacog Aug 18, 2026
d6bb454
feat(serde-ext): normalize URLs with a trailing slash on deserialize
tilacog Aug 18, 2026
177cd6d
Merge remote-tracking branch 'origin/main' into solana-driver-solver-…
tilacog Aug 18, 2026
e5d3764
doc: trim comment
tilacog Aug 18, 2026
6024107
remove unused accessor methods
tilacog Aug 18, 2026
188b53d
chore(deps): bump h2 to 0.4.16 to fix RUSTSEC-2026-0258
tilacog Aug 18, 2026
0bd6114
refactor(solana-driver): drop unused `fee` field from domain Trade
tilacog Aug 18, 2026
1a12165
refactor(solana-driver, solana-solvers): use i64 for auction id
tilacog Aug 18, 2026
973e713
reject trades that over-execute the order amount
tilacog Aug 18, 2026
72f9104
add TODO for splitting the deadline budget
tilacog Aug 18, 2026
bfdcbd6
return DeadlineExceeded error when the auction deadline expires
tilacog Aug 18, 2026
b6e7ff0
assert semaphore is never closed on acquire
tilacog Aug 18, 2026
00bbb80
solana-driver,solana-solvers: add deadline to the auction DTO
tilacog Aug 18, 2026
cf9e2c9
fmt
tilacog Aug 18, 2026
f7bf3e9
pin solver DTO wire format with serde shape tests
tilacog Aug 19, 2026
533dc4a
add todo comment on token-2022 support
tilacog Aug 19, 2026
3690b1a
test(solana-driver): cover solver deadline-exceeded path
tilacog Aug 19, 2026
c4eae2b
Merge branch 'main' into solana-driver-solver-client
tilacog Aug 19, 2026
2ee22ef
Merge branch 'main' into solana-driver-solver-client
tilacog Aug 19, 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
12 changes: 10 additions & 2 deletions Cargo.lock

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

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ bytes-hex = { path = "crates/bytes-hex" }
cached = { version = "0.49.3", default-features = false }
chain = { path = "crates/chain" }
chain-types = { path = "crates/chain-types" }
chrono = { version = "0.4.38", default-features = false }
chrono = { version = "0.4.38", default-features = false, features = ["serde"] }
clap = { version = "4.5.6", features = ["derive", "env"] }
configs = { path = "crates/configs/" }
console-subscriber = "0.5.0"
Expand Down Expand Up @@ -120,7 +120,7 @@ serde = { version = "1.0.203", features = ["derive"] }
serde-ext = { path = "crates/serde-ext" }
serde_json = { version = "1.0.117", features = ["raw_value", "unbounded_depth"] }
serde_stacker = "0.1.14"
serde_with = "3.8.1"
serde_with = { version = "3.8.1", features = ["base64"] }
sha2 = "0.10"
shared = { path = "crates/shared" }
signature-validator = { path = "crates/signature-validator" }
Expand Down
1 change: 1 addition & 0 deletions crates/serde-ext/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ const-hex = { workspace = true }
serde = { workspace = true }
serde_with = { workspace = true }
solana-sdk = { workspace = true }
url = { workspace = true, features = ["serde"] }
2 changes: 2 additions & 0 deletions crates/serde-ext/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ mod hex;
mod nonempty;
mod pubkey;
mod u256;
mod url;

pub use self::{
hex::Hex,
nonempty::deserialize_nonempty_vec,
pubkey::{deserialize_optional_solana_pubkey_b58, deserialize_solana_pubkey_b58},
u256::U256,
url::deserialize_url_with_trailing_slash,
};
42 changes: 42 additions & 0 deletions crates/serde-ext/src/url.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use {
serde::{Deserialize, Deserializer},
url::Url,
};

/// Deserialize a URL and make sure its path ends with a slash.
pub fn deserialize_url_with_trailing_slash<'de, D>(deserializer: D) -> Result<Url, D::Error>
where
D: Deserializer<'de>,
{
let mut url = Url::deserialize(deserializer)?;
if !url.path().ends_with('/') {
url.set_path(&format!("{}/", url.path()));
}
Ok(url)
}
Comment thread
tilacog marked this conversation as resolved.

#[cfg(test)]
mod tests {
use {
super::*,
serde::de::value::{Error, StrDeserializer},
};

#[test]
fn appends_trailing_slash_when_missing() {
let de = StrDeserializer::<Error>::new("http://localhost:8001/api");
let url: Url = deserialize_url_with_trailing_slash(de).unwrap();
assert_eq!(url.as_str(), "http://localhost:8001/api/");
assert_eq!(
url.join("solve").unwrap().as_str(),
"http://localhost:8001/api/solve"
);
}

#[test]
fn keeps_existing_trailing_slash() {
let de = StrDeserializer::<Error>::new("http://localhost:8001/api/");
let url: Url = deserialize_url_with_trailing_slash(de).unwrap();
assert_eq!(url.as_str(), "http://localhost:8001/api/");
}
}
7 changes: 7 additions & 0 deletions crates/solana-driver/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,21 @@ path = "src/main.rs"

[dependencies]
axum = { workspace = true }
chrono = { workspace = true, default-features = false, features = ["clock"] }
clap = { workspace = true }
configs = { workspace = true }
const-hex = { workspace = true }
cow-solana-rpc = { workspace = true }
futures = { workspace = true }
humantime-serde = { workspace = true }
observe = { workspace = true }
reqwest = { workspace = true, features = ["json"] }
serde = { workspace = true }
serde-ext = { workspace = true }
serde_json = { workspace = true }
serde_with = { workspace = true }
solana-sdk = { workspace = true }
thiserror = { workspace = true }
tikv-jemallocator = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal"] }
tokio-util = { workspace = true }
Expand All @@ -38,6 +43,8 @@ url = { workspace = true, features = ["serde"] }

[dev-dependencies]
cow-solana-rpc = { workspace = true, features = ["test-util"] }
solana-solvers = { path = "../solana-solvers" }
tokio = { workspace = true, features = ["macros", "rt"] }

[features]
default = []
Expand Down
3 changes: 2 additions & 1 deletion crates/solana-driver/example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[chain]
# Placeholder — replace with the CoW Settlement program id.
settlement-program-id = "11111111111111111111111111111111"
settlement-program-id = "MooohhPEAAHwAwEozL7JPEmnDvaahuUpccYN4Yb8ccK"

[rpc]
endpoint = "https://api.mainnet-beta.solana.com"
Expand All @@ -19,4 +19,5 @@ filter = "info,solana_driver=debug"
[[solvers]]
name = "baseline"
endpoint = "http://localhost:8001"
account = "9VXC6LH9eXMBpXLQnxMYAGkjs59Zon2ACciJwQ6iMzNB"
max-in-flight = 1
33 changes: 33 additions & 0 deletions crates/solana-driver/src/domain/auction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//! Domain model of an auction the driver asks solver engines to fill.

use {super::order_uid::OrderUid, serde::Serialize, solana_sdk::pubkey::Pubkey};

/// A collection of orders the driver wants solvers to fill.
#[derive(Clone, Debug)]
pub struct Auction {
pub id: i64,
pub orders: Vec<Order>,
/// Absolute deadline by which solver engines must return solutions. The
/// driver derives each request's timeout as the time remaining until this
/// instant, and skips the request entirely if the deadline has passed.
pub deadline: chrono::DateTime<chrono::Utc>,
}

/// One order available for solvers to fill.
#[derive(Clone, Debug)]
pub struct Order {
pub uid: OrderUid,
pub sell_mint: Pubkey,
pub buy_mint: Pubkey,
/// Sell amount for sells, buy amount for buys.
pub amount: u64,
pub side: Side,
}

/// Direction of the trade.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum Side {
Sell,
Buy,
}
13 changes: 13 additions & 0 deletions crates/solana-driver/src/domain/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//! Domain model of the Solana driver.
//!
//! These types describe the concepts the driver works with — auctions and
//! solutions — independent of any wire format or RPC representation.

pub mod auction;
pub mod order_uid;
pub mod solution;

pub use self::{
auction::{Auction, Order, Side},
solution::{Solution, Trade},
};
44 changes: 44 additions & 0 deletions crates/solana-driver/src/domain/order_uid.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//! CoW Protocol order identifier.

use std::{fmt, str::FromStr};

/// A 32-byte CoW Protocol order identifier, equal to `hash(intent)`, serialized
/// as a `0x`-prefixed hex string on the wire.
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct OrderUid(pub [u8; 32]);

impl fmt::Display for OrderUid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut buffer = const_hex::Buffer::<32, true>::new();
f.write_str(buffer.format(&self.0))
}
}

impl fmt::Debug for OrderUid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self}")
}
}

impl FromStr for OrderUid {
type Err = const_hex::FromHexError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut bytes = [0u8; 32];
const_hex::decode_to_slice(s.strip_prefix("0x").unwrap_or(s), &mut bytes)?;
Ok(Self(bytes))
}
}

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

#[test]
fn order_uid_roundtrip() {
let uid = OrderUid([0xab; 32]);
let text = uid.to_string();
assert_eq!(text, format!("0x{}", "ab".repeat(32)));
assert_eq!(OrderUid::from_str(&text).unwrap(), uid);
}
}
29 changes: 29 additions & 0 deletions crates/solana-driver/src/domain/solution.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//! Domain model of a solver engine's solution.

use {
super::order_uid::OrderUid,
solana_sdk::{instruction::Instruction, pubkey::Pubkey},
};

/// A single solver engine's response to one auction.
#[derive(Clone, Debug)]
pub struct Solution {
pub id: u64,
/// The on-chain identity of the solver that produced this solution.
pub solver: Pubkey,
pub trades: Vec<Trade>,
/// Solana instructions to execute as part of the settlement.
pub interactions: Vec<Instruction>,
/// Address lookup tables the interactions assume.
pub address_lookup_tables: Vec<Pubkey>,
/// Optional solver estimate of total settlement compute units.
pub cu_estimate: Option<u64>,
}

/// A fulfillment of one auction order.
#[derive(Clone, Debug)]
pub struct Trade {
pub order_uid: OrderUid,
/// Sell-token units for sell orders, buy-token units for buy orders.
pub executed_amount: u64,
}
18 changes: 10 additions & 8 deletions crates/solana-driver/src/infra/api/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! HTTP API server.

use {
crate::infra::solver,
axum::{
Router,
extract::DefaultBodyLimit,
Expand All @@ -22,6 +23,8 @@ pub struct Api {
pub addr: SocketAddr,
/// The shared Solana RPC client.
pub rpc: SolanaRPC,
/// Configured solver engines.
pub solvers: Vec<solver::Solver>,
}

impl Api {
Expand Down Expand Up @@ -49,7 +52,7 @@ impl Api {
.layer(TraceLayer::new_for_http().make_span_with(make_span))
.map_request(record_trace_id);

let state = State::new(self.rpc);
let state = State::new(self.rpc, self.solvers);

let app = Router::new()
.route("/healthz", get(routes::healthz))
Expand All @@ -69,22 +72,21 @@ impl Api {
}

/// Shared state available to all route handlers.
///
/// The inner field is not yet read by any handler (the `/solve` and `/settle`
/// handlers are stubs), so `#[expect(dead_code)]` suppresses the unused-field
/// warning until shared state is added.
#[derive(Clone)]
#[expect(dead_code)]
pub struct State(Arc<Inner>);

impl State {
fn new(rpc: SolanaRPC) -> Self {
Self(Arc::new(Inner { rpc }))
/// Build the shared state the handlers operate on.
fn new(rpc: SolanaRPC, solvers: Vec<solver::Solver>) -> Self {
Self(Arc::new(Inner { rpc, solvers }))
}
}

#[expect(dead_code)]
struct Inner {
/// The shared Solana RPC client.
#[expect(dead_code)]
rpc: SolanaRPC,
/// Configured solver engines.
solvers: Vec<solver::Solver>,
}
Loading
Loading