Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1318ba6
Serve one order with its fill state and status by uid
squadgazzz Aug 20, 2026
1730988
Let a full fill outrank the reclaim cancellation like the EVM status …
squadgazzz Aug 20, 2026
8f6270b
Drop the passthrough uid parse test
squadgazzz Aug 20, 2026
f03c780
Match the buy kind explicitly and pin the buy-side fill check
squadgazzz Aug 20, 2026
c255cab
Serve trades by order uid or owner
squadgazzz Aug 21, 2026
c7b0638
Trim comments to essence
squadgazzz Aug 21, 2026
82a6e02
Merge remote-tracking branch 'origin/main' into solana-orderbook/be-2…
squadgazzz Aug 21, 2026
9c765ec
Trim comments to essence
squadgazzz Aug 21, 2026
986720b
Merge branch 'solana-orderbook/be-223-get-order' into solana-orderboo…
squadgazzz Aug 21, 2026
632284e
Return structured error bodies from the trades endpoint
squadgazzz Aug 21, 2026
23fcc54
Return structured error bodies from the order endpoint
squadgazzz Aug 21, 2026
b50d653
Merge branch 'solana-orderbook/be-223-get-order' into solana-orderboo…
squadgazzz Aug 21, 2026
c575e2c
Serve the trade's instruction index
squadgazzz Aug 21, 2026
c13dffc
Drop the fee amount from the trades response
squadgazzz Aug 21, 2026
5c10659
Look up the settlement slot per trade instead of deduplicating the table
squadgazzz Aug 21, 2026
01c00dd
Describe the order endpoint in an openapi spec
squadgazzz Aug 21, 2026
15a2317
Merge branch 'solana-orderbook/be-223-get-order' into solana-orderboo…
squadgazzz Aug 21, 2026
c2973b0
Describe the trades endpoint in the openapi spec
squadgazzz Aug 21, 2026
fa60c54
Address review comments
squadgazzz Aug 21, 2026
9d8a328
Merge branch 'solana-orderbook/be-223-get-order' into solana-orderboo…
squadgazzz Aug 21, 2026
6b56142
Merge remote-tracking branch 'origin/main' into solana-orderbook/be-2…
squadgazzz Aug 25, 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
2 changes: 1 addition & 1 deletion .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ jobs:
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
# Start the build process in the background. The following cargo test command will automatically
# wait for the build process to be done before proceeding.
- run: cargo build -p orderbook -p database -p autopilot --tests &
- run: cargo build -p orderbook -p database -p autopilot -p solana-indexer -p solana-orderbook --tests &
- uses: taiki-e/install-action@4684b8405694ae9dd42c9f39ba901a70ae83f4a3 # v2.82.9
with:
tool: nextest
Expand Down
89 changes: 89 additions & 0 deletions crates/solana-orderbook/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,40 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
/api/v1/trades:
get:
operationId: getTrades
description: |
Trades of one order or one owner. Exactly one of `orderUid` and
`owner` must be set.
parameters:
- name: orderUid
in: query
required: false
schema:
$ref: "#/components/schemas/OrderUid"
- name: owner
in: query
required: false
schema:
$ref: "#/components/schemas/Pubkey"
responses:
"200":
description: The trades, ordered by slot.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Trade"
"400":
description: |
Both or neither filter set (`InvalidTradeFilter`), or a malformed
filter value (`InvalidOrderUid`, `InvalidOwner`).
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
components:
schemas:
OrderUid:
Expand Down Expand Up @@ -136,6 +170,61 @@ components:
- $ref: "#/components/schemas/TokenAmount"
status:
$ref: "#/components/schemas/OrderStatus"
Signature:
description: A base58-encoded Solana transaction signature.
type: string
example: "BUguQsv2ZuHus54HAFzjdJHzZBkygAjKhEeYwSG19tUfUyvvz3worsdQCdAXDNjakJHioSiyxhFiDJrm8XpSXRA"
Trade:
description: One fill of an order.
type: object
required:
- orderUid
- owner
- sellToken
- buyToken
- sellAmount
- buyAmount
- txSignature
- instructionIndex
properties:
orderUid:
$ref: "#/components/schemas/OrderUid"
owner:
$ref: "#/components/schemas/Pubkey"
sellToken:
description: The mint of the token sold.
allOf:
- $ref: "#/components/schemas/Pubkey"
buyToken:
description: The mint of the token bought.
allOf:
- $ref: "#/components/schemas/Pubkey"
sellAmount:
description: Sell tokens pulled by this fill.
allOf:
- $ref: "#/components/schemas/TokenAmount"
buyAmount:
description: Buy tokens paid by this fill.
allOf:
- $ref: "#/components/schemas/TokenAmount"
txSignature:
description: The settlement transaction.
allOf:
- $ref: "#/components/schemas/Signature"
instructionIndex:
description: |
Position of the settlement instruction within its transaction,
disambiguating multiple fills of one order in the same
transaction.
type: integer
format: int32
slot:
description: |
Slot the settlement landed in. Absent while the settlement row
has not been indexed.
type: integer
format: int64
nullable: true
Error:
description: A machine-readable error type and a human-readable description.
type: object
Expand Down
1 change: 1 addition & 0 deletions crates/solana-orderbook/src/infra/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ impl Api {
let app = Router::new()
.route("/healthz", get(routes::healthz))
.route("/api/v1/orders/{uid}", get(routes::order))
.route("/api/v1/trades", get(routes::trades))
.layer(cors)
.layer(RequestDecompressionLayer::new())
.layer(tracing_layer)
Expand Down
3 changes: 2 additions & 1 deletion crates/solana-orderbook/src/infra/api/routes/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod healthz;
mod order;
mod trades;

pub use {healthz::healthz, order::order};
pub use {healthz::healthz, order::order, trades::trades};
85 changes: 85 additions & 0 deletions crates/solana-orderbook/src/infra/api/routes/trades/dto.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//! Wire shape of the trades endpoint.

use {
crate::infra::db::TradeRow,
bigdecimal::BigDecimal,
serde::Serialize,
serde_with::{DisplayFromStr, serde_as},
solana_sdk::{pubkey::Pubkey, signature::Signature},
};

/// One trade on the wire.
#[serde_as]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Trade {
pub order_uid: String,
#[serde_as(as = "DisplayFromStr")]
pub owner: Pubkey,
#[serde_as(as = "DisplayFromStr")]
pub sell_token: Pubkey,
#[serde_as(as = "DisplayFromStr")]
pub buy_token: Pubkey,
#[serde_as(as = "DisplayFromStr")]
pub sell_amount: BigDecimal,
#[serde_as(as = "DisplayFromStr")]
pub buy_amount: BigDecimal,
#[serde_as(as = "DisplayFromStr")]
pub tx_signature: Signature,
/// Position of the settlement instruction within its transaction,
/// disambiguating multiple fills of one order in the same transaction.
pub instruction_index: i32,
/// Slot the settlement landed in. Absent while the settlement row has
/// not been indexed.
pub slot: Option<i64>,
}

