forked from hush/silentdragonlite-cli
Browse Source
* Dependency to note-spending-v6 branch * Spend funds * Add outgoing memos * cleanup * Incoming memo support * Refactor to LightClient * Abstract grpc client creation * TLS example * Add address command * Run blocking IO * Store memos in wallet Tx * Fetch the full txns only for new transactions * Comments * Filter first * More comments * Add commandscheckpoints
adityapk00
5 years ago
committed by
GitHub
7 changed files with 1087 additions and 644 deletions
@ -0,0 +1,80 @@ |
|||
use std::collections::HashMap; |
|||
|
|||
use crate::LightClient; |
|||
|
|||
pub trait Command { |
|||
fn help(&self); |
|||
|
|||
fn short_help(&self) -> String; |
|||
|
|||
fn exec(&self, args: &[String], lightclient: &LightClient); |
|||
} |
|||
|
|||
struct SyncCommand {} |
|||
|
|||
impl Command for SyncCommand { |
|||
fn help(&self) { |
|||
println!("Type sync for syncing"); |
|||
} |
|||
|
|||
fn short_help(&self) -> String { |
|||
"Download CompactBlocks and sync to the server".to_string() |
|||
} |
|||
|
|||
fn exec(&self, args: &[String], lightclient: &LightClient) { |
|||
lightclient.do_sync(); |
|||
} |
|||
} |
|||
|
|||
struct HelpCommand {} |
|||
|
|||
impl Command for HelpCommand { |
|||
fn help(&self) { |
|||
println!("Lists all available commands"); |
|||
} |
|||
|
|||
fn short_help(&self) -> String { |
|||
"Lists all available commands".to_string() |
|||
} |
|||
|
|||
fn exec(&self, args: &[String], _: &LightClient) { |
|||
// Print a list of all commands
|
|||
get_commands().iter().for_each(| (cmd, obj) | { |
|||
println!("{} - {}", cmd, obj.short_help()); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
struct AddressCommand {} |
|||
impl Command for AddressCommand { |
|||
fn help(&self) { |
|||
println!("Show my addresses"); |
|||
} |
|||
|
|||
fn short_help(&self) -> String { |
|||
"List all current addresses".to_string() |
|||
} |
|||
|
|||
fn exec(&self, args: &[String], lightclient: &LightClient) { |
|||
lightclient.do_address(); |
|||
} |
|||
} |
|||
|
|||
pub fn get_commands() -> Box<HashMap<String, Box<dyn Command>>> { |
|||
let mut map: HashMap<String, Box<dyn Command>> = HashMap::new(); |
|||
|
|||
map.insert("sync".to_string(), Box::new(SyncCommand{})); |
|||
map.insert("help".to_string(), Box::new(HelpCommand{})); |
|||
map.insert("address".to_string(), Box::new(AddressCommand{})); |
|||
|
|||
Box::new(map) |
|||
} |
|||
|
|||
pub fn do_user_command(cmd: String, lightclient: &LightClient) { |
|||
match get_commands().get(&cmd) { |
|||
Some(cmd) => cmd.exec(&[], lightclient), |
|||
None => { |
|||
println!("Unknown command : {}. Type 'help' for a list of commands", cmd); |
|||
} |
|||
} |
|||
} |
@ -1,526 +1,477 @@ |
|||
use std::time::SystemTime; |
|||
|
|||
use pairing::bls12_381::Bls12; |
|||
use sapling_crypto::primitives::{Diversifier, Note, PaymentAddress}; |
|||
use std::cmp; |
|||
use std::collections::HashMap; |
|||
use std::sync::{Arc, RwLock}; |
|||
use protobuf::*; |
|||
use zcash_client_backend::{ |
|||
constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS, encoding::encode_payment_address, |
|||
proto::compact_formats::CompactBlock, welding_rig::scan_block, |
|||
}; |
|||
use zcash_primitives::{ |
|||
block::BlockHash, |
|||
merkle_tree::{CommitmentTree, IncrementalWitness}, |
|||
sapling::Node, |
|||
transaction::{ |
|||
builder::{Builder, DEFAULT_FEE}, |
|||
components::Amount, |
|||
TxId, |
|||
}, |
|||
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, |
|||
JUBJUB, |
|||
}; |
|||
|
|||
use crate::address; |
|||
use crate::prover; |
|||
|
|||
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
|
|||
// allocator.
|
|||
#[cfg(feature = "wee_alloc")] |
|||
#[global_allocator] |
|||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; |
|||
|
|||
const ANCHOR_OFFSET: u32 = 10; |
|||
|
|||
const SAPLING_ACTIVATION_HEIGHT: i32 = 280_000; |
|||
|
|||
|
|||
fn now() -> f64 { |
|||
// web_sys::window()
|
|||
// .expect("should have a Window")
|
|||
// .performance()
|
|||
// .expect("should have a Performance")
|
|||
// .now()
|
|||
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as f64
|
|||
} |
|||
use crate::lightwallet::LightWallet; |
|||
|
|||
struct BlockData { |
|||
height: i32, |
|||
hash: BlockHash, |
|||
tree: CommitmentTree<Node>, |
|||
} |
|||
use std::sync::Arc; |
|||
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; |
|||
|
|||
struct SaplingNoteData { |
|||
account: usize, |
|||
diversifier: Diversifier, |
|||
note: Note<Bls12>, |
|||
witnesses: Vec<IncrementalWitness<Node>>, |
|||
nullifier: [u8; 32], |
|||
spent: Option<TxId>, |
|||
} |
|||
use std::error::Error; |
|||
use std::io::prelude::*; |
|||
use std::fs::File; |
|||
|
|||
impl SaplingNoteData { |
|||
fn new( |
|||
extfvk: &ExtendedFullViewingKey, |
|||
output: zcash_client_backend::wallet::WalletShieldedOutput, |
|||
witness: IncrementalWitness<Node>, |
|||
) -> Self { |
|||
let nf = { |
|||
let mut nf = [0; 32]; |
|||
nf.copy_from_slice( |
|||
&output |
|||
.note |
|||
.nf(&extfvk.fvk.vk, witness.position() as u64, &JUBJUB), |
|||
); |
|||
nf |
|||
}; |
|||
use zcash_primitives::transaction::{TxId, Transaction}; |
|||
use zcash_primitives::note_encryption::Memo; |
|||
|
|||
SaplingNoteData { |
|||
account: output.account, |
|||
diversifier: output.to.diversifier, |
|||
note: output.note, |
|||
witnesses: vec![witness], |
|||
nullifier: nf, |
|||
spent: None, |
|||
} |
|||
} |
|||
} |
|||
use futures::Future; |
|||
use hyper::client::connect::{Destination, HttpConnector}; |
|||
use tower_grpc::Request; |
|||
use tower_hyper::{client, util}; |
|||
use tower_util::MakeService; |
|||
use futures::stream::Stream; |
|||
|
|||
struct WalletTx { |
|||
block: i32, |
|||
notes: Vec<SaplingNoteData>, |
|||
} |
|||
use crate::grpc_client::{ChainSpec, BlockId, BlockRange, RawTransaction, TxFilter}; |
|||
use crate::grpc_client::client::CompactTxStreamer; |
|||
|
|||
struct SpendableNote { |
|||
txid: TxId, |
|||
nullifier: [u8; 32], |
|||
diversifier: Diversifier, |
|||
note: Note<Bls12>, |
|||
witness: IncrementalWitness<Node>, |
|||
} |
|||
// Used below to return the grpc "Client" type to calling methods
|
|||
type Client = crate::grpc_client::client::CompactTxStreamer<tower_request_modifier::RequestModifier<tower_hyper::client::Connection<tower_grpc::BoxBody>, tower_grpc::BoxBody>>; |
|||
|
|||
impl SpendableNote { |
|||
fn from(txid: TxId, nd: &SaplingNoteData, anchor_offset: usize) -> Option<Self> { |
|||
if nd.spent.is_none() { |
|||
let witness = nd.witnesses.get(nd.witnesses.len() - anchor_offset - 1); |
|||
|
|||
witness.map(|w| SpendableNote { |
|||
txid, |
|||
nullifier: nd.nullifier, |
|||
diversifier: nd.diversifier, |
|||
note: nd.note.clone(), |
|||
witness: w.clone(), |
|||
}) |
|||
} else { |
|||
None |
|||
} |
|||
} |
|||
} |
|||
|
|||
pub struct Client { |
|||
extsks: [ExtendedSpendingKey; 1], |
|||
extfvks: [ExtendedFullViewingKey; 1], |
|||
address: PaymentAddress<Bls12>, |
|||
blocks: Arc<RwLock<Vec<BlockData>>>, |
|||
txs: Arc<RwLock<HashMap<TxId, WalletTx>>>, |
|||
pub struct LightClient { |
|||
pub wallet : Arc<LightWallet>, |
|||
pub sapling_output : Vec<u8>, |
|||
pub sapling_spend : Vec<u8>, |
|||
} |
|||
|
|||
/// Public methods, exported to JavaScript.
|
|||
impl Client { |
|||
impl LightClient { |
|||
pub fn new() -> Self { |
|||
let mut w = LightClient { |
|||
wallet : Arc::new(LightWallet::new()),
|
|||
sapling_output : vec![],
|
|||
sapling_spend : vec![] |
|||
}; |
|||
|
|||
let extsk = ExtendedSpendingKey::master(&[0; 32]); |
|||
let extfvk = ExtendedFullViewingKey::from(&extsk); |
|||
let address = extfvk.default_address().unwrap().1; |
|||
// Read Sapling Params
|
|||
let mut f = File::open("/home/adityapk/.zcash-params/sapling-output.params").unwrap(); |
|||
f.read_to_end(&mut w.sapling_output).unwrap(); |
|||
let mut f = File::open("/home/adityapk/.zcash-params/sapling-spend.params").unwrap(); |
|||
f.read_to_end(&mut w.sapling_spend).unwrap(); |
|||
|
|||
Client { |
|||
extsks: [extsk], |
|||
extfvks: [extfvk], |
|||
address, |
|||
blocks: Arc::new(RwLock::new(vec![])), |
|||
txs: Arc::new(RwLock::new(HashMap::new())), |
|||
} |
|||
w.wallet.set_initial_block(500000, |
|||
"004fada8d4dbc5e80b13522d2c6bd0116113c9b7197f0c6be69bc7a62f2824cd", |
|||
"01b733e839b5f844287a6a491409a991ec70277f39a50c99163ed378d23a829a0700100001916db36dfb9a0cf26115ed050b264546c0fa23459433c31fd72f63d188202f2400011f5f4e3bd18da479f48d674dbab64454f6995b113fa21c9d8853a9e764fb3e1f01df9d2c233ca60360e3c2bb73caf5839a1be634c8b99aea22d02abda2e747d9100001970d41722c078288101acd0a75612acfb4c434f2a55aab09fb4e812accc2ba7301485150f0deac7774dcd0fe32043bde9ba2b6bbfff787ad074339af68e88ee70101601324f1421e00a43ef57f197faf385ee4cac65aab58048016ecbd94e022973701e1b17f4bd9d1b6ca1107f619ac6d27b53dd3350d5be09b08935923cbed97906c0000000000011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39"); |
|||
|
|||
return w; |
|||
} |
|||
|
|||
pub fn last_scanned_height(&self) -> u64 { |
|||
self.wallet.last_scanned_height() as u64 |
|||
} |
|||
|
|||
pub fn set_initial_block(&self, height: i32, hash: &str, sapling_tree: &str) -> bool { |
|||
let mut blocks = self.blocks.write().unwrap(); |
|||
if !blocks.is_empty() { |
|||
return false; |
|||
pub fn do_address(&self) {
|
|||
println!("Address: {}", self.wallet.address()); |
|||
println!("Balance: {}", self.wallet.balance()); |
|||
} |
|||
|
|||
pub fn do_sync(&self) { |
|||
// Sync is 3 parts
|
|||
// 1. Get the latest block
|
|||
// 2. Get all the blocks that we don't have
|
|||
// 3. Find all new Txns that don't have the full Tx, and get them as full transactions
|
|||
// and scan them, mainly to get the memos
|
|||
let mut last_scanned_height = self.wallet.last_scanned_height() as u64; |
|||
let mut end_height = last_scanned_height + 1000; |
|||
|
|||
// This will hold the latest block fetched from the RPC
|
|||
let latest_block_height = Arc::new(AtomicU64::new(0)); |
|||
// TODO: this could be a oneshot channel
|
|||
let latest_block_height_clone = latest_block_height.clone(); |
|||
self.fetch_latest_block(move |block: BlockId| { |
|||
latest_block_height_clone.store(block.height, Ordering::SeqCst); |
|||
}); |
|||
let last_block = latest_block_height.load(Ordering::SeqCst); |
|||
|
|||
let bytes_downloaded = Arc::new(AtomicUsize::new(0)); |
|||
|
|||
// Fetch CompactBlocks in increments
|
|||
loop { |
|||
let local_light_wallet = self.wallet.clone(); |
|||
let local_bytes_downloaded = bytes_downloaded.clone(); |
|||
|
|||
let simple_callback = move |encoded_block: &[u8]| { |
|||
local_light_wallet.scan_block(encoded_block); |
|||
local_bytes_downloaded.fetch_add(encoded_block.len(), Ordering::SeqCst); |
|||
}; |
|||
|
|||
print!("Syncing {}/{}, Balance = {} \r",
|
|||
last_scanned_height, last_block, self.wallet.balance()); |
|||
|
|||
self.fetch_blocks(last_scanned_height, end_height, simple_callback); |
|||
|
|||
last_scanned_height = end_height + 1; |
|||
end_height = last_scanned_height + 1000 - 1; |
|||
|
|||
if last_scanned_height > last_block { |
|||
break; |
|||
} else if end_height > last_block { |
|||
end_height = last_block; |
|||
}
|
|||
}
|
|||
|
|||
println!("Synced to {}, Downloaded {} kB \r",
|
|||
last_block, bytes_downloaded.load(Ordering::SeqCst) / 1024); |
|||
|
|||
|
|||
// Get the Raw transaction for all the wallet transactions
|
|||
|
|||
// We need to first copy over the Txids from the wallet struct, because
|
|||
// we need to free the read lock from here (Because we'll self.wallet.txs later)
|
|||
let txids_to_fetch: Vec<TxId>; |
|||
{ |
|||
// First, build a list of all the TxIDs and Memos that we need
|
|||
// to fetch.
|
|||
// 1. Get all (txid, Option<Memo>)
|
|||
// 2. Filter out all txids where the Memo is None
|
|||
// (Which means that particular txid was never fetched. Remember
|
|||
// that when memos are fetched, if they are empty, they become
|
|||
// Some(f60000...)
|
|||
let txids_and_memos = self.wallet.txs.read().unwrap().iter() |
|||
.flat_map( |(txid, wtx)| { // flat_map because we're collecting vector of vectors
|
|||
wtx.notes.iter() |
|||
.filter( |nd| nd.memo.is_none()) // only get if memo is None (i.e., it has not been fetched)
|
|||
.map( |nd| (txid.clone(), nd.memo.clone()) ) // collect (txid, memo) Clone everything because we want copies, so we can release the read lock
|
|||
.collect::<Vec<(TxId, Option<Memo>)>>() // convert to vector
|
|||
}) |
|||
.collect::<Vec<(TxId, Option<Memo>)>>(); |
|||
|
|||
println!("{:?}", txids_and_memos); |
|||
// TODO: Assert that all the memos here are None
|
|||
|
|||
txids_to_fetch = txids_and_memos.iter() |
|||
.map( | (txid, _) | txid.clone() ) // We're only interested in the txids, so drop the Memo, which is None anyway
|
|||
.collect::<Vec<TxId>>(); // and convert into Vec
|
|||
} |
|||
|
|||
let hash = match hex::decode(hash) { |
|||
Ok(hash) => { |
|||
let mut r = hash; |
|||
r.reverse(); |
|||
BlockHash::from_slice(&r) |
|||
}, |
|||
Err(e) => { |
|||
eprintln!("{}", e); |
|||
return false; |
|||
} |
|||
}; |
|||
// And go and fetch the txids, getting the full transaction, so we can
|
|||
// read the memos
|
|||
for txid in txids_to_fetch { |
|||
let light_wallet_clone = self.wallet.clone(); |
|||
println!("Scanning txid {}", txid); |
|||
|
|||
self.fetch_full_tx(txid, move |tx_bytes: &[u8] | { |
|||
let tx = Transaction::read(tx_bytes).unwrap(); |
|||
|
|||
let sapling_tree = match hex::decode(sapling_tree) { |
|||
Ok(tree) => tree, |
|||
Err(e) => { |
|||
eprintln!("{}", e); |
|||
return false; |
|||
} |
|||
light_wallet_clone.scan_full_tx(&tx); |
|||
}); |
|||
}; |
|||
|
|||
if let Ok(tree) = CommitmentTree::read(&sapling_tree[..]) { |
|||
blocks.push(BlockData { height, hash, tree }); |
|||
true |
|||
} else { |
|||
false |
|||
} |
|||
// Print all the memos for fun.
|
|||
let memos = self.wallet.txs.read().unwrap() |
|||
.values().flat_map(|wtx| { |
|||
wtx.notes.iter().map(|nd| nd.memo.clone() ).collect::<Vec<Option<Memo>>>() |
|||
}) |
|||
.map( |m| match m { |
|||
Some(memo) => { |
|||
match memo.to_utf8() { |
|||
Some(Ok(memo_str)) => Some(memo_str), |
|||
_ => None |
|||
} |
|||
} |
|||
_ => None |
|||
}) |
|||
.collect::<Vec<Option<String>>>(); |
|||
|
|||
println!("All Wallet Txns {:?}", memos); |
|||
} |
|||
|
|||
pub fn last_scanned_height(&self) -> i32 { |
|||
self.blocks |
|||
.read() |
|||
.unwrap() |
|||
.last() |
|||
.map(|block| block.height) |
|||
.unwrap_or(SAPLING_ACTIVATION_HEIGHT - 1) |
|||
pub fn do_send(&self, addr: String, value: u64, memo: Option<String>) { |
|||
let rawtx = self.wallet.send_to_address( |
|||
u32::from_str_radix("2bb40e60", 16).unwrap(), // Blossom ID
|
|||
&self.sapling_spend, &self.sapling_output, |
|||
&addr, value, memo |
|||
); |
|||
|
|||
match rawtx { |
|||
Some(txbytes) => self.broadcast_raw_tx(txbytes), |
|||
None => eprintln!("No Tx to broadcast") |
|||
}; |
|||
} |
|||
|
|||
/// Determines the target height for a transaction, and the offset from which to
|
|||
/// select anchors, based on the current synchronised block chain.
|
|||
fn get_target_height_and_anchor_offset(&self) -> Option<(u32, usize)> { |
|||
match { |
|||
let blocks = self.blocks.read().unwrap(); |
|||
( |
|||
blocks.first().map(|block| block.height as u32), |
|||
blocks.last().map(|block| block.height as u32), |
|||
) |
|||
} { |
|||
(Some(min_height), Some(max_height)) => { |
|||
let target_height = max_height + 1; |
|||
|
|||
// Select an anchor ANCHOR_OFFSET back from the target block,
|
|||
// unless that would be before the earliest block we have.
|
|||
let anchor_height = |
|||
cmp::max(target_height.saturating_sub(ANCHOR_OFFSET), min_height); |
|||
|
|||
Some((target_height, (target_height - anchor_height) as usize)) |
|||
} |
|||
_ => None, |
|||
} |
|||
pub fn fetch_blocks<F : 'static + std::marker::Send>(&self, start_height: u64, end_height: u64, c: F) |
|||
where F : Fn(&[u8]) { |
|||
// Fetch blocks
|
|||
let uri: http::Uri = format!("http://127.0.0.1:9067").parse().unwrap(); |
|||
|
|||
let dst = Destination::try_from_uri(uri.clone()).unwrap(); |
|||
let connector = util::Connector::new(HttpConnector::new(4)); |
|||
let settings = client::Builder::new().http2_only(true).clone(); |
|||
let mut make_client = client::Connect::with_builder(connector, settings); |
|||
|
|||
let say_hello = make_client |
|||
.make_service(dst) |
|||
.map_err(|e| panic!("connect error: {:?}", e)) |
|||
.and_then(move |conn| { |
|||
|
|||
let conn = tower_request_modifier::Builder::new() |
|||
.set_origin(uri) |
|||
.build(conn) |
|||
.unwrap(); |
|||
|
|||
// Wait until the client is ready...
|
|||
CompactTxStreamer::new(conn) |
|||
.ready() |
|||
.map_err(|e| eprintln!("streaming error {:?}", e)) |
|||
}) |
|||
.and_then(move |mut client| { |
|||
let bs = BlockId{ height: start_height, hash: vec!()}; |
|||
let be = BlockId{ height: end_height, hash: vec!()}; |
|||
|
|||
let br = Request::new(BlockRange{ start: Some(bs), end: Some(be)}); |
|||
client |
|||
.get_block_range(br) |
|||
.map_err(|e| { |
|||
eprintln!("RouteChat request failed; err={:?}", e); |
|||
}) |
|||
.and_then(move |response| { |
|||
let inbound = response.into_inner(); |
|||
inbound.for_each(move |b| { |
|||
use prost::Message; |
|||
let mut encoded_buf = vec![]; |
|||
|
|||
b.encode(&mut encoded_buf).unwrap(); |
|||
c(&encoded_buf); |
|||
|
|||
Ok(()) |
|||
}) |
|||
.map_err(|e| eprintln!("gRPC inbound stream error: {:?}", e))
|
|||
}) |
|||
}); |
|||
|
|||
tokio::runtime::current_thread::Runtime::new().unwrap().block_on(say_hello).unwrap(); |
|||
} |
|||
|
|||
pub fn address(&self) -> String { |
|||
encode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS, &self.address) |
|||
|
|||
pub fn fetch_full_tx<F : 'static + std::marker::Send>(&self, txid: TxId, c: F) |
|||
where F : Fn(&[u8]) { |
|||
let uri: http::Uri = format!("http://127.0.0.1:9067").parse().unwrap(); |
|||
|
|||
let say_hello = self.make_grpc_client(uri).unwrap() |
|||
.and_then(move |mut client| { |
|||
let txfilter = TxFilter { block: None, index: 0, hash: txid.0.to_vec() }; |
|||
client.get_transaction(Request::new(txfilter)) |
|||
}) |
|||
.and_then(move |response| { |
|||
//let tx = Transaction::read(&response.into_inner().data[..]).unwrap();
|
|||
c(&response.into_inner().data); |
|||
|
|||
Ok(()) |
|||
}) |
|||
.map_err(|e| { |
|||
println!("ERR = {:?}", e); |
|||
}); |
|||
|
|||
tokio::runtime::current_thread::Runtime::new().unwrap().block_on(say_hello).unwrap() |
|||
} |
|||
|
|||
pub fn balance(&self) -> u64 { |
|||
self.txs |
|||
.read() |
|||
.unwrap() |
|||
.values() |
|||
.map(|tx| { |
|||
tx.notes |
|||
.iter() |
|||
.map(|nd| if nd.spent.is_none() { nd.note.value } else { 0 }) |
|||
.sum::<u64>() |
|||
pub fn broadcast_raw_tx(&self, tx_bytes: Box<[u8]>) { |
|||
let uri: http::Uri = format!("http://127.0.0.1:9067").parse().unwrap(); |
|||
|
|||
let say_hello = self.make_grpc_client(uri).unwrap() |
|||
.and_then(move |mut client| { |
|||
client.send_transaction(Request::new(RawTransaction {data: tx_bytes.to_vec()})) |
|||
}) |
|||
.sum::<u64>() |
|||
.and_then(move |response| { |
|||
println!("{:?}", response.into_inner()); |
|||
Ok(()) |
|||
}) |
|||
.map_err(|e| { |
|||
println!("ERR = {:?}", e); |
|||
}); |
|||
|
|||
tokio::runtime::current_thread::Runtime::new().unwrap().block_on(say_hello).unwrap() |
|||
} |
|||
|
|||
pub fn verified_balance(&self) -> u64 { |
|||
let anchor_height = match self.get_target_height_and_anchor_offset() { |
|||
Some((height, anchor_offset)) => height - anchor_offset as u32, |
|||
None => return 0, |
|||
}; |
|||
pub fn fetch_latest_block<F : 'static + std::marker::Send>(&self, mut c : F)
|
|||
where F : FnMut(BlockId) { |
|||
let uri: http::Uri = format!("http://127.0.0.1:9067").parse().unwrap(); |
|||
|
|||
self.txs |
|||
.read() |
|||
.unwrap() |
|||
.values() |
|||
.map(|tx| { |
|||
if tx.block as u32 <= anchor_height { |
|||
tx.notes |
|||
.iter() |
|||
.map(|nd| if nd.spent.is_none() { nd.note.value } else { 0 }) |
|||
.sum::<u64>() |
|||
} else { |
|||
0 |
|||
} |
|||
let say_hello = self.make_grpc_client(uri).unwrap() |
|||
.and_then(|mut client| { |
|||
client.get_latest_block(Request::new(ChainSpec {})) |
|||
}) |
|||
.and_then(move |response| { |
|||
c(response.into_inner()); |
|||
Ok(()) |
|||
}) |
|||
.sum::<u64>() |
|||
.map_err(|e| { |
|||
println!("ERR = {:?}", e); |
|||
}); |
|||
|
|||
tokio::runtime::current_thread::Runtime::new().unwrap().block_on(say_hello).unwrap() |
|||
} |
|||
|
|||
fn make_grpc_client(&self, uri: http::Uri) -> Result<Box<dyn Future<Item=Client, Error=tower_grpc::Status> + Send>, Box<dyn Error>> { |
|||
let dst = Destination::try_from_uri(uri.clone())?; |
|||
let connector = util::Connector::new(HttpConnector::new(4)); |
|||
let settings = client::Builder::new().http2_only(true).clone(); |
|||
let mut make_client = client::Connect::with_builder(connector, settings); |
|||
|
|||
let say_hello = make_client |
|||
.make_service(dst) |
|||
.map_err(|e| panic!("connect error: {:?}", e)) |
|||
.and_then(move |conn| { |
|||
|
|||
let conn = tower_request_modifier::Builder::new() |
|||
.set_origin(uri) |
|||
.build(conn) |
|||
.unwrap(); |
|||
|
|||
pub fn scan_block(&self, block: &[u8]) -> bool { |
|||
let block: CompactBlock = match parse_from_bytes(block) { |
|||
Ok(block) => block, |
|||
Err(e) => { |
|||
eprintln!("Could not parse CompactBlock from bytes: {}", e); |
|||
return false; |
|||
} |
|||
}; |
|||
// Wait until the client is ready...
|
|||
CompactTxStreamer::new(conn).ready() |
|||
}); |
|||
Ok(Box::new(say_hello)) |
|||
} |
|||
} |
|||
|
|||
// Scanned blocks MUST be height-sequential.
|
|||
let height = block.get_height() as i32; |
|||
if height == self.last_scanned_height() { |
|||
// If the last scanned block is rescanned, check it still matches.
|
|||
if let Some(hash) = self.blocks.read().unwrap().last().map(|block| block.hash) { |
|||
if block.hash() != hash { |
|||
eprintln!("Block hash does not match for block {}. {} vs {}", height, block.hash(), hash); |
|||
return false; |
|||
} |
|||
} |
|||
return true; |
|||
} else if height != (self.last_scanned_height() + 1) { |
|||
eprintln!( |
|||
"Block is not height-sequential (expected {}, found {})", |
|||
self.last_scanned_height() + 1, |
|||
height |
|||
); |
|||
return false; |
|||
} |
|||
|
|||
// Get the most recent scanned data.
|
|||
let mut block_data = BlockData { |
|||
height, |
|||
hash: block.hash(), |
|||
tree: self |
|||
.blocks |
|||
.read() |
|||
.unwrap() |
|||
.last() |
|||
.map(|block| block.tree.clone()) |
|||
.unwrap_or(CommitmentTree::new()), |
|||
}; |
|||
let mut txs = self.txs.write().unwrap(); |
|||
|
|||
// Create a Vec containing all unspent nullifiers.
|
|||
let nfs: Vec<_> = txs |
|||
.iter() |
|||
.map(|(txid, tx)| { |
|||
let txid = *txid; |
|||
tx.notes.iter().filter_map(move |nd| { |
|||
if nd.spent.is_none() { |
|||
Some((nd.nullifier, nd.account, txid)) |
|||
} else { |
|||
None |
|||
} |
|||
}) |
|||
}) |
|||
.flatten() |
|||
.collect(); |
|||
|
|||
// Prepare the note witnesses for updating
|
|||
for tx in txs.values_mut() { |
|||
for nd in tx.notes.iter_mut() { |
|||
// Duplicate the most recent witness
|
|||
if let Some(witness) = nd.witnesses.last() { |
|||
nd.witnesses.push(witness.clone()); |
|||
} |
|||
// Trim the oldest witnesses
|
|||
nd.witnesses = nd |
|||
.witnesses |
|||
.split_off(nd.witnesses.len().saturating_sub(100)); |
|||
} |
|||
} |
|||
|
|||
let new_txs = { |
|||
let nf_refs: Vec<_> = nfs.iter().map(|(nf, acc, _)| (&nf[..], *acc)).collect(); |
|||
|
|||
// Create a single mutable slice of all the newly-added witnesses.
|
|||
let mut witness_refs: Vec<_> = txs |
|||
.values_mut() |
|||
.map(|tx| tx.notes.iter_mut().filter_map(|nd| nd.witnesses.last_mut())) |
|||
.flatten() |
|||
.collect(); |
|||
|
|||
scan_block( |
|||
block, |
|||
&self.extfvks, |
|||
&nf_refs[..], |
|||
&mut block_data.tree, |
|||
&mut witness_refs[..], |
|||
) |
|||
}; |
|||
|
|||
for (tx, new_witnesses) in new_txs { |
|||
// Mark notes as spent.
|
|||
for spend in &tx.shielded_spends { |
|||
let txid = nfs |
|||
.iter() |
|||
.find(|(nf, _, _)| &nf[..] == &spend.nf[..]) |
|||
.unwrap() |
|||
.2; |
|||
let mut spent_note = txs |
|||
.get_mut(&txid) |
|||
.unwrap() |
|||
.notes |
|||
.iter_mut() |
|||
.find(|nd| &nd.nullifier[..] == &spend.nf[..]) |
|||
.unwrap(); |
|||
spent_note.spent = Some(tx.txid); |
|||
} |
|||
|
|||
// Find the existing transaction entry, or create a new one.
|
|||
if !txs.contains_key(&tx.txid) { |
|||
let tx_entry = WalletTx { |
|||
block: block_data.height, |
|||
notes: vec![], |
|||
}; |
|||
txs.insert(tx.txid, tx_entry); |
|||
} |
|||
let tx_entry = txs.get_mut(&tx.txid).unwrap(); |
|||
|
|||
// Save notes.
|
|||
for (output, witness) in tx |
|||
.shielded_outputs |
|||
.into_iter() |
|||
.zip(new_witnesses.into_iter()) |
|||
{ |
|||
tx_entry.notes.push(SaplingNoteData::new( |
|||
&self.extfvks[output.account], |
|||
output, |
|||
witness, |
|||
)); |
|||
} |
|||
} |
|||
|
|||
// Store scanned data for this block.
|
|||
self.blocks.write().unwrap().push(block_data); |
|||
/* |
|||
TLS Example https://gist.github.com/kiratp/dfcbcf0aa713a277d5d53b06d9db9308
|
|||
|
|||
// [dependencies]
|
|||
// futures = "0.1.27"
|
|||
// http = "0.1.17"
|
|||
// tokio = "0.1.21"
|
|||
// tower-request-modifier = { git = "https://github.com/tower-rs/tower-http" }
|
|||
// tower-grpc = { version = "0.1.0", features = ["tower-hyper"] }
|
|||
// tower-service = "0.2"
|
|||
// tower-util = "0.1"
|
|||
// tokio-rustls = "0.10.0-alpha.3"
|
|||
// webpki = "0.19.1"
|
|||
// webpki-roots = "0.16.0"
|
|||
// tower-h2 = { git = "https://github.com/tower-rs/tower-h2" }
|
|||
// openssl = "*"
|
|||
// openssl-probe = "*"
|
|||
|
|||
use std::thread; |
|||
use std::sync::{Arc}; |
|||
use futures::{future, Future}; |
|||
use tower_util::MakeService; |
|||
|
|||
use tokio_rustls::client::TlsStream; |
|||
use tokio_rustls::{rustls::ClientConfig, TlsConnector}; |
|||
use std::net::SocketAddr; |
|||
|
|||
use tokio::executor::DefaultExecutor; |
|||
use tokio::net::tcp::TcpStream; |
|||
use tower_h2; |
|||
|
|||
use std::net::ToSocketAddrs; |
|||
|
|||
|
|||
true |
|||
|
|||
struct Dst(SocketAddr); |
|||
|
|||
|
|||
impl tower_service::Service<()> for Dst { |
|||
type Response = TlsStream<TcpStream>; |
|||
type Error = ::std::io::Error; |
|||
type Future = Box<dyn Future<Item = TlsStream<TcpStream>, Error = ::std::io::Error> + Send>; |
|||
|
|||
fn poll_ready(&mut self) -> futures::Poll<(), Self::Error> { |
|||
Ok(().into()) |
|||
} |
|||
|
|||
pub fn send_to_address( |
|||
&self, |
|||
consensus_branch_id: u32, |
|||
spend_params: &[u8], |
|||
output_params: &[u8], |
|||
to: &str, |
|||
value: u32, |
|||
) -> Option<Box<[u8]>> { |
|||
let start_time = now(); |
|||
println!( |
|||
"0: Creating transaction sending {} tazoshis to {}", |
|||
value, |
|||
to |
|||
); |
|||
fn call(&mut self, _: ()) -> Self::Future { |
|||
println!("{:?}", self.0); |
|||
let mut config = ClientConfig::new(); |
|||
|
|||
let extsk = &self.extsks[0]; |
|||
let extfvk = &self.extfvks[0]; |
|||
let ovk = extfvk.fvk.ovk; |
|||
config.alpn_protocols.push(b"h2".to_vec()); |
|||
config.root_store.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); |
|||
let config = Arc::new(config); |
|||
let tls_connector = TlsConnector::from(config); |
|||
|
|||
let to = match address::RecipientAddress::from_str(to) { |
|||
Some(to) => to, |
|||
None => { |
|||
eprintln!("Invalid recipient address"); |
|||
return None; |
|||
} |
|||
}; |
|||
let value = Amount(value as i64); |
|||
|
|||
// Target the next block, assuming we are up-to-date.
|
|||
let (height, anchor_offset) = match self.get_target_height_and_anchor_offset() { |
|||
Some(res) => res, |
|||
None => { |
|||
eprintln!("Cannot send funds before scanning any blocks"); |
|||
return None; |
|||
} |
|||
}; |
|||
let addr_string_local = "mydomain.com"; |
|||
|
|||
let domain = webpki::DNSNameRef::try_from_ascii_str(addr_string_local).unwrap(); |
|||
let domain_local = domain.to_owned(); |
|||
|
|||
let stream = TcpStream::connect(&self.0).and_then(move |sock| { |
|||
sock.set_nodelay(true).unwrap(); |
|||
tls_connector.connect(domain_local.as_ref(), sock) |
|||
}) |
|||
.map(move |tcp| tcp); |
|||
|
|||
Box::new(stream) |
|||
} |
|||
} |
|||
|
|||
// Select notes to cover the target value
|
|||
println!("{}: Selecting notes", now() - start_time); |
|||
let target_value = value.0 + DEFAULT_FEE.0; |
|||
let notes: Vec<_> = self |
|||
.txs |
|||
.read() |
|||
// Same implementation but without TLS. Should make it straightforward to run without TLS
|
|||
// when testing on local machine
|
|||
|
|||
// impl tower_service::Service<()> for Dst {
|
|||
// type Response = TcpStream;
|
|||
// type Error = ::std::io::Error;
|
|||
// type Future = Box<dyn Future<Item = TcpStream, Error = ::std::io::Error> + Send>;
|
|||
|
|||
// fn poll_ready(&mut self) -> futures::Poll<(), Self::Error> {
|
|||
// Ok(().into())
|
|||
// }
|
|||
|
|||
// fn call(&mut self, _: ()) -> Self::Future {
|
|||
// let mut config = ClientConfig::new();
|
|||
// config.alpn_protocols.push(b"h2".to_vec());
|
|||
// config.root_store.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
|
|||
|
|||
// let addr_string_local = "mydomain.com".to_string();
|
|||
// let addr = addr_string_local.as_str();
|
|||
|
|||
// let stream = TcpStream::connect(&self.0)
|
|||
// .and_then(move |sock| {
|
|||
// sock.set_nodelay(true).unwrap();
|
|||
// Ok(sock)
|
|||
// });
|
|||
// Box::new(stream)
|
|||
// }
|
|||
// }
|
|||
|
|||
|
|||
fn connect() { |
|||
let keepalive = future::loop_fn((), move |_| { |
|||
let uri: http::Uri = "https://mydomain.com".parse().unwrap(); |
|||
println!("Connecting to network at: {:?}", uri); |
|||
|
|||
let addr = "https://mydomain.com:443" |
|||
.to_socket_addrs() |
|||
.unwrap() |
|||
.iter() |
|||
.map(|(txid, tx)| tx.notes.iter().map(move |note| (*txid, note))) |
|||
.flatten() |
|||
.filter_map(|(txid, note)| SpendableNote::from(txid, note, anchor_offset)) |
|||
.scan(0, |running_total, spendable| { |
|||
let value = spendable.note.value; |
|||
let ret = if *running_total < target_value as u64 { |
|||
Some(spendable) |
|||
} else { |
|||
None |
|||
}; |
|||
*running_total = *running_total + value; |
|||
ret |
|||
.next() |
|||
.unwrap(); |
|||
|
|||
let h2_settings = Default::default(); |
|||
let mut make_client = tower_h2::client::Connect::new(Dst {0: addr}, h2_settings, DefaultExecutor::current()); |
|||
|
|||
make_client |
|||
.make_service(()) |
|||
.map_err(|e| { |
|||
eprintln!("HTTP/2 connection failed; err={:?}", e); |
|||
}) |
|||
.collect(); |
|||
|
|||
// Confirm we were able to select sufficient value
|
|||
let selected_value = notes |
|||
.iter() |
|||
.map(|selected| selected.note.value) |
|||
.sum::<u64>(); |
|||
if selected_value < target_value as u64 { |
|||
eprintln!( |
|||
"Insufficient funds (have {}, need {})", |
|||
selected_value, target_value |
|||
); |
|||
return None; |
|||
} |
|||
.and_then(move |conn| { |
|||
let conn = tower_request_modifier::Builder::new() |
|||
.set_origin(uri) |
|||
.build(conn) |
|||
.unwrap(); |
|||
|
|||
// Create the transaction
|
|||
println!("{}: Adding {} inputs", now() - start_time, notes.len()); |
|||
let mut builder = Builder::new(height); |
|||
for selected in notes.iter() { |
|||
if let Err(e) = builder.add_sapling_spend( |
|||
extsk.clone(), |
|||
selected.diversifier, |
|||
selected.note.clone(), |
|||
selected.witness.clone(), |
|||
) { |
|||
eprintln!("Error adding note: {:?}", e); |
|||
return None; |
|||
} |
|||
} |
|||
println!("{}: Adding output", now() - start_time); |
|||
if let Err(e) = match to { |
|||
address::RecipientAddress::Shielded(to) => { |
|||
builder.add_sapling_output(ovk, to.clone(), value, None) |
|||
} |
|||
address::RecipientAddress::Transparent(to) => { |
|||
builder.add_transparent_output(&to, value) |
|||
} |
|||
} { |
|||
eprintln!("Error adding output: {:?}", e); |
|||
return None; |
|||
} |
|||
println!("{}: Building transaction", now() - start_time); |
|||
let (tx, _) = match builder.build( |
|||
consensus_branch_id, |
|||
prover::InMemTxProver::new(spend_params, output_params), |
|||
) { |
|||
Ok(res) => res, |
|||
Err(e) => { |
|||
eprintln!("Error creating transaction: {:?}", e); |
|||
return None; |
|||
} |
|||
}; |
|||
println!("{}: Transaction created", now() - start_time); |
|||
println!("Transaction ID: {}", tx.txid()); |
|||
|
|||
// Mark notes as spent.
|
|||
let mut txs = self.txs.write().unwrap(); |
|||
for selected in notes { |
|||
let mut spent_note = txs |
|||
.get_mut(&selected.txid) |
|||
.unwrap() |
|||
.notes |
|||
.iter_mut() |
|||
.find(|nd| &nd.nullifier[..] == &selected.nullifier[..]) |
|||
.unwrap(); |
|||
spent_note.spent = Some(tx.txid()); |
|||
} |
|||
MyGrpcService::new(conn) |
|||
// Wait until the client is ready...
|
|||
.ready() |
|||
.map_err(|e| eprintln!("client closed: {:?}", e)) |
|||
}) |
|||
.and_then(move |mut client| { |
|||
// do stuff
|
|||
}) |
|||
.then(|e| { |
|||
eprintln!("Reopening client connection to network: {:?}", e); |
|||
let retry_sleep = std::time::Duration::from_secs(1); |
|||
|
|||
// Return the encoded transaction, so the caller can send it.
|
|||
let mut raw_tx = vec![]; |
|||
tx.write(&mut raw_tx).unwrap(); |
|||
Some(raw_tx.into_boxed_slice()) |
|||
} |
|||
thread::sleep(retry_sleep); |
|||
Ok(future::Loop::Continue(())) |
|||
}) |
|||
}); |
|||
|
|||
thread::spawn(move || tokio::run(keepalive)); |
|||
} |
|||
|
|||
pub fn main() { |
|||
connect(); |
|||
} |
|||
|
|||
*/ |
@ -0,0 +1,561 @@ |
|||
use std::time::SystemTime; |
|||
|
|||
use pairing::bls12_381::Bls12; |
|||
use zcash_primitives::primitives::{Diversifier, Note, PaymentAddress}; |
|||
use std::cmp; |
|||
use std::collections::HashMap; |
|||
use std::sync::{Arc, RwLock}; |
|||
use protobuf::*; |
|||
use zcash_client_backend::{ |
|||
constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS, encoding::encode_payment_address, |
|||
proto::compact_formats::CompactBlock, welding_rig::scan_block, |
|||
}; |
|||
use zcash_primitives::{ |
|||
block::BlockHash, |
|||
merkle_tree::{CommitmentTree, IncrementalWitness}, |
|||
sapling::Node, |
|||
transaction::{ |
|||
builder::{Builder}, |
|||
components::Amount, components::amount::DEFAULT_FEE, |
|||
TxId, Transaction |
|||
}, |
|||
note_encryption::{Memo, try_sapling_note_decryption}, |
|||
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, |
|||
JUBJUB, |
|||
}; |
|||
|
|||
use crate::address; |
|||
use crate::prover; |
|||
|
|||
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
|
|||
// allocator.
|
|||
#[cfg(feature = "wee_alloc")] |
|||
#[global_allocator] |
|||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; |
|||
|
|||
const ANCHOR_OFFSET: u32 = 10; |
|||
|
|||
const SAPLING_ACTIVATION_HEIGHT: i32 = 280_000; |
|||
|
|||
|
|||
fn now() -> f64 { |
|||
// web_sys::window()
|
|||
// .expect("should have a Window")
|
|||
// .performance()
|
|||
// .expect("should have a Performance")
|
|||
// .now()
|
|||
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as f64
|
|||
} |
|||
|
|||
struct BlockData { |
|||
height: i32, |
|||
hash: BlockHash, |
|||
tree: CommitmentTree<Node>, |
|||
} |
|||
|
|||
pub struct SaplingNoteData { |
|||
account: usize, |
|||
diversifier: Diversifier, |
|||
note: Note<Bls12>, |
|||
witnesses: Vec<IncrementalWitness<Node>>, |
|||
nullifier: [u8; 32], |
|||
spent: Option<TxId>, |
|||
pub memo: Option<Memo> |
|||
} |
|||
|
|||
impl SaplingNoteData { |
|||
fn new( |
|||
extfvk: &ExtendedFullViewingKey, |
|||
output: zcash_client_backend::wallet::WalletShieldedOutput |
|||
) -> Self { |
|||
let witness = output.witness; |
|||
let nf = { |
|||
let mut nf = [0; 32]; |
|||
nf.copy_from_slice( |
|||
&output |
|||
.note |
|||
.nf(&extfvk.fvk.vk, witness.position() as u64, &JUBJUB), |
|||
); |
|||
nf |
|||
}; |
|||
|
|||
SaplingNoteData { |
|||
account: output.account, |
|||
diversifier: output.to.diversifier, |
|||
note: output.note, |
|||
witnesses: vec![witness], |
|||
nullifier: nf, |
|||
spent: None, |
|||
memo: None |
|||
} |
|||
} |
|||
} |
|||
|
|||
pub struct WalletTx { |
|||
block: i32, |
|||
pub notes: Vec<SaplingNoteData>, |
|||
} |
|||
|
|||
struct SpendableNote { |
|||
txid: TxId, |
|||
nullifier: [u8; 32], |
|||
diversifier: Diversifier, |
|||
note: Note<Bls12>, |
|||
witness: IncrementalWitness<Node>, |
|||
} |
|||
|
|||
impl SpendableNote { |
|||
fn from(txid: TxId, nd: &SaplingNoteData, anchor_offset: usize) -> Option<Self> { |
|||
if nd.spent.is_none() { |
|||
let witness = nd.witnesses.get(nd.witnesses.len() - anchor_offset - 1); |
|||
|
|||
witness.map(|w| SpendableNote { |
|||
txid, |
|||
nullifier: nd.nullifier, |
|||
diversifier: nd.diversifier, |
|||
note: nd.note.clone(), |
|||
witness: w.clone(), |
|||
}) |
|||
} else { |
|||
None |
|||
} |
|||
} |
|||
} |
|||
|
|||
pub struct LightWallet { |
|||
extsks: [ExtendedSpendingKey; 1], |
|||
extfvks: [ExtendedFullViewingKey; 1], |
|||
address: PaymentAddress<Bls12>, |
|||
blocks: Arc<RwLock<Vec<BlockData>>>, |
|||
pub txs: Arc<RwLock<HashMap<TxId, WalletTx>>>, |
|||
} |
|||
|
|||
impl LightWallet { |
|||
pub fn new() -> Self { |
|||
|
|||
let extsk = ExtendedSpendingKey::master(&[1; 32]); // New key
|
|||
let extfvk = ExtendedFullViewingKey::from(&extsk); |
|||
let address = extfvk.default_address().unwrap().1; |
|||
|
|||
LightWallet { |
|||
extsks: [extsk], |
|||
extfvks: [extfvk], |
|||
address, |
|||
blocks: Arc::new(RwLock::new(vec![])), |
|||
txs: Arc::new(RwLock::new(HashMap::new())), |
|||
} |
|||
} |
|||
|
|||
pub fn set_initial_block(&self, height: i32, hash: &str, sapling_tree: &str) -> bool { |
|||
let mut blocks = self.blocks.write().unwrap(); |
|||
if !blocks.is_empty() { |
|||
return false; |
|||
} |
|||
|
|||
let hash = match hex::decode(hash) { |
|||
Ok(hash) => { |
|||
let mut r = hash; |
|||
r.reverse(); |
|||
BlockHash::from_slice(&r) |
|||
}, |
|||
Err(e) => { |
|||
eprintln!("{}", e); |
|||
return false; |
|||
} |
|||
}; |
|||
|
|||
let sapling_tree = match hex::decode(sapling_tree) { |
|||
Ok(tree) => tree, |
|||
Err(e) => { |
|||
eprintln!("{}", e); |
|||
return false; |
|||
} |
|||
}; |
|||
|
|||
if let Ok(tree) = CommitmentTree::read(&sapling_tree[..]) { |
|||
blocks.push(BlockData { height, hash, tree }); |
|||
true |
|||
} else { |
|||
false |
|||
} |
|||
} |
|||
|
|||
pub fn last_scanned_height(&self) -> i32 { |
|||
self.blocks |
|||
.read() |
|||
.unwrap() |
|||
.last() |
|||
.map(|block| block.height) |
|||
.unwrap_or(SAPLING_ACTIVATION_HEIGHT - 1) |
|||
} |
|||
|
|||
/// Determines the target height for a transaction, and the offset from which to
|
|||
/// select anchors, based on the current synchronised block chain.
|
|||
fn get_target_height_and_anchor_offset(&self) -> Option<(u32, usize)> { |
|||
match { |
|||
let blocks = self.blocks.read().unwrap(); |
|||
( |
|||
blocks.first().map(|block| block.height as u32), |
|||
blocks.last().map(|block| block.height as u32), |
|||
) |
|||
} { |
|||
(Some(min_height), Some(max_height)) => { |
|||
let target_height = max_height + 1; |
|||
|
|||
// Select an anchor ANCHOR_OFFSET back from the target block,
|
|||
// unless that would be before the earliest block we have.
|
|||
let anchor_height = |
|||
cmp::max(target_height.saturating_sub(ANCHOR_OFFSET), min_height); |
|||
|
|||
Some((target_height, (target_height - anchor_height) as usize)) |
|||
} |
|||
_ => None, |
|||
} |
|||
} |
|||
|
|||
pub fn address(&self) -> String { |
|||
encode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS, &self.address) |
|||
} |
|||
|
|||
pub fn balance(&self) -> u64 { |
|||
self.txs |
|||
.read() |
|||
.unwrap() |
|||
.values() |
|||
.map(|tx| { |
|||
tx.notes |
|||
.iter() |
|||
.map(|nd| if nd.spent.is_none() { nd.note.value } else { 0 }) |
|||
.sum::<u64>() |
|||
}) |
|||
.sum::<u64>() |
|||
} |
|||
|
|||
pub fn verified_balance(&self) -> u64 { |
|||
let anchor_height = match self.get_target_height_and_anchor_offset() { |
|||
Some((height, anchor_offset)) => height - anchor_offset as u32, |
|||
None => return 0, |
|||
}; |
|||
|
|||
self.txs |
|||
.read() |
|||
.unwrap() |
|||
.values() |
|||
.map(|tx| { |
|||
if tx.block as u32 <= anchor_height { |
|||
tx.notes |
|||
.iter() |
|||
.map(|nd| if nd.spent.is_none() { nd.note.value } else { 0 }) |
|||
.sum::<u64>() |
|||
} else { |
|||
0 |
|||
} |
|||
}) |
|||
.sum::<u64>() |
|||
} |
|||
|
|||
pub fn scan_full_tx(&self, tx: &Transaction) { |
|||
for output in tx.shielded_outputs.iter() { |
|||
|
|||
let ivks: Vec<_> = self.extfvks.iter().map(|extfvk| extfvk.fvk.vk.ivk()).collect(); |
|||
|
|||
let cmu = output.cmu; |
|||
let ct = output.enc_ciphertext; |
|||
|
|||
for (_account, ivk) in ivks.iter().enumerate() { |
|||
let epk_prime = output.ephemeral_key.as_prime_order(&JUBJUB).unwrap(); |
|||
|
|||
let (note, _to, memo) = match try_sapling_note_decryption(ivk, &epk_prime, &cmu, &ct) { |
|||
Some(ret) => ret, |
|||
None => continue, |
|||
}; |
|||
|
|||
{ |
|||
// Update the WalletTx
|
|||
// Do it in a short scope because of the write lock.
|
|||
let mut txs = self.txs.write().unwrap(); |
|||
txs.get_mut(&tx.txid()).unwrap() |
|||
.notes.iter_mut() |
|||
.find(|nd| nd.note == note).unwrap() |
|||
.memo = Some(memo); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
pub fn scan_block(&self, block: &[u8]) -> bool { |
|||
let block: CompactBlock = match parse_from_bytes(block) { |
|||
Ok(block) => block, |
|||
Err(e) => { |
|||
eprintln!("Could not parse CompactBlock from bytes: {}", e); |
|||
return false; |
|||
} |
|||
}; |
|||
|
|||
// Scanned blocks MUST be height-sequential.
|
|||
let height = block.get_height() as i32; |
|||
if height == self.last_scanned_height() { |
|||
// If the last scanned block is rescanned, check it still matches.
|
|||
if let Some(hash) = self.blocks.read().unwrap().last().map(|block| block.hash) { |
|||
if block.hash() != hash { |
|||
eprintln!("Block hash does not match for block {}. {} vs {}", height, block.hash(), hash); |
|||
return false; |
|||
} |
|||
} |
|||
return true; |
|||
} else if height != (self.last_scanned_height() + 1) { |
|||
eprintln!( |
|||
"Block is not height-sequential (expected {}, found {})", |
|||
self.last_scanned_height() + 1, |
|||
height |
|||
); |
|||
return false; |
|||
} |
|||
|
|||
// Get the most recent scanned data.
|
|||
let mut block_data = BlockData { |
|||
height, |
|||
hash: block.hash(), |
|||
tree: self |
|||
.blocks |
|||
.read() |
|||
.unwrap() |
|||
.last() |
|||
.map(|block| block.tree.clone()) |
|||
.unwrap_or(CommitmentTree::new()), |
|||
}; |
|||
let mut txs = self.txs.write().unwrap(); |
|||
|
|||
// Create a Vec containing all unspent nullifiers.
|
|||
let nfs: Vec<_> = txs |
|||
.iter() |
|||
.map(|(txid, tx)| { |
|||
let txid = *txid; |
|||
tx.notes.iter().filter_map(move |nd| { |
|||
if nd.spent.is_none() { |
|||
Some((nd.nullifier, nd.account, txid)) |
|||
} else { |
|||
None |
|||
} |
|||
}) |
|||
}) |
|||
.flatten() |
|||
.collect(); |
|||
|
|||
// Prepare the note witnesses for updating
|
|||
for tx in txs.values_mut() { |
|||
for nd in tx.notes.iter_mut() { |
|||
// Duplicate the most recent witness
|
|||
if let Some(witness) = nd.witnesses.last() { |
|||
let clone = witness.clone(); |
|||
nd.witnesses.push(clone); |
|||
} |
|||
// Trim the oldest witnesses
|
|||
nd.witnesses = nd |
|||
.witnesses |
|||
.split_off(nd.witnesses.len().saturating_sub(100)); |
|||
} |
|||
} |
|||
|
|||
let new_txs = { |
|||
let nf_refs: Vec<_> = nfs.iter().map(|(nf, acc, _)| (&nf[..], *acc)).collect(); |
|||
|
|||
// Create a single mutable slice of all the newly-added witnesses.
|
|||
let mut witness_refs: Vec<_> = txs |
|||
.values_mut() |
|||
.map(|tx| tx.notes.iter_mut().filter_map(|nd| nd.witnesses.last_mut())) |
|||
.flatten() |
|||
.collect(); |
|||
|
|||
scan_block( |
|||
block, |
|||
&self.extfvks, |
|||
&nf_refs[..], |
|||
&mut block_data.tree, |
|||
&mut witness_refs[..], |
|||
) |
|||
}; |
|||
|
|||
for tx in new_txs { |
|||
// Mark notes as spent.
|
|||
for spend in &tx.shielded_spends { |
|||
let txid = nfs |
|||
.iter() |
|||
.find(|(nf, _, _)| &nf[..] == &spend.nf[..]) |
|||
.unwrap() |
|||
.2; |
|||
let mut spent_note = txs |
|||
.get_mut(&txid) |
|||
.unwrap() |
|||
.notes |
|||
.iter_mut() |
|||
.find(|nd| &nd.nullifier[..] == &spend.nf[..]) |
|||
.unwrap(); |
|||
spent_note.spent = Some(tx.txid); |
|||
} |
|||
|
|||
// Find the existing transaction entry, or create a new one.
|
|||
if !txs.contains_key(&tx.txid) { |
|||
let tx_entry = WalletTx { |
|||
block: block_data.height, |
|||
notes: vec![], |
|||
}; |
|||
txs.insert(tx.txid, tx_entry); |
|||
} |
|||
let tx_entry = txs.get_mut(&tx.txid).unwrap(); |
|||
|
|||
// Save notes.
|
|||
for output in tx |
|||
.shielded_outputs |
|||
.into_iter() |
|||
{ |
|||
tx_entry.notes.push(SaplingNoteData::new( |
|||
&self.extfvks[output.account], |
|||
output |
|||
)); |
|||
} |
|||
} |
|||
|
|||
// Store scanned data for this block.
|
|||
self.blocks.write().unwrap().push(block_data); |
|||
|
|||
true |
|||
} |
|||
|
|||
pub fn send_to_address( |
|||
&self, |
|||
consensus_branch_id: u32, |
|||
spend_params: &[u8], |
|||
output_params: &[u8], |
|||
to: &str, |
|||
value: u64, |
|||
memo: Option<String>, |
|||
) -> Option<Box<[u8]>> { |
|||
let start_time = now(); |
|||
println!( |
|||
"0: Creating transaction sending {} tazoshis to {}", |
|||
value, |
|||
to |
|||
); |
|||
|
|||
let extsk = &self.extsks[0]; |
|||
let extfvk = &self.extfvks[0]; |
|||
let ovk = extfvk.fvk.ovk; |
|||
|
|||
let to = match address::RecipientAddress::from_str(to) { |
|||
Some(to) => to, |
|||
None => { |
|||
eprintln!("Invalid recipient address"); |
|||
return None; |
|||
} |
|||
}; |
|||
let value = Amount::from_u64(value).unwrap(); |
|||
|
|||
// Target the next block, assuming we are up-to-date.
|
|||
let (height, anchor_offset) = match self.get_target_height_and_anchor_offset() { |
|||
Some(res) => res, |
|||
None => { |
|||
eprintln!("Cannot send funds before scanning any blocks"); |
|||
return None; |
|||
} |
|||
}; |
|||
|
|||
// Select notes to cover the target value
|
|||
println!("{}: Selecting notes", now() - start_time); |
|||
let target_value = value + DEFAULT_FEE ; |
|||
let notes: Vec<_> = self |
|||
.txs |
|||
.read() |
|||
.unwrap() |
|||
.iter() |
|||
.map(|(txid, tx)| tx.notes.iter().map(move |note| (*txid, note))) |
|||
.flatten() |
|||
.filter_map(|(txid, note)| SpendableNote::from(txid, note, anchor_offset)) |
|||
.scan(0, |running_total, spendable| { |
|||
let value = spendable.note.value; |
|||
let ret = if *running_total < u64::from(target_value) { |
|||
Some(spendable) |
|||
} else { |
|||
None |
|||
}; |
|||
*running_total = *running_total + value; |
|||
ret |
|||
}) |
|||
.collect(); |
|||
|
|||
// Confirm we were able to select sufficient value
|
|||
let selected_value = notes |
|||
.iter() |
|||
.map(|selected| selected.note.value) |
|||
.sum::<u64>(); |
|||
if selected_value < u64::from(target_value) { |
|||
eprintln!( |
|||
"Insufficient funds (have {}, need {:?})", |
|||
selected_value, target_value |
|||
); |
|||
return None; |
|||
} |
|||
|
|||
// Create the transaction
|
|||
println!("{}: Adding {} inputs", now() - start_time, notes.len()); |
|||
let mut builder = Builder::new(height); |
|||
for selected in notes.iter() { |
|||
if let Err(e) = builder.add_sapling_spend( |
|||
extsk.clone(), |
|||
selected.diversifier, |
|||
selected.note.clone(), |
|||
selected.witness.clone(), |
|||
) { |
|||
eprintln!("Error adding note: {:?}", e); |
|||
return None; |
|||
} |
|||
} |
|||
|
|||
// Compute memo if it exists
|
|||
let encoded_memo = memo.map(|s| Memo::from_str(&s).unwrap() ); |
|||
|
|||
println!("{}: Adding output", now() - start_time); |
|||
if let Err(e) = match to { |
|||
address::RecipientAddress::Shielded(to) => { |
|||
builder.add_sapling_output(ovk, to.clone(), value, encoded_memo) |
|||
} |
|||
address::RecipientAddress::Transparent(to) => { |
|||
builder.add_transparent_output(&to, value) |
|||
} |
|||
} { |
|||
eprintln!("Error adding output: {:?}", e); |
|||
return None; |
|||
} |
|||
println!("{}: Building transaction", now() - start_time); |
|||
let (tx, _) = match builder.build( |
|||
consensus_branch_id, |
|||
prover::InMemTxProver::new(spend_params, output_params), |
|||
) { |
|||
Ok(res) => res, |
|||
Err(e) => { |
|||
eprintln!("Error creating transaction: {:?}", e); |
|||
return None; |
|||
} |
|||
}; |
|||
println!("{}: Transaction created", now() - start_time); |
|||
println!("Transaction ID: {}", tx.txid()); |
|||
|
|||
// Mark notes as spent.
|
|||
let mut txs = self.txs.write().unwrap(); |
|||
for selected in notes { |
|||
let mut spent_note = txs |
|||
.get_mut(&selected.txid) |
|||
.unwrap() |
|||
.notes |
|||
.iter_mut() |
|||
.find(|nd| &nd.nullifier[..] == &selected.nullifier[..]) |
|||
.unwrap(); |
|||
spent_note.spent = Some(tx.txid()); |
|||
} |
|||
|
|||
// Return the encoded transaction, so the caller can send it.
|
|||
let mut raw_tx = vec![]; |
|||
tx.write(&mut raw_tx).unwrap(); |
|||
Some(raw_tx.into_boxed_slice()) |
|||
} |
|||
} |
Loading…
Reference in new issue