use borderless::http::queries::Pagination;
use borderless::http::{PaginatedElements, TxAction};
use borderless::ContractId;
use borderless_kv_store::{Db, RawRead, Tx};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use borderless::contracts::TxCtx;
use borderless::__private::storage_keys::{StorageKey, BASE_KEY_ACTION_LOG};
#[cfg(any(feature = "contracts", feature = "agents"))]
use borderless::events::CallAction;
#[allow(unused_imports)]
use crate::log_shim::*;
use crate::{Result, CONTRACT_SUB_DB};
pub const SUB_KEY_LOG_LEN: u64 = u64::MAX;
pub struct ActionLog<'a, S: Db> {
db: &'a S,
cid: ContractId,
}
#[derive(Serialize, Deserialize)]
pub struct ActionRecord {
pub tx_ctx: TxCtx,
#[serde(with = "serde_bytes")]
pub value: Vec<u8>,
pub commited: u64,
}
impl TryFrom<ActionRecord> for TxAction {
type Error = serde_json::Error;
fn try_from(record: ActionRecord) -> std::result::Result<Self, Self::Error> {
let action = serde_json::from_slice(&record.value)?;
Ok(Self {
tx_id: record.tx_ctx.tx_id,
action,
commited: record.commited,
})
}
}
pub struct RelTxAction {
pub cid: ContractId,
pub action_idx: u64,
}
impl RelTxAction {
pub fn into_bytes(self) -> [u8; 24] {
let cid_bytes = self.cid.into_bytes();
let idx_bytes = self.action_idx.to_be_bytes();
let mut buf = [0u8; 24];
buf[..16].copy_from_slice(&cid_bytes);
buf[16..].copy_from_slice(&idx_bytes);
buf
}
pub fn from_bytes(bytes: &[u8]) -> Self {
if bytes.len() != 24 {
panic!("invalid slice length - expected 24 bytes");
}
let mut cid_bytes = [0u8; 16];
cid_bytes.copy_from_slice(&bytes[..16]);
let cid = ContractId::from_bytes(cid_bytes);
let mut idx_bytes = [0u8; 8];
idx_bytes.copy_from_slice(&bytes[16..]);
let action_idx = u64::from_be_bytes(idx_bytes);
Self { cid, action_idx }
}
}
impl<'a, S: Db> ActionLog<'a, S> {
pub fn new(db: &'a S, cid: ContractId) -> Self {
Self { db, cid }
}
#[cfg(any(feature = "contracts", feature = "agents"))]
pub(crate) fn commit(
self,
db_ptr: &S::Handle,
txn: &mut <S as Db>::RwTx<'_>,
action: &CallAction,
tx_ctx: TxCtx,
) -> Result<()> {
use borderless_kv_store::RawWrite;
use crate::ACTION_TX_REL_SUB_DB;
use super::controller::{read_system_value, write_system_value};
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("timestamp < 1970")
.as_millis()
.try_into()
.expect("u64 should fit for 584942417 years");
let len_commited: u64 = {
read_system_value::<S, _, _>(
db_ptr,
txn,
&self.cid,
BASE_KEY_ACTION_LOG,
SUB_KEY_LOG_LEN,
)?
.unwrap_or_default()
};
let full_len = len_commited + 1;
let sub_key = len_commited;
let value = ActionRecord {
tx_ctx,
value: action.to_bytes()?,
commited: timestamp,
};
write_system_value::<S, _, _>(
db_ptr,
txn,
&self.cid,
BASE_KEY_ACTION_LOG,
sub_key,
&value,
)?;
write_system_value::<S, _, _>(
db_ptr,
txn,
&self.cid,
BASE_KEY_ACTION_LOG,
SUB_KEY_LOG_LEN,
&full_len,
)?;
let rel_db = self.db.open_sub_db(ACTION_TX_REL_SUB_DB)?;
let tx_id_bytes = value.tx_ctx.tx_id.to_bytes();
let relationship = RelTxAction {
cid: self.cid,
action_idx: sub_key,
};
txn.write(&rel_db, &tx_id_bytes, &relationship.into_bytes())?;
debug!("Commited action to log. len={full_len}");
Ok(())
}
pub fn get(&self, idx: usize) -> Result<Option<ActionRecord>> {
let idx = idx as u64;
let len_commited = self.len()?;
debug_assert!(idx < SUB_KEY_LOG_LEN);
if idx < len_commited {
self.read_value(BASE_KEY_ACTION_LOG, idx)
} else {
Ok(None)
}
}
pub fn get_tx_action_paginated(
&self,
pagination: Pagination,
) -> Result<Option<PaginatedElements<TxAction>>> {
let n_actions = self.len()?;
let mut elements = Vec::new();
for idx in pagination.to_range() {
match self.read_value::<ActionRecord>(BASE_KEY_ACTION_LOG, idx as u64)? {
Some(record) => {
let action = TxAction::try_from(record)?;
elements.push(action);
}
None => break,
}
}
let paginated = PaginatedElements {
elements,
total_elements: n_actions as usize,
pagination,
};
Ok(Some(paginated))
}
pub fn last(&self) -> Result<Option<ActionRecord>> {
let len_commited = self.len()?;
self.read_value(BASE_KEY_ACTION_LOG, len_commited.saturating_sub(1))
}
pub fn len(&self) -> Result<u64> {
Ok(self
.read_value(BASE_KEY_ACTION_LOG, SUB_KEY_LOG_LEN)?
.unwrap_or_default())
}
pub fn is_empty(&self) -> Result<bool> {
Ok(self.len()? == 0)
}
fn read_value<D: DeserializeOwned>(&self, base_key: u64, sub_key: u64) -> Result<Option<D>> {
let db_ptr = self.db.open_sub_db(CONTRACT_SUB_DB)?;
let txn = self.db.begin_ro_txn()?;
let key = StorageKey::system_key(self.cid, base_key, sub_key);
let bytes = txn.read(&db_ptr, &key)?;
let result = match bytes {
Some(val) => Some(postcard::from_bytes(val)?),
None => None,
};
txn.commit()?;
Ok(result)
}
pub fn iter(&self) -> Iter<'_, S> {
Iter { log: self, idx: 0 }
}
}
pub struct Iter<'a, S: Db> {
log: &'a ActionLog<'a, S>,
idx: usize,
}
impl<'a, S: Db> Iterator for Iter<'a, S> {
type Item = Result<ActionRecord>;
fn next(&mut self) -> Option<Self::Item> {
let idx = self.idx;
self.idx += 1;
self.log.get(idx).transpose()
}
}