arkiv_sdk/
entity.rs

1use alloy::primitives::B256;
2use alloy::rpc::types::TransactionReceipt;
3use alloy::sol_types::SolEventInterface;
4use alloy_rlp::{Encodable, RlpDecodable, RlpEncodable};
5use bon::bon;
6use bytes::Bytes;
7use serde::{Deserialize, Serialize};
8use std::convert::From;
9
10use crate::eth::{self, ArkivABI};
11
12/// A generic key-value pair structure for entity annotations.
13/// Used for both string and numeric metadata attached to entities.
14#[derive(Debug, Clone, Serialize, Deserialize, RlpEncodable, RlpDecodable)]
15pub struct Annotation<T> {
16    /// The key of the annotation.
17    pub key: Key,
18    /// The value of the annotation.
19    pub value: T,
20}
21
22impl<T> Annotation<T> {
23    /// Creates a new key-value pair annotation.
24    /// Accepts any types convertible to `Key` and the annotation value.
25    pub fn new<K, V>(key: K, value: V) -> Self
26    where
27        K: Into<Key>,
28        V: Into<T>,
29    {
30        Annotation {
31            key: key.into(),
32            value: value.into(),
33        }
34    }
35}
36
37/// Type alias for string annotations (key-value pairs with `String` values).
38pub type StringAnnotation = Annotation<String>;
39
40/// Type alias for numeric annotations (key-value pairs with `u64` values).
41pub type NumericAnnotation = Annotation<u64>;
42
43/// A type alias for the hash used to identify entities in Arkiv.
44pub type Hash = B256;
45
46/// Type alias for the key used in annotations.
47pub type Key = String;
48
49/// Type representing a create transaction in Arkiv.
50/// Used to define new entities, including their data, BTL, and annotations.
51///
52/// > Note: Each block represents ~2 seconds, eg. setting the BTL (blocks-to-live) to
53/// > `15u64` is equal to 30 seconds of life for the entity.
54#[derive(Debug, Clone, Default, RlpEncodable, RlpDecodable, Deserialize)]
55#[rlp(trailing)]
56pub struct Create {
57    /// The block-to-live (BTL) for the entity.
58    pub btl: u64,
59    /// The data associated with the entity.
60    pub data: Bytes,
61    /// String annotations for the entity.
62    pub string_annotations: Vec<StringAnnotation>,
63    /// Numeric annotations for the entity.
64    pub numeric_annotations: Vec<NumericAnnotation>,
65}
66
67/// Type representing an update transaction in Arkiv.
68/// Used to update existing entities, including their data, BTL, and annotations.
69///
70/// > Note: Each block represents ~2 seconds, eg. setting the BTL (blocks-to-live) to
71/// > `15u64` is equal to 30 seconds of life for the entity.
72#[derive(Debug, Clone, Default, RlpEncodable, RlpDecodable, Deserialize)]
73#[rlp(trailing)]
74pub struct Update {
75    /// The key of the entity to update.
76    pub entity_key: Hash,
77    /// The updated block-to-live (BTL) for the entity.
78    pub btl: u64,
79    /// The updated data for the entity.
80    pub data: Bytes,
81    /// Updated string annotations for the entity.
82    pub string_annotations: Vec<StringAnnotation>,
83    /// Updated numeric annotations for the entity.
84    pub numeric_annotations: Vec<NumericAnnotation>,
85}
86
87/// Type alias for a delete operation (just the entity key).
88pub type ArkivDelete = Hash;
89
90/// Type representing an extend transaction in Arkiv.
91/// Used to extend the BTL of an entity by a number of blocks.
92///
93/// > Note: Each block represents ~2 seconds, eg. setting the BTL (blocks-to-live) to
94/// > `15u64` is equal to 30 seconds of life for the entity.
95#[derive(Debug, Clone, Default, RlpEncodable, RlpDecodable, Deserialize)]
96pub struct Extend {
97    /// The key of the entity to extend.
98    pub entity_key: Hash,
99    /// The number of blocks to extend the BTL by.
100    pub number_of_blocks: u64,
101}
102
103/// Type representing a transaction in Arkiv, including creates, updates, deletes, and extensions.
104/// Used as the main payload for submitting entity changes to the chain.
105#[derive(Debug, Clone)]
106pub struct ArkivTransaction {
107    pub encodable: EncodableArkivTransaction,
108    pub gas_limit: Option<u64>,
109    pub max_priority_fee_per_gas: Option<u128>,
110    pub max_fee_per_gas: Option<u128>,
111}
112
113// A transaction that can be encoded in RLP
114#[derive(Debug, Clone, Default, RlpEncodable, RlpDecodable)]
115pub struct EncodableArkivTransaction {
116    /// A list of entities to create.
117    pub creates: Vec<Create>,
118    /// A list of entities to update.
119    pub updates: Vec<Update>,
120    /// A list of entity keys to delete.
121    pub deletes: Vec<ArkivDelete>,
122    /// A list of entities to extend.
123    pub extensions: Vec<Extend>,
124}
125
126/// Represents an entity with data, BTL, and annotations.
127/// Used for reading entity state from the chain.
128///
129/// > Note: Each block represents ~2 seconds, eg. setting the BTL (blocks-to-live) to
130/// > `15u64` is equal to 30 seconds of life for the entity.
131#[derive(Debug, Clone, Default, RlpEncodable, RlpDecodable, Serialize, Deserialize)]
132pub struct Entity {
133    /// The data associated with the entity.
134    pub data: String,
135    /// The block-to-live (BTL) for the entity.
136    pub btl: u64,
137    /// String annotations for the entity.
138    pub string_annotations: Vec<StringAnnotation>,
139    /// Numeric annotations for the entity.
140    pub numeric_annotations: Vec<NumericAnnotation>,
141}
142
143/// Represents the result of creating or updating an entity.
144/// Contains the entity key and its expiration block.
145#[derive(Debug, Clone, Default, RlpEncodable, RlpDecodable, Serialize)]
146pub struct EntityResult {
147    /// The key of the entity.
148    pub entity_key: Hash,
149    /// The block number at which the entity expires.
150    pub expiration_block: u64,
151}
152
153/// Represents the result of extending an entity's BTL.
154/// Contains the entity key, old expiration block, and new expiration block.
155#[derive(Debug)]
156pub struct ExtendResult {
157    /// The key of the entity.
158    pub entity_key: Hash,
159    /// The old expiration block of the entity.
160    pub old_expiration_block: u64,
161    /// The new expiration block of the entity.
162    pub new_expiration_block: u64,
163}
164
165/// Represents the result of deleting an entity.
166/// Contains the key of the deleted entity.
167#[derive(Debug)]
168pub struct DeleteResult {
169    /// The key of the entity that was deleted.
170    pub entity_key: Hash,
171}
172
173#[derive(Debug, Default)]
174pub struct TransactionResult {
175    pub creates: Vec<EntityResult>,
176    pub updates: Vec<EntityResult>,
177    pub deletes: Vec<DeleteResult>,
178    pub extensions: Vec<ExtendResult>,
179}
180
181impl TryFrom<TransactionReceipt> for TransactionResult {
182    type Error = eth::Error;
183
184    fn try_from(receipt: TransactionReceipt) -> Result<Self, Self::Error> {
185        if !receipt.status() {
186            return Err(Self::Error::TransactionReceiptError(format!(
187                "Transaction {} failed: {:?}",
188                receipt.transaction_hash, receipt
189            )));
190        }
191
192        let mut txres = TransactionResult::default();
193        receipt.logs().iter().cloned().try_for_each(|log| {
194            let log: alloy::primitives::Log = log.into();
195            let parsed = ArkivABI::ArkivABIEvents::decode_log(&log).map_err(|e| {
196                Self::Error::UnexpectedLogDataError(format!("Error decoding event log: {e}"))
197            })?;
198            match parsed.data {
199                ArkivABI::ArkivABIEvents::GolemBaseStorageEntityCreated(data) => {
200                    txres.creates.push(EntityResult {
201                        entity_key: data.entityKey.into(),
202                        expiration_block: data.expirationBlock.try_into().unwrap_or_default(),
203                    });
204                    Ok(())
205                }
206                ArkivABI::ArkivABIEvents::GolemBaseStorageEntityUpdated(data) => {
207                    txres.updates.push(EntityResult {
208                        entity_key: data.entityKey.into(),
209                        expiration_block: data.expirationBlock.try_into().unwrap_or_default(),
210                    });
211                    Ok(())
212                }
213                ArkivABI::ArkivABIEvents::GolemBaseStorageEntityDeleted(data) => {
214                    txres.deletes.push(DeleteResult {
215                        entity_key: data.entityKey.into(),
216                    });
217                    Ok(())
218                }
219                ArkivABI::ArkivABIEvents::GolemBaseStorageEntityBTLExtended(data) => {
220                    txres.extensions.push(ExtendResult {
221                        entity_key: data.entityKey.into(),
222                        old_expiration_block: data
223                            .oldExpirationBlock
224                            .try_into()
225                            .unwrap_or_default(),
226                        new_expiration_block: data
227                            .newExpirationBlock
228                            .try_into()
229                            .unwrap_or_default(),
230                    });
231                    Ok(())
232                }
233            }
234        })?;
235
236        Ok(txres)
237    }
238}
239
240impl Create {
241    /// Creates a new `Create` operation with empty annotations.
242    /// Accepts a payload as bytes and a BTL value.
243    pub fn new(payload: Vec<u8>, btl: u64) -> Self {
244        Self {
245            btl,
246            data: Bytes::from(payload),
247            string_annotations: Vec::new(),
248            numeric_annotations: Vec::new(),
249        }
250    }
251
252    /// Creates a new `Create` request from any type that can be converted to `String`.
253    pub fn from_string<T: Into<String>>(payload: T, btl: u64) -> Self {
254        Self {
255            btl,
256            data: Bytes::from(payload.into().into_bytes()),
257            string_annotations: Vec::new(),
258            numeric_annotations: Vec::new(),
259        }
260    }
261
262    /// Adds a string annotation to the entity.
263    /// Returns the modified `Create` for chaining.
264    pub fn annotate_string(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
265        self.string_annotations.push(Annotation {
266            key: key.into(),
267            value: value.into(),
268        });
269        self
270    }
271
272    /// Adds a numeric annotation to the entity.
273    /// Returns the modified `Create` for chaining.
274    pub fn annotate_number(mut self, key: impl Into<String>, value: u64) -> Self {
275        self.numeric_annotations.push(Annotation {
276            key: key.into(),
277            value,
278        });
279        self
280    }
281}
282
283impl Update {
284    /// Creates a new `Update` operation with empty annotations.
285    /// Accepts an entity key, payload as bytes, and a BTL value.
286    pub fn new(entity_key: B256, payload: Vec<u8>, btl: u64) -> Self {
287        Self {
288            entity_key,
289            btl,
290            data: Bytes::from(payload),
291            string_annotations: Vec::new(),
292            numeric_annotations: Vec::new(),
293        }
294    }
295
296    /// Creates a new `Update` request from any type that can be converted to `String`.
297    pub fn from_string<T: Into<String>>(entity_key: B256, payload: T, btl: u64) -> Self {
298        Self {
299            entity_key,
300            btl,
301            data: Bytes::from(payload.into().into_bytes()),
302            string_annotations: Vec::new(),
303            numeric_annotations: Vec::new(),
304        }
305    }
306
307    /// Adds a string annotation to the entity.
308    /// Returns the modified `Update` for chaining.
309    pub fn annotate_string(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
310        self.string_annotations.push(Annotation {
311            key: key.into(),
312            value: value.into(),
313        });
314        self
315    }
316
317    /// Adds a numeric annotation to the entity.
318    /// Returns the modified `Update` for chaining.
319    pub fn annotate_number(mut self, key: impl Into<String>, value: u64) -> Self {
320        self.numeric_annotations.push(Annotation {
321            key: key.into(),
322            value,
323        });
324        self
325    }
326}
327
328impl Extend {
329    /// Creates a new `Update` operation with empty annotations.
330    /// Accepts an entity key, payload as bytes, and a BTL value.
331    pub fn new(entity_key: B256, number_of_blocks: u64) -> Self {
332        Self {
333            entity_key,
334            number_of_blocks,
335        }
336    }
337}
338
339#[bon]
340impl ArkivTransaction {
341    #[builder]
342    pub fn builder(
343        creates: Option<Vec<Create>>,
344        updates: Option<Vec<Update>>,
345        deletes: Option<Vec<ArkivDelete>>,
346        extensions: Option<Vec<Extend>>,
347        gas_limit: Option<u64>,
348        max_priority_fee_per_gas: Option<u128>,
349        max_fee_per_gas: Option<u128>,
350    ) -> Self {
351        Self {
352            encodable: EncodableArkivTransaction {
353                creates: creates.unwrap_or_default(),
354                updates: updates.unwrap_or_default(),
355                deletes: deletes.unwrap_or_default(),
356                extensions: extensions.unwrap_or_default(),
357            },
358            gas_limit,
359            max_priority_fee_per_gas,
360            max_fee_per_gas,
361        }
362    }
363}
364
365impl ArkivTransaction {
366    /// Returns the RLP-encoded bytes of the transaction.
367    /// Useful for submitting the transaction to the chain.
368    pub fn encoded(&self) -> Vec<u8> {
369        let mut encoded = Vec::new();
370        self.encodable.encode(&mut encoded);
371        encoded
372    }
373}
374
375// Tests check serialization compatibility with go implementation.
376#[cfg(test)]
377mod tests {
378    use super::*;
379    use alloy::primitives::B256;
380    use hex;
381
382    #[test]
383    fn test_empty_transaction() {
384        let tx = ArkivTransaction::builder().build();
385        assert_eq!(hex::encode(tx.encoded()), "c4c0c0c0c0");
386    }
387
388    #[test]
389    fn test_create_without_annotations() {
390        let create = Create::new(b"test payload".to_vec(), 1000);
391
392        let tx = ArkivTransaction::builder().creates(vec![create]).build();
393
394        assert_eq!(
395            hex::encode(tx.encoded()),
396            "d7d3d28203e88c74657374207061796c6f6164c0c0c0c0c0"
397        );
398    }
399
400    #[test]
401    fn test_create_with_annotations() {
402        let create = Create::new(b"test payload".to_vec(), 1000)
403            .annotate_string("foo", "bar")
404            .annotate_number("baz", 42);
405
406        let tx = ArkivTransaction::builder().creates(vec![create]).build();
407
408        assert_eq!(
409            hex::encode(tx.encoded()),
410            "e6e2e18203e88c74657374207061796c6f6164c9c883666f6f83626172c6c58362617a2ac0c0c0"
411        );
412    }
413
414    #[test]
415    fn test_update_with_annotations() {
416        let update = Update::new(
417            B256::from_slice(&[1; 32]),
418            b"updated payload".to_vec(),
419            2000,
420        )
421        .annotate_string("status", "active")
422        .annotate_number("version", 2);
423
424        let tx = ArkivTransaction::builder().updates(vec![update]).build();
425
426        assert_eq!(
427            hex::encode(tx.encoded()),
428            "f856c0f851f84fa001010101010101010101010101010101010101010101010101010101010101018207d08f75706461746564207061796c6f6164cfce8673746174757386616374697665cac98776657273696f6e02c0c0"
429        );
430    }
431
432    #[test]
433    fn test_delete_operation() {
434        let tx = ArkivTransaction::builder()
435            .deletes(vec![B256::from_slice(&[2; 32])])
436            .build();
437
438        assert_eq!(
439            hex::encode(tx.encoded()),
440            "e5c0c0e1a00202020202020202020202020202020202020202020202020202020202020202c0"
441        );
442    }
443
444    #[test]
445    fn test_extend_btl() {
446        let tx = ArkivTransaction::builder()
447            .extensions(vec![Extend {
448                entity_key: B256::from_slice(&[3; 32]),
449                number_of_blocks: 500,
450            }])
451            .build();
452
453        assert_eq!(
454            hex::encode(tx.encoded()),
455            "e9c0c0c0e5e4a003030303030303030303030303030303030303030303030303030303030303038201f4"
456        );
457    }
458
459    #[test]
460    fn test_mixed_operations() {
461        let create = Create::new(b"test payload".to_vec(), 1000).annotate_string("type", "test");
462        let update = Update::new(
463            B256::from_slice(&[1; 32]),
464            b"updated payload".to_vec(),
465            2000,
466        );
467        let tx = ArkivTransaction::builder()
468            .creates(vec![create])
469            .updates(vec![update])
470            .deletes(vec![B256::from_slice(&[2; 32])])
471            .extensions(vec![Extend {
472                entity_key: B256::from_slice(&[3; 32]),
473                number_of_blocks: 500,
474            }])
475            .build();
476
477        assert_eq!(
478            hex::encode(tx.encoded()),
479            "f89fdedd8203e88c74657374207061796c6f6164cbca84747970658474657374c0f7f6a001010101010101010101010101010101010101010101010101010101010101018207d08f75706461746564207061796c6f6164c0c0e1a00202020202020202020202020202020202020202020202020202020202020202e5e4a003030303030303030303030303030303030303030303030303030303030303038201f4"
480        );
481    }
482}