#![expect(
clippy::missing_errors_doc,
reason = "errors documented on underlying Rust methods"
)]
pub mod config;
pub mod factories;
use std::time::{SystemTime, UNIX_EPOCH};
use nautilus_common::factories::{ClientConfig, DataClientFactory, ExecutionClientFactory};
use nautilus_core::python::{to_pyruntime_err, to_pyvalue_err};
use nautilus_system::get_global_pyo3_registry;
use pyo3::prelude::*;
use crate::{
common::{
consts::{LIGHTER, LIGHTER_NAUTILUS_INTEGRATOR_ACCOUNT_INDEX},
credential::Credential,
enums::LighterEnvironment,
urls::lighter_chain_id,
},
config::{LighterDataClientConfig, LighterExecClientConfig},
factories::{LighterDataClientFactory, LighterExecutionClientFactory},
http::{
client::{LighterHttpClient, LighterRawHttpClient},
models::LighterSendTxRequest,
},
signing::{
auth_token::fresh_k,
tx::{ApproveIntegratorTxInfo, LighterTx, TxContext, TxInfoJson, sign_tx},
},
};
const TX_EXPIRY_MS: i64 = 5 * 60 * 1_000;
#[expect(clippy::needless_pass_by_value)]
fn extract_lighter_data_factory(
py: Python<'_>,
factory: Py<PyAny>,
) -> PyResult<Box<dyn DataClientFactory>> {
match factory.extract::<LighterDataClientFactory>(py) {
Ok(f) => Ok(Box::new(f)),
Err(e) => Err(to_pyvalue_err(format!(
"Failed to extract LighterDataClientFactory: {e}"
))),
}
}
#[expect(clippy::needless_pass_by_value)]
fn extract_lighter_exec_factory(
py: Python<'_>,
factory: Py<PyAny>,
) -> PyResult<Box<dyn ExecutionClientFactory>> {
match factory.extract::<LighterExecutionClientFactory>(py) {
Ok(f) => Ok(Box::new(f)),
Err(e) => Err(to_pyvalue_err(format!(
"Failed to extract LighterExecutionClientFactory: {e}"
))),
}
}
#[expect(clippy::needless_pass_by_value)]
fn extract_lighter_data_config(
py: Python<'_>,
config: Py<PyAny>,
) -> PyResult<Box<dyn ClientConfig>> {
match config.extract::<LighterDataClientConfig>(py) {
Ok(c) => Ok(Box::new(c)),
Err(e) => Err(to_pyvalue_err(format!(
"Failed to extract LighterDataClientConfig: {e}"
))),
}
}
#[expect(clippy::needless_pass_by_value)]
fn extract_lighter_exec_config(
py: Python<'_>,
config: Py<PyAny>,
) -> PyResult<Box<dyn ClientConfig>> {
match config.extract::<LighterExecClientConfig>(py) {
Ok(c) => Ok(Box::new(c)),
Err(e) => Err(to_pyvalue_err(format!(
"Failed to extract LighterExecClientConfig: {e}"
))),
}
}
async fn submit_integrator_revocation(environment: LighterEnvironment) -> anyhow::Result<String> {
let credential = Credential::resolve(None, None, None, environment)?
.ok_or_else(|| anyhow::anyhow!("no Lighter L2 credentials in env"))?;
let chain_id = lighter_chain_id(environment);
let raw = LighterRawHttpClient::new(environment, None, 30, None)?;
let http = LighterHttpClient::from_raw(raw);
let next_nonce = http
.get_next_nonce(credential.account_index(), credential.api_key_index())
.await?
.nonce;
let now_ms = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as i64;
let tx = ApproveIntegratorTxInfo {
context: TxContext {
account_index: credential.account_index(),
api_key_index: credential.api_key_index(),
nonce: next_nonce,
expired_at: now_ms.saturating_add(TX_EXPIRY_MS),
},
integrator_account_index: LIGHTER_NAUTILUS_INTEGRATOR_ACCOUNT_INDEX as i64,
max_perps_taker_fee: 0,
max_perps_maker_fee: 0,
max_spot_taker_fee: 0,
max_spot_maker_fee: 0,
approval_expiry: 0,
skip_nonce: 0,
};
let l2_signed = sign_tx(&tx, chain_id, &credential.private_key()?, fresh_k());
let tx_info_str = TxInfoJson::approve_integrator(&tx, &l2_signed, "");
let request = LighterSendTxRequest::new(tx.tx_type() as u8, tx_info_str);
let response = http.send_tx(&request).await?;
Ok(format!(
"integrator={LIGHTER_NAUTILUS_INTEGRATOR_ACCOUNT_INDEX} account_index={} tx_hash={}",
credential.account_index(),
response.tx_hash,
))
}
#[pyfunction]
#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.lighter")]
#[pyo3(name = "revoke_lighter_integrator", signature = (environment = LighterEnvironment::Mainnet))]
fn py_revoke_lighter_integrator(
py: Python<'_>,
environment: LighterEnvironment,
) -> PyResult<Bound<'_, PyAny>> {
pyo3_async_runtimes::tokio::future_into_py(py, async move {
submit_integrator_revocation(environment)
.await
.map(|s| format!("submitted revocation for {s}"))
.map_err(to_pyvalue_err)
})
}
#[pymodule]
pub fn lighter(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add(stringify!(LIGHTER), LIGHTER)?;
m.add_class::<LighterEnvironment>()?;
m.add_class::<LighterDataClientConfig>()?;
m.add_class::<LighterExecClientConfig>()?;
m.add_class::<LighterDataClientFactory>()?;
m.add_class::<LighterExecutionClientFactory>()?;
m.add_function(wrap_pyfunction!(py_revoke_lighter_integrator, m)?)?;
let registry = get_global_pyo3_registry();
if let Err(e) =
registry.register_factory_extractor(LIGHTER.to_string(), extract_lighter_data_factory)
{
return Err(to_pyruntime_err(format!(
"Failed to register Lighter data factory extractor: {e}"
)));
}
if let Err(e) =
registry.register_exec_factory_extractor(LIGHTER.to_string(), extract_lighter_exec_factory)
{
return Err(to_pyruntime_err(format!(
"Failed to register Lighter exec factory extractor: {e}"
)));
}
if let Err(e) = registry.register_config_extractor(
"LighterDataClientConfig".to_string(),
extract_lighter_data_config,
) {
return Err(to_pyruntime_err(format!(
"Failed to register Lighter data config extractor: {e}"
)));
}
if let Err(e) = registry.register_config_extractor(
"LighterExecClientConfig".to_string(),
extract_lighter_exec_config,
) {
return Err(to_pyruntime_err(format!(
"Failed to register Lighter exec config extractor: {e}"
)));
}
Ok(())
}