audit_trails/core/records/
mod.rs1use std::collections::{BTreeMap, HashMap};
7
8use iota_interaction::move_core_types::annotated_value::MoveValue;
9use iota_interaction::rpc_types::IotaMoveValue;
10use iota_interaction::types::base_types::{ObjectID, TypeTag};
11use iota_interaction::types::collection_types::LinkedTable;
12use iota_interaction::types::dynamic_field::DynamicFieldName;
13use iota_interaction::{IotaKeySignature, OptionalSync};
14use product_common::core_client::{CoreClient, CoreClientReadOnly};
15use product_common::transaction::transaction_builder::TransactionBuilder;
16use secret_storage::Signer;
17use serde::de::DeserializeOwned;
18
19use crate::core::internal::{linked_table, trail as trail_reader};
20use crate::core::trail::{AuditTrailFull, AuditTrailReadOnly};
21use crate::core::types::{Data, PaginatedRecord, Record};
22use crate::error::Error;
23
24mod operations;
25mod transactions;
26
27pub use transactions::{AddRecord, DeleteRecord, DeleteRecordsBatch};
28
29use self::operations::RecordsOps;
30
31const MAX_LIST_PAGE_LIMIT: usize = 1_000;
32
33#[derive(Debug, Clone)]
37pub struct TrailRecords<'a, C, D = Data> {
38 pub(crate) client: &'a C,
39 pub(crate) trail_id: ObjectID,
40 pub(crate) selected_capability_id: Option<ObjectID>,
41 pub(crate) _phantom: std::marker::PhantomData<D>,
42}
43
44impl<'a, C, D> TrailRecords<'a, C, D> {
45 pub(crate) fn new(client: &'a C, trail_id: ObjectID, selected_capability_id: Option<ObjectID>) -> Self {
46 Self {
47 client,
48 trail_id,
49 selected_capability_id,
50 _phantom: std::marker::PhantomData,
51 }
52 }
53
54 pub fn using_capability(mut self, capability_id: ObjectID) -> Self {
56 self.selected_capability_id = Some(capability_id);
57 self
58 }
59
60 pub async fn get(&self, sequence_number: u64) -> Result<Record<D>, Error>
66 where
67 C: AuditTrailReadOnly,
68 D: DeserializeOwned,
69 {
70 let tx = RecordsOps::get_record(self.client, self.trail_id, sequence_number).await?;
71 self.client.execute_read_only_transaction(tx).await
72 }
73
74 pub fn add<S>(&self, data: D, metadata: Option<String>, tag: Option<String>) -> TransactionBuilder<AddRecord>
79 where
80 C: AuditTrailFull + CoreClient<S>,
81 S: Signer<IotaKeySignature> + OptionalSync,
82 D: Into<Data>,
83 {
84 let owner = self.client.sender_address();
85 TransactionBuilder::new(AddRecord::new(
86 self.trail_id,
87 owner,
88 data.into(),
89 metadata,
90 tag,
91 self.selected_capability_id,
92 ))
93 }
94
95 pub fn delete<S>(&self, sequence_number: u64) -> TransactionBuilder<DeleteRecord>
99 where
100 C: AuditTrailFull + CoreClient<S>,
101 S: Signer<IotaKeySignature> + OptionalSync,
102 {
103 let owner = self.client.sender_address();
104 TransactionBuilder::new(DeleteRecord::new(
105 self.trail_id,
106 owner,
107 sequence_number,
108 self.selected_capability_id,
109 ))
110 }
111
112 pub fn delete_records_batch<S>(&self, limit: u64) -> TransactionBuilder<DeleteRecordsBatch>
138 where
139 C: AuditTrailFull + CoreClient<S>,
140 S: Signer<IotaKeySignature> + OptionalSync,
141 {
142 let owner = self.client.sender_address();
143 TransactionBuilder::new(DeleteRecordsBatch::new(
144 self.trail_id,
145 owner,
146 limit,
147 self.selected_capability_id,
148 ))
149 }
150
151 pub async fn correct(&self, _replaces: Vec<u64>, _data: D, _metadata: Option<String>) -> Result<(), Error>
157 where
158 C: AuditTrailFull,
159 {
160 Err(Error::NotImplemented("TrailRecords::correct"))
161 }
162
163 pub async fn record_count(&self) -> Result<u64, Error>
169 where
170 C: AuditTrailReadOnly,
171 {
172 let tx = RecordsOps::record_count(self.client, self.trail_id).await?;
173 self.client.execute_read_only_transaction(tx).await
174 }
175
176 pub async fn list(&self) -> Result<HashMap<u64, Record<D>>, Error>
181 where
182 C: AuditTrailReadOnly,
183 D: DeserializeOwned,
184 {
185 let records_table = self.load_records_table().await?;
186 list_linked_table::<_, Record<D>>(self.client, &records_table, None).await
187 }
188
189 pub async fn list_with_limit(&self, max_entries: usize) -> Result<HashMap<u64, Record<D>>, Error>
191 where
192 C: AuditTrailReadOnly,
193 D: DeserializeOwned,
194 {
195 let records_table = self.load_records_table().await?;
196 list_linked_table::<_, Record<D>>(self.client, &records_table, Some(max_entries)).await
197 }
198
199 pub async fn list_page(&self, cursor: Option<u64>, limit: usize) -> Result<PaginatedRecord<D>, Error>
203 where
204 C: AuditTrailReadOnly,
205 D: DeserializeOwned,
206 {
207 if limit > MAX_LIST_PAGE_LIMIT {
208 return Err(Error::InvalidArgument(format!(
209 "page limit {limit} exceeds max supported page size {MAX_LIST_PAGE_LIMIT}"
210 )));
211 }
212
213 let records_table = self.load_records_table().await?;
214 let (records, next_cursor) =
215 list_linked_table_page::<_, Record<D>>(self.client, &records_table, cursor, limit).await?;
216
217 Ok(PaginatedRecord {
218 has_next_page: next_cursor.is_some(),
219 next_cursor,
220 records,
221 })
222 }
223
224 async fn load_records_table(&self) -> Result<LinkedTable<u64>, Error>
225 where
226 C: AuditTrailReadOnly,
227 {
228 trail_reader::get_audit_trail(self.trail_id, self.client)
229 .await
230 .map(|on_chain_trail| on_chain_trail.records)
231 }
232}
233
234async fn list_linked_table_page<C, V>(
235 client: &C,
236 table: &LinkedTable<u64>,
237 start_key: Option<u64>,
238 limit: usize,
239) -> Result<(BTreeMap<u64, V>, Option<u64>), Error>
240where
241 C: CoreClientReadOnly + OptionalSync,
242 V: DeserializeOwned,
243{
244 if limit == 0 {
246 return Ok((BTreeMap::new(), start_key.or(table.head)));
247 }
248
249 let mut cursor = start_key.or(table.head);
250 let mut items = BTreeMap::new();
251
252 for _ in 0..limit {
253 let Some(key) = cursor else { break };
254
255 if items.contains_key(&key) {
256 return Err(Error::UnexpectedApiResponse(format!(
257 "cycle detected while traversing linked-table {table_id}; repeated key {key}",
258 table_id = table.id
259 )));
260 }
261
262 let node = linked_table::fetch_node::<_, u64, V>(
263 client,
264 table.id,
265 DynamicFieldName {
266 type_: TypeTag::U64,
267 value: IotaMoveValue::from(MoveValue::U64(key)).to_json_value(),
268 },
269 )
270 .await?;
271
272 cursor = node.next;
273 items.insert(key, node.value);
274 }
275
276 Ok((items, cursor))
277}
278
279async fn list_linked_table<C, V>(
280 client: &C,
281 table: &LinkedTable<u64>,
282 max_entries: Option<usize>,
283) -> Result<HashMap<u64, V>, Error>
284where
285 C: CoreClientReadOnly + OptionalSync,
286 V: DeserializeOwned,
287{
288 let expected = table.size as usize;
290 let cap = max_entries.unwrap_or(expected);
291
292 if expected > cap {
293 return Err(Error::InvalidArgument(format!(
294 "linked-table size {expected} exceeds max_entries {cap}"
295 )));
296 }
297
298 let (entries, next_key) = list_linked_table_page(client, table, None, expected).await?;
299
300 if entries.len() != expected {
301 return Err(Error::UnexpectedApiResponse(format!(
302 "linked-table traversal mismatch; expected {expected} entries, got {}",
303 entries.len()
304 )));
305 }
306
307 if next_key.is_some() {
308 return Err(Error::UnexpectedApiResponse(format!(
309 "linked-table traversal has extra entries beyond declared size {expected}"
310 )));
311 }
312
313 Ok(entries.into_iter().collect())
314}