use serde::{Deserialize, Deserializer};
use crate::client::{ApiError, Call, Transport, read};
use crate::utils::json::{BadJson, Json};
use crate::utils::lenient::{list, maybe_account, text, unsigned};
use crate::utils::sign::tx::TxType;
use crate::utils::{AccountIndex, ApiKeyIndex};
pub const MOST_TXS: u8 = 100;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TxStatus {
Failed,
Pending,
Executed,
Final,
Other(u8),
}
impl TxStatus {
#[must_use]
pub const fn from_code(code: u8) -> TxStatus {
match code {
0 => TxStatus::Failed,
1 => TxStatus::Pending,
2 => TxStatus::Executed,
3 => TxStatus::Final,
code => TxStatus::Other(code),
}
}
#[must_use]
pub const fn code(self) -> u8 {
match self {
TxStatus::Failed => 0,
TxStatus::Pending => 1,
TxStatus::Executed => 2,
TxStatus::Final => 3,
TxStatus::Other(code) => code,
}
}
#[must_use]
pub const fn is_executed(self) -> bool {
matches!(self, TxStatus::Executed | TxStatus::Final)
}
#[must_use]
pub const fn is_failed(self) -> bool {
matches!(self, TxStatus::Failed)
}
#[must_use]
pub const fn is_pending(self) -> bool {
matches!(self, TxStatus::Pending)
}
}
impl<'de> Deserialize<'de> for TxStatus {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let code = unsigned(deserializer)?;
Ok(TxStatus::from_code(u8::try_from(code).unwrap_or(u8::MAX)))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct Transaction {
pub hash: String,
#[serde(rename = "type")]
pub tx_type: u8,
#[serde(default, deserialize_with = "text")]
pub info: String,
#[serde(default, deserialize_with = "text")]
pub event_info: String,
pub status: TxStatus,
#[serde(default, deserialize_with = "unsigned")]
pub transaction_index: u64,
#[serde(default, deserialize_with = "text")]
pub l1_address: String,
#[serde(default, deserialize_with = "maybe_account")]
pub account_index: Option<AccountIndex>,
#[serde(default, deserialize_with = "unsigned")]
pub nonce: u64,
#[serde(default, deserialize_with = "unsigned")]
pub expire_at: u64,
#[serde(default, deserialize_with = "unsigned")]
pub block_height: u64,
#[serde(default, deserialize_with = "unsigned")]
pub queued_at: u64,
#[serde(default, deserialize_with = "unsigned")]
pub executed_at: u64,
#[serde(default, deserialize_with = "unsigned")]
pub sequence_index: u64,
#[serde(default, deserialize_with = "text")]
pub parent_hash: String,
#[serde(default)]
pub api_key_index: Option<u8>,
#[serde(default, deserialize_with = "unsigned")]
pub transaction_time: u64,
#[serde(default, deserialize_with = "unsigned")]
pub committed_at: u64,
#[serde(default, deserialize_with = "unsigned")]
pub verified_at: u64,
}
impl Transaction {
#[must_use]
pub fn kind(&self) -> Option<TxType> {
TxType::from_code(self.tx_type)
}
#[must_use]
pub fn slot(&self) -> Option<ApiKeyIndex> {
self.api_key_index.and_then(|slot| ApiKeyIndex::new(slot).ok())
}
pub fn info_json(&self) -> Result<Json, BadJson> {
Json::parse(&self.info)
}
pub fn event_json(&self) -> Result<Json, BadJson> {
Json::parse(&self.event_info)
}
#[must_use]
pub fn app_error(&self) -> Option<String> {
let event = self.event_json().ok()?;
event.get("ae").and_then(Json::as_str).filter(|error| !error.is_empty()).map(str::to_owned)
}
}
#[must_use]
pub fn tx_call(hash: &str) -> Call {
Call::get("tx").field("by", "hash").field("value", hash.trim_start_matches("0x"))
}
#[must_use]
pub fn tx_by_sequence_call(sequence_index: u64) -> Call {
Call::get("tx").field("by", "sequence_index").field("value", sequence_index)
}
#[must_use]
pub fn tx_from_l1_tx_hash_call(l1_hash: &str) -> Call {
Call::get("txFromL1TxHash").field("hash", l1_hash)
}
pub async fn tx(transport: &impl Transport, hash: &str) -> Result<Transaction, ApiError> {
read(transport, tx_call(hash)).await
}
pub async fn tx_by_sequence(transport: &impl Transport, sequence_index: u64) -> Result<Transaction, ApiError> {
read(transport, tx_by_sequence_call(sequence_index)).await
}
pub async fn tx_from_l1_tx_hash(transport: &impl Transport, l1_hash: &str) -> Result<Transaction, ApiError> {
read(transport, tx_from_l1_tx_hash_call(l1_hash)).await
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TxQuery {
pub account: AccountIndex,
pub before: Option<u64>,
pub types: Vec<u8>,
pub limit: u8,
}
impl TxQuery {
#[must_use]
pub const fn new(account: AccountIndex) -> TxQuery {
TxQuery { account, before: None, types: Vec::new(), limit: MOST_TXS }
}
}
#[derive(Deserialize)]
struct Txs {
#[serde(default, deserialize_with = "list")]
txs: Vec<Transaction>,
}
#[must_use]
pub fn account_txs_call(query: &TxQuery) -> Call {
let call = Call::get("accountTxs")
.maybe("index", query.before)
.field("limit", query.limit.clamp(1, MOST_TXS))
.field("by", "account_index")
.field("value", query.account);
query.types.iter().fold(call, |call, tx_type| call.field("types", tx_type)).authed()
}
pub async fn account_txs(transport: &impl Transport, query: &TxQuery) -> Result<Vec<Transaction>, ApiError> {
Ok(read::<Txs>(transport, account_txs_call(query)).await?.txs)
}
#[cfg(test)]
#[path = "tests/txs.rs"]
mod tests;