golem_base_sdk/
entity.rs

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