2024-09-14 01:38:31 -04:00
|
|
|
/*
|
2024-09-13 23:51:53 -04:00
|
|
|
async fn make_signable_transaction(
|
|
|
|
|
block_number: usize,
|
|
|
|
|
plan_id: &[u8; 32],
|
|
|
|
|
inputs: &[Output],
|
|
|
|
|
payments: &[Payment<Self>],
|
|
|
|
|
change: &Option<Address>,
|
|
|
|
|
calculating_fee: bool,
|
|
|
|
|
) -> Result<Option<MakeSignableTransactionResult>, NetworkError> {
|
|
|
|
|
for payment in payments {
|
|
|
|
|
assert_eq!(payment.balance.coin, Coin::Monero);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO2: Use an fee representative of several blocks, cached inside Self
|
|
|
|
|
let block_for_fee = self.get_block(block_number).await?;
|
|
|
|
|
let fee_rate = self.median_fee(&block_for_fee).await?;
|
|
|
|
|
|
|
|
|
|
// Determine the RCT proofs to make based off the hard fork
|
|
|
|
|
// TODO: Make a fn for this block which is duplicated with tests
|
|
|
|
|
let rct_type = match block_for_fee.header.hardfork_version {
|
|
|
|
|
14 => RctType::ClsagBulletproof,
|
|
|
|
|
15 | 16 => RctType::ClsagBulletproofPlus,
|
|
|
|
|
_ => panic!("Monero hard forked and the processor wasn't updated for it"),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut transcript =
|
|
|
|
|
RecommendedTranscript::new(b"Serai Processor Monero Transaction Transcript");
|
|
|
|
|
transcript.append_message(b"plan", plan_id);
|
|
|
|
|
|
|
|
|
|
// All signers need to select the same decoys
|
|
|
|
|
// All signers use the same height and a seeded RNG to make sure they do so.
|
|
|
|
|
let mut inputs_actual = Vec::with_capacity(inputs.len());
|
|
|
|
|
for input in inputs {
|
|
|
|
|
inputs_actual.push(
|
|
|
|
|
OutputWithDecoys::fingerprintable_deterministic_new(
|
|
|
|
|
&mut ChaCha20Rng::from_seed(transcript.rng_seed(b"decoys")),
|
|
|
|
|
&self.rpc,
|
|
|
|
|
// TODO: Have Decoys take RctType
|
|
|
|
|
match rct_type {
|
|
|
|
|
RctType::ClsagBulletproof => 11,
|
|
|
|
|
RctType::ClsagBulletproofPlus => 16,
|
|
|
|
|
_ => panic!("selecting decoys for an unsupported RctType"),
|
|
|
|
|
},
|
|
|
|
|
block_number + 1,
|
|
|
|
|
input.0.clone(),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(map_rpc_err)?,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Monero requires at least two outputs
|
|
|
|
|
// If we only have one output planned, add a dummy payment
|
|
|
|
|
let mut payments = payments.to_vec();
|
|
|
|
|
let outputs = payments.len() + usize::from(u8::from(change.is_some()));
|
|
|
|
|
if outputs == 0 {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
} else if outputs == 1 {
|
|
|
|
|
payments.push(Payment {
|
|
|
|
|
address: Address::new(
|
|
|
|
|
ViewPair::new(EdwardsPoint::generator().0, Zeroizing::new(Scalar::ONE.0))
|
|
|
|
|
.unwrap()
|
|
|
|
|
.legacy_address(MoneroNetwork::Mainnet),
|
|
|
|
|
)
|
|
|
|
|
.unwrap(),
|
|
|
|
|
balance: Balance { coin: Coin::Monero, amount: Amount(0) },
|
|
|
|
|
data: None,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let payments = payments
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|payment| (payment.address.into(), payment.balance.amount.0))
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
|
|
match MSignableTransaction::new(
|
|
|
|
|
rct_type,
|
|
|
|
|
// Use the plan ID as the outgoing view key
|
|
|
|
|
Zeroizing::new(*plan_id),
|
|
|
|
|
inputs_actual,
|
|
|
|
|
payments,
|
|
|
|
|
Change::fingerprintable(change.as_ref().map(|change| change.clone().into())),
|
|
|
|
|
vec![],
|
|
|
|
|
fee_rate,
|
|
|
|
|
) {
|
|
|
|
|
Ok(signable) => Ok(Some({
|
|
|
|
|
if calculating_fee {
|
|
|
|
|
MakeSignableTransactionResult::Fee(signable.necessary_fee())
|
|
|
|
|
} else {
|
|
|
|
|
MakeSignableTransactionResult::SignableTransaction(signable)
|
|
|
|
|
}
|
|
|
|
|
})),
|
|
|
|
|
Err(e) => match e {
|
|
|
|
|
SendError::UnsupportedRctType => {
|
|
|
|
|
panic!("trying to use an RctType unsupported by monero-wallet")
|
|
|
|
|
}
|
|
|
|
|
SendError::NoInputs |
|
|
|
|
|
SendError::InvalidDecoyQuantity |
|
|
|
|
|
SendError::NoOutputs |
|
|
|
|
|
SendError::TooManyOutputs |
|
|
|
|
|
SendError::NoChange |
|
|
|
|
|
SendError::TooMuchArbitraryData |
|
|
|
|
|
SendError::TooLargeTransaction |
|
|
|
|
|
SendError::WrongPrivateKey => {
|
|
|
|
|
panic!("created an invalid Monero transaction: {e}");
|
|
|
|
|
}
|
|
|
|
|
SendError::MultiplePaymentIds => {
|
|
|
|
|
panic!("multiple payment IDs despite not supporting integrated addresses");
|
|
|
|
|
}
|
|
|
|
|
SendError::NotEnoughFunds { inputs, outputs, necessary_fee } => {
|
|
|
|
|
log::debug!(
|
|
|
|
|
"Monero NotEnoughFunds. inputs: {:?}, outputs: {:?}, necessary_fee: {necessary_fee:?}",
|
|
|
|
|
inputs,
|
|
|
|
|
outputs
|
|
|
|
|
);
|
|
|
|
|
match necessary_fee {
|
|
|
|
|
Some(necessary_fee) => {
|
|
|
|
|
// If we're solely calculating the fee, return the fee this TX will cost
|
|
|
|
|
if calculating_fee {
|
|
|
|
|
Ok(Some(MakeSignableTransactionResult::Fee(necessary_fee)))
|
|
|
|
|
} else {
|
|
|
|
|
// If we're actually trying to make the TX, return None
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// We didn't have enough funds to even cover the outputs
|
|
|
|
|
None => {
|
|
|
|
|
// Ensure we're not misinterpreting this
|
|
|
|
|
assert!(outputs > inputs);
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
SendError::MaliciousSerialization | SendError::ClsagError(_) | SendError::FrostError(_) => {
|
|
|
|
|
panic!("supposedly unreachable (at this time) Monero error: {e}");
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-09-14 01:38:31 -04:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
use core::future::Future;
|
|
|
|
|
|
|
|
|
|
use ciphersuite::{Ciphersuite, Ed25519};
|
|
|
|
|
|
|
|
|
|
use monero_wallet::rpc::{FeeRate, RpcError};
|
2024-09-13 23:51:53 -04:00
|
|
|
|
2024-09-14 01:38:31 -04:00
|
|
|
use serai_client::{
|
|
|
|
|
primitives::{Coin, Amount},
|
|
|
|
|
networks::monero::Address,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
use primitives::{OutputType, ReceivedOutput, Payment};
|
|
|
|
|
use scanner::{KeyFor, AddressFor, OutputFor, BlockFor};
|
|
|
|
|
use utxo_scheduler::{PlannedTransaction, TransactionPlanner};
|
|
|
|
|
|
|
|
|
|
use monero_wallet::address::Network;
|
|
|
|
|
|
|
|
|
|
use crate::{
|
|
|
|
|
EXTERNAL_SUBADDRESS, BRANCH_SUBADDRESS, CHANGE_SUBADDRESS, FORWARDED_SUBADDRESS, view_pair,
|
|
|
|
|
output::Output,
|
|
|
|
|
transaction::{SignableTransaction, Eventuality},
|
|
|
|
|
rpc::Rpc,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fn address_from_serai_key(key: <Ed25519 as Ciphersuite>::G, kind: OutputType) -> Address {
|
|
|
|
|
view_pair(key)
|
|
|
|
|
.address(
|
|
|
|
|
Network::Mainnet,
|
|
|
|
|
Some(match kind {
|
|
|
|
|
OutputType::External => EXTERNAL_SUBADDRESS,
|
|
|
|
|
OutputType::Branch => BRANCH_SUBADDRESS,
|
|
|
|
|
OutputType::Change => CHANGE_SUBADDRESS,
|
|
|
|
|
OutputType::Forwarded => FORWARDED_SUBADDRESS,
|
|
|
|
|
}),
|
|
|
|
|
None,
|
|
|
|
|
)
|
|
|
|
|
.try_into()
|
|
|
|
|
.expect("created address which wasn't representable")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub(crate) struct Planner(pub(crate) Rpc);
|
|
|
|
|
impl TransactionPlanner<Rpc, ()> for Planner {
|
|
|
|
|
type EphemeralError = RpcError;
|
|
|
|
|
|
|
|
|
|
type FeeRate = FeeRate;
|
|
|
|
|
|
|
|
|
|
type SignableTransaction = SignableTransaction;
|
|
|
|
|
|
|
|
|
|
// wallet2 will not create a transaction larger than 100 KB, and Monero won't relay a transaction
|
|
|
|
|
// larger than 150 KB. This fits within the 100 KB mark to fit in and not poke the bear.
|
|
|
|
|
// Technically, it can be ~124, yet a small bit of buffer is appreciated
|
|
|
|
|
// TODO: Test creating a TX this big
|
|
|
|
|
const MAX_INPUTS: usize = 120;
|
|
|
|
|
const MAX_OUTPUTS: usize = 16;
|
|
|
|
|
|
|
|
|
|
fn fee_rate(block: &BlockFor<Rpc>, coin: Coin) -> Self::FeeRate {
|
|
|
|
|
assert_eq!(coin, Coin::Monero);
|
|
|
|
|
// TODO
|
|
|
|
|
todo!("TODO")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn branch_address(key: KeyFor<Rpc>) -> AddressFor<Rpc> {
|
|
|
|
|
address_from_serai_key(key, OutputType::Branch)
|
|
|
|
|
}
|
|
|
|
|
fn change_address(key: KeyFor<Rpc>) -> AddressFor<Rpc> {
|
|
|
|
|
address_from_serai_key(key, OutputType::Change)
|
|
|
|
|
}
|
|
|
|
|
fn forwarding_address(key: KeyFor<Rpc>) -> AddressFor<Rpc> {
|
|
|
|
|
address_from_serai_key(key, OutputType::Forwarded)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn calculate_fee(
|
|
|
|
|
fee_rate: Self::FeeRate,
|
|
|
|
|
inputs: Vec<OutputFor<Rpc>>,
|
|
|
|
|
payments: Vec<Payment<AddressFor<Rpc>>>,
|
|
|
|
|
change: Option<KeyFor<Rpc>>,
|
|
|
|
|
) -> Amount {
|
|
|
|
|
todo!("TODO")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn plan(
|
|
|
|
|
&self,
|
|
|
|
|
fee_rate: Self::FeeRate,
|
|
|
|
|
inputs: Vec<OutputFor<Rpc>>,
|
|
|
|
|
payments: Vec<Payment<AddressFor<Rpc>>>,
|
|
|
|
|
change: Option<KeyFor<Rpc>>,
|
|
|
|
|
) -> impl Send
|
|
|
|
|
+ Future<Output = Result<PlannedTransaction<Rpc, Self::SignableTransaction, ()>, RpcError>>
|
|
|
|
|
{
|
|
|
|
|
async move { todo!("TODO") }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) type Scheduler = utxo_standard_scheduler::Scheduler<Rpc, Planner>;
|
2024-09-13 23:51:53 -04:00
|
|
|
|
|
|
|
|
/*
|
2024-09-14 01:38:31 -04:00
|
|
|
use ciphersuite::{Ciphersuite, Ed25519};
|
2024-09-12 18:40:10 -04:00
|
|
|
|
|
|
|
|
use bitcoin_serai::{
|
|
|
|
|
bitcoin::ScriptBuf,
|
|
|
|
|
wallet::{TransactionError, SignableTransaction as BSignableTransaction, p2tr_script_buf},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
use serai_client::{
|
|
|
|
|
primitives::{Coin, Amount},
|
|
|
|
|
networks::bitcoin::Address,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
use serai_db::Db;
|
|
|
|
|
use primitives::{OutputType, ReceivedOutput, Payment};
|
|
|
|
|
use scanner::{KeyFor, AddressFor, OutputFor, BlockFor};
|
|
|
|
|
use utxo_scheduler::{PlannedTransaction, TransactionPlanner};
|
|
|
|
|
|
|
|
|
|
use crate::{
|
|
|
|
|
scan::{offsets_for_key, scanner},
|
|
|
|
|
output::Output,
|
|
|
|
|
transaction::{SignableTransaction, Eventuality},
|
|
|
|
|
rpc::Rpc,
|
|
|
|
|
};
|
|
|
|
|
|
2024-09-14 01:38:31 -04:00
|
|
|
fn address_from_serai_key(key: <Ed25519 as Ciphersuite>::G, kind: OutputType) -> Address {
|
|
|
|
|
let offset = <Ed25519 as Ciphersuite>::G::GENERATOR * offsets_for_key(key)[&kind];
|
2024-09-12 18:40:10 -04:00
|
|
|
Address::new(
|
|
|
|
|
p2tr_script_buf(key + offset)
|
|
|
|
|
.expect("creating address from Serai key which wasn't properly tweaked"),
|
|
|
|
|
)
|
|
|
|
|
.expect("couldn't create Serai-representable address for P2TR script")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn signable_transaction<D: Db>(
|
|
|
|
|
fee_per_vbyte: u64,
|
2024-09-14 01:38:31 -04:00
|
|
|
inputs: Vec<OutputFor<Rpc>>,
|
|
|
|
|
payments: Vec<Payment<AddressFor<Rpc>>>,
|
|
|
|
|
change: Option<KeyFor<Rpc>>,
|
2024-09-12 18:40:10 -04:00
|
|
|
) -> Result<(SignableTransaction, BSignableTransaction), TransactionError> {
|
|
|
|
|
assert!(
|
|
|
|
|
inputs.len() <
|
2024-09-14 01:38:31 -04:00
|
|
|
<Planner as TransactionPlanner<Rpc, ()>>::MAX_INPUTS
|
2024-09-12 18:40:10 -04:00
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
(payments.len() + usize::from(u8::from(change.is_some()))) <
|
2024-09-14 01:38:31 -04:00
|
|
|
<Planner as TransactionPlanner<Rpc, ()>>::MAX_OUTPUTS
|
2024-09-12 18:40:10 -04:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let inputs = inputs.into_iter().map(|input| input.output).collect::<Vec<_>>();
|
|
|
|
|
|
|
|
|
|
let mut payments = payments
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|payment| {
|
|
|
|
|
(payment.address().clone(), {
|
|
|
|
|
let balance = payment.balance();
|
2024-09-14 01:38:31 -04:00
|
|
|
assert_eq!(balance.coin, Coin::Monero);
|
2024-09-12 18:40:10 -04:00
|
|
|
balance.amount.0
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
/*
|
|
|
|
|
Push a payment to a key with a known private key which anyone can spend. If this transaction
|
|
|
|
|
gets stuck, this lets anyone create a child transaction spending this output, raising the fee,
|
|
|
|
|
getting the transaction unstuck (via CPFP).
|
|
|
|
|
*/
|
|
|
|
|
payments.push((
|
|
|
|
|
// The generator is even so this is valid
|
2024-09-14 01:38:31 -04:00
|
|
|
Address::new(p2tr_script_buf(<Ed25519 as Ciphersuite>::G::GENERATOR).unwrap()).unwrap(),
|
2024-09-12 18:40:10 -04:00
|
|
|
// This uses the minimum output value allowed, as defined as a constant in bitcoin-serai
|
|
|
|
|
// TODO: Add a test for this comparing to bitcoin's `minimal_non_dust`
|
|
|
|
|
bitcoin_serai::wallet::DUST,
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
let change = change
|
2024-09-14 01:38:31 -04:00
|
|
|
.map(<Planner as TransactionPlanner<Rpc, ()>>::change_address);
|
2024-09-12 18:40:10 -04:00
|
|
|
|
|
|
|
|
BSignableTransaction::new(
|
|
|
|
|
inputs.clone(),
|
|
|
|
|
&payments
|
|
|
|
|
.iter()
|
|
|
|
|
.cloned()
|
|
|
|
|
.map(|(address, amount)| (ScriptBuf::from(address), amount))
|
|
|
|
|
.collect::<Vec<_>>(),
|
|
|
|
|
change.clone().map(ScriptBuf::from),
|
|
|
|
|
None,
|
|
|
|
|
fee_per_vbyte,
|
|
|
|
|
)
|
|
|
|
|
.map(|bst| (SignableTransaction { inputs, payments, change, fee_per_vbyte }, bst))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) struct Planner;
|
2024-09-13 00:48:57 -04:00
|
|
|
impl TransactionPlanner<Rpc, ()> for Planner {
|
2024-09-14 01:38:31 -04:00
|
|
|
type EphemeralError = RpcError;
|
|
|
|
|
|
2024-09-12 18:40:10 -04:00
|
|
|
type FeeRate = u64;
|
|
|
|
|
|
|
|
|
|
type SignableTransaction = SignableTransaction;
|
|
|
|
|
|
|
|
|
|
/*
|
2024-09-14 01:38:31 -04:00
|
|
|
Monero has a max weight of 400,000 (MAX_STANDARD_TX_WEIGHT).
|
2024-09-12 18:40:10 -04:00
|
|
|
|
|
|
|
|
A non-SegWit TX will have 4 weight units per byte, leaving a max size of 100,000 bytes. While
|
|
|
|
|
our inputs are entirely SegWit, such fine tuning is not necessary and could create issues in
|
|
|
|
|
the future (if the size decreases or we misevaluate it). It also offers a minimal amount of
|
|
|
|
|
benefit when we are able to logarithmically accumulate inputs/fulfill payments.
|
|
|
|
|
|
|
|
|
|
For 128-byte inputs (36-byte output specification, 64-byte signature, whatever overhead) and
|
|
|
|
|
64-byte outputs (40-byte script, 8-byte amount, whatever overhead), they together take up 192
|
|
|
|
|
bytes.
|
|
|
|
|
|
|
|
|
|
100,000 / 192 = 520
|
|
|
|
|
520 * 192 leaves 160 bytes of overhead for the transaction structure itself.
|
|
|
|
|
*/
|
|
|
|
|
const MAX_INPUTS: usize = 520;
|
|
|
|
|
// We always reserve one output to create an anyone-can-spend output enabling anyone to use CPFP
|
|
|
|
|
// to unstick any transactions which had too low of a fee.
|
|
|
|
|
const MAX_OUTPUTS: usize = 519;
|
|
|
|
|
|
2024-09-14 01:38:31 -04:00
|
|
|
fn fee_rate(block: &BlockFor<Rpc>, coin: Coin) -> Self::FeeRate {
|
|
|
|
|
assert_eq!(coin, Coin::Monero);
|
2024-09-12 18:40:10 -04:00
|
|
|
// TODO
|
|
|
|
|
1
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-14 01:38:31 -04:00
|
|
|
fn branch_address(key: KeyFor<Rpc>) -> AddressFor<Rpc> {
|
2024-09-12 18:40:10 -04:00
|
|
|
address_from_serai_key(key, OutputType::Branch)
|
|
|
|
|
}
|
2024-09-14 01:38:31 -04:00
|
|
|
fn change_address(key: KeyFor<Rpc>) -> AddressFor<Rpc> {
|
2024-09-12 18:40:10 -04:00
|
|
|
address_from_serai_key(key, OutputType::Change)
|
|
|
|
|
}
|
2024-09-14 01:38:31 -04:00
|
|
|
fn forwarding_address(key: KeyFor<Rpc>) -> AddressFor<Rpc> {
|
2024-09-12 18:40:10 -04:00
|
|
|
address_from_serai_key(key, OutputType::Forwarded)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn calculate_fee(
|
|
|
|
|
fee_rate: Self::FeeRate,
|
2024-09-14 01:38:31 -04:00
|
|
|
inputs: Vec<OutputFor<Rpc>>,
|
|
|
|
|
payments: Vec<Payment<AddressFor<Rpc>>>,
|
|
|
|
|
change: Option<KeyFor<Rpc>>,
|
2024-09-12 18:40:10 -04:00
|
|
|
) -> Amount {
|
|
|
|
|
match signable_transaction::<D>(fee_rate, inputs, payments, change) {
|
|
|
|
|
Ok(tx) => Amount(tx.1.needed_fee()),
|
|
|
|
|
Err(
|
|
|
|
|
TransactionError::NoInputs | TransactionError::NoOutputs | TransactionError::DustPayment,
|
|
|
|
|
) => panic!("malformed arguments to calculate_fee"),
|
|
|
|
|
// No data, we have a minimum fee rate, we checked the amount of inputs/outputs
|
|
|
|
|
Err(
|
|
|
|
|
TransactionError::TooMuchData |
|
|
|
|
|
TransactionError::TooLowFee |
|
|
|
|
|
TransactionError::TooLargeTransaction,
|
|
|
|
|
) => unreachable!(),
|
|
|
|
|
Err(TransactionError::NotEnoughFunds { fee, .. }) => Amount(fee),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn plan(
|
|
|
|
|
fee_rate: Self::FeeRate,
|
2024-09-14 01:38:31 -04:00
|
|
|
inputs: Vec<OutputFor<Rpc>>,
|
|
|
|
|
payments: Vec<Payment<AddressFor<Rpc>>>,
|
|
|
|
|
change: Option<KeyFor<Rpc>>,
|
|
|
|
|
) -> PlannedTransaction<Rpc, Self::SignableTransaction, ()> {
|
2024-09-12 18:40:10 -04:00
|
|
|
let key = inputs.first().unwrap().key();
|
|
|
|
|
for input in &inputs {
|
|
|
|
|
assert_eq!(key, input.key());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let singular_spent_output = (inputs.len() == 1).then(|| inputs[0].id());
|
|
|
|
|
match signable_transaction::<D>(fee_rate, inputs.clone(), payments, change) {
|
|
|
|
|
Ok(tx) => PlannedTransaction {
|
|
|
|
|
signable: tx.0,
|
|
|
|
|
eventuality: Eventuality { txid: tx.1.txid(), singular_spent_output },
|
2024-09-13 00:48:57 -04:00
|
|
|
auxilliary: (),
|
2024-09-12 18:40:10 -04:00
|
|
|
},
|
|
|
|
|
Err(
|
|
|
|
|
TransactionError::NoInputs | TransactionError::NoOutputs | TransactionError::DustPayment,
|
|
|
|
|
) => panic!("malformed arguments to plan"),
|
|
|
|
|
// No data, we have a minimum fee rate, we checked the amount of inputs/outputs
|
|
|
|
|
Err(
|
|
|
|
|
TransactionError::TooMuchData |
|
|
|
|
|
TransactionError::TooLowFee |
|
|
|
|
|
TransactionError::TooLargeTransaction,
|
|
|
|
|
) => unreachable!(),
|
|
|
|
|
Err(TransactionError::NotEnoughFunds { .. }) => {
|
|
|
|
|
panic!("plan called for a transaction without enough funds")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-13 00:48:57 -04:00
|
|
|
pub(crate) type Scheduler = utxo_standard_scheduler::Scheduler<Rpc, Planner>;
|
2024-09-13 23:51:53 -04:00
|
|
|
*/
|