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}