pub mod evm;
pub mod http;
#[cfg(feature = "native")]
pub mod native;
pub mod network;
#[cfg(feature = "web")]
pub mod web;
#[cfg(feature = "sign")]
use std::future::Future;
use std::sync::{Mutex, MutexGuard, PoisonError};
use crate::account::keys::{ApiKey, AuthToken, TOKEN_S, WalletError, api_key};
#[cfg(feature = "sign")]
use crate::account::keys::{ChangePubKey, L1Signature, Wallet, next_nonce, verify};
use crate::client::http::Api;
#[cfg(feature = "sign")]
use crate::client::http::{MOST_IN_BATCH, send_tx, send_tx_batch};
#[cfg(feature = "sign")]
use crate::utils::Address;
use crate::utils::NonceCounter;
use crate::utils::sign::SignTxError;
#[cfg(feature = "sign")]
use crate::utils::sign::{BadTx, Signed, Tx};
#[cfg(feature = "sign")]
use crate::utils::time::TX_LIFETIME_MS;
use crate::utils::time::{MINUTE_MS, ms_to_seconds};
pub use http::{
ApiError, Call, Http, Kind, Method, OFFLINE, Request, Response, Sent, SentBatch, Spent, Tier, Transport,
TransportError, read,
};
pub use network::{Collateral, Network, USDC, USDG, UnknownNetwork};
pub type Clock = fn() -> u64;
const TOKEN_MARGIN_S: u64 = 30 * MINUTE_MS / 1_000;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum SendError {
#[error(transparent)]
Sign(#[from] SignTxError),
#[error(transparent)]
Api(#[from] ApiError),
#[error(transparent)]
Wallet(#[from] WalletError),
}
pub trait Credentials {
fn mint(&self, deadline_s: u64) -> Option<AuthToken>;
}
impl Credentials for () {
fn mint(&self, _deadline_s: u64) -> Option<AuthToken> {
None
}
}
impl Credentials for ApiKey {
#[cfg(feature = "sign")]
fn mint(&self, deadline_s: u64) -> Option<AuthToken> {
self.auth_token(deadline_s).ok()
}
#[cfg(not(feature = "sign"))]
fn mint(&self, _deadline_s: u64) -> Option<AuthToken> {
None
}
}
pub struct Client<H, K = ()> {
api: Api<H>,
key: K,
nonces: Mutex<NonceCounter>,
#[cfg(feature = "sign")]
turn: futures_util::lock::Mutex<()>,
token: Mutex<Option<AuthToken>>,
read_token: Option<String>,
}
impl<H: Http, K> std::fmt::Debug for Client<H, K> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Client").field("network", &self.network()).field("tier", &self.tier()).finish_non_exhaustive()
}
}
impl<H: Http> Client<H> {
pub fn reader(http: H, network: Network, clock: Clock) -> Self {
Client::new(http, network, clock, ())
}
}
impl<H: Http, K> Client<H, K> {
pub fn new(http: H, network: Network, clock: Clock, key: K) -> Self {
Client {
api: Api::new(http, network, clock),
key,
nonces: Mutex::new(NonceCounter::unknown()),
#[cfg(feature = "sign")]
turn: futures_util::lock::Mutex::new(()),
token: Mutex::new(None),
read_token: None,
}
}
#[must_use]
pub fn with_tier(mut self, tier: Tier) -> Self {
self.api = self.api.with_tier(tier);
self
}
#[must_use]
pub fn with_read_token(mut self, token: impl Into<String>) -> Self {
self.read_token = Some(token.into());
self
}
#[must_use]
pub const fn network(&self) -> Network {
self.api.network()
}
#[must_use]
pub const fn tier(&self) -> Tier {
self.api.tier()
}
#[must_use]
pub const fn http(&self) -> &H {
self.api.http()
}
#[must_use]
pub const fn key(&self) -> &K {
&self.key
}
#[must_use]
pub fn now_ms(&self) -> u64 {
(self.api.clock())()
}
pub fn spent(&self) -> Spent {
self.api.spent()
}
pub async fn warm(&self) {
let _ = self.api.call(&Call::get("/"), None).await;
}
pub fn forget_nonce(&self) {
self.nonces().forget();
}
fn nonces(&self) -> MutexGuard<'_, NonceCounter> {
self.nonces.lock().unwrap_or_else(PoisonError::into_inner)
}
}
impl<H: Http, K: Credentials> Client<H, K> {
pub fn auth_token(&self) -> Option<String> {
let now_s = ms_to_seconds(self.now_ms());
let mut cached = self.token.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(token) = cached.as_ref().filter(|token| token.deadline_s() > now_s + TOKEN_MARGIN_S) {
return Some(token.as_str().to_owned());
}
match self.key.mint(now_s + TOKEN_S) {
Some(token) => {
let text = token.as_str().to_owned();
*cached = Some(token);
Some(text)
}
None => self.read_token.clone(),
}
}
}
impl<H: Http, K: Credentials> Transport for Client<H, K> {
async fn call(&self, call: Call) -> Result<String, TransportError> {
let token = match call.auth {
true => Some(self.auth_token().ok_or_else(|| TransportError::NoToken(call.url_path()))?),
false => None,
};
self.api.call(&call, token.as_deref()).await
}
}
impl<H: Http> Client<H, ApiKey> {
pub async fn is_registered(&self) -> Result<bool, ApiError> {
let listed = api_key(self, self.key.account(), self.key.slot()).await?;
Ok(listed.is_some_and(|slot| slot.holds(&self.key.public_key())))
}
}
#[cfg(feature = "sign")]
impl<H: Http> Client<H, ApiKey> {
pub async fn next_nonce(&self) -> Result<u64, ApiError> {
if let Some(next) = self.nonces().next() {
return Ok(next);
}
let asked = next_nonce(self, self.key.account(), self.key.slot()).await?;
let mut nonces = self.nonces();
Ok(match nonces.next() {
Some(known) => known,
None => {
nonces.set(asked);
asked
}
})
}
#[must_use]
pub fn expiry(&self) -> u64 {
self.now_ms().saturating_add(TX_LIFETIME_MS)
}
pub fn sign_at(&self, tx: &(impl Tx + ?Sized), nonce: u64, expired_at: u64) -> Result<Signed, SignTxError> {
self.key.sign(tx, self.network().chain_id(), nonce, expired_at)
}
pub async fn sign(&self, tx: &(impl Tx + ?Sized)) -> Result<Signed, SendError> {
let _turn = self.turn.lock().await;
let nonce = self.next_nonce().await?;
let signed = self.sign_at(tx, nonce, self.expiry())?;
self.nonces().set(nonce.saturating_add(1));
Ok(signed)
}
pub async fn send(&self, tx: &(impl Tx + ?Sized)) -> Result<Sent, SendError> {
self.send_one(tx, None::<&Nobody>).await
}
pub async fn send_with_owner(&self, tx: &(impl Tx + ?Sized), owner: &impl Wallet) -> Result<Sent, SendError> {
self.send_one(tx, Some(owner)).await
}
async fn send_one<W: Wallet>(&self, tx: &(impl Tx + ?Sized), owner: Option<&W>) -> Result<Sent, SendError> {
let _turn = self.turn.lock().await;
let mut again = true;
loop {
let nonce = self.next_nonce().await?;
let signed = self.sign_at(tx, nonce, self.expiry())?;
let tx_info = match (signed.l1_message(), owner) {
(Some(message), Some(owner)) => {
again = false;
owned(&signed, message, owner).await?
}
_ => signed.tx_info(),
};
match self.settle(nonce, 1, send_tx(self, signed.tx_type().code(), tx_info).await) {
Err(SendError::Api(error)) if error.is_nonce() && std::mem::take(&mut again) => {}
answer => return answer,
}
}
}
pub async fn submit(&self, signed: &Signed, owner_signature: Option<&L1Signature>) -> Result<Sent, ApiError> {
let tx_info = owner_signature
.and_then(|signature| signed.tx_info_with_l1(&signature.to_hex()))
.unwrap_or_else(|| signed.tx_info());
send_tx(self, signed.tx_type().code(), tx_info).await
}
pub async fn send_batch(&self, txs: &[&dyn Tx]) -> Result<SentBatch, SendError> {
if txs.is_empty() || txs.len() > MOST_IN_BATCH {
return Err(SendError::Sign(SignTxError::Bad(BadTx::new("txs", "a batch carries 1 to 50 transactions"))));
}
let count = u64::try_from(txs.len()).unwrap_or(u64::MAX);
let _turn = self.turn.lock().await;
let mut again = true;
loop {
let first = self.next_nonce().await?;
let expired_at = self.expiry();
let signed = txs
.iter()
.zip(first..)
.map(|(tx, nonce)| {
self.sign_at(*tx, nonce, expired_at).map(|one| (one.tx_type().code(), one.tx_info()))
})
.collect::<Result<Vec<_>, _>>()?;
match self.settle(first, count, send_tx_batch(self, &signed).await) {
Err(SendError::Api(error)) if error.is_nonce() && std::mem::take(&mut again) => {}
answer => return answer,
}
}
}
pub async fn register(&self, owner: &impl Wallet) -> Result<Sent, SendError> {
if self.network().reserves_api_key(self.key.slot()) {
let why = format!("slot {} belongs to Lighter's own apps", self.key.slot());
return Err(SendError::Sign(SignTxError::Bad(BadTx::new("ApiKeyIndex", why))));
}
self.send_with_owner(&ChangePubKey::new(self.key.public_key()), owner).await
}
fn settle<T>(&self, nonce: u64, count: u64, answer: Result<T, ApiError>) -> Result<T, SendError> {
let mut nonces = self.nonces();
match &answer {
Ok(_) => nonces.set(nonce.saturating_add(count)),
Err(error) if error.may_have_landed() || error.is_nonce() => nonces.forget(),
Err(_) => {}
}
answer.map_err(SendError::Api)
}
}
#[cfg(feature = "sign")]
struct Nobody;
#[cfg(feature = "sign")]
impl Wallet for Nobody {
fn address(&self) -> Address {
Address::ZERO
}
fn sign_message(&self, _message: &str) -> impl Future<Output = Result<L1Signature, WalletError>> {
std::future::ready(Err(WalletError::Declined("no owner was given".to_owned())))
}
}
#[cfg(feature = "sign")]
async fn owned(signed: &Signed, message: &str, owner: &impl Wallet) -> Result<String, SendError> {
let signature = owner.sign_message(message).await?;
verify(message, &signature, owner.address())?;
Ok(signed.tx_info_with_l1(&signature.to_hex()).unwrap_or_else(|| signed.tx_info()))
}
#[cfg(test)]
#[cfg(feature = "sign")]
#[path = "tests/client.rs"]
mod tests;