diff --git a/src/silentpayments.rs b/src/silentpayments.rs index eebdde7..b5ed3ff 100644 --- a/src/silentpayments.rs +++ b/src/silentpayments.rs @@ -1,12 +1,146 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use anyhow::{Error, Result}; use serde::{Deserialize, Serialize}; +use sp_client::bitcoin::absolute::Height; +use sp_client::bitcoin::hex::DisplayHex; +use sp_client::bitcoin::secp256k1::{PublicKey, SecretKey}; use tsify::Tsify; use rand::{thread_rng, Rng}; -use sp_client::bitcoin::{Amount, OutPoint, Transaction}; -use sp_client::{OwnedOutput, Recipient, SilentPaymentUnsignedTransaction, SpClient}; +use sp_client::bitcoin::{Amount, OutPoint, Transaction, XOnlyPublicKey, TxOut}; +use sp_client::{FeeRate, OutputSpendStatus, OwnedOutput, Recipient, SilentPaymentUnsignedTransaction, SpClient}; +use sp_client::silentpayments::utils::receiving::calculate_ecdh_shared_secret; + +#[derive(Debug, Default, Deserialize, Serialize, Clone)] +pub struct SpWallet { + sp_client: SpClient, + outputs: HashMap, + birthday: u32, + last_scan: u32, +} + +impl SpWallet { + pub fn new(sp_client: SpClient) -> Self { + Self { + sp_client, + ..Default::default() + } + } + + pub fn get_sp_client(&self) -> &SpClient { + &self.sp_client + } + + pub fn get_outputs(&self) -> &HashMap { + &self.outputs + } + + pub fn get_mut_outputs(&mut self) -> &mut HashMap { + &mut self.outputs + } + + pub fn get_unspent_outputs(&self) -> HashMap { + self.outputs.iter() + .filter(|(_, output)| output.spend_status == OutputSpendStatus::Unspent) + .map(|(outpoint, output)| (*outpoint, output.clone())) + .collect() + } + + pub fn get_balance(&self) -> Amount { + self.outputs.values() + .filter(|output| output.spend_status == OutputSpendStatus::Unspent) + .fold(Amount::ZERO, |acc, x| acc + x.amount) + } + + pub fn get_birthday(&self) -> u32 { + self.birthday + } + + pub fn set_birthday(&mut self, birthday: u32) { + self.birthday = birthday; + } + + pub fn get_last_scan(&self) -> u32 { + self.last_scan + } + + pub fn set_last_scan(&mut self, last_scan: u32) { + self.last_scan = last_scan; + } + + pub fn update_with_transaction(&mut self, tx: &Transaction, public_tweak: &PublicKey, height: u32) -> Result> { + let receiver = &self.get_sp_client().sp_receiver; + let p2tr_outs: Vec<(usize, &TxOut)> = tx + .output + .iter() + .enumerate() + .filter(|(_, o)| o.script_pubkey.is_p2tr()) + .collect(); + + if p2tr_outs.is_empty() { + return Err(Error::msg("No taproot outputs")) + }; // That should never happen since we have a tweak_data, but anyway + + // Now we can just run sp_receiver on all the p2tr outputs + let xonlykeys: Result> = p2tr_outs + .iter() + .map(|(_, o)| { + XOnlyPublicKey::from_slice(&o.script_pubkey.as_bytes()[2..]).map_err(Error::new) + }) + .collect(); + + let tweak_data = calculate_ecdh_shared_secret(public_tweak, &self.sp_client.get_scan_key()); + let ours = receiver.scan_transaction(&tweak_data, xonlykeys?)?; + + let txid = tx.txid(); + let height = Height::from_consensus(height)?; + let mut res = HashMap::new(); + for (label, map) in ours { + res.extend(p2tr_outs.iter().filter_map(|(i, o)| { + match XOnlyPublicKey::from_slice(&o.script_pubkey.as_bytes()[2..]) { + Ok(key) => { + if let Some(scalar) = map.get(&key) { + match SecretKey::from_slice(&scalar.to_be_bytes()) { + Ok(tweak) => { + let outpoint = OutPoint { + txid, + vout: *i as u32, + }; + let label_str: Option; + if let Some(l) = &label { + label_str = + Some(l.as_inner().to_be_bytes().to_lower_hex_string()); + } else { + label_str = None; + } + return Some(( + outpoint, + OwnedOutput { + blockheight: height, + tweak: tweak.secret_bytes(), + amount: o.value, + script: o.script_pubkey.clone(), + label: label_str, + spend_status: OutputSpendStatus::Unspent, + }, + )); + } + Err(_) => { + return None; + } + } + } + None + } + Err(_) => None, + } + })); + } + self.get_mut_outputs().extend(res.clone()); + Ok(res) + } +} #[derive(Debug, Serialize, Deserialize, PartialEq, Tsify)] #[tsify(from_wasm_abi)]