1use borderless::http::queries::Pagination;
2use borderless::http::{PaginatedElements, TxAction};
3use borderless::ContractId;
4use borderless_kv_store::{Db, RawRead, Tx};
5use serde::de::DeserializeOwned;
6use serde::{Deserialize, Serialize};
7
8use borderless::contracts::TxCtx;
9
10use borderless::__private::storage_keys::{StorageKey, BASE_KEY_ACTION_LOG};
11
12#[cfg(any(feature = "contracts", feature = "agents"))]
13use borderless::events::CallAction;
14
15#[allow(unused_imports)]
16use crate::log_shim::*;
17use crate::{Result, CONTRACT_SUB_DB};
18
19pub const SUB_KEY_LOG_LEN: u64 = u64::MAX;
21
22pub struct ActionLog<'a, S: Db> {
44 db: &'a S,
45 cid: ContractId,
46}
47
48#[derive(Serialize, Deserialize)]
53pub struct ActionRecord {
54 pub tx_ctx: TxCtx,
55
56 #[serde(with = "serde_bytes")]
63 pub value: Vec<u8>,
64
65 pub commited: u64,
67}
68
69impl TryFrom<ActionRecord> for TxAction {
70 type Error = serde_json::Error;
71
72 fn try_from(record: ActionRecord) -> std::result::Result<Self, Self::Error> {
73 let action = serde_json::from_slice(&record.value)?;
76 Ok(Self {
77 tx_id: record.tx_ctx.tx_id,
78 action,
79 commited: record.commited,
80 })
81 }
82}
83
84pub struct RelTxAction {
89 pub cid: ContractId,
91 pub action_idx: u64,
93}
94
95impl RelTxAction {
96 pub fn into_bytes(self) -> [u8; 24] {
98 let cid_bytes = self.cid.into_bytes();
99 let idx_bytes = self.action_idx.to_be_bytes();
100 let mut buf = [0u8; 24];
101 buf[..16].copy_from_slice(&cid_bytes);
102 buf[16..].copy_from_slice(&idx_bytes);
103 buf
104 }
105
106 pub fn from_bytes(bytes: &[u8]) -> Self {
112 if bytes.len() != 24 {
113 panic!("invalid slice length - expected 24 bytes");
114 }
115 let mut cid_bytes = [0u8; 16];
116 cid_bytes.copy_from_slice(&bytes[..16]);
117 let cid = ContractId::from_bytes(cid_bytes);
118 let mut idx_bytes = [0u8; 8];
119 idx_bytes.copy_from_slice(&bytes[16..]);
120 let action_idx = u64::from_be_bytes(idx_bytes);
121 Self { cid, action_idx }
122 }
123}
124
125impl<'a, S: Db> ActionLog<'a, S> {
126 pub fn new(db: &'a S, cid: ContractId) -> Self {
128 Self { db, cid }
129 }
130
131 #[cfg(any(feature = "contracts", feature = "agents"))]
132 pub(crate) fn commit(
133 self,
134 db_ptr: &S::Handle,
135 txn: &mut <S as Db>::RwTx<'_>,
136 action: &CallAction,
137 tx_ctx: TxCtx,
138 ) -> Result<()> {
139 use borderless_kv_store::RawWrite;
140
141 use crate::ACTION_TX_REL_SUB_DB;
142
143 use super::controller::{read_system_value, write_system_value};
144 use std::time::{SystemTime, UNIX_EPOCH};
145
146 let timestamp = SystemTime::now()
147 .duration_since(UNIX_EPOCH)
148 .expect("timestamp < 1970")
149 .as_millis()
150 .try_into()
151 .expect("u64 should fit for 584942417 years");
152
153 let len_commited: u64 = {
154 read_system_value::<S, _, _>(
155 db_ptr,
156 txn,
157 &self.cid,
158 BASE_KEY_ACTION_LOG,
159 SUB_KEY_LOG_LEN,
160 )?
161 .unwrap_or_default()
162 };
163
164 let full_len = len_commited + 1;
165 let sub_key = len_commited;
166 let value = ActionRecord {
167 tx_ctx,
168 value: action.to_bytes()?,
169 commited: timestamp,
170 };
171 write_system_value::<S, _, _>(
172 db_ptr,
173 txn,
174 &self.cid,
175 BASE_KEY_ACTION_LOG,
176 sub_key,
177 &value,
178 )?;
179 write_system_value::<S, _, _>(
180 db_ptr,
181 txn,
182 &self.cid,
183 BASE_KEY_ACTION_LOG,
184 SUB_KEY_LOG_LEN,
185 &full_len,
186 )?;
187
188 let rel_db = self.db.open_sub_db(ACTION_TX_REL_SUB_DB)?;
190 let tx_id_bytes = value.tx_ctx.tx_id.to_bytes();
191 let relationship = RelTxAction {
192 cid: self.cid,
193 action_idx: sub_key,
194 };
195 txn.write(&rel_db, &tx_id_bytes, &relationship.into_bytes())?;
196
197 debug!("Commited action to log. len={full_len}");
198 Ok(())
199 }
200
201 pub fn get(&self, idx: usize) -> Result<Option<ActionRecord>> {
203 let idx = idx as u64;
204 let len_commited = self.len()?;
205 debug_assert!(idx < SUB_KEY_LOG_LEN);
206 if idx < len_commited {
207 self.read_value(BASE_KEY_ACTION_LOG, idx)
208 } else {
209 Ok(None)
210 }
211 }
212
213 pub fn get_tx_action_paginated(
215 &self,
216 pagination: Pagination,
217 ) -> Result<Option<PaginatedElements<TxAction>>> {
218 let n_actions = self.len()?;
220
221 let mut elements = Vec::new();
222 for idx in pagination.to_range() {
223 match self.read_value::<ActionRecord>(BASE_KEY_ACTION_LOG, idx as u64)? {
225 Some(record) => {
226 let action = TxAction::try_from(record)?;
227 elements.push(action);
228 }
229 None => break,
230 }
231 }
232 let paginated = PaginatedElements {
233 elements,
234 total_elements: n_actions as usize,
235 pagination,
236 };
237 Ok(Some(paginated))
238 }
239
240 pub fn last(&self) -> Result<Option<ActionRecord>> {
242 let len_commited = self.len()?;
243 self.read_value(BASE_KEY_ACTION_LOG, len_commited.saturating_sub(1))
244 }
245
246 pub fn len(&self) -> Result<u64> {
247 Ok(self
248 .read_value(BASE_KEY_ACTION_LOG, SUB_KEY_LOG_LEN)?
249 .unwrap_or_default())
250 }
251
252 pub fn is_empty(&self) -> Result<bool> {
253 Ok(self.len()? == 0)
254 }
255
256 fn read_value<D: DeserializeOwned>(&self, base_key: u64, sub_key: u64) -> Result<Option<D>> {
257 let db_ptr = self.db.open_sub_db(CONTRACT_SUB_DB)?;
258 let txn = self.db.begin_ro_txn()?;
259 let key = StorageKey::system_key(self.cid, base_key, sub_key);
260 let bytes = txn.read(&db_ptr, &key)?;
261 let result = match bytes {
262 Some(val) => Some(postcard::from_bytes(val)?),
263 None => None,
264 };
265 txn.commit()?;
266 Ok(result)
267 }
268
269 pub fn iter(&self) -> Iter<'_, S> {
271 Iter { log: self, idx: 0 }
272 }
273}
274
275pub struct Iter<'a, S: Db> {
277 log: &'a ActionLog<'a, S>,
278 idx: usize,
279}
280
281impl<'a, S: Db> Iterator for Iter<'a, S> {
282 type Item = Result<ActionRecord>;
283
284 fn next(&mut self) -> Option<Self::Item> {
285 let idx = self.idx;
286 self.idx += 1;
287 self.log.get(idx).transpose()
288 }
289}