Skip to main content

df_lighter_core/client/
mod.rs

1//! Connecting to Lighter: the [`Client`] that reads, signs and sends over one network's
2//! rate-limited REST API ([`http`]), the two networks, the L1 they settle on ([`evm`]), and the
3//! native and browser wires.
4
5pub 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
39/// Unix milliseconds. Injected: std has no clock in the browser or Workers, and a test pins the
40/// time.
41pub type Clock = fn() -> u64;
42
43/// An auth token is minted again when it has less than this left: half an hour.
44const TOKEN_MARGIN_S: u64 = 30 * MINUTE_MS / 1_000;
45
46/// Why a transaction was not taken. One whose answer never came ([`ApiError::may_have_landed`])
47/// may have landed, so it is never sent again.
48#[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    /// The owner's wallet did not sign, or signed as someone else: nothing was sent.
55    #[error(transparent)]
56    Wallet(#[from] WalletError),
57}
58
59/// What a client holds to read an account's own data: nothing, or an [`ApiKey`], which mints the
60/// auth tokens private reads and channels carry.
61pub trait Credentials {
62    /// A token good until `deadline_s`, when this can mint one.
63    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
72/// Mints with the operating system's randomness, so only with the `sign` feature.
73impl 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
85/// Reads for any module (`markets::order_books(&client)`) and, with an [`ApiKey`], transactions.
86/// `K` is `()` for a client that only reads.
87///
88/// Each client counts its key's nonces, so give every key one client. A read that is the
89/// account's own carries an auth token, minted from the key and kept until shortly before it
90/// lapses, or the read-only token the client was given.
91pub struct Client<H, K = ()> {
92    api: Api<H>,
93    key: K,
94    nonces: Mutex<NonceCounter>,
95    /// Held from signing to the venue's answer, so one client's transactions go out in turn.
96    #[cfg(feature = "sign")]
97    turn: futures_util::lock::Mutex<()>,
98    token: Mutex<Option<AuthToken>>,
99    read_token: Option<String>,
100}
101
102/// Its network and tier; never its key, its tokens or its transport.
103impl<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    /// A client that reads and never signs.
111    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    /// One network, through `http`, signing with `key`.
118    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    /// The account's tier, whose budget the client paces its reads against: Standard unless the
131    /// account opted into another.
132    #[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    /// A read-only token (`ro:...`, from `tokens/create`), carried by the account's own reads
139    /// when the client holds no key to mint one.
140    #[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    /// Now, by the client's clock: unix milliseconds.
167    #[must_use]
168    pub fn now_ms(&self) -> u64 {
169        (self.api.clock())()
170    }
171
172    /// What went out over the last minute, and whether the gate is closed.
173    pub fn spent(&self) -> Spent {
174        self.api.spent()
175    }
176
177    /// Opens, or refreshes, the connection transactions travel on, with the venue's status (`/`),
178    /// counted like any other read. Nothing is signed and the answer is dropped: a program warms
179    /// before its first order so that order skips DNS and the TLS handshake.
180    pub async fn warm(&self) {
181        let _ = self.api.call(&Call::get("/"), None).await;
182    }
183
184    /// The key's nonce count is wrong or unknown: the next transaction asks `nextNonce`.
185    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    /// The token the account's own reads and channels carry: one minted from the key for seven
196    /// hours (minted again with half an hour left), else the read-only token; `None` when there
197    /// is neither.
198    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
225/// Whether the key is in place, which needs no signature.
226impl<H: Http> Client<H, ApiKey> {
227    /// Whether the venue lists this client's key at its slot: true once a registration has
228    /// executed, a few seconds after it is taken.
229    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/// Sending needs the `sign` feature, for the randomness every Schnorr signature draws. Each
236/// signature is verified before anything is posted, and each owner's signature recovered.
237///
238/// One client's transactions go out in turn, each signed at the key's next nonce: counted from
239/// `nextNonce`, moved on by every one the venue takes, kept for the next after a refusal (it was
240/// turned away before it counted), and asked again after an answer that never came or a refused
241/// nonce. A refused nonce is signed and sent once more at the venue's count.
242#[cfg(feature = "sign")]
243impl<H: Http> Client<H, ApiKey> {
244    /// The nonce the key's next transaction signs at: counted here, asked of the venue
245    /// (`nextNonce`) when not known. Nothing is taken.
246    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    /// When a transaction signed now lapses: ten minutes less a second, as lighter-go signs.
262    #[must_use]
263    pub fn expiry(&self) -> u64 {
264        self.now_ms().saturating_add(TX_LIFETIME_MS)
265    }
266
267    /// Signs `tx` at `nonce`, lapsing at `expired_at`. The nonce must be the key's next; nothing
268    /// is counted.
269    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    /// Signs `tx` at the key's next nonce, for a program that posts it itself (over the
274    /// websocket, say). Its nonce is counted as used: if the venue never takes it, call
275    /// [`Client::forget_nonce`].
276    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    /// Signs `tx` at the key's next nonce and sends it.
285    pub async fn send(&self, tx: &(impl Tx + ?Sized)) -> Result<Sent, SendError> {
286        self.send_one(tx, None::<&Nobody>).await
287    }
288
289    /// Signs `tx`, has `owner` sign what the venue asks the owner to (a key registration, a
290    /// transfer to another owner, an integrator approval with fees), and sends it. The owner's
291    /// signature must recover to `owner`'s address before anything is posted, and the owner is
292    /// never asked twice: a refused nonce comes back as it is.
293    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    /// Sends a transaction signed earlier ([`Client::sign`]) once, with the owner's signature if
318    /// it carries one. The nonce is the caller's to settle: forget it if the answer never came.
319    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    /// Signs up to fifty transactions at consecutive nonces and sends them as one batch. None
327    /// may need the owner's signature.
328    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    /// Registers this client's key at its slot, signed by the key and by `owner`, the account's
353    /// L1 address. It replaces whatever key sat there: choose the slot with
354    /// [`crate::account::keys::pick_slot`]. The slots Lighter's own apps hold (0 to 3, and 157 on
355    /// Robinhood Chain) are refused: registering there logs the app out.
356    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    /// After a send: taken, the count moves past it; an answer that may have landed, or a
365    /// refused nonce, sends the next one to the venue for the count; any other refusal leaves
366    /// the nonce for the next.
367    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/// The owner [`Client::send`] passes: none, so it is never asked.
379#[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/// `tx_info` with the owner's signature over `message`, which must recover to the owner.
394#[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;