use crate::assets::{Asset, Assets, string_to_u64};
use crate::constants::{MAXIMUM_TOKENS_PER_UTXO, MAXIMUM_WALLET_UTXOS};
use crate::transaction::wallet_minimum_lovelace_with_assets;
use anyhow::{Context, Ok, Result, anyhow, bail};
use blstrs::Scalar;
use hex;
use seedelf_crypto::register::Register;
use seedelf_koios::koios::{
ProtocolParameters, UtxoResponse, address_utxos, contains_policy_id, credential_utxos,
extract_bytes_with_logging,
};
pub async fn get_credential_utxos(
wallet_contract_hash: [u8; 28],
network_flag: bool,
) -> Result<Vec<UtxoResponse>> {
let utxos: Vec<UtxoResponse> =
credential_utxos(hex::encode(wallet_contract_hash).as_str(), network_flag)
.await
.context("Failed To Get Credential UTxOs")?;
Ok(utxos)
}
pub async fn get_address_utxos(address: &str, network_flag: bool) -> Result<Vec<UtxoResponse>> {
let utxos: Vec<UtxoResponse> = address_utxos(address, network_flag)
.await
.context("Failed To Get Address UTxOs")?;
Ok(utxos)
}
pub fn collect_all_wallet_utxos(
sk: Scalar,
seedelf_policy_id: &str,
utxos: Vec<UtxoResponse>,
) -> Result<Vec<UtxoResponse>> {
let mut all_utxos: Vec<UtxoResponse> = Vec::new();
for utxo in utxos {
if let Some(inline_datum) = extract_bytes_with_logging(&utxo.inline_datum) {
if inline_datum.is_owned(sk).unwrap_or(false) {
if !contains_policy_id(&utxo.asset_list, seedelf_policy_id) {
all_utxos.push(utxo.clone());
}
}
}
}
Ok(all_utxos)
}
pub fn find_seedelf_datum(
seedelf: String,
seedelf_policy_id: &str,
utxos: Vec<UtxoResponse>,
) -> Result<Option<Register>> {
let mut seedelf_datum: Option<Register> = None;
for utxo in utxos {
if let Some(inline_datum) = extract_bytes_with_logging(&utxo.inline_datum)
&& contains_policy_id(&utxo.asset_list, seedelf_policy_id)
{
let asset_name = utxo
.asset_list
.as_ref()
.and_then(|vec| {
vec.iter()
.find(|asset| asset.policy_id == seedelf_policy_id)
.map(|asset| &asset.asset_name)
})
.context("Can't Produce Asset Name")?;
if asset_name == &seedelf {
seedelf_datum = Some(inline_datum.clone());
}
}
}
Ok(seedelf_datum)
}
pub fn find_seedelf_utxo(
seedelf: String,
seedelf_policy_id: &str,
utxos: Vec<UtxoResponse>,
) -> Result<Option<UtxoResponse>> {
for utxo in utxos {
if contains_policy_id(&utxo.asset_list, seedelf_policy_id) {
let asset_name = utxo
.asset_list
.as_ref()
.and_then(|vec| {
vec.iter()
.find(|asset| asset.policy_id == seedelf_policy_id)
.map(|asset| &asset.asset_name)
})
.context("Can't Produce Asset Name")?;
if asset_name == &seedelf {
return Ok(Some(utxo));
}
}
}
Ok(None)
}
pub fn collect_wallet_utxos(
sk: Scalar,
seedelf_policy_id: &str,
utxos: Vec<UtxoResponse>,
) -> Result<Vec<UtxoResponse>> {
let mut number_of_utxos: u64 = 0;
let mut usable_utxos: Vec<UtxoResponse> = Vec::new();
for utxo in utxos {
if let Some(inline_datum) = extract_bytes_with_logging(&utxo.inline_datum) {
if inline_datum.is_owned(sk).unwrap_or(false) {
if !contains_policy_id(&utxo.asset_list, seedelf_policy_id) {
if number_of_utxos >= MAXIMUM_WALLET_UTXOS {
break;
}
usable_utxos.push(utxo);
number_of_utxos += 1;
}
}
}
}
Ok(usable_utxos)
}
pub fn collect_address_utxos(utxos: Vec<UtxoResponse>) -> Result<Vec<UtxoResponse>> {
let mut usable_utxos: Vec<UtxoResponse> = Vec::new();
for utxo in utxos {
let lovelace: u64 = utxo.value.parse::<u64>().context("Invalid Lovelace")?;
let is_empty_asset_list = utxo
.asset_list
.as_ref()
.is_none_or(|assets| assets.is_empty());
if is_empty_asset_list && lovelace == 5_000_000 {
continue;
}
usable_utxos.push(utxo);
}
Ok(usable_utxos)
}
pub fn select(
params: &ProtocolParameters,
utxos: Vec<UtxoResponse>,
lovelace: u64,
tokens: Assets,
) -> Result<Vec<UtxoResponse>> {
do_select(params, utxos, lovelace, tokens, lovelace).context("Do Select Failed")
}
fn do_select(
params: &ProtocolParameters,
utxos: Vec<UtxoResponse>,
lovelace: u64,
tokens: Assets,
lovelace_goal: u64,
) -> Result<Vec<UtxoResponse>> {
let mut selected_utxos: Vec<UtxoResponse> = Vec::new();
let mut current_lovelace_sum: u64 = 0;
let mut found_enough: bool = false;
let mut found_assets: Assets = Assets::new();
let mut sortable: Vec<(u64, UtxoResponse)> = utxos
.into_iter()
.map(|u| -> Result<(u64, UtxoResponse)> {
let v = string_to_u64(u.value.clone()).context("Invalid UTxO Value")?;
Ok((v, u))
})
.collect::<Result<Vec<_>>>()?;
sortable.sort_by(|(a_val, a), (b_val, b)| {
let a_has_tokens = a.asset_list.as_ref().is_some_and(|list| !list.is_empty());
let b_has_tokens = b.asset_list.as_ref().is_some_and(|list| !list.is_empty());
a_has_tokens
.cmp(&b_has_tokens)
.then_with(|| b_val.cmp(a_val))
});
let utxos: Vec<UtxoResponse> = sortable.into_iter().map(|(_, u)| u).collect();
for utxo in utxos.clone() {
let value: u64 = string_to_u64(utxo.value.clone()).context("Invalid UTxO Value")?;
let mut utxo_assets: Assets = Assets::new();
let mut added: bool = false;
let asset_list: Vec<seedelf_koios::koios::Asset> =
utxo.asset_list.clone().unwrap_or_default();
if !asset_list.is_empty() {
for token in asset_list {
utxo_assets = utxo_assets
.add(
Asset::new(
token.policy_id,
token.asset_name,
string_to_u64(token.quantity).context("Invalid Asset Amount")?,
)
.context("Invalid Asset")?,
)
.context("Can't Add Assets")?;
}
if utxo_assets.any(tokens.clone()) && !found_assets.contains(tokens.clone()) {
selected_utxos.push(utxo.clone());
current_lovelace_sum = current_lovelace_sum
.checked_add(value)
.context("Lovelace sum overflow")?;
found_assets = found_assets
.merge(utxo_assets.clone())
.context("Can't Merge Assets")?;
added = true;
}
} else if current_lovelace_sum < lovelace {
selected_utxos.push(utxo.clone());
current_lovelace_sum = current_lovelace_sum
.checked_add(value)
.context("Lovelace sum overflow")?;
added = true;
}
if !added && current_lovelace_sum < lovelace && found_assets.contains(tokens.clone()) {
selected_utxos.push(utxo.clone());
current_lovelace_sum = current_lovelace_sum
.checked_add(value)
.context("Lovelace sum overflow")?;
found_assets = found_assets
.merge(utxo_assets)
.context("Can't Merge Assets")?;
}
if current_lovelace_sum >= lovelace && found_assets.contains(tokens.clone()) {
let change_assets: Assets = found_assets
.separate(tokens.clone())
.context("Can't Separate Assets")?;
let number_of_change_assets: u64 = change_assets.len();
let minimum: u64 = wallet_minimum_lovelace_with_assets(params, change_assets.clone())
.context("Invalid Minimum Lovelace")?;
let multiplier: u64 = if number_of_change_assets > MAXIMUM_TOKENS_PER_UTXO {
(number_of_change_assets / MAXIMUM_TOKENS_PER_UTXO) + 1
} else {
1
};
let change_floor: u64 = multiplier.saturating_mul(minimum);
let total_needed: u64 = lovelace_goal.saturating_add(change_floor);
if current_lovelace_sum >= total_needed {
found_enough = true;
break;
} else {
return do_select(
params,
utxos.clone(),
lovelace.saturating_add(change_floor),
tokens.clone(),
lovelace_goal,
);
}
}
}
if found_enough {
Ok(selected_utxos)
} else {
Ok(Vec::new())
}
}
pub fn assets_of(utxos: Vec<UtxoResponse>) -> Result<(u64, Assets)> {
let mut found_assets: Assets = Assets::new();
let mut current_lovelace_sum: u64 = 0;
for utxo in utxos.clone() {
let value: u64 = string_to_u64(utxo.value.clone()).context("Invalid UTxO Value")?;
current_lovelace_sum = current_lovelace_sum
.checked_add(value)
.context("Lovelace sum overflow")?;
if let Some(assets) = utxo.clone().asset_list
&& !assets.is_empty()
{
let mut utxo_assets: Assets = Assets::new();
for token in assets.clone() {
let new_asset = Asset::new(
token.policy_id,
token.asset_name,
string_to_u64(token.quantity).context("Invalid Token Quantity")?,
)
.context("Fail To Construct Asset")?;
utxo_assets = utxo_assets.add(new_asset).context("Can't Add Assets")?;
}
found_assets = found_assets
.merge(utxo_assets.clone())
.context("Can't Merge Assets")?;
}
}
Ok((current_lovelace_sum, found_assets))
}
pub fn reference_script_size(utxo: &UtxoResponse) -> Result<u64> {
let Some(script) = &utxo.reference_script else {
return Ok(0);
};
let size = script
.bytes
.as_deref()
.and_then(|bytes| hex::decode(bytes).ok())
.map(|bytes| bytes.len() as u64)
.filter(|size| *size > 0 && script.size.is_none_or(|given| given == *size));
size.with_context(|| {
format!(
"UTxO {}#{} holds a reference script the wallet can't measure, so it can't price spending it",
utxo.tx_hash, utxo.tx_index
)
})
}
pub fn reference_script_bytes(utxos: &[UtxoResponse]) -> Result<u64> {
utxos.iter().try_fold(0u64, |total, utxo| {
Ok(total.saturating_add(reference_script_size(utxo)?))
})
}
pub fn fitting(
utxos: &[UtxoResponse],
base: &Assets,
first: impl Fn(&UtxoResponse) -> bool,
) -> Result<(Vec<UtxoResponse>, Vec<UtxoResponse>)> {
let mut order: Vec<(usize, &UtxoResponse, u64, Assets)> = utxos
.iter()
.enumerate()
.map(|(i, utxo)| {
let (lovelace, tokens) = assets_of(vec![utxo.clone()])?;
Ok((i, utxo, lovelace, tokens))
})
.collect::<Result<_>>()?;
order.sort_by(
|(_, a, a_lovelace, a_tokens), (_, b, b_lovelace, b_tokens)| {
(
!a_tokens.is_empty(),
!first(a),
std::cmp::Reverse(*a_lovelace),
)
.cmp(&(
!b_tokens.is_empty(),
!first(b),
std::cmp::Reverse(*b_lovelace),
))
.then_with(|| (&a.tx_hash, a.tx_index).cmp(&(&b.tx_hash, b.tx_index)))
},
);
let mut total = base.clone();
let mut fits = vec![false; utxos.len()];
for (i, _, _, tokens) in order {
if let std::result::Result::Ok(more) = total.merge(tokens) {
total = more;
fits[i] = true;
}
}
let (taken, left): (Vec<_>, Vec<_>) = utxos.iter().zip(fits).partition(|(_, fits)| *fits);
Ok((
taken.into_iter().map(|(u, _)| u.clone()).collect(),
left.into_iter().map(|(u, _)| u.clone()).collect(),
))
}
pub fn find_all_seedelfs(
label: String,
seedelf_policy_id: &str,
utxos: Vec<UtxoResponse>,
) -> Result<Vec<String>> {
let mut matches = Vec::new();
for utxo in utxos {
if contains_policy_id(&utxo.asset_list, seedelf_policy_id) {
let asset_name = utxo
.asset_list
.as_ref()
.and_then(|vec| {
vec.iter()
.find(|asset| asset.policy_id == seedelf_policy_id)
.map(|asset| &asset.asset_name)
})
.context("Can't Produce Asset Name")?;
if asset_name.to_lowercase().contains(&label.to_lowercase()) {
matches.push(asset_name.to_string());
}
}
}
Ok(matches)
}
pub fn count_lovelace_and_utxos(
seedelf_policy_id: &str,
utxos: Vec<UtxoResponse>,
) -> Result<(usize, u64, u64)> {
let mut total_lovelace: u64 = 0;
let mut total_seedelfs: u64 = 0;
for utxo in utxos.clone() {
if contains_policy_id(&utxo.asset_list, seedelf_policy_id) {
total_seedelfs += 1;
}
let value: u64 =
string_to_u64(utxo.value.clone()).context("failed to parse lovelace value")?;
total_lovelace += value;
}
Ok((utxos.len(), total_lovelace, total_seedelfs))
}
pub fn parse_tx_utxos(utxos: Vec<String>) -> Result<Vec<(String, u64)>> {
utxos
.into_iter()
.map(|s| {
let parts: Vec<&str> = s.split('#').collect();
if parts.len() != 2 {
bail!("Invalid input format: {s}");
}
let tx_hash = parts[0].to_string();
let index = parts[1]
.parse::<u64>()
.map_err(|_| anyhow!("Invalid index in input: {s}"))?;
Ok((tx_hash, index))
})
.collect()
}
pub fn filter_utxos(utxos: Vec<UtxoResponse>, targets: Vec<(String, u64)>) -> Vec<UtxoResponse> {
use std::collections::HashSet;
let target_set: HashSet<(String, u64)> = targets.into_iter().collect();
utxos
.into_iter()
.filter(|u| target_set.contains(&(u.tx_hash.clone(), u.tx_index)))
.collect()
}