diff --git a/coins/monero/src/wallet/mod.rs b/coins/monero/src/wallet/mod.rs index 6a17588d..26d7919d 100644 --- a/coins/monero/src/wallet/mod.rs +++ b/coins/monero/src/wallet/mod.rs @@ -24,7 +24,7 @@ pub(crate) mod decoys; pub(crate) use decoys::Decoys; mod send; -pub use send::{Fee, TransactionError, SignableTransaction}; +pub use send::{Fee, TransactionError, SignableTransaction, SignableTransactionBuilder}; #[cfg(feature = "multisig")] pub use send::TransactionMachine; diff --git a/coins/monero/src/wallet/send/builder.rs b/coins/monero/src/wallet/send/builder.rs new file mode 100644 index 00000000..140c6089 --- /dev/null +++ b/coins/monero/src/wallet/send/builder.rs @@ -0,0 +1,119 @@ +use std::sync::{Arc, RwLock}; + +use zeroize::{Zeroize, ZeroizeOnDrop}; + +use crate::{ + Protocol, + wallet::{address::MoneroAddress, Fee, SpendableOutput, SignableTransaction, TransactionError}, +}; + +#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)] +struct SignableTransactionBuilderInternal { + protocol: Protocol, + fee: Fee, + + inputs: Vec, + payments: Vec<(MoneroAddress, u64)>, + change_address: Option, + data: Option>, +} + +impl SignableTransactionBuilderInternal { + // Takes in the change address so users don't miss that they have to manually set one + // If they don't, all leftover funds will become part of the fee + fn new(protocol: Protocol, fee: Fee, change_address: Option) -> Self { + Self { protocol, fee, inputs: vec![], payments: vec![], change_address, data: None } + } + + fn add_input(&mut self, input: SpendableOutput) { + self.inputs.push(input); + } + fn add_inputs(&mut self, inputs: &[SpendableOutput]) { + self.inputs.extend(inputs.iter().cloned()); + } + + fn add_payment(&mut self, dest: MoneroAddress, amount: u64) { + self.payments.push((dest, amount)); + } + fn add_payments(&mut self, payments: &[(MoneroAddress, u64)]) { + self.payments.extend(payments); + } + + fn set_data(&mut self, data: Vec) { + self.data = Some(data); + } +} + +/// A Transaction Builder for Monero transactions. +/// All methods provided will modify self while also returning a shallow copy, enabling efficient +/// chaining with a clean API. +/// In order to fork the builder at some point, clone will still return a deep copy. +#[derive(Debug)] +pub struct SignableTransactionBuilder(Arc>); +impl Clone for SignableTransactionBuilder { + fn clone(&self) -> Self { + Self(Arc::new(RwLock::new((*self.0.read().unwrap()).clone()))) + } +} + +impl PartialEq for SignableTransactionBuilder { + fn eq(&self, other: &Self) -> bool { + *self.0.read().unwrap() == *other.0.read().unwrap() + } +} +impl Eq for SignableTransactionBuilder {} + +impl Zeroize for SignableTransactionBuilder { + fn zeroize(&mut self) { + self.0.write().unwrap().zeroize() + } +} + +impl SignableTransactionBuilder { + fn shallow_copy(&self) -> Self { + Self(self.0.clone()) + } + + pub fn new(protocol: Protocol, fee: Fee, change_address: Option) -> Self { + Self(Arc::new(RwLock::new(SignableTransactionBuilderInternal::new( + protocol, + fee, + change_address, + )))) + } + + pub fn add_input(&mut self, input: SpendableOutput) -> Self { + self.0.write().unwrap().add_input(input); + self.shallow_copy() + } + pub fn add_inputs(&mut self, inputs: &[SpendableOutput]) -> Self { + self.0.write().unwrap().add_inputs(inputs); + self.shallow_copy() + } + + pub fn add_payment(&mut self, dest: MoneroAddress, amount: u64) -> Self { + self.0.write().unwrap().add_payment(dest, amount); + self.shallow_copy() + } + pub fn add_payments(&mut self, payments: &[(MoneroAddress, u64)]) -> Self { + self.0.write().unwrap().add_payments(payments); + self.shallow_copy() + } + + pub fn set_data(&mut self, data: Vec) -> Self { + self.0.write().unwrap().set_data(data); + self.shallow_copy() + } + + pub fn build(self) -> Result { + let read = self.0.read().unwrap(); + SignableTransaction::new( + read.protocol, + read.inputs.clone(), + read.payments.clone(), + read.change_address, + read.data.clone(), + read.fee, + ) + } +} diff --git a/coins/monero/src/wallet/send/mod.rs b/coins/monero/src/wallet/send/mod.rs index 903d8c92..85428240 100644 --- a/coins/monero/src/wallet/send/mod.rs +++ b/coins/monero/src/wallet/send/mod.rs @@ -28,6 +28,9 @@ use crate::{ }, }; +mod builder; +pub use builder::SignableTransactionBuilder; + #[cfg(feature = "multisig")] mod multisig; #[cfg(feature = "multisig")] @@ -156,7 +159,7 @@ async fn prepare_inputs( } /// Fee struct, defined as a per-unit cost and a mask for rounding purposes. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] pub struct Fee { pub per_weight: u64, pub mask: u64, diff --git a/coins/monero/tests/send.rs b/coins/monero/tests/send.rs index 95aff595..f3940107 100644 --- a/coins/monero/tests/send.rs +++ b/coins/monero/tests/send.rs @@ -24,7 +24,10 @@ use frost::{ use monero_serai::{ random_scalar, - wallet::{address::Network, ViewPair, Scanner, SpendableOutput, SignableTransaction}, + wallet::{ + address::Network, ViewPair, Scanner, SpendableOutput, SignableTransaction, + SignableTransactionBuilder, + }, }; mod rpc; @@ -192,3 +195,38 @@ async_sequential! { send_core(1, true).await; } } + +async_sequential! { + async fn builder() { + let rpc = rpc().await; + + // Generate an address + let spend = Zeroizing::new(random_scalar(&mut OsRng)); + let view = random_scalar(&mut OsRng); + let spend_pub = spend.deref() * &ED25519_BASEPOINT_TABLE; + + let view_pair = ViewPair::new(spend_pub, view); + let mut scanner = Scanner::from_view(view_pair, Network::Mainnet, Some(HashSet::new())); + let addr = scanner.address(); + + let fee = rpc.get_fee().await.unwrap(); + + let start = rpc.get_height().await.unwrap(); + for _ in 0 .. 7 { + mine_block(&rpc, &addr.to_string()).await.unwrap(); + } + + let coinbase = rpc.get_block_transactions(start).await.unwrap().swap_remove(0); + let output = scanner.scan_transaction(&coinbase).ignore_timelock().swap_remove(0); + rpc.publish_transaction( + &SignableTransactionBuilder::new(rpc.get_protocol().await.unwrap(), fee, Some(addr)) + .add_input(SpendableOutput::from(&rpc, output).await.unwrap()) + .add_payment(addr, 0) + .build() + .unwrap() + .sign(&mut OsRng, &rpc, &spend) + .await + .unwrap() + ).await.unwrap(); + } +}