1#![expect(
23 clippy::missing_errors_doc,
24 reason = "errors documented on underlying Rust methods"
25)]
26
27pub mod config;
28pub mod factories;
29
30use std::time::{SystemTime, UNIX_EPOCH};
31
32use nautilus_common::factories::{ClientConfig, DataClientFactory, ExecutionClientFactory};
33use nautilus_core::python::{to_pyruntime_err, to_pyvalue_err};
34use nautilus_system::get_global_pyo3_registry;
35use pyo3::prelude::*;
36
37use crate::{
38 common::{
39 consts::{
40 LIGHTER, LIGHTER_CLIENT_ID, LIGHTER_NAUTILUS_INTEGRATOR_ACCOUNT_INDEX,
41 LIGHTER_ROBINHOOD, LIGHTER_ROBINHOOD_CLIENT_ID, LIGHTER_ROBINHOOD_VENUE, LIGHTER_VENUE,
42 },
43 credential::Credential,
44 enums::{LighterDeployment, LighterEnvironment},
45 urls::lighter_chain_id,
46 },
47 config::{LighterDataClientConfig, LighterExecutionClientConfig},
48 factories::{LighterDataClientFactory, LighterExecutionClientFactory},
49 http::{
50 client::{LighterHttpClient, LighterRawHttpClient},
51 models::LighterSendTxRequest,
52 },
53 signing::{
54 auth_token::fresh_k,
55 tx::{ApproveIntegratorTxInfo, LighterTx, TxContext, TxInfoJson, sign_tx},
56 },
57};
58
59const TX_EXPIRY_MS: i64 = 5 * 60 * 1_000;
60
61#[expect(clippy::needless_pass_by_value)]
62fn extract_lighter_data_factory(
63 py: Python<'_>,
64 factory: Py<PyAny>,
65) -> PyResult<Box<dyn DataClientFactory>> {
66 match factory.extract::<LighterDataClientFactory>(py) {
67 Ok(f) => Ok(Box::new(f)),
68 Err(e) => Err(to_pyvalue_err(format!(
69 "Failed to extract LighterDataClientFactory: {e}"
70 ))),
71 }
72}
73
74#[expect(clippy::needless_pass_by_value)]
75fn extract_lighter_exec_factory(
76 py: Python<'_>,
77 factory: Py<PyAny>,
78) -> PyResult<Box<dyn ExecutionClientFactory>> {
79 match factory.extract::<LighterExecutionClientFactory>(py) {
80 Ok(f) => Ok(Box::new(f)),
81 Err(e) => Err(to_pyvalue_err(format!(
82 "Failed to extract LighterExecutionClientFactory: {e}"
83 ))),
84 }
85}
86
87#[expect(clippy::needless_pass_by_value)]
88fn extract_lighter_data_config(
89 py: Python<'_>,
90 config: Py<PyAny>,
91) -> PyResult<Box<dyn ClientConfig>> {
92 match config.extract::<LighterDataClientConfig>(py) {
93 Ok(c) => Ok(Box::new(c)),
94 Err(e) => Err(to_pyvalue_err(format!(
95 "Failed to extract LighterDataClientConfig: {e}"
96 ))),
97 }
98}
99
100#[expect(clippy::needless_pass_by_value)]
101fn extract_lighter_exec_config(
102 py: Python<'_>,
103 config: Py<PyAny>,
104) -> PyResult<Box<dyn ClientConfig>> {
105 match config.extract::<LighterExecutionClientConfig>(py) {
106 Ok(c) => Ok(Box::new(c)),
107 Err(e) => Err(to_pyvalue_err(format!(
108 "Failed to extract LighterExecutionClientConfig: {e}"
109 ))),
110 }
111}
112
113async fn submit_integrator_revocation(environment: LighterEnvironment) -> anyhow::Result<String> {
114 let credential = Credential::resolve(None, None, None, environment)?
115 .ok_or_else(|| anyhow::anyhow!("no Lighter L2 credentials in env"))?;
116 let chain_id = lighter_chain_id(environment);
117
118 let raw = LighterRawHttpClient::new(environment, None, 30, None)?;
119 let http = LighterHttpClient::from_raw(raw);
120 let next_nonce = http
121 .get_next_nonce(credential.account_index(), credential.api_key_index())
122 .await?
123 .nonce;
124
125 let now_ms = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as i64;
126
127 let tx = ApproveIntegratorTxInfo {
128 context: TxContext {
129 account_index: credential.account_index(),
130 api_key_index: credential.api_key_index(),
131 nonce: next_nonce,
132 expired_at: now_ms.saturating_add(TX_EXPIRY_MS),
133 },
134 integrator_account_index: LIGHTER_NAUTILUS_INTEGRATOR_ACCOUNT_INDEX as i64,
135 max_perps_taker_fee: 0,
136 max_perps_maker_fee: 0,
137 max_spot_taker_fee: 0,
138 max_spot_maker_fee: 0,
139 approval_expiry: 0,
140 skip_nonce: 0,
141 };
142
143 let l2_signed = sign_tx(&tx, chain_id, &credential.private_key()?, fresh_k());
144 let tx_info_str = TxInfoJson::approve_integrator(&tx, &l2_signed, "");
145 let request = LighterSendTxRequest::new(tx.tx_type() as u8, tx_info_str);
146 let response = http.send_tx(&request).await?;
147
148 Ok(format!(
149 "integrator={LIGHTER_NAUTILUS_INTEGRATOR_ACCOUNT_INDEX} account_index={} tx_hash={}",
150 credential.account_index(),
151 response.tx_hash,
152 ))
153}
154
155#[pyfunction]
170#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.lighter")]
171#[pyo3(name = "revoke_lighter_integrator", signature = (environment = LighterEnvironment::Mainnet))]
172fn py_revoke_lighter_integrator(
173 py: Python<'_>,
174 environment: LighterEnvironment,
175) -> PyResult<Bound<'_, PyAny>> {
176 pyo3_async_runtimes::tokio::future_into_py(py, async move {
177 submit_integrator_revocation(environment)
178 .await
179 .map(|s| format!("submitted revocation for {s}"))
180 .map_err(to_pyvalue_err)
181 })
182}
183
184#[pymodule]
186pub fn lighter(m: &Bound<'_, PyModule>) -> PyResult<()> {
187 m.add(stringify!(LIGHTER), LIGHTER)?;
188 m.add(stringify!(LIGHTER_CLIENT_ID), *LIGHTER_CLIENT_ID)?;
189 m.add(stringify!(LIGHTER_VENUE), *LIGHTER_VENUE)?;
190 m.add(stringify!(LIGHTER_ROBINHOOD), LIGHTER_ROBINHOOD)?;
191 m.add(
192 stringify!(LIGHTER_ROBINHOOD_CLIENT_ID),
193 *LIGHTER_ROBINHOOD_CLIENT_ID,
194 )?;
195 m.add(
196 stringify!(LIGHTER_ROBINHOOD_VENUE),
197 *LIGHTER_ROBINHOOD_VENUE,
198 )?;
199 m.add_class::<LighterDeployment>()?;
200 m.add_class::<LighterEnvironment>()?;
201 m.add_class::<LighterDataClientConfig>()?;
202 m.add_class::<LighterDataClientFactory>()?;
203 m.add_class::<LighterExecutionClientConfig>()?;
204 m.add_class::<LighterExecutionClientFactory>()?;
205 m.add_function(wrap_pyfunction!(py_revoke_lighter_integrator, m)?)?;
206
207 let registry = get_global_pyo3_registry();
208
209 if let Err(e) =
210 registry.register_factory_extractor(LIGHTER.to_string(), extract_lighter_data_factory)
211 {
212 return Err(to_pyruntime_err(format!(
213 "Failed to register Lighter data factory extractor: {e}"
214 )));
215 }
216
217 if let Err(e) =
218 registry.register_exec_factory_extractor(LIGHTER.to_string(), extract_lighter_exec_factory)
219 {
220 return Err(to_pyruntime_err(format!(
221 "Failed to register Lighter exec factory extractor: {e}"
222 )));
223 }
224
225 if let Err(e) = registry.register_config_extractor(
226 "LighterDataClientConfig".to_string(),
227 extract_lighter_data_config,
228 ) {
229 return Err(to_pyruntime_err(format!(
230 "Failed to register Lighter data config extractor: {e}"
231 )));
232 }
233
234 if let Err(e) = registry.register_config_extractor(
235 "LighterExecutionClientConfig".to_string(),
236 extract_lighter_exec_config,
237 ) {
238 return Err(to_pyruntime_err(format!(
239 "Failed to register Lighter exec config extractor: {e}"
240 )));
241 }
242
243 Ok(())
244}