1use std::collections::{BTreeMap, BTreeSet};
9use std::fmt;
10
11use anyhow::{Context, Result, bail};
12use pallas_addresses::Address;
13use pallas_codec::minicbor;
14use pallas_crypto::hash::{Hash, Hasher};
15use pallas_crypto::key::ed25519::{PublicKey, SecretKey, Signature};
16use pallas_primitives::{Fragment, NonEmptySet, conway};
17use pallas_txbuilder::{
18 BuildConway, BuiltTransaction, ExUnits, Input, Output, ScriptKind, StagingTransaction,
19};
20use pallas_wallet::PrivateKey;
21use rand_core::OsRng;
22use seedelf_crypto::register::Register;
23use seedelf_koios::koios::{ProtocolParameters, UtxoResponse, extract_bytes_with_logging};
24use serde_json::Value;
25
26use crate::address::{collateral_address, is_not_a_script, is_on_correct_network, wallet_contract};
27use crate::assets::{Asset, Assets};
28use crate::constants::{COLLATERAL_HASH, COLLATERAL_PUBLIC_KEY, Config, MAXIMUM_TOKENS_PER_UTXO};
29use crate::data_structures::{create_mint_redeemer, create_spend_redeemer};
30use crate::note::Note;
31use crate::staking::Staking;
32use crate::transaction::{
33 address_minimum_lovelace_with_assets, checked_lovelace, collateral_input, computation_fee,
34 decode_tx_hash, reference_utxo, seedelf_minimum_lovelace, seedelf_token_name,
35 wallet_minimum_lovelace_with_assets,
36};
37use crate::utxos::{assets_of, fitting, reference_script_bytes, reference_script_size};
38
39pub fn fake_signer() -> PrivateKey {
42 PrivateKey::from(SecretKey::new(OsRng))
43}
44
45pub fn linear_fee(params: &ProtocolParameters, tx_size: u64) -> u64 {
50 params
51 .min_fee_a
52 .saturating_mul(tx_size)
53 .saturating_add(params.min_fee_b)
54}
55
56pub const MAX_REFERENCE_SCRIPT_BYTES: u64 = 204_800;
60
61const REFERENCE_SCRIPT_TIER: u64 = 25_600;
63
64pub fn reference_script_fee(params: &ProtocolParameters, bytes: u64) -> Result<u64> {
71 if bytes > MAX_REFERENCE_SCRIPT_BYTES {
72 bail!(
73 "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"
74 );
75 }
76 let price = params.min_fee_ref_script_cost_per_byte;
77 if price.denominator == 0 {
78 bail!("The network's reference script price isn't a number");
79 }
80 let tiers = (bytes / REFERENCE_SCRIPT_TIER) as u32;
85 let rest = u128::from(bytes % REFERENCE_SCRIPT_TIER);
86 let tier = u128::from(REFERENCE_SCRIPT_TIER);
87 let scaled: u128 = (0..tiers)
88 .map(|k| tier * 6u128.pow(k) * 5u128.pow(tiers - k))
89 .sum::<u128>()
90 + rest * 6u128.pow(tiers);
91 let fee =
92 scaled * u128::from(price.numerator) / (u128::from(price.denominator) * 5u128.pow(tiers));
93 u64::try_from(fee).context("The reference scripts' fee doesn't fit a number")
94}
95
96pub const MAX_FEE: u64 = 10_000_000;
101
102pub fn check_fee(fee: u64) -> Result<()> {
104 if fee > MAX_FEE {
105 bail!(
106 "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",
107 ada(fee),
108 ada(MAX_FEE)
109 );
110 }
111 Ok(())
112}
113
114pub fn settle_fee(
119 params: &ProtocolParameters,
120 signers: usize,
121 build: impl FnMut(u64) -> Result<StagingTransaction>,
122) -> Result<(u64, StagingTransaction)> {
123 settle(
124 signers,
125 Patches::staking(&Staking::none()),
126 |size| linear_fee(params, size),
127 build,
128 )
129}
130
131pub const MAX_TX_SIZE: u64 = 16_384;
134
135#[derive(Clone, Copy)]
139struct Patches<'a> {
140 staking: &'a Staking,
141 note: Option<&'a Note>,
142}
143
144impl<'a> Patches<'a> {
145 fn staking(staking: &'a Staking) -> Self {
147 Patches {
148 staking,
149 note: None,
150 }
151 }
152
153 fn apply(&self, built: BuiltTransaction) -> Result<BuiltTransaction> {
154 let built = self.staking.patch(built)?;
155 match self.note {
156 Some(note) => note.patch(built),
157 None => Ok(built),
158 }
159 }
160}
161
162fn settle(
168 signers: usize,
169 patches: Patches,
170 price: impl Fn(u64) -> u64,
171 mut build: impl FnMut(u64) -> Result<StagingTransaction>,
172) -> Result<(u64, StagingTransaction)> {
173 let mut fee: u64 = 200_000;
174 for _ in 0..5 {
175 let staged = build(fee)?;
176 let size = signed_size(&staged, signers, patches)?;
177 if size > MAX_TX_SIZE {
178 bail!(
179 "This transaction would be {size} bytes, over the network's limit of {MAX_TX_SIZE}. Send fewer tokens, or pay fewer recipients, at once"
180 );
181 }
182 let needed = price(size);
183 check_fee(needed)?;
184 if needed <= fee && fee - needed < 1_000 {
185 return Ok((fee, staged));
186 }
187 fee = needed;
189 }
190 bail!("The transaction fee did not settle")
191}
192
193fn signed_size(staged: &StagingTransaction, signers: usize, patches: Patches) -> Result<u64> {
194 let mut built = built_with(staged.clone(), patches)?;
195 for _ in 0..signers.max(1) + patches.staking.signers() {
196 built = built
197 .sign(fake_signer())
198 .context("Failed To Sign The Draft Transaction")?;
199 }
200 Ok(built.tx_bytes.0.len() as u64)
201}
202
203fn built_with(staged: StagingTransaction, patches: Patches) -> Result<BuiltTransaction> {
205 patches.apply(
206 staged
207 .build_conway_raw()
208 .context("Failed To Build The Transaction")?,
209 )
210}
211
212fn expiring(tx: StagingTransaction, invalid_hereafter: Option<u64>) -> StagingTransaction {
217 match invalid_hereafter {
218 Some(slot) => tx.invalid_from_slot(slot),
219 None => tx,
220 }
221}
222
223pub fn input_of(utxo: &UtxoResponse) -> Result<Input> {
225 Ok(Input::new(
226 Hash::new(decode_tx_hash(&utxo.tx_hash)?),
227 utxo.tx_index,
228 ))
229}
230
231fn deposit_output(
234 wallet_addr: &Address,
235 owner: &Register,
236 lovelace: u64,
237 tokens: &Assets,
238) -> Result<Output> {
239 let datum: Vec<u8> = owner
240 .clone()
241 .rerandomize()
242 .context("Failed To Randomize Points")?
243 .to_vec()?;
244 let mut output = Output::new(wallet_addr.clone(), lovelace).set_inline_datum(datum);
245 for asset in &tokens.items {
246 output = output
247 .add_asset(asset.policy_id, asset.token_name.clone(), asset.amount)
248 .context("Failed To Add An Asset")?;
249 }
250 Ok(output)
251}
252
253pub fn deposit_outputs(
258 params: &ProtocolParameters,
259 wallet_addr: &Address,
260 owner: &Register,
261 lovelace: u64,
262 tokens: &Assets,
263) -> Result<Vec<Output>> {
264 let chunks: Vec<Assets> = if tokens.items.is_empty() {
265 vec![Assets::new()]
266 } else {
267 tokens.split(MAXIMUM_TOKENS_PER_UTXO as usize)
268 };
269 let last = chunks.len() - 1;
270 let mut remaining = lovelace;
271 let mut outputs = Vec::with_capacity(chunks.len());
272 for (i, chunk) in chunks.iter().enumerate() {
273 let minimum = wallet_minimum_lovelace_with_assets(params, chunk.clone())?;
274 let amount = if i == last { remaining } else { minimum };
275 if amount < minimum || remaining < amount {
276 bail!(TooLittle {
277 what: "Seedelf",
278 needed: minimum_deposit(params, tokens)?,
279 });
280 }
281 remaining -= amount;
282 outputs.push(deposit_output(wallet_addr, owner, amount, chunk)?);
283 }
284 Ok(outputs)
285}
286
287pub fn minimum_deposit(params: &ProtocolParameters, tokens: &Assets) -> Result<u64> {
290 if tokens.items.is_empty() {
291 return wallet_minimum_lovelace_with_assets(params, Assets::new());
292 }
293 tokens
294 .split(MAXIMUM_TOKENS_PER_UTXO as usize)
295 .into_iter()
296 .map(|chunk| wallet_minimum_lovelace_with_assets(params, chunk))
297 .sum()
298}
299
300fn change_outputs(
305 params: &ProtocolParameters,
306 change_addr: &Address,
307 lovelace: u64,
308 tokens: &Assets,
309 short: NotEnough,
310) -> Result<Vec<Output>> {
311 if lovelace == 0 && tokens.items.is_empty() {
312 return Ok(Vec::new());
313 }
314 let bech32 = change_addr
315 .to_bech32()
316 .map_err(|e| anyhow::anyhow!("Failed to encode the change address: {e}"))?;
317 let chunks: Vec<Assets> = if tokens.items.is_empty() {
318 vec![Assets::new()]
319 } else {
320 tokens.split(MAXIMUM_TOKENS_PER_UTXO as usize)
321 };
322 let last = chunks.len() - 1;
323 let mut remaining = lovelace;
324 let mut outputs = Vec::with_capacity(chunks.len());
325 for (i, chunk) in chunks.iter().enumerate() {
326 let minimum = address_minimum_lovelace_with_assets(params, &bech32, chunk.clone())?;
327 let amount = if i == last { remaining } else { minimum };
328 if amount < minimum || remaining < amount {
329 bail!(short);
330 }
331 remaining -= amount;
332 let mut output = Output::new(change_addr.clone(), amount);
333 for asset in &chunk.items {
334 output = output
335 .add_asset(asset.policy_id, asset.token_name.clone(), asset.amount)
336 .context("Failed To Add An Asset")?;
337 }
338 outputs.push(output);
339 }
340 Ok(outputs)
341}
342
343fn minimum_change(
345 params: &ProtocolParameters,
346 change_addr: &Address,
347 tokens: &Assets,
348) -> Result<u64> {
349 if tokens.items.is_empty() {
350 return Ok(0);
351 }
352 let bech32 = change_addr
353 .to_bech32()
354 .map_err(|e| anyhow::anyhow!("Failed to encode the change address: {e}"))?;
355 tokens
356 .split(MAXIMUM_TOKENS_PER_UTXO as usize)
357 .into_iter()
358 .map(|chunk| address_minimum_lovelace_with_assets(params, &bech32, chunk))
359 .sum()
360}
361
362#[derive(Debug, Clone, Copy)]
364struct NotEnough(&'static str);
365
366const MOVE_IN_SHORT: NotEnough =
368 NotEnough("Not enough ADA in the Cardano account for this move, its fee and the change");
369
370const SEND_SHORT: NotEnough =
372 NotEnough("Not enough ADA in the Cardano account for this payment, its fee and the change");
373
374const STAKE_SHORT: NotEnough =
376 NotEnough("Not enough ADA in the Cardano account for this, its fee and the change");
377
378const ACCOUNT_MINT_SHORT: NotEnough =
380 NotEnough("Not enough ADA in the Cardano account for the Seedelf, its fee and the change");
381
382const SEEDELF_SHORT: NotEnough =
384 NotEnough("Not enough ADA in the Seedelf balance for this, its fee and the change");
385
386impl fmt::Display for NotEnough {
387 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388 f.write_str(self.0)
389 }
390}
391
392impl std::error::Error for NotEnough {}
393
394#[derive(Debug)]
396struct TooLittle {
397 what: &'static str,
398 needed: u64,
399}
400
401impl fmt::Display for TooLittle {
402 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403 write!(
404 f,
405 "A {} deposit of these tokens needs at least {} ADA",
406 self.what,
407 ada(self.needed)
408 )
409 }
410}
411
412impl std::error::Error for TooLittle {}
413
414pub fn external_sweep(
419 params: &ProtocolParameters,
420 utxos: &[UtxoResponse],
421 owner: &Register,
422 wallet_addr: &Address,
423 signer: Hash<28>,
424) -> Result<(BuiltTransaction, u64)> {
425 if utxos.is_empty() {
426 bail!("Not Enough Lovelace/Tokens");
427 }
428 let (total, tokens) = assets_of(utxos.to_vec())?;
429 let inputs: Vec<Input> = utxos.iter().map(input_of).collect::<Result<_>>()?;
430 let script_fee = reference_script_fee(params, reference_script_bytes(utxos)?)?;
432 let price = |size| linear_fee(params, size).saturating_add(script_fee);
433 let (fee, staged) = settle(1, Patches::staking(&Staking::none()), price, |fee| {
434 let lovelace = checked_lovelace(total, &[fee])?;
435 let mut tx = StagingTransaction::new();
436 for input in &inputs {
437 tx = tx.input(input.clone());
438 }
439 for output in deposit_outputs(params, wallet_addr, owner, lovelace, &tokens)? {
440 tx = tx.output(output);
441 }
442 Ok(tx.fee(fee).disclosed_signer(signer))
443 })?;
444 let tx = staged
445 .build_conway_raw()
446 .context("Failed To Build The Transaction")?;
447 Ok((tx, fee))
448}
449
450#[derive(Debug, Clone, Copy, PartialEq, Eq)]
452pub enum AccountAmount {
453 Lovelace(u64),
454 Max,
457}
458
459#[derive(Clone, Copy)]
461pub enum Payee<'a> {
462 Seedelf {
468 owner: &'a Register,
469 wallet_addr: &'a Address,
470 },
471 Address(&'a Address),
473 Nobody,
476}
477
478impl Payee<'_> {
479 pub fn minimum(&self, params: &ProtocolParameters, tokens: &Assets) -> Result<u64> {
481 match self {
482 Payee::Seedelf { .. } => minimum_deposit(params, tokens),
483 Payee::Address(to) => minimum_address_payment(params, to, tokens),
484 Payee::Nobody => Ok(0),
485 }
486 }
487
488 fn outputs(
489 &self,
490 params: &ProtocolParameters,
491 lovelace: u64,
492 tokens: &Assets,
493 ) -> Result<Vec<Output>> {
494 match self {
495 Payee::Seedelf { owner, wallet_addr } => {
496 deposit_outputs(params, wallet_addr, owner, lovelace, tokens)
497 }
498 Payee::Address(to) => Ok(vec![pay_address(params, to, lovelace, tokens)?]),
499 Payee::Nobody if lovelace == 0 && tokens.items.is_empty() => Ok(Vec::new()),
500 Payee::Nobody => bail!("A transaction that pays no one pays nothing"),
501 }
502 }
503}
504
505#[derive(Clone)]
508pub struct AccountPay<'a> {
509 pub payee: Payee<'a>,
510 pub amount: AccountAmount,
511 pub picked: Vec<(String, String, u64)>,
512}
513
514impl<'a> AccountPay<'a> {
515 pub fn new(payee: Payee<'a>, amount: AccountAmount, picked: &[(String, String, u64)]) -> Self {
516 AccountPay {
517 payee,
518 amount,
519 picked: picked.to_vec(),
520 }
521 }
522}
523
524pub struct AccountPayment {
527 pub tx: BuiltTransaction,
530 pub inputs: Vec<UtxoResponse>,
532 pub fee: u64,
533 pub lovelace: u64,
535 pub tokens: Assets,
536 pub paid: Vec<u64>,
538 pub outputs: usize,
540 pub change_lovelace: u64,
542 pub change_tokens: Assets,
543 pub left_out: Vec<UtxoResponse>,
546}
547
548#[allow(clippy::too_many_arguments)]
573pub fn move_in(
574 params: &ProtocolParameters,
575 available: &[UtxoResponse],
576 amount: AccountAmount,
577 picked: &[(String, String, u64)],
578 owner: &Register,
579 wallet_addr: &Address,
580 change_addr: &Address,
581 staking: &Staking,
582 invalid_hereafter: Option<u64>,
583) -> Result<AccountPayment> {
584 let payee = Payee::Seedelf { owner, wallet_addr };
585 account_payment(
586 params,
587 available,
588 &[AccountPay::new(payee, amount, picked)],
589 Patches::staking(staking),
590 change_addr,
591 MOVE_IN_SHORT,
592 invalid_hereafter,
593 )
594}
595
596#[allow(clippy::too_many_arguments)]
601pub fn account_send(
602 params: &ProtocolParameters,
603 available: &[UtxoResponse],
604 amount: AccountAmount,
605 picked: &[(String, String, u64)],
606 to: &Address,
607 network_flag: bool,
608 change_addr: &Address,
609 staking: &Staking,
610 invalid_hereafter: Option<u64>,
611) -> Result<AccountPayment> {
612 let payee = Payee::Address(to);
613 account_send_many(
614 params,
615 available,
616 &[AccountPay::new(payee, amount, picked)],
617 network_flag,
618 change_addr,
619 staking,
620 None,
621 invalid_hereafter,
622 )
623}
624
625#[allow(clippy::too_many_arguments)]
633pub fn account_send_many(
634 params: &ProtocolParameters,
635 available: &[UtxoResponse],
636 recipients: &[AccountPay],
637 network_flag: bool,
638 change_addr: &Address,
639 staking: &Staking,
640 note: Option<&Note>,
641 invalid_hereafter: Option<u64>,
642) -> Result<AccountPayment> {
643 if recipients.is_empty() {
644 bail!("A payment needs someone to pay");
645 }
646 for pay in recipients {
647 match pay.payee {
648 Payee::Address(to) => check_payable(to, network_flag)?,
649 Payee::Seedelf { owner, .. } => check_register(owner)?,
650 Payee::Nobody => bail!("A payment needs someone to pay"),
651 }
652 }
653 account_payment(
654 params,
655 available,
656 recipients,
657 Patches { staking, note },
658 change_addr,
659 SEND_SHORT,
660 invalid_hereafter,
661 )
662}
663
664fn check_register(register: &Register) -> Result<()> {
666 if !is_payable(register) {
667 bail!(
668 "That Seedelf's register isn't valid: a payment to it could be locked for good, or taken by anyone"
669 );
670 }
671 Ok(())
672}
673
674pub fn account_staking(
681 params: &ProtocolParameters,
682 available: &[UtxoResponse],
683 staking: &Staking,
684 change_addr: &Address,
685 invalid_hereafter: Option<u64>,
686) -> Result<AccountPayment> {
687 if staking.is_empty() {
688 bail!("A staking transaction needs a certificate, a withdrawal or a vote");
689 }
690 account_payment(
691 params,
692 available,
693 &[AccountPay::new(
694 Payee::Nobody,
695 AccountAmount::Lovelace(0),
696 &[],
697 )],
698 Patches::staking(staking),
699 change_addr,
700 STAKE_SHORT,
701 invalid_hereafter,
702 )
703}
704
705fn account_payment(
706 params: &ProtocolParameters,
707 available: &[UtxoResponse],
708 pays: &[AccountPay],
709 patches: Patches,
710 change_addr: &Address,
711 short: NotEnough,
712 invalid_hereafter: Option<u64>,
713) -> Result<AccountPayment> {
714 let max = pays.iter().any(|p| p.amount == AccountAmount::Max);
715 if max && pays.len() > 1 {
716 bail!("Max pays a single recipient: give each of several an amount");
717 }
718 let mut picked: BTreeMap<(&str, &str), u64> = BTreeMap::new();
720 for (policy, name, quantity) in pays.iter().flat_map(|p| &p.picked) {
721 if *quantity == 0 {
722 bail!("Choose more than none of the token {policy}.{name}, or leave it out");
723 }
724 let total = picked.entry((policy, name)).or_default();
725 *total = total.saturating_add(*quantity);
726 }
727 let holds_picked = |u: &UtxoResponse| {
728 u.asset_list.as_ref().is_some_and(|assets| {
729 assets
730 .iter()
731 .any(|a| picked.contains_key(&(a.policy_id.as_str(), a.asset_name.as_str())))
732 })
733 };
734 let priced: Vec<UtxoResponse> = available
738 .iter()
739 .filter(|u| reference_script_size(u).is_ok())
740 .cloned()
741 .collect();
742 let (eligible, _) = fitting(&priced, &Assets::new(), holds_picked)?;
743 let left_out: Vec<UtxoResponse> = available
744 .iter()
745 .filter(|u| !eligible.iter().any(|e| same_utxo(e, u)))
746 .cloned()
747 .collect();
748 for ((policy, name), quantity) in &picked {
749 let held: u64 = eligible
750 .iter()
751 .flat_map(|u| u.asset_list.iter().flatten())
752 .filter(|a| a.policy_id == *policy && a.asset_name == *name)
753 .map(|a| a.quantity.parse::<u64>().unwrap_or(0))
754 .sum();
755 if held == 0 {
756 bail!("The Cardano account doesn't hold the token {policy}.{name}");
757 }
758 if *quantity > held {
759 bail!("The Cardano account holds only {held} of the token {policy}.{name}");
760 }
761 }
762
763 let (mandatory, mut rest): (Vec<UtxoResponse>, Vec<UtxoResponse>) =
764 eligible.into_iter().partition(|u| holds_picked(u));
765 rest.sort_by_key(|u| {
769 let tokens = u.asset_list.as_ref().is_some_and(|a| !a.is_empty());
770 let lovelace = u.value.parse::<u64>().unwrap_or(0);
771 (
772 tokens,
773 u.reference_script.is_some(),
774 std::cmp::Reverse(lovelace),
775 )
776 });
777
778 let attempt = |selected: &[UtxoResponse]| {
779 build_account_payment(
780 params,
781 selected,
782 pays,
783 patches,
784 change_addr,
785 short,
786 invalid_hereafter,
787 )
788 };
789
790 if max {
791 let all: Vec<UtxoResponse> = mandatory.into_iter().chain(rest).collect();
792 if all.is_empty() {
793 bail!("There is nothing in the Cardano account to spend");
794 }
795 let mut built = attempt(&all)?;
796 built.left_out = left_out;
797 return Ok(built);
798 }
799
800 let mut last_error = None;
801 for k in 0..=rest.len() {
802 let selected: Vec<UtxoResponse> = mandatory
803 .iter()
804 .chain(rest.iter().take(k))
805 .cloned()
806 .collect();
807 if selected.is_empty() {
808 continue;
809 }
810 match attempt(&selected) {
811 Ok(built) => return Ok(built),
812 Err(e) if e.downcast_ref::<NotEnough>().is_some() => last_error = Some(e),
813 Err(e) => return Err(e),
814 }
815 }
816 Err(last_error.unwrap_or_else(|| short.into()))
817}
818
819fn build_account_payment(
820 params: &ProtocolParameters,
821 selected: &[UtxoResponse],
822 pays: &[AccountPay],
823 patches: Patches,
824 change_addr: &Address,
825 short: NotEnough,
826 invalid_hereafter: Option<u64>,
827) -> Result<AccountPayment> {
828 let (inputs_total, all_tokens) = assets_of(selected.to_vec())?;
829 let total = patches.staking.net(inputs_total).ok_or(short)?;
831 let asked_in = |picked: &[(String, String, u64)], a: &Asset| -> u64 {
833 let policy = hex::encode(a.policy_id);
834 let name = hex::encode(&a.token_name);
835 picked
836 .iter()
837 .filter(|(p, n, _)| *p == policy && *n == name)
838 .map(|(_, _, q)| *q)
839 .fold(0u64, u64::saturating_add)
840 };
841 let tokens_for = |picked: &[(String, String, u64)]| Assets {
842 items: all_tokens
843 .items
844 .iter()
845 .filter(|a| asked_in(picked, a) > 0)
846 .map(|a| Asset {
847 amount: asked_in(picked, a).min(a.amount),
848 ..a.clone()
849 })
850 .collect(),
851 };
852 let each_tokens: Vec<Assets> = pays.iter().map(|p| tokens_for(&p.picked)).collect();
853 let all_picked: Vec<(String, String, u64)> =
854 pays.iter().flat_map(|p| p.picked.iter().cloned()).collect();
855 let asked = |a: &Asset| asked_in(&all_picked, a).min(a.amount);
856 let paying = tokens_for(&all_picked);
857 let staying = Assets {
858 items: all_tokens
859 .items
860 .iter()
861 .filter(|a| a.amount > asked(a))
862 .map(|a| Asset {
863 amount: a.amount - asked(a),
864 ..a.clone()
865 })
866 .collect(),
867 };
868 let inputs: Vec<Input> = selected.iter().map(input_of).collect::<Result<_>>()?;
869 let signers = selected
870 .iter()
871 .map(|u| u.payment_cred.as_str())
872 .collect::<BTreeSet<_>>()
873 .len();
874 let change_floor = minimum_change(params, change_addr, &staying)?;
875 let script_fee = reference_script_fee(params, reference_script_bytes(selected)?)?;
877
878 let mut paid: Vec<u64> = Vec::new();
879 let mut change = 0;
880 let mut outputs = 0;
881 let price = |size| linear_fee(params, size).saturating_add(script_fee);
882 let (fee, staged) = settle(signers, patches, price, |fee| {
883 (paid, change) = match pays {
884 [only] if only.amount == AccountAmount::Max => {
885 let all = total
886 .checked_sub(fee)
887 .and_then(|rest| rest.checked_sub(change_floor))
888 .ok_or(short)?;
889 (vec![all], change_floor)
890 }
891 _ => {
892 let each: Vec<u64> = pays
893 .iter()
894 .map(|p| match p.amount {
895 AccountAmount::Lovelace(lovelace) => lovelace,
896 AccountAmount::Max => 0,
897 })
898 .collect();
899 let change = each
900 .iter()
901 .try_fold(total, |rest, l| rest.checked_sub(*l))
902 .and_then(|rest| rest.checked_sub(fee))
903 .ok_or(short)?;
904 (each, change)
905 }
906 };
907 let mut tx = StagingTransaction::new();
908 for input in &inputs {
909 tx = tx.input(input.clone());
910 }
911 outputs = 0;
912 for ((pay, lovelace), tokens) in pays.iter().zip(&paid).zip(&each_tokens) {
913 for output in pay.payee.outputs(params, *lovelace, tokens)? {
914 outputs += 1;
915 tx = tx.output(output);
916 }
917 }
918 for output in change_outputs(params, change_addr, change, &staying, short)? {
919 tx = tx.output(output);
920 }
921 Ok(expiring(tx.fee(fee), invalid_hereafter))
922 })?;
923
924 Ok(AccountPayment {
925 tx: built_with(staged, patches)?,
926 inputs: selected.to_vec(),
927 fee,
928 lovelace: paid.iter().sum(),
929 tokens: paying,
930 paid,
931 outputs,
932 change_lovelace: change,
933 change_tokens: staying,
934 left_out: Vec::new(),
935 })
936}
937
938#[derive(Debug, Clone, Copy, PartialEq, Eq)]
957pub struct Budget {
958 pub mem: u64,
959 pub steps: u64,
960}
961
962pub const DRAFT_BUDGET: Budget = Budget {
965 mem: 14_000_000,
966 steps: 10_000_000_000,
967};
968
969pub const MAX_TX_BUDGET: Budget = Budget {
973 mem: 16_500_000,
974 steps: 10_000_000_000,
975};
976
977pub const SPEND_BUDGET_GUESS: Budget = Budget {
981 mem: 100_000,
982 steps: 450_000_000,
983};
984
985pub const MINT_BUDGET_GUESS: Budget = Budget {
988 mem: 100_000,
989 steps: 30_000_000,
990};
991
992pub const COLLATERAL_LOVELACE: u64 = 5_000_000;
994
995pub fn even(fee: u64) -> u64 {
998 fee + fee % 2
999}
1000
1001pub fn collateral_output(addr: Address, fee: u64) -> Result<Output> {
1005 let collateral_return: u64 = COLLATERAL_LOVELACE
1006 .checked_sub(fee * 3 / 2)
1007 .context("transaction fee is too large for the 5 ADA collateral to cover")?;
1008 Ok(Output::new(addr, collateral_return))
1009}
1010
1011pub fn collateral_signature(answer: &Value) -> Result<[u8; 64]> {
1014 let witness = answer
1015 .get("witness")
1016 .and_then(Value::as_str)
1017 .filter(|w| w.len() >= 128)
1018 .with_context(|| format!("The collateral service answered unexpectedly: {answer}"))?
1019 .as_bytes();
1020 hex::decode(&witness[witness.len() - 128..])
1021 .context("The collateral service's witness isn't hex")?
1022 .try_into()
1023 .map_err(|_| anyhow::anyhow!("The collateral service's signature isn't 64 bytes"))
1024}
1025
1026pub fn is_collateral_signature(tx_hash: &[u8], signature: &[u8; 64]) -> bool {
1028 PublicKey::from(COLLATERAL_PUBLIC_KEY).verify(tx_hash, &Signature::from(*signature))
1029}
1030
1031pub fn add_witnesses(tx_cbor: &[u8], witnesses: &[(PublicKey, Signature)]) -> Result<Vec<u8>> {
1035 let hash = tx_id(tx_cbor)?;
1036 let mut tx = conway::Tx::decode_fragment(tx_cbor)
1037 .map_err(|e| anyhow::anyhow!("The transaction to sign isn't a Conway transaction: {e}"))?;
1038 let mut vkeys: Vec<conway::VKeyWitness> = tx
1039 .transaction_witness_set
1040 .vkeywitness
1041 .map(|w| w.to_vec())
1042 .unwrap_or_default();
1043 for (key, signature) in witnesses {
1044 vkeys.push(conway::VKeyWitness {
1045 vkey: key.as_ref().to_vec().into(),
1046 signature: signature.as_ref().to_vec().into(),
1047 });
1048 }
1049 tx.transaction_witness_set.vkeywitness = NonEmptySet::from_vec(vkeys);
1050 let signed = tx
1051 .encode_fragment()
1052 .map_err(|e| anyhow::anyhow!("Failed to encode the signed transaction: {e}"))?;
1053 if tx_id(&signed)? != hash {
1054 bail!("Signing changed the transaction");
1055 }
1056 Ok(signed)
1057}
1058
1059pub fn tx_id(tx_cbor: &[u8]) -> Result<Hash<32>> {
1062 let mut decoder = minicbor::Decoder::new(tx_cbor);
1063 let body = decoder.array().and_then(|_| {
1064 let start = decoder.position();
1065 decoder.skip()?;
1066 Ok(start..decoder.position())
1067 });
1068 let body = body.map_err(|e| anyhow::anyhow!("The transaction isn't valid CBOR: {e}"))?;
1069 Ok(Hasher::<256>::hash(&tx_cbor[body]))
1070}
1071
1072pub fn required_signers(tx_cbor: &[u8]) -> Result<Vec<Hash<28>>> {
1074 let tx = conway::Tx::decode_fragment(tx_cbor)
1075 .map_err(|e| anyhow::anyhow!("The transaction isn't a Conway transaction: {e}"))?;
1076 Ok(tx
1077 .transaction_body
1078 .required_signers
1079 .map(|s| s.to_vec())
1080 .unwrap_or_default())
1081}
1082
1083#[derive(Debug, Clone, Default, PartialEq, Eq)]
1086pub struct Budgets {
1087 spend: BTreeMap<u64, Budget>,
1088 mint: BTreeMap<u64, Budget>,
1089 withdraw: BTreeMap<u64, Budget>,
1090}
1091
1092impl Budgets {
1093 pub fn from_ogmios(answer: &Value) -> Result<Self> {
1099 if let Some(error) = answer.get("error") {
1100 bail!(ogmios_failure(error));
1101 }
1102 let rows = answer
1103 .get("result")
1104 .and_then(Value::as_array)
1105 .with_context(|| format!("Ogmios answered without budgets: {answer}"))?;
1106 let mut budgets = Budgets::default();
1107 for row in rows {
1108 let purpose = row.pointer("/validator/purpose").and_then(Value::as_str);
1109 let index = row.pointer("/validator/index").and_then(Value::as_u64);
1110 let mem = row.pointer("/budget/memory").and_then(Value::as_u64);
1111 let steps = row.pointer("/budget/cpu").and_then(Value::as_u64);
1112 let (Some(purpose), Some(index), Some(mem), Some(steps)) = (purpose, index, mem, steps)
1113 else {
1114 bail!("Ogmios returned a budget the wallet can't read: {row}");
1115 };
1116 let budget = Budget { mem, steps };
1117 match purpose {
1118 "spend" => budgets.spend.insert(index, budget),
1119 "mint" => budgets.mint.insert(index, budget),
1120 "withdraw" => budgets.withdraw.insert(index, budget),
1121 other => {
1122 bail!("Ogmios measured a {other} script, which this transaction doesn't have")
1123 }
1124 };
1125 }
1126 Ok(budgets)
1127 }
1128
1129 fn guess(spends: &[u64], mints: usize) -> Self {
1132 Budgets {
1133 spend: spends.iter().map(|i| (*i, SPEND_BUDGET_GUESS)).collect(),
1134 mint: (0..mints as u64).map(|i| (i, MINT_BUDGET_GUESS)).collect(),
1135 withdraw: BTreeMap::new(),
1136 }
1137 }
1138
1139 pub fn spend(&self, index: u64) -> Option<Budget> {
1140 self.spend.get(&index).copied()
1141 }
1142
1143 pub fn mint(&self, index: u64) -> Option<Budget> {
1144 self.mint.get(&index).copied()
1145 }
1146
1147 pub fn withdraw(&self, index: u64) -> Option<Budget> {
1149 self.withdraw.get(&index).copied()
1150 }
1151
1152 pub fn with_margin(&self, percent: u64) -> Self {
1156 let raise = |m: &BTreeMap<u64, Budget>| {
1157 m.iter()
1158 .map(|(i, b)| {
1159 let up = |x: u64| x + (x * percent).div_ceil(100);
1160 (
1161 *i,
1162 Budget {
1163 mem: up(b.mem),
1164 steps: up(b.steps),
1165 },
1166 )
1167 })
1168 .collect()
1169 };
1170 Budgets {
1171 spend: raise(&self.spend),
1172 mint: raise(&self.mint),
1173 withdraw: raise(&self.withdraw),
1174 }
1175 }
1176
1177 pub fn covers(&self, used: &Budgets) -> bool {
1179 let within = |ours: &BTreeMap<u64, Budget>, theirs: &BTreeMap<u64, Budget>| {
1180 theirs.iter().all(|(i, b)| {
1181 ours.get(i)
1182 .is_some_and(|o| o.mem >= b.mem && o.steps >= b.steps)
1183 })
1184 };
1185 within(&self.spend, &used.spend)
1186 && within(&self.mint, &used.mint)
1187 && within(&self.withdraw, &used.withdraw)
1188 }
1189}
1190
1191fn ogmios_failure(error: &Value) -> String {
1195 let message = error
1196 .get("message")
1197 .and_then(Value::as_str)
1198 .unwrap_or("no reason given");
1199 let items: Vec<&Value> = error
1200 .get("data")
1201 .and_then(Value::as_array)
1202 .into_iter()
1203 .flatten()
1204 .collect();
1205 let stale = error.get("code").and_then(Value::as_i64) == Some(3117)
1207 || items.iter().any(|item| {
1208 matches!(
1209 item.pointer("/error/code").and_then(Value::as_i64),
1210 Some(3110 | 3117)
1211 )
1212 });
1213 if stale {
1214 return "Some of the UTxOs this spends aren't on chain anymore. Refresh the balance and try again.".to_string();
1215 }
1216 let failures: Vec<String> = items
1217 .iter()
1218 .filter_map(|item| {
1219 let purpose = item.pointer("/validator/purpose").and_then(Value::as_str)?;
1220 let index = item.pointer("/validator/index").and_then(Value::as_u64)?;
1221 let what = match purpose {
1222 "spend" => format!("spending input {index}"),
1223 "mint" => "the Seedelf policy".to_string(),
1224 other => format!("{other} {index}"),
1225 };
1226 let reason = item
1227 .pointer("/error/data/validationError")
1228 .and_then(Value::as_str)
1229 .and_then(|e| e.lines().map(str::trim).rfind(|l| !l.is_empty()))
1230 .or_else(|| item.pointer("/error/message").and_then(Value::as_str))
1231 .unwrap_or("failed");
1232 let traces: Vec<&str> = item
1233 .pointer("/error/data/traces")
1234 .and_then(Value::as_array)
1235 .into_iter()
1236 .flatten()
1237 .filter_map(Value::as_str)
1238 .collect();
1239 Some(if traces.is_empty() {
1240 format!("{what}: {reason}")
1241 } else {
1242 format!("{what}: {reason}, traces: {}", traces.join("; "))
1243 })
1244 })
1245 .collect();
1246 let foreign = items.iter().any(|item| {
1248 !matches!(
1249 item.pointer("/validator/purpose").and_then(Value::as_str),
1250 Some("spend" | "mint") | None
1251 )
1252 });
1253 if failures.is_empty() {
1254 format!("Ogmios couldn't evaluate the transaction: {message}")
1255 } else if foreign {
1256 format!(
1257 "A script refused this transaction ({})",
1258 failures.join("; ")
1259 )
1260 } else {
1261 format!(
1262 "The Seedelf contract refused this transaction ({})",
1263 failures.join("; ")
1264 )
1265 }
1266}
1267
1268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1271pub struct ScriptFee {
1272 pub size: u64,
1274 pub compute: u64,
1276 pub script_reference: u64,
1279 pub total: u64,
1280}
1281
1282pub struct FinalSpend {
1284 pub tx: BuiltTransaction,
1286 pub fee: ScriptFee,
1287 pub change_lovelace: u64,
1289 pub change_tokens: Assets,
1290 pub change_outputs: usize,
1291}
1292
1293#[derive(Clone)]
1295pub struct Chain {
1296 pub params: ProtocolParameters,
1297 pub network_flag: bool,
1299 pub config: Config,
1301}
1302
1303#[derive(Clone)]
1305struct OwnedInput {
1306 utxo: UtxoResponse,
1307 input: Input,
1308 register: Register,
1309}
1310
1311#[derive(Clone)]
1313struct PolicyMint {
1314 policy: Hash<28>,
1315 token_name: Vec<u8>,
1316 amount: i64,
1317 redeemer: Vec<u8>,
1318}
1319
1320#[derive(Clone)]
1328struct AccountPart {
1329 inputs: Vec<(UtxoResponse, Input)>,
1330 collateral: Input,
1331 collateral_lovelace: u64,
1332 collateral_addr: Address,
1333}
1334
1335#[derive(Clone)]
1338pub struct ScriptSpend {
1339 chain: Chain,
1340 inputs: Vec<OwnedInput>,
1341 redeemers: Option<Vec<Vec<u8>>>,
1343 outputs: Vec<Output>,
1344 paid_lovelace: u64,
1345 paid_tokens: Assets,
1346 mint: Option<PolicyMint>,
1347 change_owner: Register,
1348 change_addr: Option<Address>,
1350 signer: Hash<28>,
1351 account: Option<AccountPart>,
1353}
1354
1355impl ScriptSpend {
1356 pub fn new(
1362 chain: &Chain,
1363 inputs: &[UtxoResponse],
1364 change_owner: &Register,
1365 signer: Hash<28>,
1366 ) -> Result<Self> {
1367 if inputs.is_empty() {
1368 bail!("A Seedelf spend needs at least one UTxO to spend");
1369 }
1370 let inputs = inputs
1371 .iter()
1372 .map(|utxo| {
1373 let register =
1374 extract_bytes_with_logging(&utxo.inline_datum).with_context(|| {
1375 format!("UTxO {}#{} holds no register", utxo.tx_hash, utxo.tx_index)
1376 })?;
1377 Ok(OwnedInput {
1378 input: input_of(utxo)?,
1379 utxo: utxo.clone(),
1380 register,
1381 })
1382 })
1383 .collect::<Result<Vec<_>>>()?;
1384 Ok(ScriptSpend {
1385 chain: chain.clone(),
1386 inputs,
1387 redeemers: None,
1388 outputs: Vec::new(),
1389 paid_lovelace: 0,
1390 paid_tokens: Assets::new(),
1391 mint: None,
1392 change_owner: change_owner.clone(),
1393 change_addr: None,
1394 signer,
1395 account: None,
1396 })
1397 }
1398
1399 pub fn with_account(
1406 mut self,
1407 account: Hash<28>,
1408 inputs: &[UtxoResponse],
1409 collateral: &UtxoResponse,
1410 ) -> Result<Self> {
1411 if account == self.signer {
1412 bail!("The account's key can't also be the spend's one-time key");
1413 }
1414 let network_flag = self.chain.network_flag;
1415 let at_signer = |utxo: &UtxoResponse| -> Result<Address> {
1416 let addr = Address::from_bech32(&utxo.address).with_context(|| {
1417 format!(
1418 "UTxO {}#{} has an unreadable address",
1419 utxo.tx_hash, utxo.tx_index
1420 )
1421 })?;
1422 let key = match &addr {
1423 Address::Shelley(s) if is_on_correct_network(addr.clone(), network_flag) => {
1424 match s.payment() {
1425 pallas_addresses::ShelleyPaymentPart::Key(k) => Some(*k),
1426 pallas_addresses::ShelleyPaymentPart::Script(_) => None,
1427 }
1428 }
1429 _ => None,
1430 };
1431 if key != Some(account) {
1432 bail!(
1433 "UTxO {}#{} isn't under the account's key",
1434 utxo.tx_hash,
1435 utxo.tx_index
1436 );
1437 }
1438 Ok(addr)
1439 };
1440 let mut taken = Vec::with_capacity(inputs.len());
1441 for utxo in inputs {
1442 at_signer(utxo)?;
1443 let input = input_of(utxo)?;
1444 if self.inputs.iter().any(|o| o.input == input)
1445 || taken
1446 .iter()
1447 .any(|(_, i): &(UtxoResponse, Input)| *i == input)
1448 {
1449 bail!("UTxO {}#{} is spent twice", utxo.tx_hash, utxo.tx_index);
1450 }
1451 taken.push((utxo.clone(), input));
1452 }
1453 let collateral_addr = at_signer(collateral)?;
1454 if collateral
1455 .asset_list
1456 .as_ref()
1457 .is_some_and(|a| !a.is_empty())
1458 {
1459 bail!("A collateral must hold ADA alone");
1460 }
1461 self.account = Some(AccountPart {
1462 inputs: taken,
1463 collateral: input_of(collateral)?,
1464 collateral_lovelace: collateral
1465 .value
1466 .parse()
1467 .context("The collateral's value isn't a number")?,
1468 collateral_addr,
1469 });
1470 Ok(self)
1471 }
1472
1473 pub fn change_to(mut self, addr: &Address) -> Self {
1477 self.change_addr = Some(addr.clone());
1478 self
1479 }
1480
1481 pub fn output(mut self, output: Output, tokens: &Assets) -> Result<Self> {
1483 self.paid_lovelace = self
1484 .paid_lovelace
1485 .checked_add(output.lovelace)
1486 .context("output lovelace overflow")?;
1487 self.paid_tokens = self.paid_tokens.merge(tokens.clone())?;
1488 self.outputs.push(output);
1489 Ok(self)
1490 }
1491
1492 pub fn mint(mut self, token_name: Vec<u8>, amount: i64, redeemer: Vec<u8>) -> Result<Self> {
1495 self.mint = Some(PolicyMint {
1496 policy: policy_hash(&self.chain.config)?,
1497 token_name,
1498 amount,
1499 redeemer,
1500 });
1501 Ok(self)
1502 }
1503
1504 pub fn proven(
1508 mut self,
1509 mut prove: impl FnMut(&Register, &str) -> Result<(String, String)>,
1510 ) -> Result<Self> {
1511 let vkh = hex::encode(self.signer);
1512 let redeemers = self
1513 .inputs
1514 .iter()
1515 .map(|owned| {
1516 let (z, g_r) = prove(&owned.register, &vkh)?;
1517 create_spend_redeemer(z, g_r, vkh.clone())
1518 })
1519 .collect::<Result<Vec<_>>>()?;
1520 self.redeemers = Some(redeemers);
1521 Ok(self)
1522 }
1523
1524 pub fn inputs(&self) -> Vec<UtxoResponse> {
1526 self.inputs.iter().map(|o| o.utxo.clone()).collect()
1527 }
1528
1529 pub fn draft(&self) -> Result<BuiltTransaction> {
1532 let redeemers = self.proofs()?;
1533 let fee = self.estimate()?.fee.total;
1536 self.stage(fee, None, redeemers)?
1537 .build_conway_raw()
1538 .context("Failed To Build The Draft Transaction")
1539 }
1540
1541 pub fn finalize(&self, budgets: &Budgets) -> Result<FinalSpend> {
1544 let redeemers = self.proofs()?;
1545 self.settle(budgets, redeemers)
1546 }
1547
1548 pub fn estimate(&self) -> Result<FinalSpend> {
1551 let placeholder = create_spend_redeemer(
1552 "00".repeat(32),
1553 Register::create(blstrs::Scalar::from(1u64))?.generator,
1554 hex::encode(self.signer),
1555 )?;
1556 let redeemers = vec![placeholder; self.inputs.len()];
1557 let budgets = Budgets::guess(&self.positions()?, usize::from(self.mint.is_some()));
1558 self.settle(&budgets, &redeemers)
1559 }
1560
1561 pub fn measure_locally(&self, known: &[crate::eval::Resolved]) -> Result<FinalSpend> {
1567 let Chain {
1568 params,
1569 network_flag,
1570 ..
1571 } = &self.chain;
1572 let mut resolved: Vec<crate::eval::Resolved> = known.to_vec();
1573 let rows = self.inputs.iter().map(|o| &o.utxo).chain(
1574 self.account
1575 .iter()
1576 .flat_map(|a| a.inputs.iter().map(|(u, _)| u)),
1577 );
1578 for row in rows {
1579 let hash = decode_tx_hash(&row.tx_hash)?;
1580 if !resolved
1581 .iter()
1582 .any(|r| r.tx_hash == hash && r.index == row.tx_index)
1583 {
1584 resolved.push(crate::eval::resolve_row(row)?);
1585 }
1586 }
1587 resolved.extend(crate::eval::seedelf_references(*network_flag)?);
1588 let measure = |tx: &BuiltTransaction| -> Result<Budgets> {
1589 let answer = crate::eval::evaluate(
1590 &tx.tx_bytes.0,
1591 &resolved,
1592 ¶ms.cost_model_v3,
1593 *network_flag,
1594 )?;
1595 Budgets::from_ogmios(&answer)
1596 };
1597 let budgets = measure(&self.draft()?)?;
1598 let finished = self.finalize(&budgets)?;
1599 let again = measure(&finished.tx)?;
1600 if budgets.covers(&again) {
1601 return Ok(finished);
1602 }
1603 let finished = self.finalize(&again.with_margin(1))?;
1605 if !again.with_margin(1).covers(&measure(&finished.tx)?) {
1606 bail!("The scripts' budgets did not settle");
1607 }
1608 Ok(finished)
1609 }
1610
1611 fn proofs(&self) -> Result<&[Vec<u8>]> {
1612 self.redeemers
1613 .as_deref()
1614 .context("The Seedelf spend isn't proven yet")
1615 }
1616
1617 fn ledger_order(&self) -> Vec<Input> {
1620 let mut order: Vec<Input> = self
1621 .inputs
1622 .iter()
1623 .map(|o| o.input.clone())
1624 .chain(
1625 self.account
1626 .iter()
1627 .flat_map(|a| a.inputs.iter().map(|(_, i)| i.clone())),
1628 )
1629 .collect();
1630 order.sort_by_key(|i| (i.tx_hash.0, i.txo_index));
1631 order
1632 }
1633
1634 fn positions(&self) -> Result<Vec<u64>> {
1637 let order = self.ledger_order();
1638 self.inputs
1639 .iter()
1640 .map(|owned| {
1641 order
1642 .iter()
1643 .position(|i| *i == owned.input)
1644 .map(|p| p as u64)
1645 .context("An input is missing from the transaction")
1646 })
1647 .collect()
1648 }
1649
1650 fn settle(&self, budgets: &Budgets, redeemers: &[Vec<u8>]) -> Result<FinalSpend> {
1651 let mut used = Vec::with_capacity(self.inputs.len() + 1);
1653 let mut positions = self.positions()?;
1654 positions.sort_unstable();
1655 for index in positions {
1656 used.push(budgets.spend(index).with_context(|| {
1657 format!("Ogmios measured no budget for spending input {index}")
1658 })?);
1659 }
1660 if self.mint.is_some() {
1661 used.push(
1662 budgets
1663 .mint(0)
1664 .context("Ogmios measured no budget for the Seedelf policy")?,
1665 );
1666 }
1667 let total = used
1668 .iter()
1669 .fold(Budget { mem: 0, steps: 0 }, |a, b| Budget {
1670 mem: a.mem + b.mem,
1671 steps: a.steps + b.steps,
1672 });
1673 if total.mem > MAX_TX_BUDGET.mem || total.steps > MAX_TX_BUDGET.steps {
1674 bail!(
1675 "Spending {} UTxOs at once needs more computation than a transaction may use",
1676 self.inputs.len()
1677 );
1678 }
1679
1680 let compute: u64 = used
1681 .iter()
1682 .map(|b| computation_fee(&self.chain.params, b.mem, b.steps))
1683 .sum();
1684 let contract = &self.chain.config.contract;
1687 let mut script_bytes = contract.wallet_contract_size;
1688 if self.mint.is_some() {
1689 script_bytes += contract.seedelf_contract_size;
1690 }
1691 let mut spent = self.inputs();
1692 if let Some(account) = &self.account {
1693 spent.extend(account.inputs.iter().map(|(u, _)| u.clone()));
1694 }
1695 script_bytes += reference_script_bytes(&spent)?;
1696 let script_reference = reference_script_fee(&self.chain.params, script_bytes)?;
1697
1698 let (fee, staged) = settle(
1701 2,
1702 Patches::staking(&Staking::none()),
1703 |size| even(linear_fee(&self.chain.params, size) + compute + script_reference),
1704 |fee| self.stage(fee, Some(budgets), redeemers),
1705 )?;
1706 let (change_lovelace, change_tokens) = self.remainder(fee)?;
1707 let change_outputs = staged.outputs.as_ref().map_or(0, Vec::len) - self.outputs.len();
1708 Ok(FinalSpend {
1709 tx: staged
1710 .build_conway_raw()
1711 .context("Failed To Build The Transaction")?,
1712 fee: ScriptFee {
1713 size: fee - compute - script_reference,
1714 compute,
1715 script_reference,
1716 total: fee,
1717 },
1718 change_lovelace,
1719 change_tokens,
1720 change_outputs,
1721 })
1722 }
1723
1724 fn remainder(&self, fee: u64) -> Result<(u64, Assets)> {
1726 let mut spent = self.inputs();
1727 if let Some(account) = &self.account {
1728 spent.extend(account.inputs.iter().map(|(u, _)| u.clone()));
1729 }
1730 let (total, mut tokens) = assets_of(spent)?;
1731 if let Some(m) = &self.mint {
1732 let asset = Asset::new(
1733 hex::encode(m.policy),
1734 hex::encode(&m.token_name),
1735 m.amount.unsigned_abs(),
1736 )?;
1737 tokens = if m.amount >= 0 {
1738 tokens.add(asset)?
1739 } else {
1740 tokens
1741 .sub(asset)
1742 .context("The UTxOs spent don't hold the token to burn")?
1743 };
1744 }
1745 let lovelace = total
1746 .checked_sub(self.paid_lovelace)
1747 .and_then(|rest| rest.checked_sub(fee))
1748 .ok_or(SEEDELF_SHORT)?;
1749 let tokens = tokens
1750 .separate(self.paid_tokens.clone())
1751 .context("The UTxOs spent don't hold the tokens being sent")?;
1752 Ok((lovelace, tokens))
1753 }
1754
1755 fn stage(
1756 &self,
1757 fee: u64,
1758 budgets: Option<&Budgets>,
1759 redeemers: &[Vec<u8>],
1760 ) -> Result<StagingTransaction> {
1761 let Chain {
1762 params,
1763 network_flag,
1764 config,
1765 } = &self.chain;
1766 let (change_lovelace, change_tokens) = self.remainder(fee)?;
1767 let wallet_addr = wallet_contract(*network_flag, config.contract.wallet_contract_hash);
1768 let change = if change_lovelace == 0 && change_tokens.is_empty() {
1769 Vec::new()
1770 } else if let Some(addr) = &self.change_addr {
1771 change_outputs(params, addr, change_lovelace, &change_tokens, SEEDELF_SHORT)?
1772 } else {
1773 if change_lovelace < minimum_deposit(params, &change_tokens)? {
1774 bail!(SEEDELF_SHORT);
1775 }
1776 deposit_outputs(
1777 params,
1778 &wallet_addr,
1779 &self.change_owner,
1780 change_lovelace,
1781 &change_tokens,
1782 )?
1783 };
1784
1785 let mut tx = StagingTransaction::new();
1786 for owned in &self.inputs {
1787 tx = tx.input(owned.input.clone());
1788 }
1789 for (_, input) in self.account.iter().flat_map(|a| &a.inputs) {
1790 tx = tx.input(input.clone());
1791 }
1792 let mut outputs: Vec<Output> = self.outputs.iter().cloned().chain(change).collect();
1798 crate::lovejoin::shuffle(&mut outputs);
1799 for output in outputs {
1800 tx = tx.output(output);
1801 }
1802 tx = match &self.account {
1803 None => tx
1804 .collateral_input(collateral_input(*network_flag))
1805 .collateral_output(collateral_output(collateral_address(*network_flag), fee)?)
1806 .disclosed_signer(self.signer)
1807 .disclosed_signer(Hash::new(COLLATERAL_HASH)),
1808 Some(account) => {
1809 let back = account
1811 .collateral_lovelace
1812 .checked_sub(fee * 3 / 2)
1813 .context("The collateral can't cover this transaction's fee")?;
1814 tx.collateral_input(account.collateral.clone())
1815 .collateral_output(Output::new(account.collateral_addr.clone(), back))
1816 .disclosed_signer(self.signer)
1817 }
1818 };
1819 tx = tx
1820 .fee(fee)
1821 .reference_input(reference_utxo(config.reference.wallet_reference_utxo))
1822 .language_view(ScriptKind::PlutusV3, params.cost_model_v3.clone());
1823
1824 let order = self.ledger_order();
1826 let units = |budget: Option<Budget>| {
1827 let b = budget.unwrap_or(DRAFT_BUDGET);
1828 ExUnits {
1829 mem: b.mem,
1830 steps: b.steps,
1831 }
1832 };
1833 for (owned, redeemer) in self.inputs.iter().zip(redeemers) {
1834 let index = order
1835 .iter()
1836 .position(|i| *i == owned.input)
1837 .context("An input is missing from the transaction")?;
1838 let budget = budgets.and_then(|b| b.spend(index as u64));
1839 tx = tx.add_spend_redeemer(owned.input.clone(), redeemer.clone(), Some(units(budget)));
1840 }
1841 if let Some(m) = &self.mint {
1842 tx = tx
1843 .reference_input(reference_utxo(config.reference.seedelf_reference_utxo))
1844 .mint_asset(m.policy, m.token_name.clone(), m.amount)
1845 .context("Failed To Mint The Seedelf")?
1846 .add_mint_redeemer(
1847 m.policy,
1848 m.redeemer.clone(),
1849 Some(units(budgets.and_then(|b| b.mint(0)))),
1850 );
1851 }
1852 Ok(tx)
1853 }
1854}
1855
1856fn policy_hash(config: &Config) -> Result<Hash<28>> {
1857 let bytes: [u8; 28] = hex::decode(&config.contract.seedelf_policy_id)
1858 .context("The Seedelf policy id is not hex")?
1859 .try_into()
1860 .map_err(|_| anyhow::anyhow!("The Seedelf policy id is not 28 bytes"))?;
1861 Ok(Hash::new(bytes))
1862}
1863
1864#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1885pub enum Origin {
1886 #[default]
1888 Unknown,
1889 Own,
1891 Received,
1893 Session,
1895 Lovejoin,
1897}
1898
1899pub const UNKNOWN_CLASS: &str = "unknown";
1901
1902#[derive(Debug, Clone, PartialEq, Eq)]
1906pub struct Class {
1907 pub id: String,
1908 pub origin: Origin,
1909}
1910
1911impl Default for Class {
1912 fn default() -> Self {
1913 Class {
1914 id: UNKNOWN_CLASS.to_string(),
1915 origin: Origin::Unknown,
1916 }
1917 }
1918}
1919
1920impl Class {
1921 pub fn has(&self, part: &str) -> bool {
1923 self.id.split('+').any(|p| p == part)
1924 }
1925}
1926
1927#[derive(Debug, Clone, PartialEq, Eq, Default)]
1929pub enum Purpose {
1930 #[default]
1932 Pay,
1933 Mint,
1936 Fund { session: Option<String> },
1940}
1941
1942impl Purpose {
1943 fn rank(&self, class: &Class) -> u8 {
1945 use Origin::*;
1946 match self {
1947 Purpose::Pay => match class.origin {
1948 Own => 0,
1949 Session => 1,
1950 Received => 2,
1951 Unknown => 3,
1952 Lovejoin => 4,
1953 },
1954 Purpose::Mint => match class.origin {
1955 Received => 0,
1956 Lovejoin => 1,
1957 Unknown => 2,
1958 Own => 3,
1959 Session => 4,
1960 },
1961 Purpose::Fund { session } => {
1962 if session.as_deref().is_some_and(|s| class.has(s)) {
1963 return 0;
1964 }
1965 match class.origin {
1966 Received => 1,
1967 Lovejoin => 2,
1968 Unknown => 3,
1969 Own => 4,
1970 Session => 5,
1971 }
1972 }
1973 }
1974 }
1975
1976 fn tier(&self, class: &Class) -> u8 {
1980 match (self, class.origin) {
1981 (_, Origin::Lovejoin) => 2,
1982 (Purpose::Fund { .. }, Origin::Session) if self.rank(class) > 0 => 1,
1983 _ => 0,
1984 }
1985 }
1986}
1987
1988#[derive(Debug, Clone, Default)]
1992pub struct Histories {
1993 classes: BTreeMap<(String, u64), Class>,
1994 purpose: Purpose,
1995}
1996
1997impl Histories {
1998 pub fn new(purpose: Purpose) -> Self {
2000 Histories {
2001 classes: BTreeMap::new(),
2002 purpose,
2003 }
2004 }
2005
2006 pub fn with(mut self, tx_hash: &str, tx_index: u64, class: Class) -> Self {
2008 self.classes.insert((tx_hash.to_string(), tx_index), class);
2009 self
2010 }
2011
2012 pub fn class_of(&self, utxo: &UtxoResponse) -> Class {
2014 self.classes
2015 .get(&(utxo.tx_hash.clone(), utxo.tx_index))
2016 .cloned()
2017 .unwrap_or_default()
2018 }
2019
2020 pub fn merged(&self, inputs: &[UtxoResponse]) -> Vec<Class> {
2023 let mut classes: Vec<Class> = Vec::new();
2024 for class in inputs.iter().map(|u| self.class_of(u)) {
2025 if !classes.iter().any(|c| c.id == class.id) {
2026 classes.push(class);
2027 }
2028 }
2029 if classes.len() > 1 {
2030 classes
2031 } else {
2032 Vec::new()
2033 }
2034 }
2035
2036 fn blind(&self, utxos: &[UtxoResponse]) -> bool {
2038 utxos
2039 .iter()
2040 .all(|u| self.class_of(u).origin == Origin::Unknown)
2041 }
2042}
2043
2044fn lovelace_of(utxo: &UtxoResponse) -> u64 {
2045 utxo.value.parse::<u64>().unwrap_or(0)
2046}
2047
2048fn holds_tokens(utxo: &UtxoResponse) -> bool {
2049 utxo.asset_list.as_ref().is_some_and(|a| !a.is_empty())
2050}
2051
2052fn select_script_inputs<T>(
2079 available: &[UtxoResponse],
2080 needed: &Assets,
2081 floor: u64,
2082 histories: &Histories,
2083 mut attempt: impl FnMut(&[UtxoResponse]) -> Result<T>,
2084) -> Result<T> {
2085 let holds_needed = |u: &UtxoResponse| {
2086 needed.items.iter().any(|want| {
2087 quantity_in(
2088 u,
2089 &hex::encode(want.policy_id),
2090 &hex::encode(&want.token_name),
2091 ) > 0
2092 })
2093 };
2094 let evaluable: Vec<UtxoResponse> = available
2095 .iter()
2096 .filter(|u| crate::eval::refusal(u).is_none())
2097 .cloned()
2098 .collect();
2099 let (available, _) = fitting(&evaluable, &Assets::new(), holds_needed)?;
2100 let available = available.as_slice();
2101 let mandatory = holding(available, needed)?;
2102 let mut rest: Vec<UtxoResponse> = available
2103 .iter()
2104 .filter(|u| !mandatory.iter().any(|m| same_utxo(m, u)))
2105 .cloned()
2106 .collect();
2107 if !histories.blind(available) {
2108 return apart(mandatory, rest, floor, histories, attempt);
2109 }
2110 rest.sort_by_key(|u| (holds_tokens(u), std::cmp::Reverse(lovelace_of(u))));
2111 let mut last_error = None;
2112 for k in 0..=rest.len() {
2113 let selected: Vec<UtxoResponse> = mandatory.iter().chain(&rest[..k]).cloned().collect();
2114 if selected.is_empty() {
2115 continue;
2116 }
2117 match attempt(&selected) {
2118 Ok(built) => return Ok(built),
2119 Err(e) if e.downcast_ref::<NotEnough>().is_some() => last_error = Some(e),
2120 Err(e) => return Err(e),
2121 }
2122 }
2123 Err(last_error.unwrap_or_else(|| SEEDELF_SHORT.into()))
2124}
2125
2126const MAX_TOKEN_SINGLES: usize = 12;
2130
2131enum Tried<T> {
2133 Built(T),
2134 Short,
2136 Failed,
2138 Skipped,
2140}
2141
2142fn one_more<'a, T>(
2147 mut candidates: Vec<&'a UtxoResponse>,
2148 rank: impl Fn(&UtxoResponse) -> u8,
2149 try_with: &mut impl FnMut(&[&'a UtxoResponse]) -> Tried<T>,
2150) -> Option<T> {
2151 candidates.sort_by_key(|u| (rank(u), lovelace_of(u)));
2152 let mut short_at: Option<u64> = None;
2153 let mut token_tries = 0;
2154 for u in candidates {
2155 let ada_only = !holds_tokens(u);
2156 if ada_only && short_at.is_some_and(|at| lovelace_of(u) <= at) {
2157 continue;
2158 }
2159 if !ada_only {
2160 if token_tries == MAX_TOKEN_SINGLES {
2161 continue;
2162 }
2163 token_tries += 1;
2164 }
2165 match try_with(&[u]) {
2166 Tried::Built(built) => return Some(built),
2167 Tried::Short if ada_only => short_at = short_at.max(Some(lovelace_of(u))),
2168 _ => {}
2169 }
2170 }
2171 None
2172}
2173
2174enum Grown<T> {
2176 Built(T),
2177 Short,
2179 Failed(usize),
2182}
2183
2184fn grow<'a, T>(
2187 order: &[&'a UtxoResponse],
2188 from: usize,
2189 try_with: &mut impl FnMut(&[&'a UtxoResponse]) -> Tried<T>,
2190) -> Grown<T> {
2191 for k in from.max(1)..=order.len() {
2192 match try_with(&order[..k]) {
2193 Tried::Built(built) => return Grown::Built(built),
2194 Tried::Failed => return Grown::Failed(k),
2195 _ => {}
2196 }
2197 }
2198 Grown::Short
2199}
2200
2201fn apart<T>(
2204 base: Vec<UtxoResponse>,
2205 rest: Vec<UtxoResponse>,
2206 floor: u64,
2207 histories: &Histories,
2208 mut attempt: impl FnMut(&[UtxoResponse]) -> Result<T>,
2209) -> Result<T> {
2210 let purpose = &histories.purpose;
2211 let class = |u: &UtxoResponse| histories.class_of(u);
2212 let base_lovelace: u64 = base.iter().map(lovelace_of).sum();
2213 let base_classes: Vec<String> = base.iter().map(|u| class(u).id).collect();
2214 let joins_base = |u: &UtxoResponse| base.is_empty() || base_classes.contains(&class(u).id);
2216
2217 let mut not_enough: Option<anyhow::Error> = None;
2218 let mut other: Option<anyhow::Error> = None;
2219 let mut try_with = |extra: &[&UtxoResponse]| -> Tried<T> {
2220 let lovelace = base_lovelace + extra.iter().map(|u| lovelace_of(u)).sum::<u64>();
2221 if lovelace <= floor || (base.is_empty() && extra.is_empty()) {
2222 return Tried::Skipped;
2223 }
2224 let selected: Vec<UtxoResponse> = base
2225 .iter()
2226 .cloned()
2227 .chain(extra.iter().map(|u| (*u).clone()))
2228 .collect();
2229 match attempt(&selected) {
2230 Ok(built) => Tried::Built(built),
2231 Err(e) if e.downcast_ref::<NotEnough>().is_some() => {
2232 not_enough = Some(e);
2233 Tried::Short
2234 }
2235 Err(e) => {
2236 other = Some(e);
2237 Tried::Failed
2238 }
2239 }
2240 };
2241
2242 if let Tried::Built(built) = try_with(&[]) {
2244 return Ok(built);
2245 }
2246
2247 let singles: Vec<&UtxoResponse> = rest.iter().filter(|u| joins_base(u)).collect();
2249 if let Some(built) = one_more(singles, |u| purpose.rank(&class(u)), &mut try_with) {
2250 return Ok(built);
2251 }
2252
2253 let mut classes: Vec<(Class, Vec<&UtxoResponse>)> = Vec::new();
2257 for u in rest.iter().filter(|u| joins_base(u)) {
2258 let c = class(u);
2259 match classes.iter_mut().find(|(k, _)| k.id == c.id) {
2260 Some((_, rows)) => rows.push(u),
2261 None => classes.push((c, vec![u])),
2262 }
2263 }
2264 classes.sort_by_key(|(c, rows)| {
2265 let total: u64 = rows.iter().map(|u| lovelace_of(u)).sum();
2266 (purpose.rank(c), std::cmp::Reverse(total), c.id.clone())
2267 });
2268 for (_, rows) in &mut classes {
2269 rows.sort_by_key(|u| (holds_tokens(u), std::cmp::Reverse(lovelace_of(u))));
2270 for k in 2..=rows.len() {
2271 match try_with(&rows[..k]) {
2272 Tried::Built(built) => return Ok(built),
2273 Tried::Failed => break,
2275 _ => {}
2276 }
2277 }
2278 }
2279
2280 if !base.is_empty() {
2284 let others: Vec<&UtxoResponse> = rest
2285 .iter()
2286 .filter(|u| !joins_base(u) && purpose.tier(&class(u)) == 0)
2287 .collect();
2288 if let Some(built) = one_more(others, |u| purpose.rank(&class(u)), &mut try_with) {
2289 return Ok(built);
2290 }
2291 }
2292
2293 let tier = |u: &UtxoResponse| purpose.tier(&class(u));
2312 let mut by_history: Vec<&UtxoResponse> = rest.iter().collect();
2313 by_history.sort_by_key(|u| {
2314 (
2315 tier(u),
2316 !joins_base(u),
2317 purpose.rank(&class(u)),
2318 holds_tokens(u),
2319 std::cmp::Reverse(lovelace_of(u)),
2320 )
2321 });
2322 let mut by_tier: Vec<&UtxoResponse> = rest.iter().collect();
2323 by_tier.sort_by_key(|u| {
2324 (
2325 tier(u),
2326 !joins_base(u),
2327 holds_tokens(u),
2328 std::cmp::Reverse(lovelace_of(u)),
2329 )
2330 });
2331 let is_box = |u: &UtxoResponse| tier(u) == 2 && !holds_tokens(u);
2332 let mut boxes: Vec<&UtxoResponse> = rest.iter().filter(|u| is_box(u)).collect();
2333 boxes.sort_by_key(|u| (!joins_base(u), std::cmp::Reverse(lovelace_of(u))));
2334 let mut largest: Vec<&UtxoResponse> = rest.iter().filter(|u| !is_box(u)).collect();
2335 largest.sort_by_key(|u| (tier(u), !joins_base(u), std::cmp::Reverse(lovelace_of(u))));
2336 let mut tokens_last: Vec<&UtxoResponse> = rest.iter().filter(|u| !is_box(u)).collect();
2337 tokens_last.sort_by_key(|u| {
2338 (
2339 holds_tokens(u),
2340 tier(u),
2341 !joins_base(u),
2342 std::cmp::Reverse(lovelace_of(u)),
2343 )
2344 });
2345 let same = |a: &[&UtxoResponse], b: &[&UtxoResponse]| {
2346 a.len() == b.len() && a.iter().zip(b).all(|(x, y)| std::ptr::eq(*x, *y))
2347 };
2348 let mut rests = vec![&largest];
2349 if !same(&largest, &tokens_last) {
2350 rests.push(&tokens_last);
2351 }
2352 let (boxes, rests) = (&boxes, &rests);
2353 let fewest_boxes = (0..=boxes.len())
2355 .filter(|_| !largest.is_empty())
2356 .flat_map(|j| {
2357 rests
2358 .iter()
2359 .map(move |others| (boxes[..j].iter().chain(others.iter()).copied().collect(), j))
2360 });
2361 let mut blind: Vec<&UtxoResponse> = rest.iter().collect();
2362 blind.sort_by_key(|u| (holds_tokens(u), std::cmp::Reverse(lovelace_of(u))));
2363 let mut failed = false;
2364 let mut boxes_fail = false;
2365 let mut grown: Vec<Vec<&UtxoResponse>> = Vec::new();
2366 let orders = [(by_history, 0), (by_tier, 0)]
2368 .into_iter()
2369 .chain(fewest_boxes)
2370 .chain([(blind, 0)]);
2371 for (order, j) in orders {
2372 let grown_before =
2375 |g: &Vec<&UtxoResponse>| g.len() >= order.len() && same(&g[..order.len()], &order);
2376 if (j > 0 && boxes_fail) || grown.iter().any(grown_before) {
2377 continue;
2378 }
2379 let whole = order.len() == rest.len();
2380 match grow(&order, j, &mut try_with) {
2381 Grown::Built(built) => return Ok(built),
2382 Grown::Short if whole => {
2383 failed = false;
2384 break;
2385 }
2386 Grown::Short => {}
2388 Grown::Failed(at) => {
2389 failed = true;
2390 boxes_fail |= j > 0 && at == j;
2391 }
2392 }
2393 if whole {
2394 grown.push(order);
2395 }
2396 }
2397 Err(match (failed, other, not_enough) {
2398 (true, Some(e), _) => e,
2399 (_, _, Some(e)) => e,
2400 (_, Some(e), None) => e,
2401 _ => SEEDELF_SHORT.into(),
2402 })
2403}
2404
2405fn same_utxo(a: &UtxoResponse, b: &UtxoResponse) -> bool {
2406 a.tx_hash == b.tx_hash && a.tx_index == b.tx_index
2407}
2408
2409fn quantity_in(utxo: &UtxoResponse, policy: &str, name: &str) -> u64 {
2411 utxo.asset_list
2412 .iter()
2413 .flatten()
2414 .filter(|a| a.policy_id == policy && a.asset_name == name)
2415 .map(|a| a.quantity.parse::<u64>().unwrap_or(0))
2416 .sum()
2417}
2418
2419fn holding(available: &[UtxoResponse], needed: &Assets) -> Result<Vec<UtxoResponse>> {
2423 let mut picked: Vec<UtxoResponse> = Vec::new();
2424 for want in &needed.items {
2425 let policy = hex::encode(want.policy_id);
2426 let name = hex::encode(&want.token_name);
2427 let mut have: u64 = picked.iter().map(|u| quantity_in(u, &policy, &name)).sum();
2428 let mut candidates: Vec<&UtxoResponse> = available
2429 .iter()
2430 .filter(|u| quantity_in(u, &policy, &name) > 0)
2431 .filter(|u| !picked.iter().any(|p| same_utxo(p, u)))
2432 .collect();
2433 candidates.sort_by_key(|u| std::cmp::Reverse(quantity_in(u, &policy, &name)));
2434 for utxo in candidates {
2435 if have >= want.amount {
2436 break;
2437 }
2438 have = have.saturating_add(quantity_in(utxo, &policy, &name));
2439 picked.push(utxo.clone());
2440 }
2441 if have < want.amount {
2442 if have == 0 {
2443 bail!("The Seedelf balance doesn't hold the token {policy}.{name}");
2444 }
2445 bail!(
2446 "The Seedelf balance holds only {have} of the token {policy}.{name}, not {}",
2447 want.amount
2448 );
2449 }
2450 }
2451 Ok(picked)
2452}
2453
2454pub struct SeedelfMint {
2456 pub spend: ScriptSpend,
2457 pub token_name: Vec<u8>,
2459 pub lovelace: u64,
2461}
2462
2463pub fn mint(
2466 chain: &Chain,
2467 available: &[UtxoResponse],
2468 label: &str,
2469 seedelf: &Register,
2470 change_owner: &Register,
2471 signer: Hash<28>,
2472) -> Result<SeedelfMint> {
2473 mint_apart(
2474 chain,
2475 available,
2476 &Histories::default(),
2477 label,
2478 seedelf,
2479 change_owner,
2480 signer,
2481 )
2482}
2483
2484pub fn mint_apart(
2486 chain: &Chain,
2487 available: &[UtxoResponse],
2488 histories: &Histories,
2489 label: &str,
2490 seedelf: &Register,
2491 change_owner: &Register,
2492 signer: Hash<28>,
2493) -> Result<SeedelfMint> {
2494 let floor = seedelf_minimum_lovelace(&chain.params)?;
2495 select_script_inputs(available, &Assets::new(), floor, histories, |inputs| {
2496 let built = mint_from(chain, inputs, label, seedelf, change_owner, signer)?;
2497 built.spend.estimate()?;
2498 Ok(built)
2499 })
2500}
2501
2502pub fn mint_from(
2511 chain: &Chain,
2512 inputs: &[UtxoResponse],
2513 label: &str,
2514 seedelf: &Register,
2515 change_owner: &Register,
2516 signer: Hash<28>,
2517) -> Result<SeedelfMint> {
2518 if !is_payable(seedelf) {
2521 bail!("The Seedelf's register isn't made of valid points");
2522 }
2523 let spend = ScriptSpend::new(chain, inputs, change_owner, signer)?;
2524 let Chain {
2525 params,
2526 network_flag,
2527 config,
2528 } = chain;
2529 let policy = policy_hash(config)?;
2530 let owned: Vec<Input> = spend.inputs.iter().map(|o| o.input.clone()).collect();
2531 let token_name = seedelf_token_name(label.to_string(), Some(&owned))?;
2532 let lovelace = seedelf_minimum_lovelace(params)?;
2533 let wallet_addr = wallet_contract(*network_flag, config.contract.wallet_contract_hash);
2534 let output = Output::new(wallet_addr, lovelace)
2535 .set_inline_datum(seedelf.to_vec()?)
2536 .add_asset(policy, token_name.clone(), 1)
2537 .context("Failed To Add The Seedelf")?;
2538 let token = Asset::new(
2539 config.contract.seedelf_policy_id.clone(),
2540 hex::encode(&token_name),
2541 1,
2542 )?;
2543 let spend = spend.output(output, &Assets::new().add(token)?)?.mint(
2544 token_name.clone(),
2545 1,
2546 create_mint_redeemer(label.to_string())?,
2547 )?;
2548 Ok(SeedelfMint {
2549 spend,
2550 token_name,
2551 lovelace,
2552 })
2553}
2554
2555#[derive(Debug, Clone)]
2567pub struct Payment {
2568 pub register: Register,
2570 pub lovelace: u64,
2571 pub tokens: Assets,
2572}
2573
2574pub fn is_payable(register: &Register) -> bool {
2579 let identity = |point: &str| {
2580 hex::decode(point)
2581 .is_ok_and(|b| b.len() == 48 && b[0] == 0xc0 && b[1..].iter().all(|&x| x == 0))
2582 };
2583 register.is_valid().unwrap_or(false)
2584 && !identity(®ister.generator)
2585 && !identity(®ister.public_value)
2586}
2587
2588pub fn transfer(
2592 chain: &Chain,
2593 available: &[UtxoResponse],
2594 payments: &[Payment],
2595 change_owner: &Register,
2596 signer: Hash<28>,
2597) -> Result<ScriptSpend> {
2598 transfer_apart(
2599 chain,
2600 available,
2601 &Histories::default(),
2602 payments,
2603 change_owner,
2604 signer,
2605 )
2606}
2607
2608pub fn transfer_apart(
2610 chain: &Chain,
2611 available: &[UtxoResponse],
2612 histories: &Histories,
2613 payments: &[Payment],
2614 change_owner: &Register,
2615 signer: Hash<28>,
2616) -> Result<ScriptSpend> {
2617 let outputs = payment_outputs(chain, payments)?;
2618 let needed = payments
2619 .iter()
2620 .try_fold(Assets::new(), |all, p| all.merge(p.tokens.clone()))?;
2621 select_script_inputs(
2622 available,
2623 &needed,
2624 paid_out(&outputs),
2625 histories,
2626 |inputs| {
2627 let spend = paying(chain, inputs, &outputs, change_owner, signer)?;
2628 spend.estimate()?;
2629 Ok(spend)
2630 },
2631 )
2632}
2633
2634fn paid_out(outputs: &[(Output, Assets)]) -> u64 {
2636 outputs.iter().map(|(o, _)| o.lovelace).sum()
2637}
2638
2639pub fn transfer_from(
2642 chain: &Chain,
2643 inputs: &[UtxoResponse],
2644 payments: &[Payment],
2645 change_owner: &Register,
2646 signer: Hash<28>,
2647) -> Result<ScriptSpend> {
2648 let outputs = payment_outputs(chain, payments)?;
2649 paying(chain, inputs, &outputs, change_owner, signer)
2650}
2651
2652fn paying(
2653 chain: &Chain,
2654 inputs: &[UtxoResponse],
2655 outputs: &[(Output, Assets)],
2656 change_owner: &Register,
2657 signer: Hash<28>,
2658) -> Result<ScriptSpend> {
2659 outputs.iter().try_fold(
2660 ScriptSpend::new(chain, inputs, change_owner, signer)?,
2661 |spend, (output, tokens)| spend.output(output.clone(), tokens),
2662 )
2663}
2664
2665fn payment_outputs(chain: &Chain, payments: &[Payment]) -> Result<Vec<(Output, Assets)>> {
2668 if payments.is_empty() {
2669 bail!("A transfer pays at least one Seedelf");
2670 }
2671 let wallet_addr = wallet_contract(
2672 chain.network_flag,
2673 chain.config.contract.wallet_contract_hash,
2674 );
2675 payments
2676 .iter()
2677 .map(|payment| {
2678 if !is_payable(&payment.register) {
2679 bail!(
2680 "That Seedelf's register isn't valid: a payment to it could be locked for good, or taken by anyone"
2681 );
2682 }
2683 if payment.tokens.items.iter().any(|a| a.amount == 0) {
2684 bail!("A payment can't send none of a token");
2685 }
2686 let minimum = minimum_seedelf_payment(&chain.params, &payment.tokens)?;
2687 if payment.lovelace < minimum {
2688 bail!(
2689 "A payment to a Seedelf{} needs at least {} ADA",
2690 if payment.tokens.is_empty() { "" } else { " with these tokens" },
2691 ada(minimum)
2692 );
2693 }
2694 let output = deposit_output(&wallet_addr, &payment.register, payment.lovelace, &payment.tokens)?;
2695 Ok((output, payment.tokens.clone()))
2696 })
2697 .collect()
2698}
2699
2700pub fn minimum_seedelf_payment(params: &ProtocolParameters, tokens: &Assets) -> Result<u64> {
2703 wallet_minimum_lovelace_with_assets(params, tokens.clone())
2704}
2705
2706fn ada(lovelace: u64) -> String {
2708 format!("{}.{:06}", lovelace / 1_000_000, lovelace % 1_000_000)
2709}
2710
2711pub fn is_payable_address(addr: &Address, network_flag: bool) -> bool {
2724 matches!(addr, Address::Shelley(_))
2725 && is_not_a_script(addr.clone())
2726 && is_on_correct_network(addr.clone(), network_flag)
2727}
2728
2729fn check_address(chain: &Chain, addr: &Address) -> Result<()> {
2730 check_payable(addr, chain.network_flag)
2731}
2732
2733fn check_payable(addr: &Address, network_flag: bool) -> Result<()> {
2734 if !is_payable_address(addr, network_flag) {
2735 let network = if network_flag { "preprod" } else { "mainnet" };
2736 bail!(
2737 "Payments go to a normal {network} address: not a script, stake or other network's address"
2738 );
2739 }
2740 Ok(())
2741}
2742
2743pub fn minimum_address_payment(
2745 params: &ProtocolParameters,
2746 to: &Address,
2747 tokens: &Assets,
2748) -> Result<u64> {
2749 let bech32 = to
2750 .to_bech32()
2751 .map_err(|e| anyhow::anyhow!("Failed to encode the address: {e}"))?;
2752 address_minimum_lovelace_with_assets(params, &bech32, tokens.clone())
2753}
2754
2755fn address_output(chain: &Chain, to: &Address, lovelace: u64, tokens: &Assets) -> Result<Output> {
2757 check_address(chain, to)?;
2758 pay_address(&chain.params, to, lovelace, tokens)
2759}
2760
2761fn pay_address(
2763 params: &ProtocolParameters,
2764 to: &Address,
2765 lovelace: u64,
2766 tokens: &Assets,
2767) -> Result<Output> {
2768 if tokens.items.iter().any(|a| a.amount == 0) {
2769 bail!("A payment can't send none of a token");
2770 }
2771 let minimum = minimum_address_payment(params, to, tokens)?;
2772 if lovelace < minimum {
2773 bail!(
2774 "A payment to that address{} needs at least {} ADA",
2775 if tokens.is_empty() {
2776 ""
2777 } else {
2778 " with these tokens"
2779 },
2780 ada(minimum)
2781 );
2782 }
2783 let mut output = Output::new(to.clone(), lovelace);
2784 for asset in &tokens.items {
2785 output = output
2786 .add_asset(asset.policy_id, asset.token_name.clone(), asset.amount)
2787 .context("Failed To Add An Asset")?;
2788 }
2789 Ok(output)
2790}
2791
2792pub fn sweep(
2797 chain: &Chain,
2798 available: &[UtxoResponse],
2799 to: &Address,
2800 lovelace: u64,
2801 tokens: &Assets,
2802 change_owner: &Register,
2803 signer: Hash<28>,
2804) -> Result<ScriptSpend> {
2805 let payment = AddressPayment {
2806 to: to.clone(),
2807 lovelace,
2808 tokens: tokens.clone(),
2809 };
2810 sweep_many(chain, available, &[payment], change_owner, signer)
2811}
2812
2813#[derive(Debug, Clone)]
2815pub struct AddressPayment {
2816 pub to: Address,
2817 pub lovelace: u64,
2818 pub tokens: Assets,
2819}
2820
2821pub fn sweep_many(
2825 chain: &Chain,
2826 available: &[UtxoResponse],
2827 payments: &[AddressPayment],
2828 change_owner: &Register,
2829 signer: Hash<28>,
2830) -> Result<ScriptSpend> {
2831 sweep_many_apart(
2832 chain,
2833 available,
2834 &Histories::default(),
2835 payments,
2836 change_owner,
2837 signer,
2838 )
2839}
2840
2841pub fn sweep_many_apart(
2843 chain: &Chain,
2844 available: &[UtxoResponse],
2845 histories: &Histories,
2846 payments: &[AddressPayment],
2847 change_owner: &Register,
2848 signer: Hash<28>,
2849) -> Result<ScriptSpend> {
2850 if payments.is_empty() {
2851 bail!("A withdrawal pays at least one address");
2852 }
2853 let outputs: Vec<(Output, Assets)> = payments
2854 .iter()
2855 .map(|p| {
2856 Ok((
2857 address_output(chain, &p.to, p.lovelace, &p.tokens)?,
2858 p.tokens.clone(),
2859 ))
2860 })
2861 .collect::<Result<_>>()?;
2862 let needed = payments
2863 .iter()
2864 .try_fold(Assets::new(), |all, p| all.merge(p.tokens.clone()))?;
2865 select_script_inputs(
2866 available,
2867 &needed,
2868 paid_out(&outputs),
2869 histories,
2870 |inputs| {
2871 let spend = paying(chain, inputs, &outputs, change_owner, signer)?;
2872 spend.estimate()?;
2873 Ok(spend)
2874 },
2875 )
2876}
2877
2878pub fn sweep_from(
2880 chain: &Chain,
2881 inputs: &[UtxoResponse],
2882 to: &Address,
2883 lovelace: u64,
2884 tokens: &Assets,
2885 change_owner: &Register,
2886 signer: Hash<28>,
2887) -> Result<ScriptSpend> {
2888 let output = address_output(chain, to, lovelace, tokens)?;
2889 ScriptSpend::new(chain, inputs, change_owner, signer)?.output(output, tokens)
2890}
2891
2892pub fn sweep_all(
2895 chain: &Chain,
2896 inputs: &[UtxoResponse],
2897 to: &Address,
2898 change_owner: &Register,
2899 signer: Hash<28>,
2900) -> Result<ScriptSpend> {
2901 check_address(chain, to)?;
2902 Ok(ScriptSpend::new(chain, inputs, change_owner, signer)?.change_to(to))
2903}
2904
2905pub fn seedelf_in(chain: &Chain, utxo: &UtxoResponse) -> Result<Vec<u8>> {
2907 let policy = &chain.config.contract.seedelf_policy_id;
2908 let held: Vec<&seedelf_koios::koios::Asset> = utxo
2909 .asset_list
2910 .iter()
2911 .flatten()
2912 .filter(|a| &a.policy_id == policy)
2913 .collect();
2914 match held.as_slice() {
2915 [one] if one.quantity == "1" => {
2916 hex::decode(&one.asset_name).context("The Seedelf's name isn't hex")
2917 }
2918 _ => bail!(
2919 "UTxO {}#{} doesn't hold exactly one Seedelf",
2920 utxo.tx_hash,
2921 utxo.tx_index
2922 ),
2923 }
2924}
2925
2926pub fn remove(
2931 chain: &Chain,
2932 seedelf_utxo: &UtxoResponse,
2933 change_owner: &Register,
2934 signer: Hash<28>,
2935) -> Result<ScriptSpend> {
2936 let token_name = seedelf_in(chain, seedelf_utxo)?;
2937 ScriptSpend::new(
2938 chain,
2939 std::slice::from_ref(seedelf_utxo),
2940 change_owner,
2941 signer,
2942 )?
2943 .mint(token_name, -1, create_mint_redeemer(String::new())?)
2944}
2945
2946const ACCOUNT_COLLATERAL_MINIMUM: u64 = 2_000_000;
2966
2967#[derive(Clone)]
2969pub struct AccountMint {
2970 chain: Chain,
2971 inputs: Vec<UtxoResponse>,
2972 collateral: UtxoResponse,
2973 seedelf: Output,
2974 redeemer: Vec<u8>,
2975 change_addr: Address,
2976 staking: Staking,
2978 invalid_hereafter: Option<u64>,
2980 pub token_name: Vec<u8>,
2982 pub lovelace: u64,
2984}
2985
2986pub struct FinalAccountMint {
2988 pub tx: BuiltTransaction,
2991 pub fee: ScriptFee,
2992 pub change_lovelace: u64,
2994 pub change_tokens: Assets,
2995 pub change_outputs: usize,
2996}
2997
2998impl AccountMint {
2999 pub fn inputs(&self) -> &[UtxoResponse] {
3001 &self.inputs
3002 }
3003
3004 pub fn collateral(&self) -> &UtxoResponse {
3007 &self.collateral
3008 }
3009
3010 pub fn draft(&self) -> Result<BuiltTransaction> {
3013 let fee = self.estimate()?.fee.total;
3014 built_with(self.stage(fee, None)?, Patches::staking(&self.staking))
3015 }
3016
3017 pub fn finalize(&self, budgets: &Budgets) -> Result<FinalAccountMint> {
3020 self.settle(budgets)
3021 }
3022
3023 fn estimate(&self) -> Result<FinalAccountMint> {
3025 self.settle(&Budgets::guess(&[], 1))
3026 }
3027
3028 fn signers(&self) -> usize {
3031 self.inputs
3032 .iter()
3033 .chain(std::iter::once(&self.collateral))
3034 .map(|u| u.payment_cred.as_str())
3035 .collect::<BTreeSet<_>>()
3036 .len()
3037 }
3038
3039 fn settle(&self, budgets: &Budgets) -> Result<FinalAccountMint> {
3040 let budget = budgets
3041 .mint(0)
3042 .context("Ogmios measured no budget for the Seedelf policy")?;
3043 let compute = computation_fee(&self.chain.params, budget.mem, budget.steps);
3044 let script_reference = reference_script_fee(
3047 &self.chain.params,
3048 self.chain.config.contract.seedelf_contract_size
3049 + reference_script_bytes(&self.inputs)?,
3050 )?;
3051 let (fee, staged) = settle(
3052 self.signers(),
3053 Patches::staking(&self.staking),
3054 |size| even(linear_fee(&self.chain.params, size) + compute + script_reference),
3055 |fee| self.stage(fee, Some(budgets)),
3056 )?;
3057 let (change_lovelace, change_tokens) = self.change(fee)?;
3058 let change_outputs = staged.outputs.as_ref().map_or(0, Vec::len) - 1;
3059 Ok(FinalAccountMint {
3060 tx: built_with(staged, Patches::staking(&self.staking))?,
3061 fee: ScriptFee {
3062 size: fee - compute - script_reference,
3063 compute,
3064 script_reference,
3065 total: fee,
3066 },
3067 change_lovelace,
3068 change_tokens,
3069 change_outputs,
3070 })
3071 }
3072
3073 fn change(&self, fee: u64) -> Result<(u64, Assets)> {
3076 let (inputs, tokens) = assets_of(self.inputs.clone())?;
3077 let lovelace = self
3078 .staking
3079 .net(inputs)
3080 .and_then(|total| total.checked_sub(self.lovelace))
3081 .and_then(|rest| rest.checked_sub(fee))
3082 .ok_or(ACCOUNT_MINT_SHORT)?;
3083 Ok((lovelace, tokens))
3084 }
3085
3086 fn stage(&self, fee: u64, budgets: Option<&Budgets>) -> Result<StagingTransaction> {
3087 let Chain { params, config, .. } = &self.chain;
3088 let (change_lovelace, change_tokens) = self.change(fee)?;
3089 let change = change_outputs(
3090 params,
3091 &self.change_addr,
3092 change_lovelace,
3093 &change_tokens,
3094 ACCOUNT_MINT_SHORT,
3095 )?;
3096
3097 let collateral_addr = Address::from_bech32(&self.collateral.address)
3100 .map_err(|e| anyhow::anyhow!("The collateral UTxO's address is invalid: {e}"))?;
3101 let (collateral_lovelace, collateral_tokens) = assets_of(vec![self.collateral.clone()])?;
3102 let collateral_back = collateral_lovelace.saturating_sub(fee * 3 / 2);
3103 if collateral_back
3104 < address_minimum_lovelace_with_assets(
3105 params,
3106 &self.collateral.address,
3107 collateral_tokens.clone(),
3108 )?
3109 {
3110 bail!("The Cardano account's UTxOs are too small to put up as collateral");
3111 }
3112 let mut collateral_return = Output::new(collateral_addr, collateral_back);
3113 for asset in &collateral_tokens.items {
3114 collateral_return = collateral_return
3115 .add_asset(asset.policy_id, asset.token_name.clone(), asset.amount)
3116 .context("Failed To Add An Asset")?;
3117 }
3118
3119 let policy = policy_hash(config)?;
3120 let budget = budgets.and_then(|b| b.mint(0)).unwrap_or(DRAFT_BUDGET);
3121 let mut tx = StagingTransaction::new();
3122 for utxo in &self.inputs {
3123 tx = tx.input(input_of(utxo)?);
3124 }
3125 for output in std::iter::once(self.seedelf.clone()).chain(change) {
3126 tx = tx.output(output);
3127 }
3128 tx = expiring(tx, self.invalid_hereafter);
3129 Ok(tx
3130 .collateral_input(input_of(&self.collateral)?)
3131 .collateral_output(collateral_return)
3132 .fee(fee)
3133 .reference_input(reference_utxo(config.reference.seedelf_reference_utxo))
3134 .mint_asset(policy, self.token_name.clone(), 1)
3135 .context("Failed To Mint The Seedelf")?
3136 .add_mint_redeemer(
3137 policy,
3138 self.redeemer.clone(),
3139 Some(ExUnits {
3140 mem: budget.mem,
3141 steps: budget.steps,
3142 }),
3143 )
3144 .language_view(ScriptKind::PlutusV3, params.cost_model_v3.clone()))
3145 }
3146}
3147
3148#[allow(clippy::too_many_arguments)]
3167pub fn account_mint(
3168 chain: &Chain,
3169 available: &[UtxoResponse],
3170 collateral: Option<&UtxoResponse>,
3171 label: &str,
3172 seedelf: &Register,
3173 change_addr: &Address,
3174 staking: &Staking,
3175 invalid_hereafter: Option<u64>,
3176) -> Result<AccountMint> {
3177 if !is_payable(seedelf) {
3178 bail!("The Seedelf's register isn't made of valid points");
3179 }
3180 let pure_ada = |u: &UtxoResponse| u.asset_list.as_ref().is_none_or(|a| a.is_empty());
3181 let lovelace_of = |u: &UtxoResponse| u.value.parse::<u64>().unwrap_or(0);
3182 let same =
3183 |a: &UtxoResponse, b: &UtxoResponse| a.tx_hash == b.tx_hash && a.tx_index == b.tx_index;
3184
3185 let spendable: Vec<UtxoResponse> = available
3188 .iter()
3189 .filter(|u| collateral.is_none_or(|c| !same(u, c)))
3190 .filter(|u| reference_script_size(u).is_ok())
3191 .cloned()
3192 .collect();
3193 let (mut spendable, _) = fitting(&spendable, &Assets::new(), |_| false)?;
3194 if spendable.is_empty() {
3195 bail!("There is nothing in the Cardano account to pay for a Seedelf");
3196 }
3197 spendable.sort_by_key(|u| {
3199 (
3200 !pure_ada(u),
3201 u.reference_script.is_some(),
3202 std::cmp::Reverse(lovelace_of(u)),
3203 )
3204 });
3205
3206 let config = &chain.config;
3207 let policy = policy_hash(config)?;
3208 let lovelace = seedelf_minimum_lovelace(&chain.params)?;
3209 let wallet_addr = wallet_contract(chain.network_flag, config.contract.wallet_contract_hash);
3210 let redeemer = create_mint_redeemer(label.to_string())?;
3211
3212 let mut last_error = None;
3213 for k in 1..=spendable.len() {
3214 let inputs = &spendable[..k];
3215 let spent = |u: &UtxoResponse| inputs.iter().any(|i| same(i, u));
3216 let mut candidates: Vec<&UtxoResponse> = available.iter().collect();
3217 candidates.sort_by_key(|u| {
3218 (
3219 !pure_ada(u),
3220 lovelace_of(u) < ACCOUNT_COLLATERAL_MINIMUM,
3221 spent(u),
3222 lovelace_of(u) != COLLATERAL_LOVELACE,
3223 std::cmp::Reverse(lovelace_of(u)),
3224 )
3225 });
3226 let Some(collateral) = collateral.or(candidates.first().copied()) else {
3227 bail!("There is nothing in the Cardano account to pay for a Seedelf");
3228 };
3229
3230 let owned: Vec<Input> = inputs.iter().map(input_of).collect::<Result<_>>()?;
3231 let token_name = seedelf_token_name(label.to_string(), Some(&owned))?;
3232 let seedelf_output = Output::new(wallet_addr.clone(), lovelace)
3233 .set_inline_datum(seedelf.to_vec()?)
3234 .add_asset(policy, token_name.clone(), 1)
3235 .context("Failed To Add The Seedelf")?;
3236 let mint = AccountMint {
3237 chain: chain.clone(),
3238 inputs: inputs.to_vec(),
3239 collateral: collateral.clone(),
3240 seedelf: seedelf_output,
3241 redeemer: redeemer.clone(),
3242 change_addr: change_addr.clone(),
3243 staking: staking.clone(),
3244 invalid_hereafter,
3245 token_name,
3246 lovelace,
3247 };
3248 match mint.estimate() {
3249 Ok(_) => return Ok(mint),
3250 Err(e) if e.downcast_ref::<NotEnough>().is_some() => last_error = Some(e),
3251 Err(e) => return Err(e),
3252 }
3253 }
3254 Err(last_error.unwrap_or_else(|| ACCOUNT_MINT_SHORT.into()))
3255}