1pub mod evm;
6pub mod http;
7#[cfg(feature = "native")]
8pub mod native;
9pub mod network;
10#[cfg(feature = "web")]
11pub mod web;
12
13#[cfg(feature = "sign")]
14use std::future::Future;
15use std::sync::{Mutex, MutexGuard, PoisonError};
16
17use crate::account::keys::{ApiKey, AuthToken, TOKEN_S, WalletError, api_key};
18#[cfg(feature = "sign")]
19use crate::account::keys::{ChangePubKey, L1Signature, Wallet, next_nonce, verify};
20use crate::client::http::Api;
21#[cfg(feature = "sign")]
22use crate::client::http::{MOST_IN_BATCH, send_tx, send_tx_batch};
23#[cfg(feature = "sign")]
24use crate::utils::Address;
25use crate::utils::NonceCounter;
26use crate::utils::sign::SignTxError;
27#[cfg(feature = "sign")]
28use crate::utils::sign::{BadTx, Signed, Tx};
29#[cfg(feature = "sign")]
30use crate::utils::time::TX_LIFETIME_MS;
31use crate::utils::time::{MINUTE_MS, ms_to_seconds};
32
33pub use http::{
34 ApiError, Call, Http, Kind, Method, OFFLINE, Request, Response, Sent, SentBatch, Spent, Tier, Transport,
35 TransportError, read,
36};
37pub use network::{Collateral, Network, USDC, USDG, UnknownNetwork};
38
39pub type Clock = fn() -> u64;
42
43const TOKEN_MARGIN_S: u64 = 30 * MINUTE_MS / 1_000;
45
46#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
49pub enum SendError {
50 #[error(transparent)]
51 Sign(#[from] SignTxError),
52 #[error(transparent)]
53 Api(#[from] ApiError),
54 #[error(transparent)]
56 Wallet(#[from] WalletError),
57}
58
59pub trait Credentials {
62 fn mint(&self, deadline_s: u64) -> Option<AuthToken>;
64}
65
66impl Credentials for () {
67 fn mint(&self, _deadline_s: u64) -> Option<AuthToken> {
68 None
69 }
70}
71
72impl Credentials for ApiKey {
74 #[cfg(feature = "sign")]
75 fn mint(&self, deadline_s: u64) -> Option<AuthToken> {
76 self.auth_token(deadline_s).ok()
77 }
78
79 #[cfg(not(feature = "sign"))]
80 fn mint(&self, _deadline_s: u64) -> Option<AuthToken> {
81 None
82 }
83}
84
85pub struct Client<H, K = ()> {
92 api: Api<H>,
93 key: K,
94 nonces: Mutex<NonceCounter>,
95 #[cfg(feature = "sign")]
97 turn: futures_util::lock::Mutex<()>,
98 token: Mutex<Option<AuthToken>>,
99 read_token: Option<String>,
100}
101
102impl<H: Http, K> std::fmt::Debug for Client<H, K> {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 f.debug_struct("Client").field("network", &self.network()).field("tier", &self.tier()).finish_non_exhaustive()
106 }
107}
108
109impl<H: Http> Client<H> {
110 pub fn reader(http: H, network: Network, clock: Clock) -> Self {
112 Client::new(http, network, clock, ())
113 }
114}
115
116impl<H: Http, K> Client<H, K> {
117 pub fn new(http: H, network: Network, clock: Clock, key: K) -> Self {
119 Client {
120 api: Api::new(http, network, clock),
121 key,
122 nonces: Mutex::new(NonceCounter::unknown()),
123 #[cfg(feature = "sign")]
124 turn: futures_util::lock::Mutex::new(()),
125 token: Mutex::new(None),
126 read_token: None,
127 }
128 }
129
130 #[must_use]
133 pub fn with_tier(mut self, tier: Tier) -> Self {
134 self.api = self.api.with_tier(tier);
135 self
136 }
137
138 #[must_use]
141 pub fn with_read_token(mut self, token: impl Into<String>) -> Self {
142 self.read_token = Some(token.into());
143 self
144 }
145
146 #[must_use]
147 pub const fn network(&self) -> Network {
148 self.api.network()
149 }
150
151 #[must_use]
152 pub const fn tier(&self) -> Tier {
153 self.api.tier()
154 }
155
156 #[must_use]
157 pub const fn http(&self) -> &H {
158 self.api.http()
159 }
160
161 #[must_use]
162 pub const fn key(&self) -> &K {
163 &self.key
164 }
165
166 #[must_use]
168 pub fn now_ms(&self) -> u64 {
169 (self.api.clock())()
170 }
171
172 pub fn spent(&self) -> Spent {
174 self.api.spent()
175 }
176
177 pub async fn warm(&self) {
181 let _ = self.api.call(&Call::get("/"), None).await;
182 }
183
184 pub fn forget_nonce(&self) {
186 self.nonces().forget();
187 }
188
189 fn nonces(&self) -> MutexGuard<'_, NonceCounter> {
190 self.nonces.lock().unwrap_or_else(PoisonError::into_inner)
191 }
192}
193
194impl<H: Http, K: Credentials> Client<H, K> {
195 pub fn auth_token(&self) -> Option<String> {
199 let now_s = ms_to_seconds(self.now_ms());
200 let mut cached = self.token.lock().unwrap_or_else(PoisonError::into_inner);
201 if let Some(token) = cached.as_ref().filter(|token| token.deadline_s() > now_s + TOKEN_MARGIN_S) {
202 return Some(token.as_str().to_owned());
203 }
204 match self.key.mint(now_s + TOKEN_S) {
205 Some(token) => {
206 let text = token.as_str().to_owned();
207 *cached = Some(token);
208 Some(text)
209 }
210 None => self.read_token.clone(),
211 }
212 }
213}
214
215impl<H: Http, K: Credentials> Transport for Client<H, K> {
216 async fn call(&self, call: Call) -> Result<String, TransportError> {
217 let token = match call.auth {
218 true => Some(self.auth_token().ok_or_else(|| TransportError::NoToken(call.url_path()))?),
219 false => None,
220 };
221 self.api.call(&call, token.as_deref()).await
222 }
223}
224
225impl<H: Http> Client<H, ApiKey> {
227 pub async fn is_registered(&self) -> Result<bool, ApiError> {
230 let listed = api_key(self, self.key.account(), self.key.slot()).await?;
231 Ok(listed.is_some_and(|slot| slot.holds(&self.key.public_key())))
232 }
233}
234
235#[cfg(feature = "sign")]
243impl<H: Http> Client<H, ApiKey> {
244 pub async fn next_nonce(&self) -> Result<u64, ApiError> {
247 if let Some(next) = self.nonces().next() {
248 return Ok(next);
249 }
250 let asked = next_nonce(self, self.key.account(), self.key.slot()).await?;
251 let mut nonces = self.nonces();
252 Ok(match nonces.next() {
253 Some(known) => known,
254 None => {
255 nonces.set(asked);
256 asked
257 }
258 })
259 }
260
261 #[must_use]
263 pub fn expiry(&self) -> u64 {
264 self.now_ms().saturating_add(TX_LIFETIME_MS)
265 }
266
267 pub fn sign_at(&self, tx: &(impl Tx + ?Sized), nonce: u64, expired_at: u64) -> Result<Signed, SignTxError> {
270 self.key.sign(tx, self.network().chain_id(), nonce, expired_at)
271 }
272
273 pub async fn sign(&self, tx: &(impl Tx + ?Sized)) -> Result<Signed, SendError> {
277 let _turn = self.turn.lock().await;
278 let nonce = self.next_nonce().await?;
279 let signed = self.sign_at(tx, nonce, self.expiry())?;
280 self.nonces().set(nonce.saturating_add(1));
281 Ok(signed)
282 }
283
284 pub async fn send(&self, tx: &(impl Tx + ?Sized)) -> Result<Sent, SendError> {
286 self.send_one(tx, None::<&Nobody>).await
287 }
288
289 pub async fn send_with_owner(&self, tx: &(impl Tx + ?Sized), owner: &impl Wallet) -> Result<Sent, SendError> {
294 self.send_one(tx, Some(owner)).await
295 }
296
297 async fn send_one<W: Wallet>(&self, tx: &(impl Tx + ?Sized), owner: Option<&W>) -> Result<Sent, SendError> {
298 let _turn = self.turn.lock().await;
299 let mut again = true;
300 loop {
301 let nonce = self.next_nonce().await?;
302 let signed = self.sign_at(tx, nonce, self.expiry())?;
303 let tx_info = match (signed.l1_message(), owner) {
304 (Some(message), Some(owner)) => {
305 again = false;
306 owned(&signed, message, owner).await?
307 }
308 _ => signed.tx_info(),
309 };
310 match self.settle(nonce, 1, send_tx(self, signed.tx_type().code(), tx_info).await) {
311 Err(SendError::Api(error)) if error.is_nonce() && std::mem::take(&mut again) => {}
312 answer => return answer,
313 }
314 }
315 }
316
317 pub async fn submit(&self, signed: &Signed, owner_signature: Option<&L1Signature>) -> Result<Sent, ApiError> {
320 let tx_info = owner_signature
321 .and_then(|signature| signed.tx_info_with_l1(&signature.to_hex()))
322 .unwrap_or_else(|| signed.tx_info());
323 send_tx(self, signed.tx_type().code(), tx_info).await
324 }
325
326 pub async fn send_batch(&self, txs: &[&dyn Tx]) -> Result<SentBatch, SendError> {
329 if txs.is_empty() || txs.len() > MOST_IN_BATCH {
330 return Err(SendError::Sign(SignTxError::Bad(BadTx::new("txs", "a batch carries 1 to 50 transactions"))));
331 }
332 let count = u64::try_from(txs.len()).unwrap_or(u64::MAX);
333 let _turn = self.turn.lock().await;
334 let mut again = true;
335 loop {
336 let first = self.next_nonce().await?;
337 let expired_at = self.expiry();
338 let signed = txs
339 .iter()
340 .zip(first..)
341 .map(|(tx, nonce)| {
342 self.sign_at(*tx, nonce, expired_at).map(|one| (one.tx_type().code(), one.tx_info()))
343 })
344 .collect::<Result<Vec<_>, _>>()?;
345 match self.settle(first, count, send_tx_batch(self, &signed).await) {
346 Err(SendError::Api(error)) if error.is_nonce() && std::mem::take(&mut again) => {}
347 answer => return answer,
348 }
349 }
350 }
351
352 pub async fn register(&self, owner: &impl Wallet) -> Result<Sent, SendError> {
357 if self.network().reserves_api_key(self.key.slot()) {
358 let why = format!("slot {} belongs to Lighter's own apps", self.key.slot());
359 return Err(SendError::Sign(SignTxError::Bad(BadTx::new("ApiKeyIndex", why))));
360 }
361 self.send_with_owner(&ChangePubKey::new(self.key.public_key()), owner).await
362 }
363
364 fn settle<T>(&self, nonce: u64, count: u64, answer: Result<T, ApiError>) -> Result<T, SendError> {
368 let mut nonces = self.nonces();
369 match &answer {
370 Ok(_) => nonces.set(nonce.saturating_add(count)),
371 Err(error) if error.may_have_landed() || error.is_nonce() => nonces.forget(),
372 Err(_) => {}
373 }
374 answer.map_err(SendError::Api)
375 }
376}
377
378#[cfg(feature = "sign")]
380struct Nobody;
381
382#[cfg(feature = "sign")]
383impl Wallet for Nobody {
384 fn address(&self) -> Address {
385 Address::ZERO
386 }
387
388 fn sign_message(&self, _message: &str) -> impl Future<Output = Result<L1Signature, WalletError>> {
389 std::future::ready(Err(WalletError::Declined("no owner was given".to_owned())))
390 }
391}
392
393#[cfg(feature = "sign")]
395async fn owned(signed: &Signed, message: &str, owner: &impl Wallet) -> Result<String, SendError> {
396 let signature = owner.sign_message(message).await?;
397 verify(message, &signature, owner.address())?;
398 Ok(signed.tx_info_with_l1(&signature.to_hex()).unwrap_or_else(|| signed.tx_info()))
399}
400
401#[cfg(test)]
402#[cfg(feature = "sign")]
403#[path = "tests/client.rs"]
404mod tests;