impl From<TradeRow> for Trade {
fn from(row: TradeRow) -> Self {
Self {
order_uid: format!("0x{}", const_hex::encode(row.order_uid.0)),
owner: Pubkey::new_from_array(row.owner.0),
sell_token: Pubkey::new_from_array(row.sell_token.0),
buy_token: Pubkey::new_from_array(row.buy_token.0),
sell_amount: row.sell_amount,
buy_amount: row.buy_amount,
tx_signature: Signature::from(row.tx_signature.0),
instruction_index: row.instruction_index,
slot: row.slot,
}
}
}

#[cfg(test)]
mod tests {
use {super::*, database::byte_array::ByteArray};

#[test]
fn wire_format_is_stable() {
let trade = Trade::from(TradeRow {
order_uid: ByteArray([0x11; 32]),
owner: ByteArray([0x22; 32]),
sell_token: ByteArray([0x33; 32]),
buy_token: ByteArray([0x44; 32]),
sell_amount: 1_000.into(),
buy_amount: 500.into(),
tx_signature: ByteArray([9; 64]),
instruction_index: 3,
slot: Some(42),
});
let json = serde_json::to_value(&trade).unwrap();
assert_eq!(json["orderUid"], format!("0x{}", "11".repeat(32)));
assert_eq!(
json["owner"],
"3JF3sEqM796hk5WFqA6EtmEwJQ9quALszsfJyvXNQKy3"
);
assert_eq!(json["sellAmount"], "1000");
assert_eq!(json["buyAmount"], "500");
assert_eq!(
json["txSignature"],
"BUguQsv2ZuHus54HAFzjdJHzZBkygAjKhEeYwSG19tUfUyvvz3worsdQCdAXDNjakJHioSiyxhFiDJrm8XpSXRA"
);
assert_eq!(json["instructionIndex"], 3);
assert_eq!(json["slot"], 42);
}
}
69 changes: 69 additions & 0 deletions crates/solana-orderbook/src/infra/api/routes/trades/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//! The trades endpoint: fills by order uid or owner.

pub mod dto;

use {
crate::infra::{
api::{State, error},
db,
},
axum::{Json, extract::Query, http::StatusCode},
serde::Deserialize,
solana_sdk::pubkey::Pubkey,
std::str::FromStr,
};

/// Query parameters. Exactly one of `orderUid` or `owner` must be set.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Params {
pub order_uid: Option<String>,
pub owner: Option<String>,
}

/// Handle `GET /api/v1/trades`.
pub async fn trades(
state: axum::extract::State<State>,
Query(params): Query<Params>,
) -> Result<Json<Vec<dto::Trade>>, error::Reply> {
let (order_uid, owner) = match (&params.order_uid, &params.owner) {
(Some(uid), None) => (
Some(const_hex::decode_to_array(uid).map_err(|_| {
error::reply(
StatusCode::BAD_REQUEST,
"InvalidOrderUid",
"orderUid must be 32 bytes of hex",
)
})?),
None,
),
(None, Some(owner)) => (
None,
Some(
Pubkey::from_str(owner)
.map_err(|_| {
error::reply(
StatusCode::BAD_REQUEST,
"InvalidOwner",
"owner must be a base58-encoded public key",
)
})?
.to_bytes(),
),
),
_ => {
return Err(error::reply(
StatusCode::BAD_REQUEST,
"InvalidTradeFilter",
"Must specify exactly one of owner or orderUid.",
));
}
};
let rows = db::trades(state.pool(), order_uid, owner)
.await
.map_err(|err| {
tracing::error!(?err, "trades lookup failed");
error::reply(StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", "")
})?;
Ok(Json(rows.into_iter().map(dto::Trade::from).collect()))
}
Loading
Loading