seedelf_cli/
assets.rs

1use pallas_crypto::hash::Hash;
2use serde::{Deserialize, Serialize};
3
4/// Represents an asset in the Cardano blockchain.
5///
6/// An `Asset` is identified by a `policy_id` and a `token_name`, and it tracks
7/// the amount of tokens associated with the asset.
8#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Hash, Clone)]
9pub struct Asset {
10    pub policy_id: Hash<28>,
11    pub token_name: Vec<u8>,
12    pub amount: u64,
13}
14
15impl Asset {
16    /// Creates a new `Asset` instance.
17    ///
18    /// # Arguments
19    ///
20    /// * `policy_id` - A hex-encoded string representing the policy ID.
21    /// * `token_name` - A hex-encoded string representing the token name.
22    /// * `amount` - The amount of tokens for the asset.
23    pub fn new(policy_id: String, token_name: String, amount: u64) -> Self {
24        Self {
25            policy_id: Hash::new(
26                hex::decode(policy_id)
27                    .unwrap()
28                    .try_into()
29                    .expect("Incorrect Length"),
30            ),
31            token_name: hex::decode(token_name).unwrap(),
32            amount,
33        }
34    }
35
36    /// Adds two assets together if they have the same `policy_id` and `token_name`.
37    ///
38    /// # Arguments
39    ///
40    /// * `other` - The other asset to add.
41    ///
42    /// # Returns
43    ///
44    /// * `Ok(Self)` - The resulting `Asset` with the combined amounts.
45    /// * `Err(String)` - If the `policy_id` or `token_name` do not match.
46    pub fn add(&self, other: &Asset) -> Result<Self, String> {
47        if self.policy_id != other.policy_id || self.token_name != other.token_name {
48            return Err(
49                "Assets must have the same policy_id and token_name to be subtracted".to_string(),
50            );
51        }
52        Ok(Self {
53            policy_id: self.policy_id,
54            token_name: self.token_name.clone(),
55            amount: self.amount + other.amount,
56        })
57    }
58
59    /// Subtracts the amount of another asset if they have the same `policy_id` and `token_name`.
60    ///
61    /// # Arguments
62    ///
63    /// * `other` - The other asset to subtract.
64    ///
65    /// # Returns
66    ///
67    /// * `Ok(Self)` - The resulting `Asset` after subtraction.
68    /// * `Err(String)` - If the `policy_id` or `token_name` do not match.
69    pub fn sub(&self, other: &Asset) -> Result<Self, String> {
70        if self.policy_id != other.policy_id || self.token_name != other.token_name {
71            return Err(
72                "Assets must have the same policy_id and token_name to be subtracted".to_string(),
73            );
74        }
75        Ok(Self {
76            policy_id: self.policy_id,
77            token_name: self.token_name.clone(),
78            amount: self.amount - other.amount,
79        })
80    }
81
82    /// Compares two assets for equivalence in `policy_id` and `token_name`,
83    /// and checks if the amount is greater or equal.
84    ///
85    /// # Arguments
86    ///
87    /// * `other` - The other asset to compare against.
88    ///
89    /// # Returns
90    ///
91    /// * `true` if the `policy_id` and `token_name` match and the amount is greater or equal.
92    /// * `false` otherwise.
93    pub fn compare(&self, other: Asset) -> bool {
94        if self.policy_id != other.policy_id || self.token_name != other.token_name {
95            false
96        } else {
97            self.amount >= other.amount
98        }
99    }
100
101    pub fn quantity_of(&self, policy_id: String, token_name: String) -> Option<u64> {
102        let pid = Hash::new(
103            hex::decode(policy_id)
104                .unwrap()
105                .try_into()
106                .expect("Incorrect Length"),
107        );
108        let tkn = hex::decode(token_name).unwrap();
109        if self.policy_id == pid && self.token_name == tkn {
110            Some(self.amount)
111        } else {
112            None
113        }
114    }
115}
116
117/// Represents a collection of `Asset` instances.
118#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Hash, Clone)]
119pub struct Assets {
120    pub items: Vec<Asset>,
121}
122
123impl Default for Assets {
124    fn default() -> Self {
125        Self::new()
126    }
127}
128
129impl Assets {
130    /// Creates a new, empty `Assets` instance.
131    pub fn new() -> Self {
132        Self { items: Vec::new() }
133    }
134
135    /// Adds an asset to the collection, combining amounts if the asset already exists.
136    ///
137    /// # Arguments
138    ///
139    /// * `other` - The asset to add.
140    ///
141    /// # Returns
142    ///
143    /// * A new `Assets` instance with the updated list of assets.
144    pub fn add(&self, other: Asset) -> Self {
145        let mut new_items: Vec<Asset> = self.items.clone();
146        if let Some(existing) = new_items.iter_mut().find(|existing| {
147            existing.policy_id == other.policy_id && existing.token_name == other.token_name
148        }) {
149            *existing = existing.add(&other).unwrap();
150        } else {
151            new_items.push(other);
152        }
153        Self { items: new_items }
154    }
155
156    /// Subtracts an asset from the collection, removing it if the amount becomes zero.
157    ///
158    /// # Arguments
159    ///
160    /// * `other` - The asset to subtract.
161    ///
162    /// # Returns
163    ///
164    /// * A new `Assets` instance with updated asset amounts.
165    pub fn sub(&self, other: Asset) -> Self {
166        let mut new_items: Vec<Asset> = self.items.clone();
167        if let Some(existing) = new_items.iter_mut().find(|existing| {
168            existing.policy_id == other.policy_id && existing.token_name == other.token_name
169        }) {
170            *existing = existing.sub(&other).unwrap();
171        } else {
172            new_items.push(other);
173        }
174        Self { items: new_items }.remove_zero_amounts()
175    }
176
177    /// Removes assets with zero amounts from the collection.
178    pub fn remove_zero_amounts(&self) -> Self {
179        let filtered_items: Vec<Asset> = self
180            .items
181            .iter()
182            .filter(|asset| asset.amount > 0)
183            .cloned()
184            .collect();
185        Self {
186            items: filtered_items,
187        }
188    }
189
190    /// Checks if all assets in `other` are contained in this collection.
191    pub fn contains(&self, other: Assets) -> bool {
192        // search all other tokens and make sure they exist in these assets
193        for other_token in other.items {
194            // we assume we cant find it
195            let mut found = false;
196            // lets check all the assets in these assets
197            for token in self.items.clone() {
198                if token.compare(other_token.clone()) {
199                    found = true;
200                    break;
201                }
202            }
203            // if we didnt find it then false
204            if !found {
205                return false;
206            }
207        }
208        // we found all the other tokens
209        true
210    }
211
212    pub fn quantity_of(&self, policy_id: String, token_name: String) -> Option<u64> {
213        for this_asset in &self.items {
214            match Asset::quantity_of(this_asset, policy_id.clone(), token_name.clone()) {
215                Some(amount) => {
216                    return Some(amount);
217                }
218                _ => continue,
219            }
220        }
221        None
222    }
223
224    /// Checks if any asset in `other` exists in this collection.
225    pub fn any(&self, other: Assets) -> bool {
226        if other.items.is_empty() {
227            return true;
228        }
229        // search all other tokens and make sure they exist in these assets
230        for other_token in other.items {
231            // lets check all the assets in these assets
232            for token in self.items.clone() {
233                // if its greater than or equal then break
234                if token.policy_id == other_token.policy_id
235                    && token.token_name == other_token.token_name
236                {
237                    return true;
238                }
239            }
240        }
241        // we found nothing
242        false
243    }
244
245    /// Merges two collections of assets, combining amounts of matching assets.
246    pub fn merge(&self, other: Assets) -> Self {
247        let mut merged: Assets = self.clone(); // Clone the current `Assets` as a starting point
248
249        for other_asset in other.items {
250            merged = merged.add(other_asset); // Use `add` to handle merging logic
251        }
252
253        merged
254    }
255
256    /// Separates two collections of assets, subtracting amounts of matching assets.
257    pub fn separate(&self, other: Assets) -> Self {
258        let mut separated: Assets = self.clone(); // Clone the current `Assets` as a starting point
259
260        for other_asset in other.items {
261            separated = separated.sub(other_asset); // Use `add` to handle merging logic
262        }
263
264        separated
265    }
266
267    pub fn is_empty(&self) -> bool {
268        self.items.is_empty()
269    }
270
271    pub fn len(&self) -> u64 {
272        self.items.len() as u64
273    }
274
275    pub fn split(&self, k: usize) -> Vec<Self> {
276        self.items
277            .chunks(k) // Divide the `items` into slices of at most `k` elements
278            .map(|chunk| Assets {
279                items: chunk.to_vec(),
280            }) // Convert each slice into an `Assets` struct
281            .collect()
282    }
283}
284
285/// Converts a string into a `u64` value.
286///
287/// # Arguments
288///
289/// * `input` - The string to parse into a `u64`.
290///
291/// # Returns
292///
293/// * `Ok(u64)` - If the conversion is successful.
294/// * `Err(String)` - If the conversion fails.
295pub fn string_to_u64(input: String) -> Result<u64, String> {
296    match input.parse::<u64>() {
297        Ok(value) => Ok(value),
298        Err(e) => Err(format!("Failed to convert: {}", e)),
299    }
300}
301
302pub fn asset_id_to_asset(asset_id: String) -> Asset {
303    // Assume NFT for now
304    Asset::new(asset_id[..56].to_string(), asset_id[56..].to_string(), 1)
305}