use alloy_primitives::{Address, B256};
use serde::{Deserialize, Serialize};
use crate::app_data::AppDataHash;
use crate::cancellation::{SignedOrderCancellation, SignedOrderCancellations};
use crate::chain::Chain;
use crate::error::{Error, Result};
use crate::order::OrderUid;
use crate::signature::{EcdsaSignature, ecdsa_wire};
use crate::signing_scheme::EcdsaSigningScheme;
use crate::transport::{HttpMethod, HttpRequest, HttpTransport};
use super::orders::{Order, OrderCreation};
use super::quote::{OrderQuoteResponse, QuoteRequest};
use super::types::{
AppDataDocument, Auction, AuctionStatus, NativePrice, TokenMetadata, TotalSurplus, Trade,
};
#[cfg(feature = "http-client")]
use crate::transport::DefaultTransport;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct OrdersByUidsRequest<'a> {
pub(crate) order_uids: &'a [OrderUid],
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct CancellationPayload {
#[serde(with = "ecdsa_wire")]
signature: EcdsaSignature,
signing_scheme: EcdsaSigningScheme,
}
#[cfg(feature = "http-client")]
#[derive(Debug, Clone)]
pub struct OrderBookApi<T = DefaultTransport> {
base_url: url::Url,
transport: T,
chain: Option<Chain>,
}
#[cfg(not(feature = "http-client"))]
#[derive(Debug, Clone)]
pub struct OrderBookApi<T> {
base_url: url::Url,
transport: T,
chain: Option<Chain>,
}
impl<T: HttpTransport> OrderBookApi<T> {
pub fn new_with_transport(base_url: url::Url, transport: T) -> Self {
Self {
base_url: ensure_trailing_slash(base_url),
transport,
chain: None,
}
}
#[must_use]
pub const fn with_chain_hint(mut self, chain: Chain) -> Self {
self.chain = Some(chain);
self
}
pub const fn base_url(&self) -> &url::Url {
&self.base_url
}
pub const fn chain(&self) -> Option<Chain> {
self.chain
}
pub async fn quote(&self, request: &QuoteRequest) -> Result<OrderQuoteResponse> {
request.validate()?;
self.post_json(self.endpoint("api/v1/quote")?, request)
.await
}
pub async fn post_order(&self, order: &OrderCreation) -> Result<OrderUid> {
if let Some(chain) = self.chain {
order.verify_owner(&chain.settlement_domain())?;
}
self.post_json(self.endpoint("api/v1/orders")?, order).await
}
pub async fn order(&self, uid: &OrderUid) -> Result<Order> {
self.get_json(self.endpoint(&format!("api/v1/orders/{uid}"))?)
.await
}
pub async fn order_status(&self, uid: &OrderUid) -> Result<AuctionStatus> {
self.get_json(self.endpoint(&format!("api/v1/orders/{uid}/status"))?)
.await
}
pub async fn poll_until<P, S, Fut>(
&self,
uid: &OrderUid,
mut should_stop: P,
mut sleep: S,
) -> Result<Order>
where
P: FnMut(&Order) -> bool,
S: FnMut() -> Fut,
Fut: core::future::Future<Output = ()>,
{
loop {
let order = self.order(uid).await?;
if should_stop(&order) {
return Ok(order);
}
sleep().await;
}
}
pub async fn account_orders(
&self,
owner: Address,
offset: Option<u32>,
limit: Option<u32>,
) -> Result<Vec<Order>> {
let url = self.endpoint(&format!("api/v1/account/{owner:?}/orders"))?;
self.get_json_paginated(url, offset, limit).await
}
pub async fn orders_by_uids(&self, uids: &[OrderUid]) -> Result<Vec<Order>> {
self.post_json(
self.endpoint("api/v1/orders/by_uids")?,
&OrdersByUidsRequest { order_uids: uids },
)
.await
}
pub async fn trades_by_owner(
&self,
owner: Address,
offset: Option<u32>,
limit: Option<u32>,
) -> Result<Vec<Trade>> {
let mut url = self.endpoint("api/v2/trades")?;
url.query_pairs_mut()
.append_pair("owner", &format!("{owner:?}"));
self.get_json_paginated(url, offset, limit).await
}
pub async fn trades_by_order_uid(
&self,
uid: &OrderUid,
offset: Option<u32>,
limit: Option<u32>,
) -> Result<Vec<Trade>> {
let mut url = self.endpoint("api/v2/trades")?;
url.query_pairs_mut()
.append_pair("orderUid", &uid.to_string());
self.get_json_paginated(url, offset, limit).await
}
pub async fn native_price(&self, token: Address) -> Result<NativePrice> {
self.get_json(self.endpoint(&format!("api/v1/token/{token:?}/native_price"))?)
.await
}
pub async fn token_metadata(&self, token: Address) -> Result<TokenMetadata> {
self.get_json(self.endpoint(&format!("api/v1/token/{token:?}/metadata"))?)
.await
}
pub async fn orders_by_tx(&self, tx_hash: B256) -> Result<Vec<Order>> {
self.get_json(self.endpoint(&format!("api/v1/transactions/{tx_hash:?}/orders"))?)
.await
}
pub async fn auction(&self) -> Result<Auction> {
self.get_json(self.endpoint("api/v1/auction")?).await
}
pub async fn total_surplus(&self, user: Address) -> Result<TotalSurplus> {
self.get_json(self.endpoint(&format!("api/v1/users/{user:?}/total_surplus"))?)
.await
}
pub async fn app_data(&self, hash: &AppDataHash) -> Result<AppDataDocument> {
let document: AppDataDocument = self
.get_json(self.endpoint(&format!("api/v1/app_data/{hash}"))?)
.await?;
let computed = document.computed_hash();
if computed != *hash {
return Err(Error::AppDataHashMismatch {
expected: hash.to_string(),
computed: computed.to_string(),
});
}
Ok(document)
}
pub async fn put_app_data(&self, hash: &AppDataHash, document: &AppDataDocument) -> Result<()> {
let computed = document.computed_hash();
if computed != *hash {
return Err(Error::AppDataHashMismatch {
expected: hash.to_string(),
computed: computed.to_string(),
});
}
self.send(
HttpMethod::Put,
self.endpoint(&format!("api/v1/app_data/{hash}"))?,
Some(serde_json::to_vec(document)?),
)
.await?
.decode_empty()
}
pub async fn upload_app_data(&self, document: &AppDataDocument) -> Result<AppDataHash> {
let computed = document.computed_hash();
let server_hash: AppDataHash = self
.put_json(self.endpoint("api/v1/app_data")?, document)
.await?;
if server_hash != computed {
return Err(Error::AppDataHashMismatch {
expected: server_hash.to_string(),
computed: computed.to_string(),
});
}
Ok(server_hash)
}
pub async fn version(&self) -> Result<String> {
self.send(HttpMethod::Get, self.endpoint("api/v1/version")?, None)
.await?
.decode_text()
}
pub async fn cancel_orders(&self, signed: &SignedOrderCancellations) -> Result<()> {
self.send(
HttpMethod::Delete,
self.endpoint("api/v1/orders")?,
Some(serde_json::to_vec(signed)?),
)
.await?
.decode_empty()
}
pub async fn cancel_order(&self, cancellation: &SignedOrderCancellation) -> Result<()> {
let body = CancellationPayload {
signature: cancellation.signature,
signing_scheme: cancellation.signing_scheme,
};
self.send(
HttpMethod::Delete,
self.endpoint(&format!("api/v1/orders/{}", cancellation.order_uid))?,
Some(serde_json::to_vec(&body)?),
)
.await?
.decode_empty()
}
fn endpoint(&self, path: &str) -> Result<url::Url> {
Ok(self.base_url.join(path)?)
}
async fn send(
&self,
method: HttpMethod,
url: url::Url,
json_body: Option<Vec<u8>>,
) -> Result<crate::transport::HttpResponse> {
self.transport
.execute(HttpRequest {
method,
url,
json_body,
bearer: None,
})
.await
}
async fn post_json<TReq, TResp>(&self, url: url::Url, body: &TReq) -> Result<TResp>
where
TReq: Serialize + ?Sized,
TResp: for<'de> Deserialize<'de>,
{
self.send(HttpMethod::Post, url, Some(serde_json::to_vec(body)?))
.await?
.decode_json()
}
async fn put_json<TReq, TResp>(&self, url: url::Url, body: &TReq) -> Result<TResp>
where
TReq: Serialize + ?Sized,
TResp: for<'de> Deserialize<'de>,
{
self.send(HttpMethod::Put, url, Some(serde_json::to_vec(body)?))
.await?
.decode_json()
}
async fn get_json<TResp>(&self, url: url::Url) -> Result<TResp>
where
TResp: for<'de> Deserialize<'de>,
{
self.send(HttpMethod::Get, url, None).await?.decode_json()
}
async fn get_json_paginated<TResp>(
&self,
mut url: url::Url,
offset: Option<u32>,
limit: Option<u32>,
) -> Result<TResp>
where
TResp: for<'de> Deserialize<'de>,
{
append_pagination(&mut url.query_pairs_mut(), offset, limit);
self.get_json(url).await
}
}
pub(crate) fn ensure_trailing_slash(mut url: url::Url) -> url::Url {
if !url.path().ends_with('/') {
let new_path = format!("{}/", url.path());
url.set_path(&new_path);
}
url
}
fn append_pagination(
q: &mut url::form_urlencoded::Serializer<'_, url::UrlQuery<'_>>,
offset: Option<u32>,
limit: Option<u32>,
) {
if let Some(offset) = offset {
q.append_pair("offset", &offset.to_string());
}
if let Some(limit) = limit {
q.append_pair("limit", &limit.to_string());
}
}