diff --git a/processor/src/coins/monero.rs b/processor/src/coins/monero.rs index 6e9140f9..834ab63d 100644 --- a/processor/src/coins/monero.rs +++ b/processor/src/coins/monero.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use async_trait::async_trait; use rand_core::{RngCore, CryptoRng}; @@ -16,6 +18,7 @@ use monero_serai::{ use crate::{Output as OutputTrait, CoinError, Coin, view_key}; +#[derive(Clone)] pub struct Output(SpendableOutput); impl OutputTrait for Output { // While we could use (tx, o), using the key ensures we won't be susceptible to the burning bug. @@ -104,9 +107,9 @@ impl Coin for Monero { .collect() } - async fn prepare_send( + async fn prepare_send( &self, - _keys: MultisigKeys, + _keys: Arc>, _label: Vec, _height: usize, _inputs: Vec, diff --git a/processor/src/lib.rs b/processor/src/lib.rs index e87ef456..fae4bfad 100644 --- a/processor/src/lib.rs +++ b/processor/src/lib.rs @@ -1,4 +1,4 @@ -use std::marker::Send; +use std::{marker::Send, sync::Arc}; use async_trait::async_trait; use thiserror::Error; @@ -14,7 +14,7 @@ mod wallet; #[cfg(test)] mod tests; -pub trait Output: Sized { +pub trait Output: Sized + Clone { type Id; fn id(&self) -> Self::Id; @@ -53,9 +53,9 @@ pub trait Coin { key: ::G ) -> Vec; - async fn prepare_send( + async fn prepare_send( &self, - keys: MultisigKeys, + keys: Arc>, label: Vec, height: usize, inputs: Vec, diff --git a/processor/src/wallet.rs b/processor/src/wallet.rs index d55c3a11..d1ab088d 100644 --- a/processor/src/wallet.rs +++ b/processor/src/wallet.rs @@ -1,9 +1,9 @@ -use std::collections::HashMap; +use std::{sync::Arc, collections::HashMap}; use transcript::{Transcript, DigestTranscript}; use frost::{Curve, MultisigKeys}; -use crate::{CoinError, Coin}; +use crate::{CoinError, Output, Coin}; pub struct WalletKeys { keys: MultisigKeys, @@ -43,9 +43,8 @@ pub struct CoinDb { pub struct Wallet { db: CoinDb, coin: C, - keys: Vec>, - pending: Vec<(usize, MultisigKeys)>, - outputs: Vec + keys: Vec<(Arc>, Vec)>, + pending: Vec<(usize, MultisigKeys)> } impl Wallet { @@ -59,8 +58,7 @@ impl Wallet { coin, keys: vec![], - pending: vec![], - outputs: vec![] + pending: vec![] } } @@ -80,21 +78,107 @@ impl Wallet { pub async fn poll(&mut self) -> Result<(), CoinError> { let confirmed_height = self.coin.get_height().await? - C::confirmations(); - for h in self.scanned_height() .. confirmed_height { - let mut k = 0; - while k < self.pending.len() { - if h == self.pending[k].0 { - self.keys.push(self.pending.swap_remove(k).1); - } else { - k += 1; + for height in self.scanned_height() .. confirmed_height { + // If any keys activated at this height, shift them over + { + let mut k = 0; + while k < self.pending.len() { + if height >= self.pending[k].0 { + self.keys.push((Arc::new(self.pending.swap_remove(k).1), vec![])); + } else { + k += 1; + } } } - let block = self.coin.get_block(h).await?; - for keys in &self.keys { - let outputs = self.coin.get_outputs(&block, keys.group_key()); + let block = self.coin.get_block(height).await?; + for (keys, outputs) in self.keys.iter_mut() { + outputs.extend( + self.coin.get_outputs(&block, keys.group_key()).await.iter().cloned().filter( + |_output| true // !self.db.handled.contains_key(output.id()) // TODO + ) + ); } } Ok(()) } + + pub async fn prepare_sends( + &mut self, + canonical: usize, + payments: Vec<(C::Address, u64)> + ) -> Result, CoinError> { + if payments.len() == 0 { + return Ok(vec![]); + } + + let acknowledged_height = self.acknowledged_height(canonical); + + // TODO: Log schedule outputs when max_outputs is low + // Payments is the first set of TXs in the schedule + // As each payment re-appears, let mut payments = schedule[payment] where the only input is + // the source payment + // let (mut payments, schedule) = payments; + let mut payments = payments; + payments.sort_by(|a, b| a.1.cmp(&b.1).reverse()); + + let mut txs = vec![]; + for (keys, outputs) in self.keys.iter_mut() { + // Select the highest value outputs to minimize the amount of inputs needed + outputs.sort_by(|a, b| a.amount().cmp(&b.amount()).reverse()); + + while outputs.len() != 0 { + // Select the maximum amount of outputs possible + let mut inputs = &outputs[0 .. C::max_inputs().min(outputs.len())]; + + // Calculate their sum value, minus the fee needed to spend them + let mut sum = inputs.iter().map(|input| input.amount()).sum::(); + // sum -= C::MAX_FEE; // TODO + + // Grab the payments this will successfully fund + let mut these_payments = vec![]; + for payment in &payments { + if sum > payment.1 { + these_payments.push(payment); + sum -= payment.1; + } + // Doesn't break in this else case as a smaller payment may still fit + } + + // Move to the next set of keys if none of these outputs remain significant + if these_payments.len() == 0 { + break; + } + + // Drop any uneeded outputs + while sum > inputs[inputs.len() - 1].amount() { + sum -= inputs[inputs.len() - 1].amount(); + inputs = &inputs[.. (inputs.len() - 1)]; + } + + // We now have a minimal effective outputs/payments set + // Take ownership while removing these candidates from the provided list + let inputs = outputs.drain(.. inputs.len()).collect(); + let payments = payments.drain(.. these_payments.len()).collect::>(); + + let tx = self.coin.prepare_send( + keys.clone(), + format!( + "Serai Processor Wallet Send (height {}, index {})", + canonical, + txs.len() + ).as_bytes().to_vec(), + acknowledged_height, + inputs, + &payments + ).await?; + // self.db.save_tx(tx) // TODO + txs.push(tx); + } + } + + // TODO: Remaining payments? + + Ok(txs) + } }