lnm-sdk 0.4.2

Rust SDK for interacting with LN Markets.
Documentation
use std::{num::NonZeroU64, sync::Arc};

use async_trait::async_trait;
use chrono::{DateTime, Utc};
use reqwest::{self, Method};
use serde_json::json;
use uuid::Uuid;

use crate::shared::{
    models::{
        leverage::Leverage,
        price::Price,
        trade::{TradeExecution, TradeSide, TradeSize, TradeStatus},
    },
    rest::{error::Result, lnm::base::LnmRestBase},
};

use super::{
    super::{
        error::RestApiV2Error,
        models::{
            price_history::PriceEntry,
            ticker::Ticker,
            trade::{
                FuturesTradeRequestBody, FuturesUpdateTradeRequestBody, NestedTradesResponse,
                Trade, TradeUpdateType,
            },
        },
        repositories::FuturesRepository,
    },
    path::RestPathV2,
    signature::SignatureGeneratorV2,
};

pub(in crate::api_v2) struct LnmFuturesRepository {
    base: Arc<LnmRestBase<SignatureGeneratorV2>>,
}

impl LnmFuturesRepository {
    pub fn new(base: Arc<LnmRestBase<SignatureGeneratorV2>>) -> Self {
        Self { base }
    }

    async fn update_trade(
        &self,
        id: Uuid,
        update_type: TradeUpdateType,
        value: Price,
    ) -> Result<Trade> {
        let body = FuturesUpdateTradeRequestBody::new(id, update_type, value);

        self.base
            .make_request_with_body(Method::PUT, RestPathV2::FuturesTrade, body, true)
            .await
    }
}

impl crate::sealed::Sealed for LnmFuturesRepository {}

#[async_trait]
impl FuturesRepository for LnmFuturesRepository {
    async fn get_trades(
        &self,
        status: TradeStatus,
        from: Option<DateTime<Utc>>,
        to: Option<DateTime<Utc>>,
        limit: Option<usize>,
    ) -> Result<Vec<Trade>> {
        let mut query_params = Vec::new();

        query_params.push(("type", status.to_string()));

        if let Some(from) = from {
            query_params.push(("from", from.timestamp_millis().to_string()));
        }
        if let Some(to) = to {
            query_params.push(("to", to.timestamp_millis().to_string()));
        }
        if let Some(limit) = limit {
            query_params.push(("limit", limit.to_string()));
        }

        self.base
            .make_request_with_query_params(
                Method::GET,
                RestPathV2::FuturesTrade,
                query_params,
                true,
            )
            .await
    }

    async fn get_trades_open(
        &self,
        from: Option<DateTime<Utc>>,
        to: Option<DateTime<Utc>>,
        limit: Option<usize>,
    ) -> Result<Vec<Trade>> {
        self.get_trades(TradeStatus::Open, from, to, limit).await
    }

    async fn get_trades_running(
        &self,
        from: Option<DateTime<Utc>>,
        to: Option<DateTime<Utc>>,
        limit: Option<usize>,
    ) -> Result<Vec<Trade>> {
        self.get_trades(TradeStatus::Running, from, to, limit).await
    }

    async fn get_trades_closed(
        &self,
        from: Option<DateTime<Utc>>,
        to: Option<DateTime<Utc>>,
        limit: Option<usize>,
    ) -> Result<Vec<Trade>> {
        self.get_trades(TradeStatus::Closed, from, to, limit).await
    }

    async fn price_history(
        &self,
        from: Option<DateTime<Utc>>,
        to: Option<DateTime<Utc>>,
        limit: Option<usize>,
    ) -> Result<Vec<PriceEntry>> {
        let mut query_params = Vec::new();

        if let Some(from) = from {
            query_params.push(("from", from.timestamp_millis().to_string()));
        }
        if let Some(to) = to {
            query_params.push(("to", to.timestamp_millis().to_string()));
        }
        if let Some(limit) = limit {
            query_params.push(("limit", limit.to_string()));
        }

        self.base
            .make_request_with_query_params(
                Method::GET,
                RestPathV2::FuturesPriceHistory,
                query_params,
                false,
            )
            .await
    }

    async fn create_new_trade(
        &self,
        side: TradeSide,
        size: TradeSize,
        leverage: Leverage,
        execution: TradeExecution,
        stoploss: Option<Price>,
        takeprofit: Option<Price>,
    ) -> Result<Trade> {
        let body =
            FuturesTradeRequestBody::new(leverage, stoploss, takeprofit, side, size, execution)
                .map_err(RestApiV2Error::FuturesTradeRequestValidation)?;

        self.base
            .make_request_with_body(Method::POST, RestPathV2::FuturesTrade, body, true)
            .await
    }

    async fn get_trade(&self, id: Uuid) -> Result<Trade> {
        self.base
            .make_request_without_params(Method::GET, RestPathV2::FuturesGetTrade(id), true)
            .await
    }

    async fn cancel_trade(&self, id: Uuid) -> Result<Trade> {
        self.base
            .make_request_with_body(
                Method::POST,
                RestPathV2::FuturesCancelTrade,
                json!({ "id": id }),
                true,
            )
            .await
    }

    async fn cancel_all_trades(&self) -> Result<Vec<Trade>> {
        let res: NestedTradesResponse = self
            .base
            .make_request_without_params(Method::DELETE, RestPathV2::FuturesCancelAllTrades, true)
            .await?;

        Ok(res.trades)
    }

    async fn close_trade(&self, id: Uuid) -> Result<Trade> {
        self.base
            .make_request_with_query_params(
                Method::DELETE,
                RestPathV2::FuturesTrade,
                vec![("id", id.to_string())],
                true,
            )
            .await
    }

    async fn close_all_trades(&self) -> Result<Vec<Trade>> {
        let res: NestedTradesResponse = self
            .base
            .make_request_without_params(Method::DELETE, RestPathV2::FuturesCloseAllTrades, true)
            .await?;

        Ok(res.trades)
    }

    async fn ticker(&self) -> Result<Ticker> {
        self.base
            .make_request_without_params(Method::GET, RestPathV2::FuturesTicker, false)
            .await
    }

    async fn update_trade_stoploss(&self, id: Uuid, stoploss: Price) -> Result<Trade> {
        self.update_trade(id, TradeUpdateType::Stoploss, stoploss)
            .await
    }

    async fn update_trade_takeprofit(&self, id: Uuid, takeprofit: Price) -> Result<Trade> {
        self.update_trade(id, TradeUpdateType::Takeprofit, takeprofit)
            .await
    }

    async fn add_margin(&self, id: Uuid, amount: NonZeroU64) -> Result<Trade> {
        self.base
            .make_request_with_body(
                Method::POST,
                RestPathV2::FuturesAddMargin,
                json!({ "id": id, "amount": amount }),
                true,
            )
            .await
    }

    async fn cash_in(&self, id: Uuid, amount: NonZeroU64) -> Result<Trade> {
        self.base
            .make_request_with_body(
                Method::POST,
                RestPathV2::FuturesCashIn,
                json!({ "id": id, "amount": amount }),
                true,
            )
            .await
    }
}

// Tests disabled: LN Markets disabled API v2 on Mar 31 2026, so live-endpoint tests can no longer
// pass. Kept for reference.
// #[cfg(test)]
// mod tests;