use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use anyhow::{Context, Result, bail};
use pallas_addresses::Address;
use pallas_codec::minicbor;
use pallas_crypto::hash::{Hash, Hasher};
use pallas_crypto::key::ed25519::{PublicKey, SecretKey, Signature};
use pallas_primitives::{Fragment, NonEmptySet, conway};
use pallas_txbuilder::{
BuildConway, BuiltTransaction, ExUnits, Input, Output, ScriptKind, StagingTransaction,
};
use pallas_wallet::PrivateKey;
use rand_core::OsRng;
use seedelf_crypto::register::Register;
use seedelf_koios::koios::{ProtocolParameters, UtxoResponse, extract_bytes_with_logging};
use serde_json::Value;
use crate::address::{collateral_address, is_not_a_script, is_on_correct_network, wallet_contract};
use crate::assets::{Asset, Assets};
use crate::constants::{COLLATERAL_HASH, COLLATERAL_PUBLIC_KEY, Config, MAXIMUM_TOKENS_PER_UTXO};
use crate::data_structures::{create_mint_redeemer, create_spend_redeemer};
use crate::note::Note;
use crate::staking::Staking;
use crate::transaction::{
address_minimum_lovelace_with_assets, checked_lovelace, collateral_input, computation_fee,
decode_tx_hash, reference_utxo, seedelf_minimum_lovelace, seedelf_token_name,
wallet_minimum_lovelace_with_assets,
};
use crate::utxos::{assets_of, fitting, reference_script_bytes, reference_script_size};
pub fn fake_signer() -> PrivateKey {
PrivateKey::from(SecretKey::new(OsRng))
}
pub fn linear_fee(params: &ProtocolParameters, tx_size: u64) -> u64 {
params
.min_fee_a
.saturating_mul(tx_size)
.saturating_add(params.min_fee_b)
}
pub const MAX_REFERENCE_SCRIPT_BYTES: u64 = 204_800;
const REFERENCE_SCRIPT_TIER: u64 = 25_600;
pub fn reference_script_fee(params: &ProtocolParameters, bytes: u64) -> Result<u64> {
if bytes > MAX_REFERENCE_SCRIPT_BYTES {
bail!(
"This transaction would use {bytes} bytes of reference scripts, over the network's limit of {MAX_REFERENCE_SCRIPT_BYTES}. Spend fewer UTxOs holding one at once"
);
}
let price = params.min_fee_ref_script_cost_per_byte;
if price.denominator == 0 {
bail!("The network's reference script price isn't a number");
}
let tiers = (bytes / REFERENCE_SCRIPT_TIER) as u32;
let rest = u128::from(bytes % REFERENCE_SCRIPT_TIER);
let tier = u128::from(REFERENCE_SCRIPT_TIER);
let scaled: u128 = (0..tiers)
.map(|k| tier * 6u128.pow(k) * 5u128.pow(tiers - k))
.sum::<u128>()
+ rest * 6u128.pow(tiers);
let fee =
scaled * u128::from(price.numerator) / (u128::from(price.denominator) * 5u128.pow(tiers));
u64::try_from(fee).context("The reference scripts' fee doesn't fit a number")
}
pub const MAX_FEE: u64 = 10_000_000;
pub fn check_fee(fee: u64) -> Result<()> {
if fee > MAX_FEE {
bail!(
"This transaction's fee would be {} ADA, over the wallet's limit of {} ADA, so it wasn't built. The network's fee settings look wrong: try again later",
ada(fee),
ada(MAX_FEE)
);
}
Ok(())
}
pub fn settle_fee(
params: &ProtocolParameters,
signers: usize,
build: impl FnMut(u64) -> Result<StagingTransaction>,
) -> Result<(u64, StagingTransaction)> {
settle(
signers,
Patches::staking(&Staking::none()),
|size| linear_fee(params, size),
build,
)
}
pub const MAX_TX_SIZE: u64 = 16_384;
#[derive(Clone, Copy)]
struct Patches<'a> {
staking: &'a Staking,
note: Option<&'a Note>,
}
impl<'a> Patches<'a> {
fn staking(staking: &'a Staking) -> Self {
Patches {
staking,
note: None,
}
}
fn apply(&self, built: BuiltTransaction) -> Result<BuiltTransaction> {
let built = self.staking.patch(built)?;
match self.note {
Some(note) => note.patch(built),
None => Ok(built),
}
}
}
fn settle(
signers: usize,
patches: Patches,
price: impl Fn(u64) -> u64,
mut build: impl FnMut(u64) -> Result<StagingTransaction>,
) -> Result<(u64, StagingTransaction)> {
let mut fee: u64 = 200_000;
for _ in 0..5 {
let staged = build(fee)?;
let size = signed_size(&staged, signers, patches)?;
if size > MAX_TX_SIZE {
bail!(
"This transaction would be {size} bytes, over the network's limit of {MAX_TX_SIZE}. Send fewer tokens, or pay fewer recipients, at once"
);
}
let needed = price(size);
check_fee(needed)?;
if needed <= fee && fee - needed < 1_000 {
return Ok((fee, staged));
}
fee = needed;
}
bail!("The transaction fee did not settle")
}
fn signed_size(staged: &StagingTransaction, signers: usize, patches: Patches) -> Result<u64> {
let mut built = built_with(staged.clone(), patches)?;
for _ in 0..signers.max(1) + patches.staking.signers() {
built = built
.sign(fake_signer())
.context("Failed To Sign The Draft Transaction")?;
}
Ok(built.tx_bytes.0.len() as u64)
}
fn built_with(staged: StagingTransaction, patches: Patches) -> Result<BuiltTransaction> {
patches.apply(
staged
.build_conway_raw()
.context("Failed To Build The Transaction")?,
)
}
fn expiring(tx: StagingTransaction, invalid_hereafter: Option<u64>) -> StagingTransaction {
match invalid_hereafter {
Some(slot) => tx.invalid_from_slot(slot),
None => tx,
}
}
pub fn input_of(utxo: &UtxoResponse) -> Result<Input> {
Ok(Input::new(
Hash::new(decode_tx_hash(&utxo.tx_hash)?),
utxo.tx_index,
))
}
fn deposit_output(
wallet_addr: &Address,
owner: &Register,
lovelace: u64,
tokens: &Assets,
) -> Result<Output> {
let datum: Vec<u8> = owner
.clone()
.rerandomize()
.context("Failed To Randomize Points")?
.to_vec()?;
let mut output = Output::new(wallet_addr.clone(), lovelace).set_inline_datum(datum);
for asset in &tokens.items {
output = output
.add_asset(asset.policy_id, asset.token_name.clone(), asset.amount)
.context("Failed To Add An Asset")?;
}
Ok(output)
}
pub fn deposit_outputs(
params: &ProtocolParameters,
wallet_addr: &Address,
owner: &Register,
lovelace: u64,
tokens: &Assets,
) -> Result<Vec<Output>> {
let chunks: Vec<Assets> = if tokens.items.is_empty() {
vec![Assets::new()]
} else {
tokens.split(MAXIMUM_TOKENS_PER_UTXO as usize)
};
let last = chunks.len() - 1;
let mut remaining = lovelace;
let mut outputs = Vec::with_capacity(chunks.len());
for (i, chunk) in chunks.iter().enumerate() {
let minimum = wallet_minimum_lovelace_with_assets(params, chunk.clone())?;
let amount = if i == last { remaining } else { minimum };
if amount < minimum || remaining < amount {
bail!(TooLittle {
what: "Seedelf",
needed: minimum_deposit(params, tokens)?,
});
}
remaining -= amount;
outputs.push(deposit_output(wallet_addr, owner, amount, chunk)?);
}
Ok(outputs)
}
pub fn minimum_deposit(params: &ProtocolParameters, tokens: &Assets) -> Result<u64> {
if tokens.items.is_empty() {
return wallet_minimum_lovelace_with_assets(params, Assets::new());
}
tokens
.split(MAXIMUM_TOKENS_PER_UTXO as usize)
.into_iter()
.map(|chunk| wallet_minimum_lovelace_with_assets(params, chunk))
.sum()
}
fn change_outputs(
params: &ProtocolParameters,
change_addr: &Address,
lovelace: u64,
tokens: &Assets,
short: NotEnough,
) -> Result<Vec<Output>> {
if lovelace == 0 && tokens.items.is_empty() {
return Ok(Vec::new());
}
let bech32 = change_addr
.to_bech32()
.map_err(|e| anyhow::anyhow!("Failed to encode the change address: {e}"))?;
let chunks: Vec<Assets> = if tokens.items.is_empty() {
vec![Assets::new()]
} else {
tokens.split(MAXIMUM_TOKENS_PER_UTXO as usize)
};
let last = chunks.len() - 1;
let mut remaining = lovelace;
let mut outputs = Vec::with_capacity(chunks.len());
for (i, chunk) in chunks.iter().enumerate() {
let minimum = address_minimum_lovelace_with_assets(params, &bech32, chunk.clone())?;
let amount = if i == last { remaining } else { minimum };
if amount < minimum || remaining < amount {
bail!(short);
}
remaining -= amount;
let mut output = Output::new(change_addr.clone(), amount);
for asset in &chunk.items {
output = output
.add_asset(asset.policy_id, asset.token_name.clone(), asset.amount)
.context("Failed To Add An Asset")?;
}
outputs.push(output);
}
Ok(outputs)
}
fn minimum_change(
params: &ProtocolParameters,
change_addr: &Address,
tokens: &Assets,
) -> Result<u64> {
if tokens.items.is_empty() {
return Ok(0);
}
let bech32 = change_addr
.to_bech32()
.map_err(|e| anyhow::anyhow!("Failed to encode the change address: {e}"))?;
tokens
.split(MAXIMUM_TOKENS_PER_UTXO as usize)
.into_iter()
.map(|chunk| address_minimum_lovelace_with_assets(params, &bech32, chunk))
.sum()
}
#[derive(Debug, Clone, Copy)]
struct NotEnough(&'static str);
const MOVE_IN_SHORT: NotEnough =
NotEnough("Not enough ADA in the Cardano account for this move, its fee and the change");
const SEND_SHORT: NotEnough =
NotEnough("Not enough ADA in the Cardano account for this payment, its fee and the change");
const STAKE_SHORT: NotEnough =
NotEnough("Not enough ADA in the Cardano account for this, its fee and the change");
const ACCOUNT_MINT_SHORT: NotEnough =
NotEnough("Not enough ADA in the Cardano account for the Seedelf, its fee and the change");
const SEEDELF_SHORT: NotEnough =
NotEnough("Not enough ADA in the Seedelf balance for this, its fee and the change");
impl fmt::Display for NotEnough {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0)
}
}
impl std::error::Error for NotEnough {}
#[derive(Debug)]
struct TooLittle {
what: &'static str,
needed: u64,
}
impl fmt::Display for TooLittle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"A {} deposit of these tokens needs at least {} ADA",
self.what,
ada(self.needed)
)
}
}
impl std::error::Error for TooLittle {}
pub fn external_sweep(
params: &ProtocolParameters,
utxos: &[UtxoResponse],
owner: &Register,
wallet_addr: &Address,
signer: Hash<28>,
) -> Result<(BuiltTransaction, u64)> {
if utxos.is_empty() {
bail!("Not Enough Lovelace/Tokens");
}
let (total, tokens) = assets_of(utxos.to_vec())?;
let inputs: Vec<Input> = utxos.iter().map(input_of).collect::<Result<_>>()?;
let script_fee = reference_script_fee(params, reference_script_bytes(utxos)?)?;
let price = |size| linear_fee(params, size).saturating_add(script_fee);
let (fee, staged) = settle(1, Patches::staking(&Staking::none()), price, |fee| {
let lovelace = checked_lovelace(total, &[fee])?;
let mut tx = StagingTransaction::new();
for input in &inputs {
tx = tx.input(input.clone());
}
for output in deposit_outputs(params, wallet_addr, owner, lovelace, &tokens)? {
tx = tx.output(output);
}
Ok(tx.fee(fee).disclosed_signer(signer))
})?;
let tx = staged
.build_conway_raw()
.context("Failed To Build The Transaction")?;
Ok((tx, fee))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccountAmount {
Lovelace(u64),
Max,
}
#[derive(Clone, Copy)]
pub enum Payee<'a> {
Seedelf {
owner: &'a Register,
wallet_addr: &'a Address,
},
Address(&'a Address),
Nobody,
}
impl Payee<'_> {
pub fn minimum(&self, params: &ProtocolParameters, tokens: &Assets) -> Result<u64> {
match self {
Payee::Seedelf { .. } => minimum_deposit(params, tokens),
Payee::Address(to) => minimum_address_payment(params, to, tokens),
Payee::Nobody => Ok(0),
}
}
fn outputs(
&self,
params: &ProtocolParameters,
lovelace: u64,
tokens: &Assets,
) -> Result<Vec<Output>> {
match self {
Payee::Seedelf { owner, wallet_addr } => {
deposit_outputs(params, wallet_addr, owner, lovelace, tokens)
}
Payee::Address(to) => Ok(vec![pay_address(params, to, lovelace, tokens)?]),
Payee::Nobody if lovelace == 0 && tokens.items.is_empty() => Ok(Vec::new()),
Payee::Nobody => bail!("A transaction that pays no one pays nothing"),
}
}
}
#[derive(Clone)]
pub struct AccountPay<'a> {
pub payee: Payee<'a>,
pub amount: AccountAmount,
pub picked: Vec<(String, String, u64)>,
}
impl<'a> AccountPay<'a> {
pub fn new(payee: Payee<'a>, amount: AccountAmount, picked: &[(String, String, u64)]) -> Self {
AccountPay {
payee,
amount,
picked: picked.to_vec(),
}
}
}
pub struct AccountPayment {
pub tx: BuiltTransaction,
pub inputs: Vec<UtxoResponse>,
pub fee: u64,
pub lovelace: u64,
pub tokens: Assets,
pub paid: Vec<u64>,
pub outputs: usize,
pub change_lovelace: u64,
pub change_tokens: Assets,
pub left_out: Vec<UtxoResponse>,
}
#[allow(clippy::too_many_arguments)]
pub fn move_in(
params: &ProtocolParameters,
available: &[UtxoResponse],
amount: AccountAmount,
picked: &[(String, String, u64)],
owner: &Register,
wallet_addr: &Address,
change_addr: &Address,
staking: &Staking,
invalid_hereafter: Option<u64>,
) -> Result<AccountPayment> {
let payee = Payee::Seedelf { owner, wallet_addr };
account_payment(
params,
available,
&[AccountPay::new(payee, amount, picked)],
Patches::staking(staking),
change_addr,
MOVE_IN_SHORT,
invalid_hereafter,
)
}
#[allow(clippy::too_many_arguments)]
pub fn account_send(
params: &ProtocolParameters,
available: &[UtxoResponse],
amount: AccountAmount,
picked: &[(String, String, u64)],
to: &Address,
network_flag: bool,
change_addr: &Address,
staking: &Staking,
invalid_hereafter: Option<u64>,
) -> Result<AccountPayment> {
let payee = Payee::Address(to);
account_send_many(
params,
available,
&[AccountPay::new(payee, amount, picked)],
network_flag,
change_addr,
staking,
None,
invalid_hereafter,
)
}
#[allow(clippy::too_many_arguments)]
pub fn account_send_many(
params: &ProtocolParameters,
available: &[UtxoResponse],
recipients: &[AccountPay],
network_flag: bool,
change_addr: &Address,
staking: &Staking,
note: Option<&Note>,
invalid_hereafter: Option<u64>,
) -> Result<AccountPayment> {
if recipients.is_empty() {
bail!("A payment needs someone to pay");
}
for pay in recipients {
match pay.payee {
Payee::Address(to) => check_payable(to, network_flag)?,
Payee::Seedelf { owner, .. } => check_register(owner)?,
Payee::Nobody => bail!("A payment needs someone to pay"),
}
}
account_payment(
params,
available,
recipients,
Patches { staking, note },
change_addr,
SEND_SHORT,
invalid_hereafter,
)
}
fn check_register(register: &Register) -> Result<()> {
if !is_payable(register) {
bail!(
"That Seedelf's register isn't valid: a payment to it could be locked for good, or taken by anyone"
);
}
Ok(())
}
pub fn account_staking(
params: &ProtocolParameters,
available: &[UtxoResponse],
staking: &Staking,
change_addr: &Address,
invalid_hereafter: Option<u64>,
) -> Result<AccountPayment> {
if staking.is_empty() {
bail!("A staking transaction needs a certificate, a withdrawal or a vote");
}
account_payment(
params,
available,
&[AccountPay::new(
Payee::Nobody,
AccountAmount::Lovelace(0),
&[],
)],
Patches::staking(staking),
change_addr,
STAKE_SHORT,
invalid_hereafter,
)
}
fn account_payment(
params: &ProtocolParameters,
available: &[UtxoResponse],
pays: &[AccountPay],
patches: Patches,
change_addr: &Address,
short: NotEnough,
invalid_hereafter: Option<u64>,
) -> Result<AccountPayment> {
let max = pays.iter().any(|p| p.amount == AccountAmount::Max);
if max && pays.len() > 1 {
bail!("Max pays a single recipient: give each of several an amount");
}
let mut picked: BTreeMap<(&str, &str), u64> = BTreeMap::new();
for (policy, name, quantity) in pays.iter().flat_map(|p| &p.picked) {
if *quantity == 0 {
bail!("Choose more than none of the token {policy}.{name}, or leave it out");
}
let total = picked.entry((policy, name)).or_default();
*total = total.saturating_add(*quantity);
}
let holds_picked = |u: &UtxoResponse| {
u.asset_list.as_ref().is_some_and(|assets| {
assets
.iter()
.any(|a| picked.contains_key(&(a.policy_id.as_str(), a.asset_name.as_str())))
})
};
let priced: Vec<UtxoResponse> = available
.iter()
.filter(|u| reference_script_size(u).is_ok())
.cloned()
.collect();
let (eligible, _) = fitting(&priced, &Assets::new(), holds_picked)?;
let left_out: Vec<UtxoResponse> = available
.iter()
.filter(|u| !eligible.iter().any(|e| same_utxo(e, u)))
.cloned()
.collect();
for ((policy, name), quantity) in &picked {
let held: u64 = eligible
.iter()
.flat_map(|u| u.asset_list.iter().flatten())
.filter(|a| a.policy_id == *policy && a.asset_name == *name)
.map(|a| a.quantity.parse::<u64>().unwrap_or(0))
.sum();
if held == 0 {
bail!("The Cardano account doesn't hold the token {policy}.{name}");
}
if *quantity > held {
bail!("The Cardano account holds only {held} of the token {policy}.{name}");
}
}
let (mandatory, mut rest): (Vec<UtxoResponse>, Vec<UtxoResponse>) =
eligible.into_iter().partition(|u| holds_picked(u));
rest.sort_by_key(|u| {
let tokens = u.asset_list.as_ref().is_some_and(|a| !a.is_empty());
let lovelace = u.value.parse::<u64>().unwrap_or(0);
(
tokens,
u.reference_script.is_some(),
std::cmp::Reverse(lovelace),
)
});
let attempt = |selected: &[UtxoResponse]| {
build_account_payment(
params,
selected,
pays,
patches,
change_addr,
short,
invalid_hereafter,
)
};
if max {
let all: Vec<UtxoResponse> = mandatory.into_iter().chain(rest).collect();
if all.is_empty() {
bail!("There is nothing in the Cardano account to spend");
}
let mut built = attempt(&all)?;
built.left_out = left_out;
return Ok(built);
}
let mut last_error = None;
for k in 0..=rest.len() {
let selected: Vec<UtxoResponse> = mandatory
.iter()
.chain(rest.iter().take(k))
.cloned()
.collect();
if selected.is_empty() {
continue;
}
match attempt(&selected) {
Ok(built) => return Ok(built),
Err(e) if e.downcast_ref::<NotEnough>().is_some() => last_error = Some(e),
Err(e) => return Err(e),
}
}
Err(last_error.unwrap_or_else(|| short.into()))
}
fn build_account_payment(
params: &ProtocolParameters,
selected: &[UtxoResponse],
pays: &[AccountPay],
patches: Patches,
change_addr: &Address,
short: NotEnough,
invalid_hereafter: Option<u64>,
) -> Result<AccountPayment> {
let (inputs_total, all_tokens) = assets_of(selected.to_vec())?;
let total = patches.staking.net(inputs_total).ok_or(short)?;
let asked_in = |picked: &[(String, String, u64)], a: &Asset| -> u64 {
let policy = hex::encode(a.policy_id);
let name = hex::encode(&a.token_name);
picked
.iter()
.filter(|(p, n, _)| *p == policy && *n == name)
.map(|(_, _, q)| *q)
.fold(0u64, u64::saturating_add)
};
let tokens_for = |picked: &[(String, String, u64)]| Assets {
items: all_tokens
.items
.iter()
.filter(|a| asked_in(picked, a) > 0)
.map(|a| Asset {
amount: asked_in(picked, a).min(a.amount),
..a.clone()
})
.collect(),
};
let each_tokens: Vec<Assets> = pays.iter().map(|p| tokens_for(&p.picked)).collect();
let all_picked: Vec<(String, String, u64)> =
pays.iter().flat_map(|p| p.picked.iter().cloned()).collect();
let asked = |a: &Asset| asked_in(&all_picked, a).min(a.amount);
let paying = tokens_for(&all_picked);
let staying = Assets {
items: all_tokens
.items
.iter()
.filter(|a| a.amount > asked(a))
.map(|a| Asset {
amount: a.amount - asked(a),
..a.clone()
})
.collect(),
};
let inputs: Vec<Input> = selected.iter().map(input_of).collect::<Result<_>>()?;
let signers = selected
.iter()
.map(|u| u.payment_cred.as_str())
.collect::<BTreeSet<_>>()
.len();
let change_floor = minimum_change(params, change_addr, &staying)?;
let script_fee = reference_script_fee(params, reference_script_bytes(selected)?)?;
let mut paid: Vec<u64> = Vec::new();
let mut change = 0;
let mut outputs = 0;
let price = |size| linear_fee(params, size).saturating_add(script_fee);
let (fee, staged) = settle(signers, patches, price, |fee| {
(paid, change) = match pays {
[only] if only.amount == AccountAmount::Max => {
let all = total
.checked_sub(fee)
.and_then(|rest| rest.checked_sub(change_floor))
.ok_or(short)?;
(vec![all], change_floor)
}
_ => {
let each: Vec<u64> = pays
.iter()
.map(|p| match p.amount {
AccountAmount::Lovelace(lovelace) => lovelace,
AccountAmount::Max => 0,
})
.collect();
let change = each
.iter()
.try_fold(total, |rest, l| rest.checked_sub(*l))
.and_then(|rest| rest.checked_sub(fee))
.ok_or(short)?;
(each, change)
}
};
let mut tx = StagingTransaction::new();
for input in &inputs {
tx = tx.input(input.clone());
}
outputs = 0;
for ((pay, lovelace), tokens) in pays.iter().zip(&paid).zip(&each_tokens) {
for output in pay.payee.outputs(params, *lovelace, tokens)? {
outputs += 1;
tx = tx.output(output);
}
}
for output in change_outputs(params, change_addr, change, &staying, short)? {
tx = tx.output(output);
}
Ok(expiring(tx.fee(fee), invalid_hereafter))
})?;
Ok(AccountPayment {
tx: built_with(staged, patches)?,
inputs: selected.to_vec(),
fee,
lovelace: paid.iter().sum(),
tokens: paying,
paid,
outputs,
change_lovelace: change,
change_tokens: staying,
left_out: Vec::new(),
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Budget {
pub mem: u64,
pub steps: u64,
}
pub const DRAFT_BUDGET: Budget = Budget {
mem: 14_000_000,
steps: 10_000_000_000,
};
pub const MAX_TX_BUDGET: Budget = Budget {
mem: 16_500_000,
steps: 10_000_000_000,
};
pub const SPEND_BUDGET_GUESS: Budget = Budget {
mem: 100_000,
steps: 450_000_000,
};
pub const MINT_BUDGET_GUESS: Budget = Budget {
mem: 100_000,
steps: 30_000_000,
};
pub const COLLATERAL_LOVELACE: u64 = 5_000_000;
pub fn even(fee: u64) -> u64 {
fee + fee % 2
}
pub fn collateral_output(addr: Address, fee: u64) -> Result<Output> {
let collateral_return: u64 = COLLATERAL_LOVELACE
.checked_sub(fee * 3 / 2)
.context("transaction fee is too large for the 5 ADA collateral to cover")?;
Ok(Output::new(addr, collateral_return))
}
pub fn collateral_signature(answer: &Value) -> Result<[u8; 64]> {
let witness = answer
.get("witness")
.and_then(Value::as_str)
.filter(|w| w.len() >= 128)
.with_context(|| format!("The collateral service answered unexpectedly: {answer}"))?
.as_bytes();
hex::decode(&witness[witness.len() - 128..])
.context("The collateral service's witness isn't hex")?
.try_into()
.map_err(|_| anyhow::anyhow!("The collateral service's signature isn't 64 bytes"))
}
pub fn is_collateral_signature(tx_hash: &[u8], signature: &[u8; 64]) -> bool {
PublicKey::from(COLLATERAL_PUBLIC_KEY).verify(tx_hash, &Signature::from(*signature))
}
pub fn add_witnesses(tx_cbor: &[u8], witnesses: &[(PublicKey, Signature)]) -> Result<Vec<u8>> {
let hash = tx_id(tx_cbor)?;
let mut tx = conway::Tx::decode_fragment(tx_cbor)
.map_err(|e| anyhow::anyhow!("The transaction to sign isn't a Conway transaction: {e}"))?;
let mut vkeys: Vec<conway::VKeyWitness> = tx
.transaction_witness_set
.vkeywitness
.map(|w| w.to_vec())
.unwrap_or_default();
for (key, signature) in witnesses {
vkeys.push(conway::VKeyWitness {
vkey: key.as_ref().to_vec().into(),
signature: signature.as_ref().to_vec().into(),
});
}
tx.transaction_witness_set.vkeywitness = NonEmptySet::from_vec(vkeys);
let signed = tx
.encode_fragment()
.map_err(|e| anyhow::anyhow!("Failed to encode the signed transaction: {e}"))?;
if tx_id(&signed)? != hash {
bail!("Signing changed the transaction");
}
Ok(signed)
}
pub fn tx_id(tx_cbor: &[u8]) -> Result<Hash<32>> {
let mut decoder = minicbor::Decoder::new(tx_cbor);
let body = decoder.array().and_then(|_| {
let start = decoder.position();
decoder.skip()?;
Ok(start..decoder.position())
});
let body = body.map_err(|e| anyhow::anyhow!("The transaction isn't valid CBOR: {e}"))?;
Ok(Hasher::<256>::hash(&tx_cbor[body]))
}
pub fn required_signers(tx_cbor: &[u8]) -> Result<Vec<Hash<28>>> {
let tx = conway::Tx::decode_fragment(tx_cbor)
.map_err(|e| anyhow::anyhow!("The transaction isn't a Conway transaction: {e}"))?;
Ok(tx
.transaction_body
.required_signers
.map(|s| s.to_vec())
.unwrap_or_default())
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Budgets {
spend: BTreeMap<u64, Budget>,
mint: BTreeMap<u64, Budget>,
withdraw: BTreeMap<u64, Budget>,
}
impl Budgets {
pub fn from_ogmios(answer: &Value) -> Result<Self> {
if let Some(error) = answer.get("error") {
bail!(ogmios_failure(error));
}
let rows = answer
.get("result")
.and_then(Value::as_array)
.with_context(|| format!("Ogmios answered without budgets: {answer}"))?;
let mut budgets = Budgets::default();
for row in rows {
let purpose = row.pointer("/validator/purpose").and_then(Value::as_str);
let index = row.pointer("/validator/index").and_then(Value::as_u64);
let mem = row.pointer("/budget/memory").and_then(Value::as_u64);
let steps = row.pointer("/budget/cpu").and_then(Value::as_u64);
let (Some(purpose), Some(index), Some(mem), Some(steps)) = (purpose, index, mem, steps)
else {
bail!("Ogmios returned a budget the wallet can't read: {row}");
};
let budget = Budget { mem, steps };
match purpose {
"spend" => budgets.spend.insert(index, budget),
"mint" => budgets.mint.insert(index, budget),
"withdraw" => budgets.withdraw.insert(index, budget),
other => {
bail!("Ogmios measured a {other} script, which this transaction doesn't have")
}
};
}
Ok(budgets)
}
fn guess(spends: &[u64], mints: usize) -> Self {
Budgets {
spend: spends.iter().map(|i| (*i, SPEND_BUDGET_GUESS)).collect(),
mint: (0..mints as u64).map(|i| (i, MINT_BUDGET_GUESS)).collect(),
withdraw: BTreeMap::new(),
}
}
pub fn spend(&self, index: u64) -> Option<Budget> {
self.spend.get(&index).copied()
}
pub fn mint(&self, index: u64) -> Option<Budget> {
self.mint.get(&index).copied()
}
pub fn withdraw(&self, index: u64) -> Option<Budget> {
self.withdraw.get(&index).copied()
}
pub fn with_margin(&self, percent: u64) -> Self {
let raise = |m: &BTreeMap<u64, Budget>| {
m.iter()
.map(|(i, b)| {
let up = |x: u64| x + (x * percent).div_ceil(100);
(
*i,
Budget {
mem: up(b.mem),
steps: up(b.steps),
},
)
})
.collect()
};
Budgets {
spend: raise(&self.spend),
mint: raise(&self.mint),
withdraw: raise(&self.withdraw),
}
}
pub fn covers(&self, used: &Budgets) -> bool {
let within = |ours: &BTreeMap<u64, Budget>, theirs: &BTreeMap<u64, Budget>| {
theirs.iter().all(|(i, b)| {
ours.get(i)
.is_some_and(|o| o.mem >= b.mem && o.steps >= b.steps)
})
};
within(&self.spend, &used.spend)
&& within(&self.mint, &used.mint)
&& within(&self.withdraw, &used.withdraw)
}
}
fn ogmios_failure(error: &Value) -> String {
let message = error
.get("message")
.and_then(Value::as_str)
.unwrap_or("no reason given");
let items: Vec<&Value> = error
.get("data")
.and_then(Value::as_array)
.into_iter()
.flatten()
.collect();
let stale = error.get("code").and_then(Value::as_i64) == Some(3117)
|| items.iter().any(|item| {
matches!(
item.pointer("/error/code").and_then(Value::as_i64),
Some(3110 | 3117)
)
});
if stale {
return "Some of the UTxOs this spends aren't on chain anymore. Refresh the balance and try again.".to_string();
}
let failures: Vec<String> = items
.iter()
.filter_map(|item| {
let purpose = item.pointer("/validator/purpose").and_then(Value::as_str)?;
let index = item.pointer("/validator/index").and_then(Value::as_u64)?;
let what = match purpose {
"spend" => format!("spending input {index}"),
"mint" => "the Seedelf policy".to_string(),
other => format!("{other} {index}"),
};
let reason = item
.pointer("/error/data/validationError")
.and_then(Value::as_str)
.and_then(|e| e.lines().map(str::trim).rfind(|l| !l.is_empty()))
.or_else(|| item.pointer("/error/message").and_then(Value::as_str))
.unwrap_or("failed");
let traces: Vec<&str> = item
.pointer("/error/data/traces")
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter_map(Value::as_str)
.collect();
Some(if traces.is_empty() {
format!("{what}: {reason}")
} else {
format!("{what}: {reason}, traces: {}", traces.join("; "))
})
})
.collect();
let foreign = items.iter().any(|item| {
!matches!(
item.pointer("/validator/purpose").and_then(Value::as_str),
Some("spend" | "mint") | None
)
});
if failures.is_empty() {
format!("Ogmios couldn't evaluate the transaction: {message}")
} else if foreign {
format!(
"A script refused this transaction ({})",
failures.join("; ")
)
} else {
format!(
"The Seedelf contract refused this transaction ({})",
failures.join("; ")
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScriptFee {
pub size: u64,
pub compute: u64,
pub script_reference: u64,
pub total: u64,
}
pub struct FinalSpend {
pub tx: BuiltTransaction,
pub fee: ScriptFee,
pub change_lovelace: u64,
pub change_tokens: Assets,
pub change_outputs: usize,
}
#[derive(Clone)]
pub struct Chain {
pub params: ProtocolParameters,
pub network_flag: bool,
pub config: Config,
}
#[derive(Clone)]
struct OwnedInput {
utxo: UtxoResponse,
input: Input,
register: Register,
}
#[derive(Clone)]
struct PolicyMint {
policy: Hash<28>,
token_name: Vec<u8>,
amount: i64,
redeemer: Vec<u8>,
}
#[derive(Clone)]
struct AccountPart {
inputs: Vec<(UtxoResponse, Input)>,
collateral: Input,
collateral_lovelace: u64,
collateral_addr: Address,
}
#[derive(Clone)]
pub struct ScriptSpend {
chain: Chain,
inputs: Vec<OwnedInput>,
redeemers: Option<Vec<Vec<u8>>>,
outputs: Vec<Output>,
paid_lovelace: u64,
paid_tokens: Assets,
mint: Option<PolicyMint>,
change_owner: Register,
change_addr: Option<Address>,
signer: Hash<28>,
account: Option<AccountPart>,
}
impl ScriptSpend {
pub fn new(
chain: &Chain,
inputs: &[UtxoResponse],
change_owner: &Register,
signer: Hash<28>,
) -> Result<Self> {
if inputs.is_empty() {
bail!("A Seedelf spend needs at least one UTxO to spend");
}
let inputs = inputs
.iter()
.map(|utxo| {
let register =
extract_bytes_with_logging(&utxo.inline_datum).with_context(|| {
format!("UTxO {}#{} holds no register", utxo.tx_hash, utxo.tx_index)
})?;
Ok(OwnedInput {
input: input_of(utxo)?,
utxo: utxo.clone(),
register,
})
})
.collect::<Result<Vec<_>>>()?;
Ok(ScriptSpend {
chain: chain.clone(),
inputs,
redeemers: None,
outputs: Vec::new(),
paid_lovelace: 0,
paid_tokens: Assets::new(),
mint: None,
change_owner: change_owner.clone(),
change_addr: None,
signer,
account: None,
})
}
pub fn with_account(
mut self,
account: Hash<28>,
inputs: &[UtxoResponse],
collateral: &UtxoResponse,
) -> Result<Self> {
if account == self.signer {
bail!("The account's key can't also be the spend's one-time key");
}
let network_flag = self.chain.network_flag;
let at_signer = |utxo: &UtxoResponse| -> Result<Address> {
let addr = Address::from_bech32(&utxo.address).with_context(|| {
format!(
"UTxO {}#{} has an unreadable address",
utxo.tx_hash, utxo.tx_index
)
})?;
let key = match &addr {
Address::Shelley(s) if is_on_correct_network(addr.clone(), network_flag) => {
match s.payment() {
pallas_addresses::ShelleyPaymentPart::Key(k) => Some(*k),
pallas_addresses::ShelleyPaymentPart::Script(_) => None,
}
}
_ => None,
};
if key != Some(account) {
bail!(
"UTxO {}#{} isn't under the account's key",
utxo.tx_hash,
utxo.tx_index
);
}
Ok(addr)
};
let mut taken = Vec::with_capacity(inputs.len());
for utxo in inputs {
at_signer(utxo)?;
let input = input_of(utxo)?;
if self.inputs.iter().any(|o| o.input == input)
|| taken
.iter()
.any(|(_, i): &(UtxoResponse, Input)| *i == input)
{
bail!("UTxO {}#{} is spent twice", utxo.tx_hash, utxo.tx_index);
}
taken.push((utxo.clone(), input));
}
let collateral_addr = at_signer(collateral)?;
if collateral
.asset_list
.as_ref()
.is_some_and(|a| !a.is_empty())
{
bail!("A collateral must hold ADA alone");
}
self.account = Some(AccountPart {
inputs: taken,
collateral: input_of(collateral)?,
collateral_lovelace: collateral
.value
.parse()
.context("The collateral's value isn't a number")?,
collateral_addr,
});
Ok(self)
}
pub fn change_to(mut self, addr: &Address) -> Self {
self.change_addr = Some(addr.clone());
self
}
pub fn output(mut self, output: Output, tokens: &Assets) -> Result<Self> {
self.paid_lovelace = self
.paid_lovelace
.checked_add(output.lovelace)
.context("output lovelace overflow")?;
self.paid_tokens = self.paid_tokens.merge(tokens.clone())?;
self.outputs.push(output);
Ok(self)
}
pub fn mint(mut self, token_name: Vec<u8>, amount: i64, redeemer: Vec<u8>) -> Result<Self> {
self.mint = Some(PolicyMint {
policy: policy_hash(&self.chain.config)?,
token_name,
amount,
redeemer,
});
Ok(self)
}
pub fn proven(
mut self,
mut prove: impl FnMut(&Register, &str) -> Result<(String, String)>,
) -> Result<Self> {
let vkh = hex::encode(self.signer);
let redeemers = self
.inputs
.iter()
.map(|owned| {
let (z, g_r) = prove(&owned.register, &vkh)?;
create_spend_redeemer(z, g_r, vkh.clone())
})
.collect::<Result<Vec<_>>>()?;
self.redeemers = Some(redeemers);
Ok(self)
}
pub fn inputs(&self) -> Vec<UtxoResponse> {
self.inputs.iter().map(|o| o.utxo.clone()).collect()
}
pub fn draft(&self) -> Result<BuiltTransaction> {
let redeemers = self.proofs()?;
let fee = self.estimate()?.fee.total;
self.stage(fee, None, redeemers)?
.build_conway_raw()
.context("Failed To Build The Draft Transaction")
}
pub fn finalize(&self, budgets: &Budgets) -> Result<FinalSpend> {
let redeemers = self.proofs()?;
self.settle(budgets, redeemers)
}
pub fn estimate(&self) -> Result<FinalSpend> {
let placeholder = create_spend_redeemer(
"00".repeat(32),
Register::create(blstrs::Scalar::from(1u64))?.generator,
hex::encode(self.signer),
)?;
let redeemers = vec![placeholder; self.inputs.len()];
let budgets = Budgets::guess(&self.positions()?, usize::from(self.mint.is_some()));
self.settle(&budgets, &redeemers)
}
pub fn measure_locally(&self, known: &[crate::eval::Resolved]) -> Result<FinalSpend> {
let Chain {
params,
network_flag,
..
} = &self.chain;
let mut resolved: Vec<crate::eval::Resolved> = known.to_vec();
let rows = self.inputs.iter().map(|o| &o.utxo).chain(
self.account
.iter()
.flat_map(|a| a.inputs.iter().map(|(u, _)| u)),
);
for row in rows {
let hash = decode_tx_hash(&row.tx_hash)?;
if !resolved
.iter()
.any(|r| r.tx_hash == hash && r.index == row.tx_index)
{
resolved.push(crate::eval::resolve_row(row)?);
}
}
resolved.extend(crate::eval::seedelf_references(*network_flag)?);
let measure = |tx: &BuiltTransaction| -> Result<Budgets> {
let answer = crate::eval::evaluate(
&tx.tx_bytes.0,
&resolved,
¶ms.cost_model_v3,
*network_flag,
)?;
Budgets::from_ogmios(&answer)
};
let budgets = measure(&self.draft()?)?;
let finished = self.finalize(&budgets)?;
let again = measure(&finished.tx)?;
if budgets.covers(&again) {
return Ok(finished);
}
let finished = self.finalize(&again.with_margin(1))?;
if !again.with_margin(1).covers(&measure(&finished.tx)?) {
bail!("The scripts' budgets did not settle");
}
Ok(finished)
}
fn proofs(&self) -> Result<&[Vec<u8>]> {
self.redeemers
.as_deref()
.context("The Seedelf spend isn't proven yet")
}
fn ledger_order(&self) -> Vec<Input> {
let mut order: Vec<Input> = self
.inputs
.iter()
.map(|o| o.input.clone())
.chain(
self.account
.iter()
.flat_map(|a| a.inputs.iter().map(|(_, i)| i.clone())),
)
.collect();
order.sort_by_key(|i| (i.tx_hash.0, i.txo_index));
order
}
fn positions(&self) -> Result<Vec<u64>> {
let order = self.ledger_order();
self.inputs
.iter()
.map(|owned| {
order
.iter()
.position(|i| *i == owned.input)
.map(|p| p as u64)
.context("An input is missing from the transaction")
})
.collect()
}
fn settle(&self, budgets: &Budgets, redeemers: &[Vec<u8>]) -> Result<FinalSpend> {
let mut used = Vec::with_capacity(self.inputs.len() + 1);
let mut positions = self.positions()?;
positions.sort_unstable();
for index in positions {
used.push(budgets.spend(index).with_context(|| {
format!("Ogmios measured no budget for spending input {index}")
})?);
}
if self.mint.is_some() {
used.push(
budgets
.mint(0)
.context("Ogmios measured no budget for the Seedelf policy")?,
);
}
let total = used
.iter()
.fold(Budget { mem: 0, steps: 0 }, |a, b| Budget {
mem: a.mem + b.mem,
steps: a.steps + b.steps,
});
if total.mem > MAX_TX_BUDGET.mem || total.steps > MAX_TX_BUDGET.steps {
bail!(
"Spending {} UTxOs at once needs more computation than a transaction may use",
self.inputs.len()
);
}
let compute: u64 = used
.iter()
.map(|b| computation_fee(&self.chain.params, b.mem, b.steps))
.sum();
let contract = &self.chain.config.contract;
let mut script_bytes = contract.wallet_contract_size;
if self.mint.is_some() {
script_bytes += contract.seedelf_contract_size;
}
let mut spent = self.inputs();
if let Some(account) = &self.account {
spent.extend(account.inputs.iter().map(|(u, _)| u.clone()));
}
script_bytes += reference_script_bytes(&spent)?;
let script_reference = reference_script_fee(&self.chain.params, script_bytes)?;
let (fee, staged) = settle(
2,
Patches::staking(&Staking::none()),
|size| even(linear_fee(&self.chain.params, size) + compute + script_reference),
|fee| self.stage(fee, Some(budgets), redeemers),
)?;
let (change_lovelace, change_tokens) = self.remainder(fee)?;
let change_outputs = staged.outputs.as_ref().map_or(0, Vec::len) - self.outputs.len();
Ok(FinalSpend {
tx: staged
.build_conway_raw()
.context("Failed To Build The Transaction")?,
fee: ScriptFee {
size: fee - compute - script_reference,
compute,
script_reference,
total: fee,
},
change_lovelace,
change_tokens,
change_outputs,
})
}
fn remainder(&self, fee: u64) -> Result<(u64, Assets)> {
let mut spent = self.inputs();
if let Some(account) = &self.account {
spent.extend(account.inputs.iter().map(|(u, _)| u.clone()));
}
let (total, mut tokens) = assets_of(spent)?;
if let Some(m) = &self.mint {
let asset = Asset::new(
hex::encode(m.policy),
hex::encode(&m.token_name),
m.amount.unsigned_abs(),
)?;
tokens = if m.amount >= 0 {
tokens.add(asset)?
} else {
tokens
.sub(asset)
.context("The UTxOs spent don't hold the token to burn")?
};
}
let lovelace = total
.checked_sub(self.paid_lovelace)
.and_then(|rest| rest.checked_sub(fee))
.ok_or(SEEDELF_SHORT)?;
let tokens = tokens
.separate(self.paid_tokens.clone())
.context("The UTxOs spent don't hold the tokens being sent")?;
Ok((lovelace, tokens))
}
fn stage(
&self,
fee: u64,
budgets: Option<&Budgets>,
redeemers: &[Vec<u8>],
) -> Result<StagingTransaction> {
let Chain {
params,
network_flag,
config,
} = &self.chain;
let (change_lovelace, change_tokens) = self.remainder(fee)?;
let wallet_addr = wallet_contract(*network_flag, config.contract.wallet_contract_hash);
let change = if change_lovelace == 0 && change_tokens.is_empty() {
Vec::new()
} else if let Some(addr) = &self.change_addr {
change_outputs(params, addr, change_lovelace, &change_tokens, SEEDELF_SHORT)?
} else {
if change_lovelace < minimum_deposit(params, &change_tokens)? {
bail!(SEEDELF_SHORT);
}
deposit_outputs(
params,
&wallet_addr,
&self.change_owner,
change_lovelace,
&change_tokens,
)?
};
let mut tx = StagingTransaction::new();
for owned in &self.inputs {
tx = tx.input(owned.input.clone());
}
for (_, input) in self.account.iter().flat_map(|a| &a.inputs) {
tx = tx.input(input.clone());
}
let mut outputs: Vec<Output> = self.outputs.iter().cloned().chain(change).collect();
crate::lovejoin::shuffle(&mut outputs);
for output in outputs {
tx = tx.output(output);
}
tx = match &self.account {
None => tx
.collateral_input(collateral_input(*network_flag))
.collateral_output(collateral_output(collateral_address(*network_flag), fee)?)
.disclosed_signer(self.signer)
.disclosed_signer(Hash::new(COLLATERAL_HASH)),
Some(account) => {
let back = account
.collateral_lovelace
.checked_sub(fee * 3 / 2)
.context("The collateral can't cover this transaction's fee")?;
tx.collateral_input(account.collateral.clone())
.collateral_output(Output::new(account.collateral_addr.clone(), back))
.disclosed_signer(self.signer)
}
};
tx = tx
.fee(fee)
.reference_input(reference_utxo(config.reference.wallet_reference_utxo))
.language_view(ScriptKind::PlutusV3, params.cost_model_v3.clone());
let order = self.ledger_order();
let units = |budget: Option<Budget>| {
let b = budget.unwrap_or(DRAFT_BUDGET);
ExUnits {
mem: b.mem,
steps: b.steps,
}
};
for (owned, redeemer) in self.inputs.iter().zip(redeemers) {
let index = order
.iter()
.position(|i| *i == owned.input)
.context("An input is missing from the transaction")?;
let budget = budgets.and_then(|b| b.spend(index as u64));
tx = tx.add_spend_redeemer(owned.input.clone(), redeemer.clone(), Some(units(budget)));
}
if let Some(m) = &self.mint {
tx = tx
.reference_input(reference_utxo(config.reference.seedelf_reference_utxo))
.mint_asset(m.policy, m.token_name.clone(), m.amount)
.context("Failed To Mint The Seedelf")?
.add_mint_redeemer(
m.policy,
m.redeemer.clone(),
Some(units(budgets.and_then(|b| b.mint(0)))),
);
}
Ok(tx)
}
}
fn policy_hash(config: &Config) -> Result<Hash<28>> {
let bytes: [u8; 28] = hex::decode(&config.contract.seedelf_policy_id)
.context("The Seedelf policy id is not hex")?
.try_into()
.map_err(|_| anyhow::anyhow!("The Seedelf policy id is not 28 bytes"))?;
Ok(Hash::new(bytes))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Origin {
#[default]
Unknown,
Own,
Received,
Session,
Lovejoin,
}
pub const UNKNOWN_CLASS: &str = "unknown";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Class {
pub id: String,
pub origin: Origin,
}
impl Default for Class {
fn default() -> Self {
Class {
id: UNKNOWN_CLASS.to_string(),
origin: Origin::Unknown,
}
}
}
impl Class {
pub fn has(&self, part: &str) -> bool {
self.id.split('+').any(|p| p == part)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum Purpose {
#[default]
Pay,
Mint,
Fund { session: Option<String> },
}
impl Purpose {
fn rank(&self, class: &Class) -> u8 {
use Origin::*;
match self {
Purpose::Pay => match class.origin {
Own => 0,
Session => 1,
Received => 2,
Unknown => 3,
Lovejoin => 4,
},
Purpose::Mint => match class.origin {
Received => 0,
Lovejoin => 1,
Unknown => 2,
Own => 3,
Session => 4,
},
Purpose::Fund { session } => {
if session.as_deref().is_some_and(|s| class.has(s)) {
return 0;
}
match class.origin {
Received => 1,
Lovejoin => 2,
Unknown => 3,
Own => 4,
Session => 5,
}
}
}
}
fn tier(&self, class: &Class) -> u8 {
match (self, class.origin) {
(_, Origin::Lovejoin) => 2,
(Purpose::Fund { .. }, Origin::Session) if self.rank(class) > 0 => 1,
_ => 0,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Histories {
classes: BTreeMap<(String, u64), Class>,
purpose: Purpose,
}
impl Histories {
pub fn new(purpose: Purpose) -> Self {
Histories {
classes: BTreeMap::new(),
purpose,
}
}
pub fn with(mut self, tx_hash: &str, tx_index: u64, class: Class) -> Self {
self.classes.insert((tx_hash.to_string(), tx_index), class);
self
}
pub fn class_of(&self, utxo: &UtxoResponse) -> Class {
self.classes
.get(&(utxo.tx_hash.clone(), utxo.tx_index))
.cloned()
.unwrap_or_default()
}
pub fn merged(&self, inputs: &[UtxoResponse]) -> Vec<Class> {
let mut classes: Vec<Class> = Vec::new();
for class in inputs.iter().map(|u| self.class_of(u)) {
if !classes.iter().any(|c| c.id == class.id) {
classes.push(class);
}
}
if classes.len() > 1 {
classes
} else {
Vec::new()
}
}
fn blind(&self, utxos: &[UtxoResponse]) -> bool {
utxos
.iter()
.all(|u| self.class_of(u).origin == Origin::Unknown)
}
}
fn lovelace_of(utxo: &UtxoResponse) -> u64 {
utxo.value.parse::<u64>().unwrap_or(0)
}
fn holds_tokens(utxo: &UtxoResponse) -> bool {
utxo.asset_list.as_ref().is_some_and(|a| !a.is_empty())
}
fn select_script_inputs<T>(
available: &[UtxoResponse],
needed: &Assets,
floor: u64,
histories: &Histories,
mut attempt: impl FnMut(&[UtxoResponse]) -> Result<T>,
) -> Result<T> {
let holds_needed = |u: &UtxoResponse| {
needed.items.iter().any(|want| {
quantity_in(
u,
&hex::encode(want.policy_id),
&hex::encode(&want.token_name),
) > 0
})
};
let evaluable: Vec<UtxoResponse> = available
.iter()
.filter(|u| crate::eval::refusal(u).is_none())
.cloned()
.collect();
let (available, _) = fitting(&evaluable, &Assets::new(), holds_needed)?;
let available = available.as_slice();
let mandatory = holding(available, needed)?;
let mut rest: Vec<UtxoResponse> = available
.iter()
.filter(|u| !mandatory.iter().any(|m| same_utxo(m, u)))
.cloned()
.collect();
if !histories.blind(available) {
return apart(mandatory, rest, floor, histories, attempt);
}
rest.sort_by_key(|u| (holds_tokens(u), std::cmp::Reverse(lovelace_of(u))));
let mut last_error = None;
for k in 0..=rest.len() {
let selected: Vec<UtxoResponse> = mandatory.iter().chain(&rest[..k]).cloned().collect();
if selected.is_empty() {
continue;
}
match attempt(&selected) {
Ok(built) => return Ok(built),
Err(e) if e.downcast_ref::<NotEnough>().is_some() => last_error = Some(e),
Err(e) => return Err(e),
}
}
Err(last_error.unwrap_or_else(|| SEEDELF_SHORT.into()))
}
const MAX_TOKEN_SINGLES: usize = 12;
enum Tried<T> {
Built(T),
Short,
Failed,
Skipped,
}
fn one_more<'a, T>(
mut candidates: Vec<&'a UtxoResponse>,
rank: impl Fn(&UtxoResponse) -> u8,
try_with: &mut impl FnMut(&[&'a UtxoResponse]) -> Tried<T>,
) -> Option<T> {
candidates.sort_by_key(|u| (rank(u), lovelace_of(u)));
let mut short_at: Option<u64> = None;
let mut token_tries = 0;
for u in candidates {
let ada_only = !holds_tokens(u);
if ada_only && short_at.is_some_and(|at| lovelace_of(u) <= at) {
continue;
}
if !ada_only {
if token_tries == MAX_TOKEN_SINGLES {
continue;
}
token_tries += 1;
}
match try_with(&[u]) {
Tried::Built(built) => return Some(built),
Tried::Short if ada_only => short_at = short_at.max(Some(lovelace_of(u))),
_ => {}
}
}
None
}
enum Grown<T> {
Built(T),
Short,
Failed(usize),
}
fn grow<'a, T>(
order: &[&'a UtxoResponse],
from: usize,
try_with: &mut impl FnMut(&[&'a UtxoResponse]) -> Tried<T>,
) -> Grown<T> {
for k in from.max(1)..=order.len() {
match try_with(&order[..k]) {
Tried::Built(built) => return Grown::Built(built),
Tried::Failed => return Grown::Failed(k),
_ => {}
}
}
Grown::Short
}
fn apart<T>(
base: Vec<UtxoResponse>,
rest: Vec<UtxoResponse>,
floor: u64,
histories: &Histories,
mut attempt: impl FnMut(&[UtxoResponse]) -> Result<T>,
) -> Result<T> {
let purpose = &histories.purpose;
let class = |u: &UtxoResponse| histories.class_of(u);
let base_lovelace: u64 = base.iter().map(lovelace_of).sum();
let base_classes: Vec<String> = base.iter().map(|u| class(u).id).collect();
let joins_base = |u: &UtxoResponse| base.is_empty() || base_classes.contains(&class(u).id);
let mut not_enough: Option<anyhow::Error> = None;
let mut other: Option<anyhow::Error> = None;
let mut try_with = |extra: &[&UtxoResponse]| -> Tried<T> {
let lovelace = base_lovelace + extra.iter().map(|u| lovelace_of(u)).sum::<u64>();
if lovelace <= floor || (base.is_empty() && extra.is_empty()) {
return Tried::Skipped;
}
let selected: Vec<UtxoResponse> = base
.iter()
.cloned()
.chain(extra.iter().map(|u| (*u).clone()))
.collect();
match attempt(&selected) {
Ok(built) => Tried::Built(built),
Err(e) if e.downcast_ref::<NotEnough>().is_some() => {
not_enough = Some(e);
Tried::Short
}
Err(e) => {
other = Some(e);
Tried::Failed
}
}
};
if let Tried::Built(built) = try_with(&[]) {
return Ok(built);
}
let singles: Vec<&UtxoResponse> = rest.iter().filter(|u| joins_base(u)).collect();
if let Some(built) = one_more(singles, |u| purpose.rank(&class(u)), &mut try_with) {
return Ok(built);
}
let mut classes: Vec<(Class, Vec<&UtxoResponse>)> = Vec::new();
for u in rest.iter().filter(|u| joins_base(u)) {
let c = class(u);
match classes.iter_mut().find(|(k, _)| k.id == c.id) {
Some((_, rows)) => rows.push(u),
None => classes.push((c, vec![u])),
}
}
classes.sort_by_key(|(c, rows)| {
let total: u64 = rows.iter().map(|u| lovelace_of(u)).sum();
(purpose.rank(c), std::cmp::Reverse(total), c.id.clone())
});
for (_, rows) in &mut classes {
rows.sort_by_key(|u| (holds_tokens(u), std::cmp::Reverse(lovelace_of(u))));
for k in 2..=rows.len() {
match try_with(&rows[..k]) {
Tried::Built(built) => return Ok(built),
Tried::Failed => break,
_ => {}
}
}
}
if !base.is_empty() {
let others: Vec<&UtxoResponse> = rest
.iter()
.filter(|u| !joins_base(u) && purpose.tier(&class(u)) == 0)
.collect();
if let Some(built) = one_more(others, |u| purpose.rank(&class(u)), &mut try_with) {
return Ok(built);
}
}
let tier = |u: &UtxoResponse| purpose.tier(&class(u));
let mut by_history: Vec<&UtxoResponse> = rest.iter().collect();
by_history.sort_by_key(|u| {
(
tier(u),
!joins_base(u),
purpose.rank(&class(u)),
holds_tokens(u),
std::cmp::Reverse(lovelace_of(u)),
)
});
let mut by_tier: Vec<&UtxoResponse> = rest.iter().collect();
by_tier.sort_by_key(|u| {
(
tier(u),
!joins_base(u),
holds_tokens(u),
std::cmp::Reverse(lovelace_of(u)),
)
});
let is_box = |u: &UtxoResponse| tier(u) == 2 && !holds_tokens(u);
let mut boxes: Vec<&UtxoResponse> = rest.iter().filter(|u| is_box(u)).collect();
boxes.sort_by_key(|u| (!joins_base(u), std::cmp::Reverse(lovelace_of(u))));
let mut largest: Vec<&UtxoResponse> = rest.iter().filter(|u| !is_box(u)).collect();
largest.sort_by_key(|u| (tier(u), !joins_base(u), std::cmp::Reverse(lovelace_of(u))));
let mut tokens_last: Vec<&UtxoResponse> = rest.iter().filter(|u| !is_box(u)).collect();
tokens_last.sort_by_key(|u| {
(
holds_tokens(u),
tier(u),
!joins_base(u),
std::cmp::Reverse(lovelace_of(u)),
)
});
let same = |a: &[&UtxoResponse], b: &[&UtxoResponse]| {
a.len() == b.len() && a.iter().zip(b).all(|(x, y)| std::ptr::eq(*x, *y))
};
let mut rests = vec![&largest];
if !same(&largest, &tokens_last) {
rests.push(&tokens_last);
}
let (boxes, rests) = (&boxes, &rests);
let fewest_boxes = (0..=boxes.len())
.filter(|_| !largest.is_empty())
.flat_map(|j| {
rests
.iter()
.map(move |others| (boxes[..j].iter().chain(others.iter()).copied().collect(), j))
});
let mut blind: Vec<&UtxoResponse> = rest.iter().collect();
blind.sort_by_key(|u| (holds_tokens(u), std::cmp::Reverse(lovelace_of(u))));
let mut failed = false;
let mut boxes_fail = false;
let mut grown: Vec<Vec<&UtxoResponse>> = Vec::new();
let orders = [(by_history, 0), (by_tier, 0)]
.into_iter()
.chain(fewest_boxes)
.chain([(blind, 0)]);
for (order, j) in orders {
let grown_before =
|g: &Vec<&UtxoResponse>| g.len() >= order.len() && same(&g[..order.len()], &order);
if (j > 0 && boxes_fail) || grown.iter().any(grown_before) {
continue;
}
let whole = order.len() == rest.len();
match grow(&order, j, &mut try_with) {
Grown::Built(built) => return Ok(built),
Grown::Short if whole => {
failed = false;
break;
}
Grown::Short => {}
Grown::Failed(at) => {
failed = true;
boxes_fail |= j > 0 && at == j;
}
}
if whole {
grown.push(order);
}
}
Err(match (failed, other, not_enough) {
(true, Some(e), _) => e,
(_, _, Some(e)) => e,
(_, Some(e), None) => e,
_ => SEEDELF_SHORT.into(),
})
}
fn same_utxo(a: &UtxoResponse, b: &UtxoResponse) -> bool {
a.tx_hash == b.tx_hash && a.tx_index == b.tx_index
}
fn quantity_in(utxo: &UtxoResponse, policy: &str, name: &str) -> u64 {
utxo.asset_list
.iter()
.flatten()
.filter(|a| a.policy_id == policy && a.asset_name == name)
.map(|a| a.quantity.parse::<u64>().unwrap_or(0))
.sum()
}
fn holding(available: &[UtxoResponse], needed: &Assets) -> Result<Vec<UtxoResponse>> {
let mut picked: Vec<UtxoResponse> = Vec::new();
for want in &needed.items {
let policy = hex::encode(want.policy_id);
let name = hex::encode(&want.token_name);
let mut have: u64 = picked.iter().map(|u| quantity_in(u, &policy, &name)).sum();
let mut candidates: Vec<&UtxoResponse> = available
.iter()
.filter(|u| quantity_in(u, &policy, &name) > 0)
.filter(|u| !picked.iter().any(|p| same_utxo(p, u)))
.collect();
candidates.sort_by_key(|u| std::cmp::Reverse(quantity_in(u, &policy, &name)));
for utxo in candidates {
if have >= want.amount {
break;
}
have = have.saturating_add(quantity_in(utxo, &policy, &name));
picked.push(utxo.clone());
}
if have < want.amount {
if have == 0 {
bail!("The Seedelf balance doesn't hold the token {policy}.{name}");
}
bail!(
"The Seedelf balance holds only {have} of the token {policy}.{name}, not {}",
want.amount
);
}
}
Ok(picked)
}
pub struct SeedelfMint {
pub spend: ScriptSpend,
pub token_name: Vec<u8>,
pub lovelace: u64,
}
pub fn mint(
chain: &Chain,
available: &[UtxoResponse],
label: &str,
seedelf: &Register,
change_owner: &Register,
signer: Hash<28>,
) -> Result<SeedelfMint> {
mint_apart(
chain,
available,
&Histories::default(),
label,
seedelf,
change_owner,
signer,
)
}
pub fn mint_apart(
chain: &Chain,
available: &[UtxoResponse],
histories: &Histories,
label: &str,
seedelf: &Register,
change_owner: &Register,
signer: Hash<28>,
) -> Result<SeedelfMint> {
let floor = seedelf_minimum_lovelace(&chain.params)?;
select_script_inputs(available, &Assets::new(), floor, histories, |inputs| {
let built = mint_from(chain, inputs, label, seedelf, change_owner, signer)?;
built.spend.estimate()?;
Ok(built)
})
}
pub fn mint_from(
chain: &Chain,
inputs: &[UtxoResponse],
label: &str,
seedelf: &Register,
change_owner: &Register,
signer: Hash<28>,
) -> Result<SeedelfMint> {
if !is_payable(seedelf) {
bail!("The Seedelf's register isn't made of valid points");
}
let spend = ScriptSpend::new(chain, inputs, change_owner, signer)?;
let Chain {
params,
network_flag,
config,
} = chain;
let policy = policy_hash(config)?;
let owned: Vec<Input> = spend.inputs.iter().map(|o| o.input.clone()).collect();
let token_name = seedelf_token_name(label.to_string(), Some(&owned))?;
let lovelace = seedelf_minimum_lovelace(params)?;
let wallet_addr = wallet_contract(*network_flag, config.contract.wallet_contract_hash);
let output = Output::new(wallet_addr, lovelace)
.set_inline_datum(seedelf.to_vec()?)
.add_asset(policy, token_name.clone(), 1)
.context("Failed To Add The Seedelf")?;
let token = Asset::new(
config.contract.seedelf_policy_id.clone(),
hex::encode(&token_name),
1,
)?;
let spend = spend.output(output, &Assets::new().add(token)?)?.mint(
token_name.clone(),
1,
create_mint_redeemer(label.to_string())?,
)?;
Ok(SeedelfMint {
spend,
token_name,
lovelace,
})
}
#[derive(Debug, Clone)]
pub struct Payment {
pub register: Register,
pub lovelace: u64,
pub tokens: Assets,
}
pub fn is_payable(register: &Register) -> bool {
let identity = |point: &str| {
hex::decode(point)
.is_ok_and(|b| b.len() == 48 && b[0] == 0xc0 && b[1..].iter().all(|&x| x == 0))
};
register.is_valid().unwrap_or(false)
&& !identity(®ister.generator)
&& !identity(®ister.public_value)
}
pub fn transfer(
chain: &Chain,
available: &[UtxoResponse],
payments: &[Payment],
change_owner: &Register,
signer: Hash<28>,
) -> Result<ScriptSpend> {
transfer_apart(
chain,
available,
&Histories::default(),
payments,
change_owner,
signer,
)
}
pub fn transfer_apart(
chain: &Chain,
available: &[UtxoResponse],
histories: &Histories,
payments: &[Payment],
change_owner: &Register,
signer: Hash<28>,
) -> Result<ScriptSpend> {
let outputs = payment_outputs(chain, payments)?;
let needed = payments
.iter()
.try_fold(Assets::new(), |all, p| all.merge(p.tokens.clone()))?;
select_script_inputs(
available,
&needed,
paid_out(&outputs),
histories,
|inputs| {
let spend = paying(chain, inputs, &outputs, change_owner, signer)?;
spend.estimate()?;
Ok(spend)
},
)
}
fn paid_out(outputs: &[(Output, Assets)]) -> u64 {
outputs.iter().map(|(o, _)| o.lovelace).sum()
}
pub fn transfer_from(
chain: &Chain,
inputs: &[UtxoResponse],
payments: &[Payment],
change_owner: &Register,
signer: Hash<28>,
) -> Result<ScriptSpend> {
let outputs = payment_outputs(chain, payments)?;
paying(chain, inputs, &outputs, change_owner, signer)
}
fn paying(
chain: &Chain,
inputs: &[UtxoResponse],
outputs: &[(Output, Assets)],
change_owner: &Register,
signer: Hash<28>,
) -> Result<ScriptSpend> {
outputs.iter().try_fold(
ScriptSpend::new(chain, inputs, change_owner, signer)?,
|spend, (output, tokens)| spend.output(output.clone(), tokens),
)
}
fn payment_outputs(chain: &Chain, payments: &[Payment]) -> Result<Vec<(Output, Assets)>> {
if payments.is_empty() {
bail!("A transfer pays at least one Seedelf");
}
let wallet_addr = wallet_contract(
chain.network_flag,
chain.config.contract.wallet_contract_hash,
);
payments
.iter()
.map(|payment| {
if !is_payable(&payment.register) {
bail!(
"That Seedelf's register isn't valid: a payment to it could be locked for good, or taken by anyone"
);
}
if payment.tokens.items.iter().any(|a| a.amount == 0) {
bail!("A payment can't send none of a token");
}
let minimum = minimum_seedelf_payment(&chain.params, &payment.tokens)?;
if payment.lovelace < minimum {
bail!(
"A payment to a Seedelf{} needs at least {} ADA",
if payment.tokens.is_empty() { "" } else { " with these tokens" },
ada(minimum)
);
}
let output = deposit_output(&wallet_addr, &payment.register, payment.lovelace, &payment.tokens)?;
Ok((output, payment.tokens.clone()))
})
.collect()
}
pub fn minimum_seedelf_payment(params: &ProtocolParameters, tokens: &Assets) -> Result<u64> {
wallet_minimum_lovelace_with_assets(params, tokens.clone())
}
fn ada(lovelace: u64) -> String {
format!("{}.{:06}", lovelace / 1_000_000, lovelace % 1_000_000)
}
pub fn is_payable_address(addr: &Address, network_flag: bool) -> bool {
matches!(addr, Address::Shelley(_))
&& is_not_a_script(addr.clone())
&& is_on_correct_network(addr.clone(), network_flag)
}
fn check_address(chain: &Chain, addr: &Address) -> Result<()> {
check_payable(addr, chain.network_flag)
}
fn check_payable(addr: &Address, network_flag: bool) -> Result<()> {
if !is_payable_address(addr, network_flag) {
let network = if network_flag { "preprod" } else { "mainnet" };
bail!(
"Payments go to a normal {network} address: not a script, stake or other network's address"
);
}
Ok(())
}
pub fn minimum_address_payment(
params: &ProtocolParameters,
to: &Address,
tokens: &Assets,
) -> Result<u64> {
let bech32 = to
.to_bech32()
.map_err(|e| anyhow::anyhow!("Failed to encode the address: {e}"))?;
address_minimum_lovelace_with_assets(params, &bech32, tokens.clone())
}
fn address_output(chain: &Chain, to: &Address, lovelace: u64, tokens: &Assets) -> Result<Output> {
check_address(chain, to)?;
pay_address(&chain.params, to, lovelace, tokens)
}
fn pay_address(
params: &ProtocolParameters,
to: &Address,
lovelace: u64,
tokens: &Assets,
) -> Result<Output> {
if tokens.items.iter().any(|a| a.amount == 0) {
bail!("A payment can't send none of a token");
}
let minimum = minimum_address_payment(params, to, tokens)?;
if lovelace < minimum {
bail!(
"A payment to that address{} needs at least {} ADA",
if tokens.is_empty() {
""
} else {
" with these tokens"
},
ada(minimum)
);
}
let mut output = Output::new(to.clone(), lovelace);
for asset in &tokens.items {
output = output
.add_asset(asset.policy_id, asset.token_name.clone(), asset.amount)
.context("Failed To Add An Asset")?;
}
Ok(output)
}
pub fn sweep(
chain: &Chain,
available: &[UtxoResponse],
to: &Address,
lovelace: u64,
tokens: &Assets,
change_owner: &Register,
signer: Hash<28>,
) -> Result<ScriptSpend> {
let payment = AddressPayment {
to: to.clone(),
lovelace,
tokens: tokens.clone(),
};
sweep_many(chain, available, &[payment], change_owner, signer)
}
#[derive(Debug, Clone)]
pub struct AddressPayment {
pub to: Address,
pub lovelace: u64,
pub tokens: Assets,
}
pub fn sweep_many(
chain: &Chain,
available: &[UtxoResponse],
payments: &[AddressPayment],
change_owner: &Register,
signer: Hash<28>,
) -> Result<ScriptSpend> {
sweep_many_apart(
chain,
available,
&Histories::default(),
payments,
change_owner,
signer,
)
}
pub fn sweep_many_apart(
chain: &Chain,
available: &[UtxoResponse],
histories: &Histories,
payments: &[AddressPayment],
change_owner: &Register,
signer: Hash<28>,
) -> Result<ScriptSpend> {
if payments.is_empty() {
bail!("A withdrawal pays at least one address");
}
let outputs: Vec<(Output, Assets)> = payments
.iter()
.map(|p| {
Ok((
address_output(chain, &p.to, p.lovelace, &p.tokens)?,
p.tokens.clone(),
))
})
.collect::<Result<_>>()?;
let needed = payments
.iter()
.try_fold(Assets::new(), |all, p| all.merge(p.tokens.clone()))?;
select_script_inputs(
available,
&needed,
paid_out(&outputs),
histories,
|inputs| {
let spend = paying(chain, inputs, &outputs, change_owner, signer)?;
spend.estimate()?;
Ok(spend)
},
)
}
pub fn sweep_from(
chain: &Chain,
inputs: &[UtxoResponse],
to: &Address,
lovelace: u64,
tokens: &Assets,
change_owner: &Register,
signer: Hash<28>,
) -> Result<ScriptSpend> {
let output = address_output(chain, to, lovelace, tokens)?;
ScriptSpend::new(chain, inputs, change_owner, signer)?.output(output, tokens)
}
pub fn sweep_all(
chain: &Chain,
inputs: &[UtxoResponse],
to: &Address,
change_owner: &Register,
signer: Hash<28>,
) -> Result<ScriptSpend> {
check_address(chain, to)?;
Ok(ScriptSpend::new(chain, inputs, change_owner, signer)?.change_to(to))
}
pub fn seedelf_in(chain: &Chain, utxo: &UtxoResponse) -> Result<Vec<u8>> {
let policy = &chain.config.contract.seedelf_policy_id;
let held: Vec<&seedelf_koios::koios::Asset> = utxo
.asset_list
.iter()
.flatten()
.filter(|a| &a.policy_id == policy)
.collect();
match held.as_slice() {
[one] if one.quantity == "1" => {
hex::decode(&one.asset_name).context("The Seedelf's name isn't hex")
}
_ => bail!(
"UTxO {}#{} doesn't hold exactly one Seedelf",
utxo.tx_hash,
utxo.tx_index
),
}
}
pub fn remove(
chain: &Chain,
seedelf_utxo: &UtxoResponse,
change_owner: &Register,
signer: Hash<28>,
) -> Result<ScriptSpend> {
let token_name = seedelf_in(chain, seedelf_utxo)?;
ScriptSpend::new(
chain,
std::slice::from_ref(seedelf_utxo),
change_owner,
signer,
)?
.mint(token_name, -1, create_mint_redeemer(String::new())?)
}
const ACCOUNT_COLLATERAL_MINIMUM: u64 = 2_000_000;
#[derive(Clone)]
pub struct AccountMint {
chain: Chain,
inputs: Vec<UtxoResponse>,
collateral: UtxoResponse,
seedelf: Output,
redeemer: Vec<u8>,
change_addr: Address,
staking: Staking,
invalid_hereafter: Option<u64>,
pub token_name: Vec<u8>,
pub lovelace: u64,
}
pub struct FinalAccountMint {
pub tx: BuiltTransaction,
pub fee: ScriptFee,
pub change_lovelace: u64,
pub change_tokens: Assets,
pub change_outputs: usize,
}
impl AccountMint {
pub fn inputs(&self) -> &[UtxoResponse] {
&self.inputs
}
pub fn collateral(&self) -> &UtxoResponse {
&self.collateral
}
pub fn draft(&self) -> Result<BuiltTransaction> {
let fee = self.estimate()?.fee.total;
built_with(self.stage(fee, None)?, Patches::staking(&self.staking))
}
pub fn finalize(&self, budgets: &Budgets) -> Result<FinalAccountMint> {
self.settle(budgets)
}
fn estimate(&self) -> Result<FinalAccountMint> {
self.settle(&Budgets::guess(&[], 1))
}
fn signers(&self) -> usize {
self.inputs
.iter()
.chain(std::iter::once(&self.collateral))
.map(|u| u.payment_cred.as_str())
.collect::<BTreeSet<_>>()
.len()
}
fn settle(&self, budgets: &Budgets) -> Result<FinalAccountMint> {
let budget = budgets
.mint(0)
.context("Ogmios measured no budget for the Seedelf policy")?;
let compute = computation_fee(&self.chain.params, budget.mem, budget.steps);
let script_reference = reference_script_fee(
&self.chain.params,
self.chain.config.contract.seedelf_contract_size
+ reference_script_bytes(&self.inputs)?,
)?;
let (fee, staged) = settle(
self.signers(),
Patches::staking(&self.staking),
|size| even(linear_fee(&self.chain.params, size) + compute + script_reference),
|fee| self.stage(fee, Some(budgets)),
)?;
let (change_lovelace, change_tokens) = self.change(fee)?;
let change_outputs = staged.outputs.as_ref().map_or(0, Vec::len) - 1;
Ok(FinalAccountMint {
tx: built_with(staged, Patches::staking(&self.staking))?,
fee: ScriptFee {
size: fee - compute - script_reference,
compute,
script_reference,
total: fee,
},
change_lovelace,
change_tokens,
change_outputs,
})
}
fn change(&self, fee: u64) -> Result<(u64, Assets)> {
let (inputs, tokens) = assets_of(self.inputs.clone())?;
let lovelace = self
.staking
.net(inputs)
.and_then(|total| total.checked_sub(self.lovelace))
.and_then(|rest| rest.checked_sub(fee))
.ok_or(ACCOUNT_MINT_SHORT)?;
Ok((lovelace, tokens))
}
fn stage(&self, fee: u64, budgets: Option<&Budgets>) -> Result<StagingTransaction> {
let Chain { params, config, .. } = &self.chain;
let (change_lovelace, change_tokens) = self.change(fee)?;
let change = change_outputs(
params,
&self.change_addr,
change_lovelace,
&change_tokens,
ACCOUNT_MINT_SHORT,
)?;
let collateral_addr = Address::from_bech32(&self.collateral.address)
.map_err(|e| anyhow::anyhow!("The collateral UTxO's address is invalid: {e}"))?;
let (collateral_lovelace, collateral_tokens) = assets_of(vec![self.collateral.clone()])?;
let collateral_back = collateral_lovelace.saturating_sub(fee * 3 / 2);
if collateral_back
< address_minimum_lovelace_with_assets(
params,
&self.collateral.address,
collateral_tokens.clone(),
)?
{
bail!("The Cardano account's UTxOs are too small to put up as collateral");
}
let mut collateral_return = Output::new(collateral_addr, collateral_back);
for asset in &collateral_tokens.items {
collateral_return = collateral_return
.add_asset(asset.policy_id, asset.token_name.clone(), asset.amount)
.context("Failed To Add An Asset")?;
}
let policy = policy_hash(config)?;
let budget = budgets.and_then(|b| b.mint(0)).unwrap_or(DRAFT_BUDGET);
let mut tx = StagingTransaction::new();
for utxo in &self.inputs {
tx = tx.input(input_of(utxo)?);
}
for output in std::iter::once(self.seedelf.clone()).chain(change) {
tx = tx.output(output);
}
tx = expiring(tx, self.invalid_hereafter);
Ok(tx
.collateral_input(input_of(&self.collateral)?)
.collateral_output(collateral_return)
.fee(fee)
.reference_input(reference_utxo(config.reference.seedelf_reference_utxo))
.mint_asset(policy, self.token_name.clone(), 1)
.context("Failed To Mint The Seedelf")?
.add_mint_redeemer(
policy,
self.redeemer.clone(),
Some(ExUnits {
mem: budget.mem,
steps: budget.steps,
}),
)
.language_view(ScriptKind::PlutusV3, params.cost_model_v3.clone()))
}
}
#[allow(clippy::too_many_arguments)]
pub fn account_mint(
chain: &Chain,
available: &[UtxoResponse],
collateral: Option<&UtxoResponse>,
label: &str,
seedelf: &Register,
change_addr: &Address,
staking: &Staking,
invalid_hereafter: Option<u64>,
) -> Result<AccountMint> {
if !is_payable(seedelf) {
bail!("The Seedelf's register isn't made of valid points");
}
let pure_ada = |u: &UtxoResponse| u.asset_list.as_ref().is_none_or(|a| a.is_empty());
let lovelace_of = |u: &UtxoResponse| u.value.parse::<u64>().unwrap_or(0);
let same =
|a: &UtxoResponse, b: &UtxoResponse| a.tx_hash == b.tx_hash && a.tx_index == b.tx_index;
let spendable: Vec<UtxoResponse> = available
.iter()
.filter(|u| collateral.is_none_or(|c| !same(u, c)))
.filter(|u| reference_script_size(u).is_ok())
.cloned()
.collect();
let (mut spendable, _) = fitting(&spendable, &Assets::new(), |_| false)?;
if spendable.is_empty() {
bail!("There is nothing in the Cardano account to pay for a Seedelf");
}
spendable.sort_by_key(|u| {
(
!pure_ada(u),
u.reference_script.is_some(),
std::cmp::Reverse(lovelace_of(u)),
)
});
let config = &chain.config;
let policy = policy_hash(config)?;
let lovelace = seedelf_minimum_lovelace(&chain.params)?;
let wallet_addr = wallet_contract(chain.network_flag, config.contract.wallet_contract_hash);
let redeemer = create_mint_redeemer(label.to_string())?;
let mut last_error = None;
for k in 1..=spendable.len() {
let inputs = &spendable[..k];
let spent = |u: &UtxoResponse| inputs.iter().any(|i| same(i, u));
let mut candidates: Vec<&UtxoResponse> = available.iter().collect();
candidates.sort_by_key(|u| {
(
!pure_ada(u),
lovelace_of(u) < ACCOUNT_COLLATERAL_MINIMUM,
spent(u),
lovelace_of(u) != COLLATERAL_LOVELACE,
std::cmp::Reverse(lovelace_of(u)),
)
});
let Some(collateral) = collateral.or(candidates.first().copied()) else {
bail!("There is nothing in the Cardano account to pay for a Seedelf");
};
let owned: Vec<Input> = inputs.iter().map(input_of).collect::<Result<_>>()?;
let token_name = seedelf_token_name(label.to_string(), Some(&owned))?;
let seedelf_output = Output::new(wallet_addr.clone(), lovelace)
.set_inline_datum(seedelf.to_vec()?)
.add_asset(policy, token_name.clone(), 1)
.context("Failed To Add The Seedelf")?;
let mint = AccountMint {
chain: chain.clone(),
inputs: inputs.to_vec(),
collateral: collateral.clone(),
seedelf: seedelf_output,
redeemer: redeemer.clone(),
change_addr: change_addr.clone(),
staking: staking.clone(),
invalid_hereafter,
token_name,
lovelace,
};
match mint.estimate() {
Ok(_) => return Ok(mint),
Err(e) if e.downcast_ref::<NotEnough>().is_some() => last_error = Some(e),
Err(e) => return Err(e),
}
}
Err(last_error.unwrap_or_else(|| ACCOUNT_MINT_SHORT.into()))
}