Add SpWallet
This commit is contained in:
parent
db3db3a497
commit
8e7bc05e76
@ -1,12 +1,146 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use anyhow::{Error, Result};
|
use anyhow::{Error, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
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 tsify::Tsify;
|
||||||
|
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
use sp_client::bitcoin::{Amount, OutPoint, Transaction};
|
use sp_client::bitcoin::{Amount, OutPoint, Transaction, XOnlyPublicKey, TxOut};
|
||||||
use sp_client::{OwnedOutput, Recipient, SilentPaymentUnsignedTransaction, SpClient};
|
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<OutPoint, OwnedOutput>,
|
||||||
|
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<OutPoint, OwnedOutput> {
|
||||||
|
&self.outputs
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mut_outputs(&mut self) -> &mut HashMap<OutPoint, OwnedOutput> {
|
||||||
|
&mut self.outputs
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_unspent_outputs(&self) -> HashMap<OutPoint, OwnedOutput> {
|
||||||
|
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<HashMap<OutPoint, OwnedOutput>> {
|
||||||
|
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<Vec<XOnlyPublicKey>> = 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<String>;
|
||||||
|
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)]
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Tsify)]
|
||||||
#[tsify(from_wasm_abi)]
|
#[tsify(from_wasm_abi)]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user