1use crate::assets::{Asset, Assets, string_to_u64};
2use crate::constants::{MAXIMUM_TOKENS_PER_UTXO, MAXIMUM_WALLET_UTXOS};
3use crate::transaction::wallet_minimum_lovelace_with_assets;
4use anyhow::{Context, Ok, Result, anyhow, bail};
5use blstrs::Scalar;
6use hex;
7use seedelf_crypto::register::Register;
8use seedelf_koios::koios::{
9 ProtocolParameters, UtxoResponse, address_utxos, contains_policy_id, credential_utxos,
10 extract_bytes_with_logging,
11};
12
13pub async fn get_credential_utxos(
14 wallet_contract_hash: [u8; 28],
15 network_flag: bool,
16) -> Result<Vec<UtxoResponse>> {
17 let utxos: Vec<UtxoResponse> =
18 credential_utxos(hex::encode(wallet_contract_hash).as_str(), network_flag)
19 .await
20 .context("Failed To Get Credential UTxOs")?;
21 Ok(utxos)
22}
23
24pub async fn get_address_utxos(address: &str, network_flag: bool) -> Result<Vec<UtxoResponse>> {
25 let utxos: Vec<UtxoResponse> = address_utxos(address, network_flag)
26 .await
27 .context("Failed To Get Address UTxOs")?;
28 Ok(utxos)
29}
30
31pub fn collect_all_wallet_utxos(
33 sk: Scalar,
34 seedelf_policy_id: &str,
35 utxos: Vec<UtxoResponse>,
36) -> Result<Vec<UtxoResponse>> {
37 let mut all_utxos: Vec<UtxoResponse> = Vec::new();
38 for utxo in utxos {
39 if let Some(inline_datum) = extract_bytes_with_logging(&utxo.inline_datum) {
40 if inline_datum.is_owned(sk).unwrap_or(false) {
45 if !contains_policy_id(&utxo.asset_list, seedelf_policy_id) {
47 all_utxos.push(utxo.clone());
48 }
49 }
50 }
51 }
52 Ok(all_utxos)
53}
54
55pub fn find_seedelf_datum(
56 seedelf: String,
57 seedelf_policy_id: &str,
58 utxos: Vec<UtxoResponse>,
59) -> Result<Option<Register>> {
60 let mut seedelf_datum: Option<Register> = None;
61
62 for utxo in utxos {
63 if let Some(inline_datum) = extract_bytes_with_logging(&utxo.inline_datum)
65 && contains_policy_id(&utxo.asset_list, seedelf_policy_id)
66 {
67 let asset_name = utxo
68 .asset_list
69 .as_ref()
70 .and_then(|vec| {
71 vec.iter()
72 .find(|asset| asset.policy_id == seedelf_policy_id)
73 .map(|asset| &asset.asset_name)
74 })
75 .context("Can't Produce Asset Name")?;
76 if asset_name == &seedelf {
77 seedelf_datum = Some(inline_datum.clone());
78 }
79 }
80 }
81 Ok(seedelf_datum)
82}
83
84pub fn find_seedelf_utxo(
86 seedelf: String,
87 seedelf_policy_id: &str,
88 utxos: Vec<UtxoResponse>,
89) -> Result<Option<UtxoResponse>> {
90 for utxo in utxos {
91 if contains_policy_id(&utxo.asset_list, seedelf_policy_id) {
92 let asset_name = utxo
93 .asset_list
94 .as_ref()
95 .and_then(|vec| {
96 vec.iter()
97 .find(|asset| asset.policy_id == seedelf_policy_id)
98 .map(|asset| &asset.asset_name)
99 })
100 .context("Can't Produce Asset Name")?;
101 if asset_name == &seedelf {
102 return Ok(Some(utxo));
104 }
105 }
106 }
107 Ok(None)
108}
109
110pub fn collect_wallet_utxos(
112 sk: Scalar,
113 seedelf_policy_id: &str,
114 utxos: Vec<UtxoResponse>,
115) -> Result<Vec<UtxoResponse>> {
116 let mut number_of_utxos: u64 = 0;
117 let mut usable_utxos: Vec<UtxoResponse> = Vec::new();
118
119 for utxo in utxos {
120 if let Some(inline_datum) = extract_bytes_with_logging(&utxo.inline_datum) {
122 if inline_datum.is_owned(sk).unwrap_or(false) {
125 if !contains_policy_id(&utxo.asset_list, seedelf_policy_id) {
127 if number_of_utxos >= MAXIMUM_WALLET_UTXOS {
128 break;
130 }
131 usable_utxos.push(utxo);
132 number_of_utxos += 1;
133 }
134 }
135 }
136 }
137 Ok(usable_utxos)
138}
139
140pub fn collect_address_utxos(utxos: Vec<UtxoResponse>) -> Result<Vec<UtxoResponse>> {
146 let mut usable_utxos: Vec<UtxoResponse> = Vec::new();
147 for utxo in utxos {
148 let lovelace: u64 = utxo.value.parse::<u64>().context("Invalid Lovelace")?;
149 let is_empty_asset_list = utxo
150 .asset_list
151 .as_ref()
152 .is_none_or(|assets| assets.is_empty());
153 if is_empty_asset_list && lovelace == 5_000_000 {
154 continue;
156 }
157 usable_utxos.push(utxo);
158 }
159 Ok(usable_utxos)
160}
161
162pub fn select(
165 params: &ProtocolParameters,
166 utxos: Vec<UtxoResponse>,
167 lovelace: u64,
168 tokens: Assets,
169) -> Result<Vec<UtxoResponse>> {
170 do_select(params, utxos, lovelace, tokens, lovelace).context("Do Select Failed")
171}
172fn do_select(
173 params: &ProtocolParameters,
174 utxos: Vec<UtxoResponse>,
175 lovelace: u64,
176 tokens: Assets,
177 lovelace_goal: u64,
178) -> Result<Vec<UtxoResponse>> {
179 let mut selected_utxos: Vec<UtxoResponse> = Vec::new();
180
181 let mut current_lovelace_sum: u64 = 0;
182 let mut found_enough: bool = false;
183
184 let mut found_assets: Assets = Assets::new();
186
187 let mut sortable: Vec<(u64, UtxoResponse)> = utxos
196 .into_iter()
197 .map(|u| -> Result<(u64, UtxoResponse)> {
198 let v = string_to_u64(u.value.clone()).context("Invalid UTxO Value")?;
199 Ok((v, u))
200 })
201 .collect::<Result<Vec<_>>>()?;
202 sortable.sort_by(|(a_val, a), (b_val, b)| {
203 let a_has_tokens = a.asset_list.as_ref().is_some_and(|list| !list.is_empty());
204 let b_has_tokens = b.asset_list.as_ref().is_some_and(|list| !list.is_empty());
205 a_has_tokens
206 .cmp(&b_has_tokens)
207 .then_with(|| b_val.cmp(a_val))
208 });
209 let utxos: Vec<UtxoResponse> = sortable.into_iter().map(|(_, u)| u).collect();
210
211 for utxo in utxos.clone() {
212 let value: u64 = string_to_u64(utxo.value.clone()).context("Invalid UTxO Value")?;
214
215 let mut utxo_assets: Assets = Assets::new();
216 let mut added: bool = false;
217
218 let asset_list: Vec<seedelf_koios::koios::Asset> =
223 utxo.asset_list.clone().unwrap_or_default();
224 if !asset_list.is_empty() {
225 for token in asset_list {
226 utxo_assets = utxo_assets
227 .add(
228 Asset::new(
229 token.policy_id,
230 token.asset_name,
231 string_to_u64(token.quantity).context("Invalid Asset Amount")?,
232 )
233 .context("Invalid Asset")?,
234 )
235 .context("Can't Add Assets")?;
236 }
237 if utxo_assets.any(tokens.clone()) && !found_assets.contains(tokens.clone()) {
239 selected_utxos.push(utxo.clone());
240 current_lovelace_sum = current_lovelace_sum
241 .checked_add(value)
242 .context("Lovelace sum overflow")?;
243 found_assets = found_assets
244 .merge(utxo_assets.clone())
245 .context("Can't Merge Assets")?;
246 added = true;
247 }
248 } else if current_lovelace_sum < lovelace {
249 selected_utxos.push(utxo.clone());
251 current_lovelace_sum = current_lovelace_sum
252 .checked_add(value)
253 .context("Lovelace sum overflow")?;
254 added = true;
255 }
256
257 if !added && current_lovelace_sum < lovelace && found_assets.contains(tokens.clone()) {
259 selected_utxos.push(utxo.clone());
260 current_lovelace_sum = current_lovelace_sum
261 .checked_add(value)
262 .context("Lovelace sum overflow")?;
263 found_assets = found_assets
264 .merge(utxo_assets)
265 .context("Can't Merge Assets")?;
266 }
267
268 if current_lovelace_sum >= lovelace && found_assets.contains(tokens.clone()) {
270 let change_assets: Assets = found_assets
272 .separate(tokens.clone())
273 .context("Can't Separate Assets")?;
274 let number_of_change_assets: u64 = change_assets.len();
275 let minimum: u64 = wallet_minimum_lovelace_with_assets(params, change_assets.clone())
276 .context("Invalid Minimum Lovelace")?;
277 let multiplier: u64 = if number_of_change_assets > MAXIMUM_TOKENS_PER_UTXO {
279 (number_of_change_assets / MAXIMUM_TOKENS_PER_UTXO) + 1
281 } else {
282 1
283 };
284 let change_floor: u64 = multiplier.saturating_mul(minimum);
289 let total_needed: u64 = lovelace_goal.saturating_add(change_floor);
290 if current_lovelace_sum >= total_needed {
291 found_enough = true;
292 break;
293 } else {
294 return do_select(
296 params,
297 utxos.clone(),
298 lovelace.saturating_add(change_floor),
299 tokens.clone(),
300 lovelace_goal,
301 );
302 }
303 }
304 }
305 if found_enough {
306 Ok(selected_utxos)
308 } else {
309 Ok(Vec::new())
311 }
312}
313
314pub fn assets_of(utxos: Vec<UtxoResponse>) -> Result<(u64, Assets)> {
316 let mut found_assets: Assets = Assets::new();
317 let mut current_lovelace_sum: u64 = 0;
318
319 for utxo in utxos.clone() {
320 let value: u64 = string_to_u64(utxo.value.clone()).context("Invalid UTxO Value")?;
321 current_lovelace_sum = current_lovelace_sum
322 .checked_add(value)
323 .context("Lovelace sum overflow")?;
324
325 if let Some(assets) = utxo.clone().asset_list
326 && !assets.is_empty()
327 {
328 let mut utxo_assets: Assets = Assets::new();
329
330 for token in assets.clone() {
331 let new_asset = Asset::new(
332 token.policy_id,
333 token.asset_name,
334 string_to_u64(token.quantity).context("Invalid Token Quantity")?,
335 )
336 .context("Fail To Construct Asset")?;
337 utxo_assets = utxo_assets.add(new_asset).context("Can't Add Assets")?;
338 }
339
340 found_assets = found_assets
341 .merge(utxo_assets.clone())
342 .context("Can't Merge Assets")?;
343 }
344 }
345 Ok((current_lovelace_sum, found_assets))
346}
347
348pub fn reference_script_size(utxo: &UtxoResponse) -> Result<u64> {
355 let Some(script) = &utxo.reference_script else {
356 return Ok(0);
357 };
358 let size = script
359 .bytes
360 .as_deref()
361 .and_then(|bytes| hex::decode(bytes).ok())
362 .map(|bytes| bytes.len() as u64)
363 .filter(|size| *size > 0 && script.size.is_none_or(|given| given == *size));
364 size.with_context(|| {
365 format!(
366 "UTxO {}#{} holds a reference script the wallet can't measure, so it can't price spending it",
367 utxo.tx_hash, utxo.tx_index
368 )
369 })
370}
371
372pub fn reference_script_bytes(utxos: &[UtxoResponse]) -> Result<u64> {
374 utxos.iter().try_fold(0u64, |total, utxo| {
375 Ok(total.saturating_add(reference_script_size(utxo)?))
376 })
377}
378
379pub fn fitting(
393 utxos: &[UtxoResponse],
394 base: &Assets,
395 first: impl Fn(&UtxoResponse) -> bool,
396) -> Result<(Vec<UtxoResponse>, Vec<UtxoResponse>)> {
397 let mut order: Vec<(usize, &UtxoResponse, u64, Assets)> = utxos
398 .iter()
399 .enumerate()
400 .map(|(i, utxo)| {
401 let (lovelace, tokens) = assets_of(vec![utxo.clone()])?;
402 Ok((i, utxo, lovelace, tokens))
403 })
404 .collect::<Result<_>>()?;
405 order.sort_by(
406 |(_, a, a_lovelace, a_tokens), (_, b, b_lovelace, b_tokens)| {
407 (
408 !a_tokens.is_empty(),
409 !first(a),
410 std::cmp::Reverse(*a_lovelace),
411 )
412 .cmp(&(
413 !b_tokens.is_empty(),
414 !first(b),
415 std::cmp::Reverse(*b_lovelace),
416 ))
417 .then_with(|| (&a.tx_hash, a.tx_index).cmp(&(&b.tx_hash, b.tx_index)))
418 },
419 );
420 let mut total = base.clone();
421 let mut fits = vec![false; utxos.len()];
422 for (i, _, _, tokens) in order {
423 if let std::result::Result::Ok(more) = total.merge(tokens) {
424 total = more;
425 fits[i] = true;
426 }
427 }
428 let (taken, left): (Vec<_>, Vec<_>) = utxos.iter().zip(fits).partition(|(_, fits)| *fits);
429 Ok((
430 taken.into_iter().map(|(u, _)| u.clone()).collect(),
431 left.into_iter().map(|(u, _)| u.clone()).collect(),
432 ))
433}
434
435pub fn find_all_seedelfs(
437 label: String,
438 seedelf_policy_id: &str,
439 utxos: Vec<UtxoResponse>,
440) -> Result<Vec<String>> {
441 let mut matches = Vec::new();
442 for utxo in utxos {
443 if contains_policy_id(&utxo.asset_list, seedelf_policy_id) {
444 let asset_name = utxo
445 .asset_list
446 .as_ref()
447 .and_then(|vec| {
448 vec.iter()
449 .find(|asset| asset.policy_id == seedelf_policy_id)
450 .map(|asset| &asset.asset_name)
451 })
452 .context("Can't Produce Asset Name")?;
453 if asset_name.to_lowercase().contains(&label.to_lowercase()) {
454 matches.push(asset_name.to_string());
456 }
457 }
458 }
459 Ok(matches)
460}
461
462pub fn count_lovelace_and_utxos(
464 seedelf_policy_id: &str,
465 utxos: Vec<UtxoResponse>,
466) -> Result<(usize, u64, u64)> {
467 let mut total_lovelace: u64 = 0;
468 let mut total_seedelfs: u64 = 0;
469
470 for utxo in utxos.clone() {
471 if contains_policy_id(&utxo.asset_list, seedelf_policy_id) {
473 total_seedelfs += 1;
474 }
475 let value: u64 =
477 string_to_u64(utxo.value.clone()).context("failed to parse lovelace value")?;
478 total_lovelace += value;
479 }
480 Ok((utxos.len(), total_lovelace, total_seedelfs))
481}
482
483pub fn parse_tx_utxos(utxos: Vec<String>) -> Result<Vec<(String, u64)>> {
484 utxos
485 .into_iter()
486 .map(|s| {
487 let parts: Vec<&str> = s.split('#').collect();
488 if parts.len() != 2 {
489 bail!("Invalid input format: {s}");
490 }
491
492 let tx_hash = parts[0].to_string();
493 let index = parts[1]
494 .parse::<u64>()
495 .map_err(|_| anyhow!("Invalid index in input: {s}"))?;
496
497 Ok((tx_hash, index))
498 })
499 .collect()
500}
501
502pub fn filter_utxos(utxos: Vec<UtxoResponse>, targets: Vec<(String, u64)>) -> Vec<UtxoResponse> {
503 use std::collections::HashSet;
505
506 let target_set: HashSet<(String, u64)> = targets.into_iter().collect();
507
508 utxos
509 .into_iter()
510 .filter(|u| target_set.contains(&(u.tx_hash.clone(), u.tx_index)))
511 .collect()
512}