use super::wire::{UserSnapshotBalance, UserSnapshotOrder};
use crate::client::LightconeClient;
use crate::domain::order::UserOrderFillsResponse;
use crate::error::SdkError;
use crate::http::RetryPolicy;
#[cfg(feature = "trigger_orders")]
use crate::program::envelope::TriggerOrderEnvelope;
use crate::program::envelope::{LimitOrderEnvelope, OrderEnvelope};
use crate::program::error::{SdkError as ProgramSdkError, SdkResult};
use crate::program::instructions;
use crate::program::orders::OrderPayload;
use crate::program::types::CloseOrderStatusParams;
use crate::shared::{OrderBookId, PubkeyStr};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use solana_instruction::Instruction;
use solana_pubkey::Pubkey;
use solana_signature::Signature;
use solana_transaction::Transaction;
#[cfg(feature = "native-auth")]
use solana_keypair::Keypair;
#[cfg(feature = "native-auth")]
use solana_signer::Signer;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CancelBody {
pub order_hash: String,
pub maker: PubkeyStr,
pub signature: String,
}
impl CancelBody {
pub fn from_base58(order_hash: String, maker: PubkeyStr, sig_bs58: &str) -> SdkResult<Self> {
let sig = sig_bs58
.parse::<Signature>()
.map_err(|_| ProgramSdkError::InvalidSignature)?;
Ok(Self {
order_hash,
maker,
signature: hex::encode(sig.as_ref()),
})
}
#[cfg(feature = "native-auth")]
pub fn signed(order_hash: String, maker: PubkeyStr, keypair: &Keypair) -> Self {
let message = crate::program::orders::cancel_order_message(&order_hash);
let sig = keypair.sign_message(&message);
Self {
order_hash,
maker,
signature: hex::encode(sig.as_ref()),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CancelAllBody {
pub user_pubkey: PubkeyStr,
#[serde(default)]
pub orderbook_id: OrderBookId,
pub signature: String,
pub timestamp: i64,
pub salt: String,
}
impl CancelAllBody {
pub fn from_base58(
user_pubkey: PubkeyStr,
orderbook_id: OrderBookId,
timestamp: i64,
salt: String,
sig_bs58: &str,
) -> SdkResult<Self> {
let sig = sig_bs58
.parse::<Signature>()
.map_err(|_| ProgramSdkError::InvalidSignature)?;
Ok(Self {
user_pubkey,
orderbook_id,
signature: hex::encode(sig.as_ref()),
timestamp,
salt,
})
}
#[cfg(feature = "native-auth")]
pub fn signed(
user_pubkey: PubkeyStr,
orderbook_id: OrderBookId,
timestamp: i64,
salt: String,
keypair: &Keypair,
) -> Self {
let message = crate::program::orders::cancel_all_message(
user_pubkey.as_str(),
orderbook_id.as_str(),
timestamp,
&salt,
);
let sig = keypair.sign_message(message.as_bytes());
Self {
user_pubkey,
orderbook_id,
signature: hex::encode(sig.as_ref()),
timestamp,
salt,
}
}
}
#[cfg(feature = "trigger_orders")]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CancelTriggerBody {
pub trigger_order_id: String,
pub maker: PubkeyStr,
pub signature: String,
}
#[cfg(feature = "trigger_orders")]
impl CancelTriggerBody {
pub fn from_base58(
trigger_order_id: String,
maker: PubkeyStr,
sig_bs58: &str,
) -> SdkResult<Self> {
let sig = sig_bs58
.parse::<Signature>()
.map_err(|_| ProgramSdkError::InvalidSignature)?;
Ok(Self {
trigger_order_id,
maker,
signature: hex::encode(sig.as_ref()),
})
}
#[cfg(feature = "native-auth")]
pub fn signed(trigger_order_id: String, maker: PubkeyStr, keypair: &Keypair) -> Self {
let message = crate::program::orders::cancel_trigger_order_message(&trigger_order_id);
let sig = keypair.sign_message(&message);
Self {
trigger_order_id,
maker,
signature: hex::encode(sig.as_ref()),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FillInfo {
pub counterparty: PubkeyStr,
pub counterparty_order_hash: String,
pub fill_amount: Decimal,
pub price: Decimal,
pub is_maker: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SubmitOrderResponse {
pub order_hash: String,
pub remaining: Decimal,
pub filled: Decimal,
pub fills: Vec<FillInfo>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CancelSuccess {
pub order_hash: String,
pub remaining: Decimal,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CancelAllSuccess {
pub cancelled_order_hashes: Vec<String>,
pub count: u64,
pub user_pubkey: PubkeyStr,
pub orderbook_id: OrderBookId,
pub message: String,
}
#[cfg(feature = "trigger_orders")]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TriggerOrderResponse {
pub trigger_order_id: String,
pub order_hash: String,
}
#[cfg(feature = "trigger_orders")]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CancelTriggerSuccess {
pub trigger_order_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct UserOrdersResponse {
pub user_pubkey: PubkeyStr,
pub orders: Vec<UserSnapshotOrder>,
pub balances: Vec<UserSnapshotBalance>,
pub next_cursor: Option<String>,
pub has_more: bool,
}
pub struct Orders<'a> {
pub(crate) client: &'a LightconeClient,
}
impl<'a> Orders<'a> {
pub fn status_pda(&self, order_hash: &[u8; 32]) -> Pubkey {
crate::program::pda::get_order_status_pda(order_hash, &self.client.program_id).0
}
pub fn nonce_pda(&self, user: &Pubkey) -> Pubkey {
crate::program::pda::get_user_nonce_pda(user, &self.client.program_id).0
}
pub async fn limit_order(&self) -> LimitOrderEnvelope {
let deposit_source = self.client.deposit_source().await;
LimitOrderEnvelope::new().deposit_source(deposit_source)
}
#[cfg(feature = "trigger_orders")]
pub async fn trigger_order(&self) -> TriggerOrderEnvelope {
let deposit_source = self.client.deposit_source().await;
TriggerOrderEnvelope::new().deposit_source(deposit_source)
}
pub fn generate_cancel_all_salt(&self) -> String {
crate::program::orders::generate_cancel_all_salt()
}
pub async fn submit(
&self,
request: &impl serde::Serialize,
) -> Result<SubmitOrderResponse, SdkError> {
let url = format!("{}/api/orders/submit", self.client.http.base_url());
self.client
.http
.post(&url, request, RetryPolicy::None)
.await
}
pub async fn cancel(&self, body: &CancelBody) -> Result<CancelSuccess, SdkError> {
let url = format!("{}/api/orders/cancel", self.client.http.base_url());
self.client.http.post(&url, body, RetryPolicy::None).await
}
pub async fn cancel_all(&self, body: &CancelAllBody) -> Result<CancelAllSuccess, SdkError> {
let url = format!("{}/api/orders/cancel-all", self.client.http.base_url());
self.client.http.post(&url, body, RetryPolicy::None).await
}
#[cfg(feature = "trigger_orders")]
pub async fn submit_trigger(
&self,
request: &impl serde::Serialize,
) -> Result<TriggerOrderResponse, SdkError> {
let url = format!("{}/api/orders/submit", self.client.http.base_url());
self.client
.http
.post(&url, request, RetryPolicy::None)
.await
}
#[cfg(feature = "trigger_orders")]
pub async fn cancel_trigger(
&self,
body: &CancelTriggerBody,
) -> Result<CancelTriggerSuccess, SdkError> {
let url = format!("{}/api/orders/cancel", self.client.http.base_url());
self.client.http.post(&url, body, RetryPolicy::None).await
}
pub async fn get_user_orders(
&self,
limit: Option<u32>,
cursor: Option<&str>,
) -> Result<UserOrdersResponse, SdkError> {
let url = format!("{}/api/users/orders", self.client.http.base_url());
let mut query = Vec::new();
if let Some(limit) = limit {
query.push(("limit", limit.to_string()));
}
if let Some(cursor) = cursor {
query.push(("cursor", cursor.to_string()));
}
self.client
.http
.get_with_query(&url, &query, RetryPolicy::Idempotent)
.await
}
pub async fn get_user_orders_with_cookies(
&self,
limit: Option<u32>,
cursor: Option<&str>,
cookie_header: &str,
) -> Result<UserOrdersResponse, SdkError> {
let url = format!("{}/api/users/orders", self.client.http.base_url());
let mut query = Vec::new();
if let Some(limit) = limit {
query.push(("limit", limit.to_string()));
}
if let Some(cursor) = cursor {
query.push(("cursor", cursor.to_string()));
}
self.client
.http
.get_with_cookies_and_query(&url, &query, RetryPolicy::Idempotent, cookie_header)
.await
}
pub async fn get_user_order_fills(
&self,
market_pubkey: Option<&str>,
limit: Option<u32>,
cursor: Option<&str>,
) -> Result<UserOrderFillsResponse, SdkError> {
let url = format!("{}/api/users/order-fills", self.client.http.base_url());
let mut query = Vec::new();
if let Some(market_pubkey) = market_pubkey {
query.push(("market_pubkey", market_pubkey.to_string()));
}
if let Some(limit) = limit {
query.push(("limit", limit.to_string()));
}
if let Some(cursor) = cursor {
query.push(("cursor", cursor.to_string()));
}
self.client
.http
.get_with_query(&url, &query, RetryPolicy::Idempotent)
.await
}
pub async fn get_user_order_fills_with_cookies(
&self,
market_pubkey: Option<&str>,
limit: Option<u32>,
cursor: Option<&str>,
cookie_header: &str,
) -> Result<UserOrderFillsResponse, SdkError> {
let url = format!("{}/api/users/order-fills", self.client.http.base_url());
let mut query = Vec::new();
if let Some(market_pubkey) = market_pubkey {
query.push(("market_pubkey", market_pubkey.to_string()));
}
if let Some(limit) = limit {
query.push(("limit", limit.to_string()));
}
if let Some(cursor) = cursor {
query.push(("cursor", cursor.to_string()));
}
self.client
.http
.get_with_cookies_and_query(&url, &query, RetryPolicy::Idempotent, cookie_header)
.await
}
pub async fn get_user_order_fills_by_wallet(
&self,
wallet_address: &str,
market_pubkey: Option<&str>,
limit: Option<u32>,
cursor: Option<&str>,
) -> Result<UserOrderFillsResponse, SdkError> {
let url = format!(
"{}/api/users/{}/order-fills",
self.client.http.base_url(),
wallet_address
);
let mut query = Vec::new();
if let Some(market_pubkey) = market_pubkey {
query.push(("market_pubkey", market_pubkey.to_string()));
}
if let Some(limit) = limit {
query.push(("limit", limit.to_string()));
}
if let Some(cursor) = cursor {
query.push(("cursor", cursor.to_string()));
}
self.client
.http
.get_with_query(&url, &query, RetryPolicy::Idempotent)
.await
}
pub async fn cancel_order_signed(
&self,
order_hash: &str,
maker: &PubkeyStr,
) -> Result<CancelSuccess, SdkError> {
use crate::shared::signing::SigningStrategy;
let strategy = self.client.signing_strategy().await.ok_or_else(|| {
SdkError::Validation("signing strategy is not set on the client".into())
})?;
match strategy {
#[cfg(feature = "native-auth")]
SigningStrategy::Native(keypair) => {
let body = CancelBody::signed(order_hash.to_string(), maker.clone(), &keypair);
self.cancel(&body).await
}
SigningStrategy::WalletAdapter(signer) => {
let message = crate::program::orders::cancel_order_message(order_hash);
let sig_bytes = signer
.sign_message(&message)
.await
.map_err(crate::shared::signing::classify_signer_error)?;
let sig_bs58 = bs58::encode(&sig_bytes).into_string();
let body =
CancelBody::from_base58(order_hash.to_string(), maker.clone(), &sig_bs58)
.map_err(|error| SdkError::Program(error))?;
self.cancel(&body).await
}
}
}
pub async fn cancel_all_signed(
&self,
user_pubkey: &PubkeyStr,
timestamp: i64,
salt: &str,
orderbook_id: Option<&OrderBookId>,
) -> Result<CancelAllSuccess, SdkError> {
use crate::shared::signing::SigningStrategy;
let strategy = self.client.signing_strategy().await.ok_or_else(|| {
SdkError::Validation("signing strategy is not set on the client".into())
})?;
let resolved_orderbook_id = orderbook_id
.cloned()
.unwrap_or_else(|| OrderBookId::from(""));
let orderbook_id_str = resolved_orderbook_id.as_str();
match strategy {
#[cfg(feature = "native-auth")]
SigningStrategy::Native(keypair) => {
let body = CancelAllBody::signed(
user_pubkey.clone(),
resolved_orderbook_id.clone(),
timestamp,
salt.to_string(),
&keypair,
);
self.cancel_all(&body).await
}
SigningStrategy::WalletAdapter(signer) => {
let message = crate::program::orders::cancel_all_message(
user_pubkey.as_str(),
orderbook_id_str,
timestamp,
salt,
);
let sig_bytes = signer
.sign_message(message.as_bytes())
.await
.map_err(crate::shared::signing::classify_signer_error)?;
let sig_bs58 = bs58::encode(&sig_bytes).into_string();
let body = CancelAllBody::from_base58(
user_pubkey.clone(),
resolved_orderbook_id.clone(),
timestamp,
salt.to_string(),
&sig_bs58,
)
.map_err(|error| SdkError::Program(error))?;
self.cancel_all(&body).await
}
}
}
#[cfg(feature = "trigger_orders")]
pub async fn cancel_trigger_signed(
&self,
trigger_order_id: &str,
maker: &PubkeyStr,
) -> Result<CancelTriggerSuccess, SdkError> {
use crate::shared::signing::SigningStrategy;
let strategy = self.client.signing_strategy().await.ok_or_else(|| {
SdkError::Validation("signing strategy is not set on the client".into())
})?;
match strategy {
#[cfg(feature = "native-auth")]
SigningStrategy::Native(keypair) => {
let body = CancelTriggerBody::signed(
trigger_order_id.to_string(),
maker.clone(),
&keypair,
);
self.cancel_trigger(&body).await
}
SigningStrategy::WalletAdapter(signer) => {
let message =
crate::program::orders::cancel_trigger_order_message(trigger_order_id);
let sig_bytes = signer
.sign_message(&message)
.await
.map_err(crate::shared::signing::classify_signer_error)?;
let sig_bs58 = bs58::encode(&sig_bytes).into_string();
let body = CancelTriggerBody::from_base58(
trigger_order_id.to_string(),
maker.clone(),
&sig_bs58,
)
.map_err(|error| SdkError::Program(error))?;
self.cancel_trigger(&body).await
}
}
}
pub fn cancel_order_ix(
&self,
operator: &Pubkey,
market: &Pubkey,
order: &OrderPayload,
) -> Instruction {
let pid = &self.client.program_id;
instructions::build_cancel_order_ix(operator, market, order, pid)
}
pub fn cancel_order_tx(
&self,
operator: &Pubkey,
market: &Pubkey,
order: &OrderPayload,
) -> Result<Transaction, SdkError> {
let ix = self.cancel_order_ix(operator, market, order);
Ok(Transaction::new_with_payer(&[ix], Some(operator)))
}
pub fn increment_nonce_ix(&self, user: &Pubkey) -> Instruction {
let pid = &self.client.program_id;
instructions::build_increment_nonce_ix(user, pid)
}
pub fn increment_nonce_tx(&self, user: &Pubkey) -> Result<Transaction, SdkError> {
let ix = self.increment_nonce_ix(user);
Ok(Transaction::new_with_payer(&[ix], Some(user)))
}
pub fn close_order_status_ix(&self, params: &CloseOrderStatusParams) -> Instruction {
let pid = &self.client.program_id;
instructions::build_close_order_status_ix(params, pid)
}
pub fn close_order_status_tx(
&self,
params: CloseOrderStatusParams,
) -> Result<Transaction, SdkError> {
let ix = self.close_order_status_ix(¶ms);
Ok(Transaction::new_with_payer(&[ix], Some(¶ms.operator)))
}
pub fn create_bid_order(&self, params: crate::program::types::BidOrderParams) -> OrderPayload {
OrderPayload::new_bid(params)
}
pub fn create_ask_order(&self, params: crate::program::types::AskOrderParams) -> OrderPayload {
OrderPayload::new_ask(params)
}
#[cfg(feature = "native-auth")]
pub fn create_signed_bid_order(
&self,
params: crate::program::types::BidOrderParams,
keypair: &Keypair,
) -> OrderPayload {
OrderPayload::new_bid_signed(params, keypair)
}
#[cfg(feature = "native-auth")]
pub fn create_signed_ask_order(
&self,
params: crate::program::types::AskOrderParams,
keypair: &Keypair,
) -> OrderPayload {
OrderPayload::new_ask_signed(params, keypair)
}
pub fn hash_order(&self, order: &OrderPayload) -> [u8; 32] {
order.hash()
}
#[cfg(feature = "native-auth")]
pub fn sign_order(&self, order: &mut OrderPayload, keypair: &Keypair) {
order.sign(keypair);
}
}
#[cfg(feature = "solana-rpc")]
impl<'a> Orders<'a> {
pub async fn get_status(
&self,
order_hash: &[u8; 32],
) -> Result<Option<crate::program::accounts::OrderStatus>, SdkError> {
let rpc = crate::rpc::resolve_solana_rpc(self.client).await?;
let pda = self.status_pda(order_hash);
match rpc.get_account(&pda).await {
Ok(account) => Ok(Some(crate::program::accounts::OrderStatus::deserialize(
&account.data,
)?)),
Err(_) => Ok(None),
}
}
pub async fn get_nonce(&self, user: &Pubkey) -> Result<u64, SdkError> {
let rpc = crate::rpc::resolve_solana_rpc(self.client).await?;
let pda = self.nonce_pda(user);
match rpc.get_account(&pda).await {
Ok(account) => {
let user_nonce = crate::program::accounts::UserNonce::deserialize(&account.data)?;
Ok(user_nonce.nonce)
}
Err(_) => Ok(0),
}
}
pub async fn current_nonce(&self, user: &Pubkey) -> Result<u32, SdkError> {
let nonce = self.get_nonce(user).await?;
u32::try_from(nonce)
.map_err(|_| SdkError::Program(crate::program::error::SdkError::Overflow))
}
}